Smart Doorbell Security (Part 4) (Bootloader analysis)
Analysing the bootloader
This part of our series intends to inspect the U-Boot bootloader in use by the device in order to understand the firmware decoding routine. It should be noted that I wasn’t able to gain a full understanding of the decoding procedure as this operation seems to have been delegated to hardware, nevertheless, I’ve included my investigation as to how I reached this conclusion. In the end, I opted to bypass this step entirely and dump the full decoded firmware from memory, forgoing the need to understand the decoding routine(s).
If you’d like to continue reading my boot loader analysis, keep reading or alternatively, skip to the next part where we’ll do some firmware analysis.
Onwards, we dump the U-Boot console’s environment to understand how our firmware is executed, as well as view its available commands.
U-Boot available commands:
hisilicon # help
? - alias for 'help'
base - print or set address offset
bootm - boot application image from memory
bootp - boot image via network using BOOTP/TFTP protocol
cmp - memory compare
cp - memory copy
crc32 - checksum calculation
ddr - ddr training function
fatinfo - print information about filesystem
fatload - load binary file from a dos filesystem
fatls - list files in a directory (default /)
getinfo - print hardware information
go - start application at address 'addr'
help - print command description/usage
hwdec - usage:simple version. hwdec dest_addr src_addr
loadb - load binary file over serial line (kermit mode)
loady - load binary file over serial line (ymodem mode)
loop - infinite loop on address range
md - memory display
mii - MII utility commands
mm - memory modify (auto-incrementing address)
mmc - MMC sub system
mmcinfo - mmcinfo <dev num>-- display MMC info
mtest - simple RAM read/write test
mw - memory write (fill)
nm - memory modify (constant address)
ping - send ICMP ECHO_REQUEST to network host
printenv- print environment variables
rarpboot- boot image via network using RARP/TFTP protocol
reset - Perform RESET of the CPU
saveenv - save environment variables to persistent storage
setenv - set environment variables
sf - SPI flash sub-system
tftp - tftp - download or upload image via network using TFTP protocol
usb - USB sub-system
usbboot - boot from USB device
version - print monitor version
U-Boot environment:
hisilicon # printenv
bootargs=mem=96M console=ttyAMA0,115200
bootcmd=sf probe 0;sf read 0x82000000 0x1a0000 0x5b0000;hwdec 0x80008000 0x82000000;go 0x80008000
baudrate=115200
ethaddr=00:00:23:34:45:66
ipaddr=192.168.1.10
serverip=192.168.1.2
netmask=255.255.255.0
bootfile="uImage"
auversion2=5b3602eb
auversion0=5b1e8b16
auversion3=5b9616df
bootdelay=5
stdin=serial
stdout=serial
stderr=serial
verify=n
ver=U-Boot 2010.06 (Jun 11 2018 - 22:44:56)
Environment size: 427/65532 bytes
One of the important lines here is the bootcmd directive:
bootcmd=sf probe 0;sf read 0x82000000 0x1a0000 0x5b0000;hwdec 0x80008000 0x82000000;go 0x80008000
Where the ‘sf’ command is used to interface with SPI Flash memory, in this case reading into flash. The syntax for the ‘sf read’ is:
sf read [addr to read into] [offset from start of flash] [number of bytes to read]
Per the above, we see that the bootcmd instructs U-Boot to read (following an initial probe) 0x5b0000 (5963776u) bytes into address 0x82000000 from offset [start of flash]+0x1a00000 and then calls the hwdec command on the loaded memory, supplying both the source and destination addresses, before finally issuing go command to boot the decompressed memory.
The hwdec command is described as follows in the help output:
hwdec - usage:simple version. hwdec dest_addr src_addr
This command is non-standard to U-Boot and included by the device’s developers, most likely responsible for firmware decryption/decompression.
We note that our earlier obtained firmware image and the memory at 0x82000000 are identical:
Firmware obtained from sniffing traffic:
$ hexdump -C unpacked | head
00000000 90 c2 3d 00 4c 0d 67 00 00 00 00 00 00 00 00 00 |..=.L.g.........|
00000010 30 c0 fb 54 5e 8c 71 a9 00 04 10 92 73 48 00 51 |0..T^.q.....sH.Q|
00000020 0d d0 0d 10 0e 50 0e e0 a1 f7 03 10 69 a0 01 84 |.....P......i...|
00000030 f8 ff fe fe fe fe fe fe fe fe fe fe fe fe fe fe |................|
00000040 36 67 02 10 0c 05 26 00 81 c0 c5 03 10 87 5f 3c |6g....&......._<|
00000050 00 41 10 c6 03 10 09 1f 40 00 e1 1a 12 04 10 8b |.A......@.......|
00000060 22 42 fd bf fd fe fe fe fe fe fe fe fe fe fe fe |"B..............|
00000070 fe fe fe b6 bf 32 01 c7 c1 bb 70 06 90 9a 4e 62 |.....2....p...Nb|
00000080 00 21 3a 25 06 10 9d 0c 67 00 31 b2 e4 00 10 a5 |.!:%....g.1.....|
00000090 49 0e 00 a1 05 36 04 b3 da 00 02 a5 4b 0c 20 30 |I....6......K. 0|
Firmware present on the device:
hisilicon # md.b 0x82000000
82000000: 90 c2 3d 00 4c 0d 67 00 00 00 00 00 00 00 00 00 ..=.L.g.........
82000010: 30 c0 fb 54 5e 8c 71 a9 00 04 10 92 73 48 00 51 0..T^.q.....sH.Q
82000020: 0d d0 0d 10 0e 50 0e e0 a1 f7 03 10 69 a0 01 84 .....P......i...
82000030: f8 ff fe fe fe fe fe fe fe fe fe fe fe fe fe fe ................
82000040: 36 67 02 10 0c 05 26 00 81 c0 c5 03 10 87 5f 3c 6g....&......._<
82000050: 00 41 10 c6 03 10 09 1f 40 00 e1 1a 12 04 10 8b .A......@.......
82000060: 22 42 fd bf fd fe fe fe fe fe fe fe fe fe fe fe "B..............
82000070: fe fe fe b6 bf 32 01 c7 c1 bb 70 06 90 9a 4e 62 .....2....p...Nb
82000080: 00 21 3a 25 06 10 9d 0c 67 00 31 b2 e4 00 10 a5 .!:%....g.1.....
82000090: 49 0e 00 a1 05 36 04 b3 da 00 02 a5 4b 0c 20 30 I....6......K. 0
820000a0: 49 c8 00 a2 c2 4a 0c 20 f4 20 0c 2c 35 68 00 c1 I....J. . .,5h..
820000b0: 83 71 00 10 81 cb 20 9d 1e 64 00 d1 79 25 06 f0 .q.... ..d..y%..
820000c0: 23 12 c2 d6 00 e2 61 8c 03 20 58 1c ce 00 c2 85 #.....a.. X.....
820000d0: 08 0d 20 32 59 80 00 22 e1 25 08 20 27 bc c4 00 .. 2Y..".%. '...
820000e0: c2 61 c1 0d 20 50 4a 0e 00 42 41 e2 0c 20 70 20 .a.. PJ..BA.. p
820000f0: ce 00 82 a0 e1 0c 20 4c 1d ce 00 02 fd 00 cf 8e ...... L........
In addition, we can observe the command’s output is indeed valid firmware from the previously mangled data:
hisilicon # sf probe 0;sf read 0x82000000 0x1a0000 0x5b0000;
8192 KiB hi_fmc at 0:0 is now current device
hisilicon # md.b 0x80008000
80008000: 00 00 00 00 57 00 09 00 9d 14 57 9d 14 57 00 00 ....W.....W..W..
80008010: 18 f0 9f e5 18 f0 9f e5 18 f0 9f e5 18 f0 9f e5 ................
80008020: 5c 15 40 80 48 e7 48 80 54 e7 48 80 74 e7 48 80 \.@.H.H.T.H.t.H.
80008030: 84 e7 48 80 94 e7 48 80 78 f4 3f 80 a4 e7 48 80 ..H...H.x.?...H.
hisilicon # hwdec 0x80008000 0x82000000
hisilicon # md.b 0x80008000
80008000: 18 f0 9f e5 18 f0 9f e5 18 f0 9f e5 18 f0 9f e5 ................
80008010: 18 f0 9f e5 18 f0 9f e5 18 f0 9f e5 18 f0 9f e5 ................
80008020: 5c 15 40 80 48 e7 48 80 54 e7 48 80 74 e7 48 80 \.@.H.H.T.H.t.H.
80008030: 84 e7 48 80 94 e7 48 80 78 f4 3f 80 a4 e7 48 80 ..H...H.x.?...H.
hisilicon #
Time to find out what hwdec does. At this point, per my earlier living off the land motto, I have dumped the decoded memory to obtain unobfuscated firmware, but I have included my analysis of the bootloader for completeness.
Dumping the bootloader
U-Boot helpfully supplies commands, as you may have noticed above, that allow both memory modification and memory reading. We can dump the device’s bootloader using these commands and convert the output to binary, ready for analysis.
Initially, I dumped the following memory:
# sf read 0x82000000 0x00000000 0x1a0000
# md.b 0x82000000 0x1a0000
Having converted the output to binary and loaded into IDA, something didn’t look quite right with my assuming the base address of 0x82000000:
This was clearly the wrong base address and moreover, there are no debugging symbols available. U-Boot is open source, so we can analyse the source code to understand how commands are used by the console, as well as extract symbols and find the correct base address.
Inspecting the source code
After some searching, we turn up https://github.com/xuhuashan/hi3518-osdrv/tree/master/uboot/u-boot-2010.06 which is a version of U-Boot that matches the banner shown in the serial console as well as our board type (hi3518), likely to be a good candidate for function signatures. Before we look at recovering symbols, however, let’s look at the command parsing logic of U-Boot via its source code:
//./hi3518-osdrv/uboot/u-boot-2010.06/common/main.c
/* Look up command in command table */
if ((cmdtp = find_cmd(argv[0])) == NULL) {
printf ("Unknown command '%s' - try 'help'\n", argv[0]);
rc = -1; /* give up after bad command */
continue;
}
// ./hi3518-osdrv/uboot/u-boot-2010.06/common/command.c:
/***************************************************************************
* find command table entry for a command
*/
cmd_tbl_t *find_cmd_tbl (const char *cmd, cmd_tbl_t *table, int table_len)
{
cmd_tbl_t *cmdtp;
cmd_tbl_t *cmdtp_temp = table; /*Init value */
const char *p;
int len;
int n_found = 0;
/*
* Some commands allow length modifiers (like "cp.b");
* compare command name only until first dot.
*/
len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);
for (cmdtp = table;
cmdtp != table + table_len;
cmdtp++) {
if (strncmp (cmd, cmdtp->name, len) == 0) {
if (len == strlen (cmdtp->name))
return cmdtp; /* full match */
cmdtp_temp = cmdtp; /* abbreviated command ? */
n_found++;
}
}
if (n_found == 1) { /* exactly one match */
return cmdtp_temp;
}
return NULL; /* not found or ambiguous command */
}
cmd_tbl_t *find_cmd (const char *cmd)
{
int len = &__u_boot_cmd_end - &__u_boot_cmd_start;
return find_cmd_tbl(cmd, &__u_boot_cmd_start, len);
}
[...]
typedef struct cmd_tbl_s cmd_tbl_t;
extern cmd_tbl_t __u_boot_cmd_start;
extern cmd_tbl_t __u_boot_cmd_end;
The above tells us that the function find_cmd() calls find_cmd_tbl() with a pointer to the start of multiple cmd_tbl_t structures. The structure is defined as follows and contains a pointer to the command’s implementation:
//./hi3518-osdrv/uboot/u-boot-2010.06/include/command.h
// Cmd structure definition
struct cmd_tbl_s {
char *name; /* Command Name */
int maxargs; /* maximum number of arguments */
int repeatable; /* autorepeat allowed? */
/* Implementation function */
int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);
char *usage; /* Usage message (short) */
#ifdef CONFIG_SYS_LONGHELP
char *help; /* Help message (long) */
#endif
#ifdef CONFIG_AUTO_COMPLETE
/* do auto completion on the arguments */
int (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]);
#endif
};
So, if we can locate the find_cmd() function, we can locate the table of available commands and their implementations.
What about that base address?
We can recover this too. Looking at uboot_data/hi3518-osdrv/uboot/u-boot-2010.06/arch/arm/cpu/hi3518/start.S which contains the initialisation code for our particular board, we see a several symbols surrounded by constants:
.globl _start
_start: b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
_undefined_instruction: .word undefined_instruction
_software_interrupt: .word software_interrupt
_prefetch_abort: .word prefetch_abort
_data_abort: .word data_abort
_not_used: .word not_used
_irq: .word irq
_fiq: .word fiq
_pad: .word 0x12345678 /* now 16*4=64 */
__blank_zone_start:
.fill 1024*4,1,0
__blank_zone_end:
.globl _blank_zone_start
_blank_zone_start:
.word __blank_zone_start
.globl _blank_zone_end
_blank_zone_end:
.word __blank_zone_end
.balignl 16,0xdeadbeef
/*
*************************************************************************
*
* Startup Code (reset vector)
*
* do important init only if we don't start from memory!
* setup Memory and board specific bits prior to relocation.
* relocate armboot to ram
* setup stack
*
*************************************************************************
*/
_TEXT_BASE:
.word TEXT_BASE
.globl _armboot_start
_armboot_start:
.word _start
/*
* These are defined in the board-specific linker script.
*/
.globl _bss_start
_bss_start:
.word __bss_start
.globl _bss_end
_bss_end:
.word _end
In this case the TEXT_BASE symbol contains our base address and is right next to the 0xDEADBEEF constant. Searching for this constant in our obtained device bootloader, we see our bootloader’s correct base address:
We re-extract the bootloader with the expected base address to ensure things align. This base address was obvious to my eye looking at the incorrect addresses in the disassembly, but it helps to be methodical.
Let’s recover symbols!
A note on buildchains
Symbol recovery isn’t always ideal when you don’t know the compiler used to build the data. There are multiple compiler toolchains available:
- arm-hisiv100-linux
- arm-hisiv100npt-linux
- arm-hisiv200-linux
- arm-hisiv300-linux
- arm-hisiv400-linux
Fortunately, Kevin Lawson has grouped all of these into a Docker image at https://hub.docker.com/r/kelvinlawson/hisilicon/. This allows us to build U-Boot with symbols for multiple compilers with minimal fuss.
Symbol recovery
Unlike my previous attempts with symbol recovery, this time I’ve opted to use Diaphora, to allow for fuzzy identification of symbols. I don’t particularly care about 100% accuracy in this case.
Diffing the exported database against our obtained bootloader, we have several partial matches:
As you can see, we’ve matched the find_cmd() function, which has a pointer to the start of our cmd_table_t structures. We can import the symbols and locate the pointer to our raw data:
And the raw data:
This is workable, but isn’t particularly helpful. We import our cmd_tbl_t structures from U-Boot’s command.h source code (with some minor fixups) and then apply the structure across the data.
Once the file is imported, we can use IDA’s local types view to import the structure:
After having defined the structures across the data, we can see that our structure is potentially missing a 4-byte pointer:
Re-checking the structure definition:
struct cmd_tbl_s {
char *name; /* Command Name */
int maxargs; /* maximum number of arguments */
int repeatable; /* autorepeat allowed? */
/* Implementation function */
int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);
char *usage; /* Usage message (short) */
#ifdef CONFIG_SYS_LONGHELP
char *help; /* Help message (long) */
#endif
#ifdef CONFIG_AUTO_COMPLETE
/* do auto completion on the arguments */
int (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]);
#endif
};
It does look like CONFIG_SYS_LONGHELP (or CONFIG_AUTO_COMPLETE) was defined when our bootloader was built. Let’s add an extra member to our structure:
And our disassembly now looks much better:
Analysing the ‘hwdec’ command
We’re now in a position to inspect the disassembly of the function (having declared it as a function). But first, let’s look at a standard, simple U-Boot command implementation for reference:
// ./hi3518-osdrv/uboot/u-boot-2010.06/common/cmd_strings.c
/*
* cmd_strings.c - just like `strings` command
*
* Copyright (c) 2008 Analog Devices Inc.
*
* Licensed under the GPL-2 or later.
*/
#include <config.h>
#include <common.h>
#include <command.h>
static char *start_addr, *last_addr;
int do_strings(cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
if (argc == 1) {
cmd_usage(cmdtp);
return 1;
}
if ((flag & CMD_FLAG_REPEAT) == 0) {
start_addr = (char *)simple_strtoul(argv[1], NULL, 16);
if (argc > 2)
last_addr = (char *)simple_strtoul(argv[2], NULL, 16);
else
last_addr = (char *)-1;
}
char *addr = start_addr;
do {
puts(addr);
puts("\n");
addr += strlen(addr) + 1;
} while (addr[0] && addr < last_addr);
last_addr = addr + (last_addr - start_addr);
start_addr = addr;
return 0;
}
U_BOOT_CMD(strings, 3, 1, do_strings,
"display strings",
"<addr> [byte count]\n"
" - display strings at <addr> for at least [byte count] or first double NUL"
);
Updating the types of our function and doing some cursory analysis, we end up with the following ‘hwdec’ function:
// hwdec - usage:simple version. hwdec dest_addr src_addr
int __fastcall hwdec(cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
unsigned int dest_addr; // r0 MAPDST
unsigned int *src_addr; // r0
int lock; // r7
char *obfuscated_data; // r5
unsigned int data_size; // r10
int result; // r0
unsigned int unknown; // [sp+Ch] [bp-24h]
if ( argc == 3 )
{
dest_addr = simple_strtol(argv[1], 0, 16u);
unknown = 0;
src_addr = simple_strtol(argv[2], 0, 16u);
unknown = src_addr[1];
lock = MEMORY[0x20120004]; // #define DDR_PHY_BASE 0x20120000
obfuscated_data = (src_addr + 4);
data_size = *src_addr;
MEMORY[0x20120004] &= 0xFFFFEFFF;
set_lock(); // sets some global DWORDs to true/false
unknown += 0x100000; // = 0x001000C2
do_decode(dest_addr, &unknown, obfuscated_data, data_size);
set_unlock(); // sets some global DWORDs to true/false
MEMORY[0x20120004] = lock;
result = 0;
}
else
{
cmd_usage(cmdtp, flag, argc, argv);
result = 1;
}
return result;
}
It’s not clear what’s actually happening here. The code seems to reference U-Boot’s DDR_PHY_BASE constant, where it sets various flags and state. The do_decode() function follows suit and sets our passed addresses in memory locations also:
int __fastcall do_decode(unsigned int dest_addr, unsigned int *unknown, char *obfuscated_data, unsigned int data_size)
{
int result; // r0
set_globals(1);
assign_addresses(obfuscated_data, dest_addr, *unknown, 0, 0, 0);
bitshift(1, 0, 0, 0, data_size); // does some shifting
result = hwdec_wait_delay(); // an actual loop!
if ( result )
result = -1;
return result;
}
// write access to const memory has been detected, the output may be wrong!
int __fastcall set_globals(int result)
{
if ( result != 1 )
{
dword_80CA16F8 = 1;
dword_80CA16FC = 0;
}
if ( result == 1 )
{
dword_80CA16F8 = 1;
dword_80CA16FC = 1;
}
dword_80CA1700 = 0;
return result;
}
int __fastcall assign_addresses(char *obfuscated_data, unsigned int dest_addr, char *unknown, int zero_1, int zero_2, int zero_3)
{
int result; // r0
MEMORY[0x206B2010] = obfuscated_data;
result = zero_3;
if ( zero_3 )
unknown = (4 * ((unknown + 4095) >> 12));
else
MEMORY[0x206B2020] = dest_addr;
if ( zero_3 )
MEMORY[0x206B2028] = dest_addr;
else
MEMORY[0x206B2024] = unknown;
if ( zero_3 )
MEMORY[0x206B202C] = unknown;
MEMORY[0x206B4000] = zero_1;
MEMORY[0x206B2030] = zero_2;
return result;
}
unsigned int __fastcall bitshift(int bOne, int bZero, int bZero_2, int bZero_3, unsigned int data_size)
{
unsigned int result; // r0
result = data_size & 0xFFFFFF | ((((MEMORY[0x206B2040] & 0x7FFFFFFF | (bOne << 31)) & 0xDFFFFFFF | ((bZero & 1) << 29)) & 0xEFFFFFFF | ((bZero_2 & 1) << 28)) & 0xFCFFFFFF | ((bZero_3 & 3) << 24)) & 0xFF000000;
MEMORY[0x206B2040] = result;
return result;
}
These routines do some initilisation of memory addresses and finally hwdec_wait_delay() is called, which starts a timer and loops, checking the status of various flags before returning successfully or timing out.
int hwdec_wait_delay()
{
signed int timer; // r4
int ret; // r5
timer = 100001;
while ( 1 )
{
--timer;
ret = wait_for_decode();
if ( !timer )
break;
udelay(1000u);
if ( ret != -1 )
return ret;
}
printf("hardware decompress overtime! func:%s, line:%d\n", "hw_dec_wait_finish", 272);// give decode error
return ret;
}
The string ‘hardware decompress overtime’ indeed alludes that our decompression routine is happening in hardware and the use of a PHY would align with this. The final call that checks that status of decompression in a loop:
int wait_for_decode()
{
int result; // r0
if ( MEMORY[0x206B0124] & 2 )
{
if ( MEMORY[0x206B2080] < 0 )
MEMORY[0x206B2090] = 1;
MEMORY[0x206B0130] |= 2u;
}
if ( !(MEMORY[0x206B0124] & 1) )
return -1;
result = MEMORY[0x206B2087] & 0x80;
if ( MEMORY[0x206B2087] & 0x80 )
{
if ( MEMORY[0x206B2084] )
{
printf("err = 0x%x, dec_data_len = 0x%x\n", MEMORY[0x206B2084], (2 * MEMORY[0x206B2084]) >> 9);
result = -2;
}
else
{
result = 0;
}
MEMORY[0x206B2094] = 1;
}
MEMORY[0x206B0130] |= 1u;
return result;
}
Again, this call doesn’t appear to be doing anything typical of a decompression routine, it is just checking statuses and toggling flags on/off where necessary. Given I’m able to dump the firmware directly from the device’s memory in its decompressed state, I’m not willing to invest more time into investigation of this decompression routine, but I’d love to hear feedback or comments or next steps to move forward.
In the next part of this series, for completeness, we’ll perform some cursory analysis of the decompressed firmware.