0x00 前言
手头有一个马蹄形状的自行车锁,手机通过蓝牙可以操作开锁。正好最近在学习低功耗蓝牙(BLE)协议,就拿他练练手。
0x01 抓包
网上有挺多抓包方式,包括手机端HCI,这里推荐国外的开源项目Sniffle。Sniffle是英国网络安全公司NCC Group在2019年底开源的一个基于使用TI CC1352/CC26x2硬件的蓝牙5和4.x LE嗅探器(抓包工具),最新的release版本是2022年8月发布的1.7。使用Sniffle需要购买指定的蓝牙开发板,并刷入固件才能使用,电脑通过串口与蓝牙开发板通信。
Sniffle项目中fw文件夹是固件源码,如果只是抓包的话,在Sniffle项目release1.7中有上传的编译好的固件,根据型号下载。
搜索相关资料得知,可以在TI网站上安装UNIFLASH软件来进行刷固件:https://www.ti.com/tool/UNIFLASH?keyMatch=&tisearch=search-everything&usecase=software%23downloads#downloads
抓包测试:
Sniffle抓包方式是开发板抓到数据之后,通过串口发送给PC,PC收到数据包之后再根据设定条件来过滤数据,最后根据参数决定是否保存成pcap文件。因此需要用参数-s 指定某个串口,串口可以通过设备管理器查看,共有两个,选择UART的串口:
从README中可以知道python_cli中的sniff_receiver.py为抓包脚本,支持多个参数,这里简单介绍下常用的设置:
-a 只抓广播包,不知道设备mac地址的情况下,可以用此参数
-m 只抓特定mac地址的数据包,可以从广播包中分析出目标设备mac地址
-o 抓包结果保存到pcap文件
下图是命令sniff_receiver.py -s COM7 -m xx:xx:xx:xx:xx:xx -o data.pcap的显示:
下图是保存的pcap文件中的BLE开始连接过程截图
至此抓包工作完成,下面开始分析数据包。
0x02 分析数据包
根据BLE协议栈,链路层主要是维持连接,我们暂时不关注链路层,所以使用wireshark显示过滤器btatt只显示att协议数据包。
前面主要是GATT的过程,这里只简单介绍一下: GATT层定义了一个4层树形框架,其中根节点为Profile(配置),它有不同的Services(服务),不同的服务有不同的Characteristics(特征),不同的特征通过一个具体的Value(值)或者Descriptor(描述符)来定义。网上关于GATT的资料很多,看的迷迷糊糊地,反正是没特别明白这东西。大致意思就是通过GATT交互可以知道BLE设备有哪些接口,这些接口是可读/可写等等。
可以把GATT当做接口说明,是固定的,跟应用层交互关系不大,直接跳过。往下翻在215帧Sent Write Command, Handle: 0x000d出现了可变数据,很大概率是应用层数据。
简单查看后续数据包发现所有的发送数据包长度都是16字节,无可见字符串与00等数据,很明显是加密了,大概率是AES(因为16字节正好一整行,并且安卓上对称加密普遍采用AES)。
有加密就要有密钥交换方式,有的开发者采用固定字符串在代码中写死,有的是动态传输或计算的,比如SSL的密钥交换算法。一帧一帧的往前翻数据包,发现第一个应用层数据帧在214帧:
这一帧是Handle Value Notification,之前没有Master发送给Slave的应用层数据,并且这个数据长度超过16,多次抓包前面的14 00 80是固定的,根据经验猜测是每次开始通信之前,Slave先把本次会话密钥发送给Master,一次一密,防止重放攻击。
0x03 逆向APP分析
既然数据包加密了,再分析pcap就没有啥意义了,于是打开jadx开始逆向APP。首先搜索AES,简单翻找后发现提取key的方式:
这里的20不就是刚才数据包开头的0x14么,密钥是从下标3开始的,0x15的话是从下标4开始。最开始看数据包还有疑问,为什么前3个字节不变的情况下,后面是17个随机字节而不是16呢?看到代码就明白了有两种情况,干脆生成17个字节,各取所需。
再看加密函数,ECB模式无IV,无填充,直接拿key解密后续数据就行。
这里有个疑问,ECB模式要求数据16字节对齐,不填充怎么凑齐16字节?直到解密数据后发现:
好家伙,怪不得不用填充,自己手动填充了00。填充00意味着得有地方写明长度,不然末尾字节恰好为00咋办?继续逆向看代码吧。
通过logcat可以知道,command和command2就是发送的数据包,在调用mBleService.sendMessage之前进行了加密。那数据的组包就是getCommand干的了。继续进入getCommand发现jadx出错无法显示:
于是祭出版主的GDA:
通过GDA的逆向分析,弄清出了组包格式。
数据包示例:08 01 02 000D9038 E0
长度:1字节,包含自身的有效数据长度
方向:1字节,0表示BLE设备回复,1表示发送给BLE设备
功能码:示例中02表示验证密码,05表示开锁
参数:不同功能码参数数量和长度不同,具体可看getCommand函数。示例中0xD9038换算成十进制是888888,即锁的密码(这个锁限制密码只能是6位数字)
校验码:前面的数据逐字节相加,最后与上0xFF
通过简单分析不难弄懂开锁流程:
1、蓝牙完成连接后,锁主动发送本次会话密钥给手机端;
2、手机端生成确认密钥功能码并加密发送,锁再回复确认数据包;
3、手机端将十进制密码转成16进制,发送验证密码功能包,锁回复验证结果;
4、如果密码正确,手机端点开锁会发送05开锁功能码打开锁;
5、开锁完成后,锁马上主动断开连接,节省功耗。
中间有穿插时间戳同步的数据包,就不多介绍了。这里额外提一句:逆向最怕的是无法反编译、反汇编,GDA还是很强大的,只要能反编译,再难看也能给他一点点的梳理明白。有总比没有强!
0x04 电脑发包解锁
既然已经搞清楚了加解密方式,并且数据包已经解密,那我们构建数据包通过电脑发送过去,一方面能验证加密,另一方面也能熟悉电脑端蓝牙怎么使用。说干就干,网上一通资料查找,说是PC端Python环境下,只有bleak库支持低功耗蓝牙(文章是几年前的,不确定现在其他库是否支持)。安装完bleak库,在examples文件夹里有不少示例,网上也有不少代码可以参考。下图是其中一个示例代码:
可以看到bleak用起来还是很简单的,给定设备的mac地址,之后就是连接、配对(BLE多数不用配对)、收、发完事。针对锁的流程,我们将发包过程写在了数据包接收的回调函数中:
已知密码的情况下,上述代码可以直接使用电脑完成开锁。
0x05 第一代远程开锁
通过以上工作,算是初步掌握了BLE蓝牙的一些基本协议和使用,比如GATT就是BLE设备(服务器)的接口说明,定义了一些Characteristics(特征)的读、写、通知权限,PC/手机(客户端)通过Characteristics可以实现与BLE设备通信。Characteristics在代码中是UUID形式,在数据包中是以Handle形式,Handle与Characteristics是一一对应的,对应关系是在GATT中定义。
只是电脑开锁,还需要密码。进一步探索发现APP中有远程开锁的功能,用户在输入密码之后会生成一个有效期10分钟的临时密码,发送给车附近的好友,就可以通过点击输入临时密码按钮开锁。如下图:
经过测试,这个锁是3年前的,属于第一代远程开锁。通过解密分析临时密码开锁数据包如下:
08 01 06 6532E77A 07
密码开锁功能码是05,这个临时密码是06,但是参数怎么和密码开锁一样是4个字节呢?继续去逆向APP找代码,第一代远程开锁临时密码生成函数如下:
参数str就是用户的密码,比如888888,临时密码就是时间戳与密码异或了一下。如此简(Fu)单(Yan)的算法,那10分钟有效是怎么实现的?因为锁的固件没有获得,只能猜测锁收到后与密码进行异或还原时间戳,然后判断是否(当前时间戳-还原时间戳)≤600。怎么验证呢?
一番思索后发现,这个异或操作将密码的精确判断和时间戳的范围判断混在了一起,聪明的读者可能会想到一个问题,还原的时候只是拿密码进行了运算,并没有对密码精确判断(临时密码丢失了密码信息,也无法精确判断)。如果以上推断成立的话,保证时间戳相差很少的情况下,那么理论上密码就可以范围一些。怎么验证?很简单,输入一个相近的密码比如888886来生成临时密码,如果这个临时密码能开锁就验证推断正确。
实验证实了猜测,在临时密码那里,888886一样可以开锁!那这个……岂不是可以爆破?
这个锁密码必须是6位数字且不能以0开头,那范围从100000到999999,总共是90万个可能。10分钟有效就是600秒,考虑到时间戳本来可能就差几秒,我们取590来计算,900000/590≈1525。也就是说时间戳在10秒以内,我们最多尝试1525次就可以开锁!当然,这个爆破的前提是锁不限制次数和频率。先生成爆破代码试试:
经过实验发现没有限制可以爆破,时间上来看,1秒钟至少可以尝试10次以上,最长在2分钟多一点就可以解锁,平均在1分钟多一些就可以爆破成功。
进一步分析,通过临时密码成功开锁的遍历密码与真实密码之间相差最多600,我们可以再回到密码开锁的方式,最多尝试600次就可以获得真实密码,差不多再花费1分钟。
综上,无密码情况下可以实现2分钟左右开锁,再花1分钟即可获得精确密码,这个异或的设计真是“神来之笔”。
0x06 第二代远程开锁
第一代临时密码这么大的坑,那么有理由相信第二代远程开锁理论上应该功能更多,安全性更强。但是笔者没有二代锁,所以没法进行尝试。这里贴出来逆向代码:
第一代临时密码参数只有真实密码一项,有效期固定10分钟。第二代临时密码有效期最长居然能有一年(365天),如果算法设计不佳,那岂不是稀释的更厉害?createTempPwd2G函数的逆向可以通过GDA来分析,这里就不贴出来生成过程了。由于密码长度限制前几位是0,而时间戳前几位短时间内又不变,一代算法异或后前几位是不变的,二代临时密码在设计上增大了随机性,但是就笔者经验来看安全性依旧堪忧。只是没有二代锁,无法验证,就此停笔。
0x07结论
在这个万物互联的时代,形形色色的产品层出不穷,受限于产品竞争压力,各个厂商首先要做的是功能迭代,在安全性上不会投入太多精力,毕竟首先要生存下来再说安全性,因此笔者对锁的安全性设计并不感到意外。本文重点是展现了BLE抓包方法,以及过程中分析的思路,希望能对刚入行的新人起到参考作用。
原文始发于看雪社区(supertyj):某蓝牙马蹄锁的分析过程