This was an Arbitrary Address Write (AAW) vulnerability discovered by the NCC group 1 in the Unisoc BootROM where an attacker with physical access to a vulnerable device can exploit the bug to overwrite a function pointer in the address space of the BootROM or a return address stored on the stack to execute their own code with BootROM priviledges.

This blog post will be an analysis of the vulnerability to identify its root cause and to write a Proof-Of-Concept code to bypass signature verification in the Unisoc Download Mode. The introduction to the Unisoc Download Mode protocol was covered in the first post — here.

Root Cause Analysis (RCA)

When a new firmware is written to a device during production, the technician/programmer will boot the device to BROM mode and use an OEM specific tool —built on top of the open source SPD Flash Tool 2— to write firmware. These tools perform an auth check before firmware writes to limit them to their specific OEM devices.

When the device is in BROM, the tool scans the available USB interfaces to find the device via the Unisoc Vendor and Product Ids. It will then extract the provided firmware files to first read a special XML file that defines the download addresses of FDL-1 and FDL-2. The following is a sample snippet XML file for the SC9863A chipset model.

<file>
	<ID>FDL</ID>
	<IDAlias></IDAlias>
	<Type>FDL</Type>
	<Block>
		<Base>0x005000</Base>
		<Size>0x0</Size>
	</Block>
	<Flag>1</Flag>
	<CheckFlag>1</CheckFlag>
	<Description>First nand flash download image file </Description>
	...
</file>

The tool then sends a handshake to initiate communication with the BootROM afterwhich FDL-1 is downloaded to its defined address and executed. This is followed by an FDL-2 download to its defined address where its executed to begin the flashing process — the flashing tool auth is performed at this stage, but if it fails, the flashing process is aborted.

The Arbitrary Address Write (AAW) vulnerability is introduced at this stage because the BootROM does not perform checks on the destination download address where the first preloader (FDL-1) is written to. The following is the decompiled BootROM code that I dumped from a vulnerable device running the SC9863A chipset —a list of vulnerable bootROM binaries can be found in this github repo 4 for analysis and Reverse Eng.

cmd_start

The cmd_start() function is called when the BootROM first receives the BSL_START_DATA|0x2 command. The above snippet shows that this function does not perform any validation checks on the provided download address but instead, this is saved in a global variable _g_cur_write_addr introducing a write-what-where primitive.

Technical Details — PoC

The BootROM is the very first code to be executed on the processor; it does not rely on the kernel to setup its memory environment, therefore is has to self-boostrap. This means that it knows the location of its stack —which is hardcoded in the BootROM code.

The following snippet shows the BootROM initializing its stack-pointer to the address 0x5000 from a hardcoded value stored in a global variable.

stack_pointer

This is the same address to where FDL-1 is downloaded 0x005000, but remember that the stack grows in the opposite direction in memory, therefore this data will not overlap. The location of the stack is already known, since the aim of the PoC is to overwrite a function pointer in the stack, we need to look at the function call stack after the call to cmd_start()

Function Call Stack

The function cmd_start() is responsible for handling the BSL_CMD_START_DATA|0x1 that is used to save the download address to _g_cur_write_ptr as described above. This is then followed by a call to cmd_recv_data() that is used to process the BSL_CMD_MIDST_DATA|0x2 command to copy the FDL payload to its destination address stored in _g_cur_write_ptr. The following is the source code snippet where the payload is copied via memcpy() — this is the function call that allows for a Write-What-Where.

cmd_recv_data

The following table shows a snippet of the function call stack 3 with the address of the stack frame and return addresses on the stack. This eased the hustle of emulating & debugging the bootROM allowing us to focus on developing the final exploit.

Function SP addr-1 Reg addr-2
memcpy 0x4F40 0x4F40 X29 0X4F48
0x4F48 X30 0x1054a0
cmd_recv_data 0x4F50 0x4F50 X29 0x4F70
0X4F58 X30 0X105740

From the above table, based on the AARCH64 calling convention, when memcpy() is called from cmd_recv_data() the stack pointer will be at the address 0x4f40 and the previous function’s stack frame will be at 0x4f48 —this value will be returned to x29 to adjust the stack to the callers function’s stack frame.

