Skip to content

Bored Pentester

Bored Pentester

A collection of spare time spent reverse engineering, hardware hacking and conducting vulnerability research.

1st June 2024 / Uncategorised

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:

An image showing our bootloader, loaded at with a 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:

An image showing our identified 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.

An image showing Diaphora ready to export symbols for us.

Diffing the exported database against our obtained bootloader, we have several partial matches:

An image showing partially matched symbols via Diaphora

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:

An image showing our located find_cmd() function

And the raw data:

An image showing cmd_tbl_t structures as 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.

An image showing IDA’s load C source file function

Once the file is imported, we can use IDA’s local types view to import the structure:

An image showing local types gained from importing UBoot’s command.h file, containing structure definitions

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.

Post navigation

Previous Post:

Smart Doorbell Security (Part 3) (Wireless credential theft)

Next Post:

Smart Doorbell Security (Part 5) (LiteOS analysis)

Leave a Reply Cancel reply

You must be logged in to post a comment.

©2025 Bored Pentester - Powered by Simpleasy