2016年,我开始研究宝马,对IDrive和BMW Connected Apps有了基本认识,通过一些蓝牙协议,我手机上的Spotify会被添加到桌面上的音乐列表中。单击条目会显示一个丰富的用户界面,提供了很多浏览选项以选择播放列表并根据当前歌曲启动广播电台,这比蓝牙音乐控件更具交互性。
这激发了我对宝马app分析的好奇心,制造商添加应用程序可以自动升级汽车,不必局限于制造商在车上加载了哪些应用程序。也不需要从口袋里拿出手机并看着手机屏幕来切换音乐,可以使用带有信息娱乐屏幕的触觉控制器旋钮来安全地控制任何应用程序!
但是,与蓝牙一样,这种体验并不十分流畅。有时其他蓝牙应用程序协议无法连接,有时个别应用程序本身也不会响应。在Spotify论坛上寻求帮助的呼声也被忽略了,BMW Connected应用程序的评论糟透了,没有任何修复的迹象。
此外,与iPhone上可用的应用程序相比,适用于Android的BMW Connected仅共享了非常有限的应用程序选择:仅Spotify和iHeartRadio,以及基本的日历应用程序。由于我喜欢其他音乐应用程序,因此我致电BMW支持,询问是否可以访问BMW Ready SDK,以便可以构建自己的应用程序,他们拒绝了这种做法。
因此,我决定找出这个BMW Apps协议,并在没有他们帮助的情况下将自己的音乐应用程序添加到系统中。
蓝牙是一种标准协议,我只需要学习对汽车说些什么,Android内置了Bluetooth Capture日志记录!我只记录电话应用程序对汽车说的内容,然后看看发现了什么:
这个SPP协议似乎有很多有用的信息:我看到了一些X509证书,一些XML数据,一些看起来像歌曲元数据的字符串,还有很多东西!
我开始注意到大多数数据包的前一个字节中的模式:第0个字节为0,第一个字节为1或6,第二个字节为0,第3个字节通常很低,接下来的字节几乎总是0x0FA4
。我发现接下来的2个字节是剩余数据的长度,接下来的4个字节的数据几乎总是0xDEADBEEF
。
我编写 Wireshark Lua插件来帮助我理解数据,将第一个字节解析为名为Val1,Val2,Val3和Length的4个16位值,然后输出剩余的数据字节。
@@ -0,0 +1,158 @@ print("hello world!") local debug_level = { DISABLED = 0, LEVEL_1 = 1, LEVEL_2 = 2 } local DEBUG = debug_level.LEVEL_1 local dprint = function() end local dprint2 = function() end local function resetDebugLevel() current_debug_level = 2 if current_debug_level > debug_level.DISABLED then dprint = function(...) info(table.concat({"Lua: ", ...}," ")) end if current_debug_level > debug_level.LEVEL_1 then dprint2 = dprint end else dprint = function() end dprint2 = dprint end end -- call it now resetDebugLevel() local bmw_proto = Proto("bmw", "BMW BCL") local hdr_fields = { val1 = ProtoField.uint16 ("bmw.val1", "Val1", base.HEX), val2 = ProtoField.uint16 ("bmw.val2", "Val2", base.HEX), val3 = ProtoField.uint16 ("bmw.val3", "Val3", base.HEX), len = ProtoField.uint16 ("bmw.len", "Length", base.DEC) } bmw_proto.fields = hdr_fields dprint2("bmw_proto ProtoFields registered") local dissect_data = Dissector.get("data") function bmw_proto.init() end local BMW_MSG_HDR_LEN = 8 -- mention future helper methods local check_bmw_length function bmw_proto.dissector(tvbuf, pktinfo, root) dprint2("bmw_proto.dissector called") -- check packet length local pktlen = tvbuf:len() local bytes_consumed = 0 while bytes_consumed < pktlen do -- call dissect_packet for this single packet -- it will return a positive number for the amount of bytes consumed -- or a negative number for a request for more bytes -- or 0 for an error local result = dissect_packet(tvbuf, pktinfo, root, bytes_consumed) if result > 0 then -- successfully parsed packet bytes_consumed = bytes_consumed + result elseif result == 0 then -- not a valid packet return 0 else -- need more data to finish parsing pktinfo.desegment_offset = bytes_consumed pktinfo.desegment_len = 0 - result return pktlen end end return bytes_consumed end local function heuristic(tvbuf, pktinfo, root) ETCH_MAGIC = ByteArray.new("de ad be ef") if tvbuf:len() < BMW_MSG_HDR_LEN + 4 then return false end local etch_magic = tvbuf:range(BMW_MSG_HDR_LEN, 4):bytes() return etch_magic == ETCH_MAGIC end function dissect_packet(tvbuf, pktinfo, root, offset) dprint2("dissect_packet function called") local length_val, length_tvbf = check_bmw_length(tvbuf, offset) if length_val <= 0 then -- not enough data to get the header return length_val end -- update the packet list info pktinfo.cols.protocol:set("BMW BCL") if string.find(tostring(pktinfo.cols.info), "^BMW") == nil then pktinfo.cols.info:set("BMW BCL") end -- create the protocol tree field local tree = root:add(bmw_proto, tvbuf:range(offset, BMW_MSG_HDR_LEN + length_val)) -- get the vals tree:add(hdr_fields.val1, tvbuf:range(offset, 2)) tree:add(hdr_fields.val2, tvbuf:range(offset+2, 2)) tree:add(hdr_fields.val3, tvbuf:range(offset+4, 2)) tree:add(hdr_fields.len, length_tvbf) remaining_tvb = tvbuf(offset + BMW_MSG_HDR_LEN, length_val):tvb() dissect_data:call(remaining_tvb, pktinfo, root) return BMW_MSG_HDR_LEN + length_val end function check_bmw_length(tvbuf, offset) -- remaining bytes in the packet to look through local msglen = tvbuf:len() - offset -- check if capture was only capturing partial packet size if msglen ~= tvbuf:reported_length_remaining(offset) then -- captured packets are being sliced/cut-off, so don't try to desegment/reassemble dprint2("Captured packet was shorter than original, can't reassemble") return 0 end if msglen < BMW_MSG_HDR_LEN then -- we need more bytes to parse the header dprint2("Need more bytes to look at the header") return -DESEGMENT_ONE_MORE_SEGMENT end -- we have enough to parse the length from the header local length_tvbr = tvbuf:range(offset+6, 2) local length_val = length_tvbr:uint() -- check if we have the whole packet somewhere if msglen < BMW_MSG_HDR_LEN + length_val then dprint2("Need more bytes to desegment full packet") return -(BMW_MSG_HDR_LEN + length_val - msglen) end return length_val, length_tvbr end function enable_dissector() DissectorTable.get("btrfcomm.dlci"):add_for_decode_as(bmw_proto) end enable_dissector() --bmw_proto:register_heuristic("btspp", heuristic)
似乎该协议用于将连接多路复用到单个蓝牙串行套接字,而Val2是不同的连接ID。在Wireshark中解析了该字段之后,我可以使用显示过滤器来遵循单个的通信流程。
经过一番研究,我发现了一篇文章,解释了宝马将Apache Etch用作“用于BMW Apps的基本通信协议”。速浏览Apache Etch文档可确认这0xDEADBEEF
是每个Etch RPC调用开始时的魔术标识符。因此,这意味着我只需要将每个BCL流解码为Apache Etch数据包,Wireshark的内置Etch解析就会解析数据!
除了Apache Etch,每个函数名称和任何其他符号名称都编译为32位哈希值。Wireshark可以使用Etch调试组件将哈希值替换为适合的名称,但是我首先需要弄清楚这些名称并自己对它们进行哈希处理。该散列算法是公开的,所以我写了如下代码,以帮助手动生成此调试名。
extern crate etch_hash; use etch_hash::*; use std::hash::Hasher; #[test] fn null() { assert_eq!(5381, hash("".as_bytes())); } #[test] fn single_letter() { assert_eq!(0x150a2c9e, hash("c".as_bytes())); assert_eq!(352988316, hash("a".as_bytes())); } #[test] fn all_letters() { assert_eq!(352988316, hash("a".as_bytes())); assert_eq!(1511848646, hash("ab".as_bytes())); assert_eq!(669497117, hash("abc".as_bytes())); assert_eq!(2300776583, hash("abcd".as_bytes())); assert_eq!(3492286878, hash("abcde".as_bytes())); assert_eq!(1266308680, hash("abcdef".as_bytes())); assert_eq!(3915594783, hash("abcdefg".as_bytes())); assert_eq!(2878000137, hash("abcdefgh".as_bytes())); assert_eq!(53556896, hash("abcdefghi".as_bytes())); assert_eq!(4290539978, hash("abcdefghij".as_bytes())); } #[test] fn long_names() { assert_eq!(0x28e34aa1, hash("org.apache.etch.example.binary.binaryExample.f".as_bytes())); assert_eq!(0x0972201e, hash("org.apache.etch.example.binary.binaryExample._result_f".as_bytes())); assert_eq!(0x28e34a7c, hash("org.apache.etch.example.binary.binaryExample.A".as_bytes())); } #[test] fn long_names_iterative() { let result = hash("org.apache.etch.example.binary.binaryExample".as_bytes()); assert_eq!(0x28e34aa1, hash_more(result, ".f".as_bytes())); assert_eq!(0x0972201e, hash_more(result, "._result_f".as_bytes())); assert_eq!(0x28e34a7c, hash_more(result, ".A".as_bytes())); } #[test] fn obj_null() { let mut hasher = EtchHash::new(); hasher.write("".as_bytes()); assert_eq!(5381, hasher.finish()); } #[test] fn obj_single_letter() { let mut hasher = EtchHash::new(); hasher.write("c".as_bytes()); assert_eq!(0x150a2c9e, hasher.finish()); hasher = EtchHash::new(); hasher.write("a".as_bytes()); assert_eq!(352988316, hasher.finish()); } #[test] fn obj_long_names_iterative() { let mut hasher = EtchHash::new(); hasher.write("org.apache.etch.example.binary.binaryExample".as_bytes()); let mut sub_hasher; sub_hasher = hasher.clone(); sub_hasher.write(".f".as_bytes()); assert_eq!(0x28e34aa1, sub_hasher.finish()); sub_hasher = hasher.clone(); sub_hasher.write("._result_f".as_bytes()); assert_eq!(0x0972201e, sub_hasher.finish()); sub_hasher = hasher.clone(); sub_hasher.write(".A".as_bytes()); assert_eq!(0x28e34a7c, sub_hasher.finish()); sub_hasher = EtchHash::new_with_state(hasher.finish()); sub_hasher.write(".A".as_bytes()); assert_eq!(0x28e34a7c, sub_hasher.finish()); }
但是,在哪里获得名称?事实证明,JVM字节码很容易反编译。变量名称有些模糊,但是Etch生成的类包含所有可用Etch符号的确切列表:
ValueFactoryBMWRemoting.java: private static void a() { a = (Type)hn.get("de.bmw.idrive.BMWRemoting.VersionInfo"); b = (Type)hn.get("de.bmw.idrive.BMWRemoting.SecurityException"); c = (Type)hn.get("de.bmw.idrive.BMWRemoting.ServiceException"); d = (Type)hn.get("de.bmw.idrive.BMWRemoting.IllegalArgumentException"); e = (Type)hn.get("de.bmw.idrive.BMWRemoting.IllegalStateException"); f = (Type)hn.get("de.bmw.idrive.BMWRemoting.ver_getVersion"); g = (Type)hn.get("de.bmw.idrive.BMWRemoting._result_ver_getVersion"); h = (Type)hn.get("de.bmw.idrive.BMWRemoting.info_getSystemInfo"); i = (Type)hn.get("de.bmw.idrive.BMWRemoting._result_info_getSystemInfo"); j = (Type)hn.get("de.bmw.idrive.BMWRemoting.sas_crl"); k = (Type)hn.get("de.bmw.idrive.BMWRemoting._result_sas_crl"); l = (Type)hn.get("de.bmw.idrive.BMWRemoting.sas_certificate"); m = (Type)hn.get("de.bmw.idrive.BMWRemoting._result_sas_certificate"); n = (Type)hn.get("de.bmw.idrive.BMWRemoting.sas_login"); o = (Type)hn.get("de.bmw.idrive.BMWRemoting._result_sas_login"); ...
因此,只是通过自定义哈希运行此名称列表,然后将结果文件提供给Wireshark,就会获得协议转储文件!
在对代码进行反编译的同时,我对协议连接方法进行了一些研究,发现BMW Connected应用程序会运行TCP localhost服务器,该服务器通过此BCL复用连接代理所有连接。这意味着任何root call都可以运行tcpdump
并记录每个应用程序与汽车的通信,而无需运行蓝牙嗅探。
另外,这意味着我不需要重写主要的BMW Connected应用程序,只需要打开与手机上的TCP端口的TCP连接即可直接与汽车建立连接,并可以扮演任何其他BMW角色启用的应用。
如何找出代理运行在哪个端口?BMW Connected应用程序广播系统范围内的Android Intent,其中包含连接细节,以便在汽车连接时使用。此外,它已硬编码到特定端口,因此我可以尝试手动连接到该端口。
这完全改变了我的方法:我要做的就是学会这种高级RPC协议!
使用从应用程序生成的Etch RPC类,可以使用所需的位来重构Etch IDL文件。Etch编译器返回了一些代理对象,我有了自己的一套Etch RPC类!
我刚开始实现一个Etch RPC的服务器端,然后将自己的连接通知发送到官方应用程序。Android Intent是应用程序组件之间通过松散耦合彼此通信的标准方式,并且完全未经身份验证。这欺骗了官方应用程序模块,使其无法在我的控制下连接到我自己的Etch服务器,而不是连接到汽车的BCL代理。
Etch对象带有NotImplementedExceptions,因此很容易看到官方应用程序发出了哪些调用。在填写完我需要使官方应用程序满意的Fake Car资料后,该应用程序(在模拟器中运行)使用提供的VIN号下载了从未与之实际连接的汽车的图像:
从应用程序到汽车建立新的连接时,它所做的第一件事就是向汽车发送PKCS7证书。汽车以随机数作为响应,应用程序应以一些身份验证数据作为响应。
该证书由BMW CA签署,因此没有真正的解决方法。但是,仅从应用APK中提取文件即可轻松获得这些证书,而主要的联网应用大约包含10个。
接下来,如何找出正确的随机数响应?它看起来足够长,可以成为RSA4096签名,因此尝试破解。
事实证明,JVM字节码很容易反编译:将整个应用程序反编译为混淆的Java文件后,可以将整个程序加载到Android Studio中,并使用其强大的代码搜索和重构工具来帮助导航代码。可以找到线索com.bmwgroup.connected.internal.security.CarSecurityManager
?
实际上,代码说明了它如何使用Android Binder RPC连接到CarSecurityService
对象,以及如何将质询随机数交换为质询响应。此外,该服务已导出,可供手机上的任何应用使用。
一些快速测试代码验证了此服务所返回的答案与Wireshark捕获的答案相同。服务实现是围绕本机库的一个小的JNI包装,该库包含一些OpenSSL符号。
掌握了这些知识后,我迅速构建了一个测试应用程序,试图使我第一次与汽车建立联系!官方应用程序从查找数据包捕获中所做的第一件事之一就是调用rhmi_getCapabilities
以获取汽车支持的功能标志列表。
这涉及实现一个BroadcastReceiver来侦听汽车的连接通知,充当SecurityService的客户端以准备好挑战随机数,并实际使用适当的连接详细信息实例化Etch代理对象。
在台式机和车库之间经过数次尝试之后,我成功建立了连接!
这个项目的主要目标是向汽车添加更多音乐应用程序,因此我自然而然地将注意力集中到RHMI调用命名空间,并使用诸如rhmi_setData
和名称rhmi_onActionEvent
。应用程序最先调用的一个是rhmi_setResource
,用于发送XML小部件布局和一些zip文件。这些资源可在任何BMW应用程序的APK中轻松获得,从而便于检查。
第一个也是最重要的是XML的layout。它包含组织成窗口的组件列表,组件中显示的模型列表以及链接到小部件的动作列表。这些模型中的某些模型可以保存来自电话应用程序的任意数据,而另一些模型可以包含指向zip文件中资源的数字ID。
压缩后的资源很简单:一个图形包,其中的每个文件都用数字ID命名,或者是一个翻译文本包,其中的字符串由ID键入。在应用程序中环顾四周,有适用于BMW或Mini品牌的不同资源包。
因此,在我第一次尝试在汽车中创建应用程序时,我复制了初始化调用以发送远程UI资源,然后测试了该rhmi_setData
调用以尝试将图像加载到汽车中:
弄清楚动作事件系统非常容易:在调用rhmi_addActionEventHandler
信号通知汽车发送输入事件后,汽车将开始调用rhmi_onActionEvent
有关触发了哪个动作的详细信息,并将该动作链接回原始组件。
我的下一个实验是尝试编辑发送到汽车的Layout。但是失败了,汽车拒绝了上传。但是,上传原始工件仍然非常有效。
我在身份验证证书中找到了SHA256校验的列表,原始资源与这些校验和匹配。这意味着我无法更改任何窗口小部件布局或图形包。这主要是<entryButton>
组件的问题,该组件被硬编码到图形包中的特定图标,因此,基本上任何创建RHMI应用程序的应用程序都将显示属于原始身份验证证书的应用程序图标。
最初,我很失望:如果我被锁定在原始的小部件布局中,那么我将无法构建一个自定义的应用程序。但是,有时rhmi_setProperty
在Wireshark捕获中看到了正确的调用,并且小部件布局中的某些组件<properties>
定义了一个集合,但是属性是由数字ID定义的。
事实证明,Java字节码确实很容易反编译,我发现此RhmiPropertyType
枚举包含整个列表。
因此,即使无法更改窗口小部件配置,也可以设置窗口小部件属性,包括可见性,位置和大小,这仍然具有很大的灵活性。
有了这些基本构建块,我便开始为汽车构建自己的音乐应用程序!除了我的手机上已经有几个出色的音乐应用程序外,我将这些应用程序添加到汽车中,而不是编写自己的应用程序。有什么方法可以代替我控制现有的音乐应用程序吗?
事实证明,Android音乐应用程序实现称为MediaBrowserService的协议。通过实现此单一API,该应用程序可以通过Android Auto,Android Wear和蓝牙堆栈自动使用。我可以充当该接口的另一个客户端,并在此API之上将汽车实现为前端。
这种策略使实施相对容易:我只需要为此MediaBrowserService构建一个客户端,每当发生任何元数据回调时,都使用适当的信息更新汽车的标签。汽车上的按钮回调可以针对音乐应用运行命令。
然后经过几个月的测试,我有了一个具有完整浏览和搜索支持的音乐应用程序,它可以控制任何MediaBrowserService音乐应用程序:
此Connected Apps协议仅在通过USB或蓝牙从手机到(行驶中的)汽车的本地连接上起作用。还有许多其他安全限制:
为了进一步减少攻击,宝马应该利用自己的沙盒经验,并像通用汽车一样,启动一个开发人员计划,让用户表达自己的创造力并构建自己的应用程序以为应用程序商店做贡献。