The return address 0x1054a0 is pushed on the stack to 0x4f58 from the x30 register because memcpy() calls do_memcpy(). When the function exits, the value at that stack address will be loaded back to x30 and execution will jump to that address.

The following is the disassembly for cmd_recv_data() with the return address of memcpy():

cmd_recv_data_disass

Controlling Execution Flow

Since we now know the structure of the stack, we can leverage the AAW to overwrite the return address at 0x4f58 to control execution flow. The aim of this attack is to bypass FDL verification to execute custom FDL code— we can therefore direct execution to the code reponsible for executing FDL code after signature verification.

From analysing the BootROM code, the function that is responsible for executing FDL-1 was found at the address 0x10016c renamed to data_exec(). This function takes the download address of FDL-1 as a parameter and jumps to the payload at offset 0x200 for code execution — this is because of how the Unisoc Secure Boot is implemented where the initial values store metadata for image verification.

data_exec

This function is called at the address 0x108e3c after image verification inside a function renamed to verify_and_exec with g_addr+0x200 passed as an argument.

data_exec_call

Rop Chain

A rop chain can now be created to overwrite the return addresses on the stack for a successful exploit starting from the address 0x4f48 — where the return address of memcpy() is stored. The stack after the overwrite should be as follows:

0X4F48: 0x00000000001054A0 memcpy
0X4F50: 0X0000000000004F70 cmd_recv_data() stack_frame
0X4F58: 0X0000000000108E3C cmd_recv_data() ret
0X4F60: 0x0000000000005000

The above payload ovewrites the return address of cmd_recv_data() stored at 0x4f58 with the address 0x108e3c where the call to data_exec() is made. Since data_exec() requires a parameter g_addr, the download address of FDL-1 0x5000 is also written to the stack at the address 0x4f60. Before cmd_recv_data() exits, the instruction ldp x19, x20,[sp, #local_10] will move the value at 0x4f60 to the x19 register which is later used to calculate the payload offset and passed as an argument to data_exec()add g_addr, w19, 0x200.

The above payload can be written to a bin file in its little-endian form —since this will be downloaded to the device via BSL_CMD_MIDST_DATA|0x1. The following is a simple Python script that can be used for this conversion:

#! /usr/bin/python3 

import struct

def create_payload():
    payload = struct.pack("<Q", 0x1054a0)
    payload += struct.pack("<Q", 0x4f70)
    payload += struct.pack("<Q", 0x108e3c)
    payload += struct.pack("<Q", 0x5000)    


    with open("payload.bin", "wb") as fp:
	fp.write(payload)

def main():
    create_payload()

if __name__ == "__main__":
    main()

The following is a high-level workflow of the exploit in action:

  1. The device is booted to BROM mode and FDL-1 is downloaded to its address 0x5000.
  2. The payload is then written to the device via the AAW to overwrite the stack address starting from 0x4f48.
  3. After step 2, when cmd_recv_data() returns, code execution will jump to data_exec() which will execute FDL-1.
  4. Context is now switched to FDL-1 and verification bypassed, you can now send a handshake & change the baudrate to begin communication.

The device can also fallback to BROM Mode when it powers on — this is most common because of corrupt or missing files. At this point the function call stack and the structure of the stack changes. The rop chain payload will be the same but the ovewritten return will be at a different offset on the stack because more functions are called.

Conclusion

FDL preloaders are not readily available because they are provided by the OEM as part of the device’s firmware. The bug however can be used to bypass signature verification on leaked FDLs — provided they are compatible with the device. On the other hand, you can write/compile your own custom loader to introduce custom commands to the FDL-1 preloader and/or patch it to remove FDL-2 verification. This is what commercial “flashing tools” do — to provide additional functionalities (apart from flashing & formatting partitions) like read/write the RPMB partitions or unlocking bootloaders.

The chipram source code 6 for UMS512 that can be compiled to produce an unsigned FDL-1 preloader — you can for example modify this code to introduce the BSL_CMD_READ_FLASH to dump the current device’s BootROM. This is the method I used to dump the BootROM of my DUT but using a different signature verification misconfig for verification bypass.

The spreadtrum_flash 7 built on top of the SPD Flash Tool is an awesome tool weaponized to send the payload and/or trigger the vulnerability.