hello world; echo from Divya!

I read and write but never execute. A cs major in cool pajamas. Alias rachejazz OR h3ck

DM me on Twitter Checkout my GitHub View My Resume Back to Portfolio
24 March 2024 Back to Blogs list

I got my hands on a Raspberry Pi Pico firmware.

by Divya

So I reversed it on Ghidra and emulated it on Unicorn.

How I came up with the idea

This was part of my university challenge. I feared hardware for a long time. This time however, I was guided to use Unicorn, a CPU emulator to do this challenge. I went through the documentation but couldn’t find much on to go on for emulating pico architecture (ARM arch).

Well problem statement was, I given a firmware dump from a pico and a debug flags compiled ELF binary of the same firmware. The pico accepts a random pin, and if correctly guessed, it will print the flag.

Bruteforce by loading it on my pico?

Nah, pin was somewhere in [0,9999] and firmware starts messing up with the terminal if given the wrong pin. We will bruteforce it, however not in the pico way, but in the Unicorn way!

Identify pico architecture, load the firmware dump. CAUTION: Careful with Memory!!

I discovered that pico uses ARM architecture. Now ARM processors use 2 modes. Thum mode and ARM mode. In the challenge it was hinted if we had issues emulating, we need to make sure to include the Thumb Bit. The thumb mode makes execution faster, decreasing code density. So now what remains is loading the firmware dump and see if it works in this Thumb Mode.

But wait, how much memory does it need? Which address should it start emulating from? Ghidra time

Remember the debug flags compiled elf version of the firmware? Yeah we need it now. Loading it on ghidra I saw the starting address of main.

                     **************************************************************
                     *                          FUNCTION                          *
                     **************************************************************
                     undefined main()
                       assume LRset = 0x0
                       assume TMode = 0x1
     undefined         r0:1           <RETURN>
     undefined4        Stack[-0x21c   local_21c                               XREF[5]:     10000a92(R),
                                                                                           10000aac(R),
                                                                                           10000ab2(R),
                                                                                           10000abc(R),
                                                                                           10000ac6(R)
                     main+1                                          XREF[2,1]:   Entry Point(*),
                     main                                                         _reset_handler:10000220(c),
                                                                                  _reset_handler:1000021e(*)
...Snipped...
                     LAB_10000a8a                                    XREF[2]:     10000ac0(j), 10000ac8(j)
10000a8a 03 2b           cmp        r3,#0x3
10000a8c 02 d1           bne        LAB_10000a94
10000a8e ff f7 e7 fc     bl         assignment_2B_rehost                             undefined assignment_2B_rehost()
10000a92 01 9b           ldr        r3,[sp,#local_21c]
                     LAB_10000a94                                    XREF[1]:     10000a8c(j)
10000a94 e2 5c           ldrb       r2,[r4,r3]
10000a96 01 32           adds       r2,#0x1
10000a98 d2 b2           uxtb       r2,r2
10000a9a e2 54           strb       r2,[r4,r3]

The function or the choice we are interested in, is the rehosting function at 0x10000a8e Now once we reach it’s definition, we see the starting address of rehosting is 0x10000460. Now once we have the starting address, we need the ending address of this function. Scroll down where it ends:

                     **************************************************************
                     *                          FUNCTION                          *
                     **************************************************************
                     undefined assignment_2B_rehost()
                       assume LRset = 0x0
                       assume TMode = 0x1
     undefined         r0:1           <RETURN>
     undefined2        Stack[-0x126   local_126                               XREF[1]:     1000048c(W)  
     undefined2        Stack[-0x128   local_128                               XREF[1]:     10000488(*)  
                     assignment_2B_rehost                            XREF[2]:     Entry Point(*), main:10000a8e(c)  
10000460 70 b5           push       {r4,r5,r6,lr}
10000462 18 48           ldr        r0=>s_Enter_the_pin:_1000d080,[DAT_100004c4]     = "Enter the pin: "
                                                                                     = 1000D080h
10000464 c6 b0           sub        sp,#0x118
10000466 04 f0 6d ff     bl         __wrap_printf                                    undefined __wrap_printf()
1000046a 69 46           mov        r1,sp
1000046c 16 48           ldr        r0=>s_%hd_1000d090,[DAT_100004c8]                = "%hd"
                                                                                     = 1000D090h
1000046e 07 f0 1f fe     bl         scanf                                            int scanf(char * __format, ...)
10000472 05 ad           add        r5,sp,#0x14
10000474 64 20           movs       r0,#0x64
10000476 01 f0 c7 f9     bl         sleep_ms                                         undefined sleep_ms()
1000047a 41 22           movs       r2,#0x41
1000047c 28 00           movs       r0,r5
1000047e 13 49           ldr        r1=>DAT_1000d094,[DAT_100004cc]                  = 42h
                                                                                     = 1000D094h
10000480 04 f0 a8 fc     bl         __wrap_memcpy                                    undefined __wrap_memcpy()
10000484 6b 46           mov        r3,sp
10000486 9e 1c           adds       r6,r3,#0x2
10000488 1b 88           ldrh       r3=>local_128,[r3,#0x0]
1000048a 01 ac           add        r4,sp,#0x4
1000048c 33 80           strh       r3,[r6,#0x0]=>local_126
                     LAB_1000048e                                    XREF[1]:     1000049c(j)  
1000048e 20 00           movs       r0,r4
10000490 02 22           movs       r2,#0x2
10000492 31 00           movs       r1,r6
10000494 02 34           adds       r4,#0x2
10000496 04 f0 9d fc     bl         __wrap_memcpy                                    undefined __wrap_memcpy()
1000049a a5 42           cmp        r5,r4
1000049c f7 d1           bne        LAB_1000048e
1000049e 01 a9           add        r1,sp,#0x4
100004a0 16 a8           add        r0,sp,#0x58
100004a2 00 f0 6d fa     bl         AES_init_ctx                                     undefined AES_init_ctx()
100004a6 2c 00           movs       r4,r5
100004a8 15 ae           add        r6,sp,#0x54
                     LAB_100004aa                                    XREF[1]:     100004b6(j)  
100004aa 21 00           movs       r1,r4
100004ac 16 a8           add        r0,sp,#0x58
100004ae 10 34           adds       r4,#0x10
100004b0 00 f0 6a fa     bl         AES_ECB_decrypt                                  undefined AES_ECB_decrypt()
100004b4 b4 42           cmp        r4,r6
100004b6 f8 d1           bne        LAB_100004aa
100004b8 28 00           movs       r0,r5
100004ba 04 f0 ef fd     bl         __wrap_puts                                      undefined __wrap_puts()
100004be 46 b0           add        sp,#0x118
100004c0 70 bd           pop        {r4,r5,r6,pc}
100004c2 c0              ??         C0h
100004c3 46              ??         46h    F

So it is the region where the pop operation takes place at 0x100004c0. This means it’s now clearing its stack and ready to return to main. We can quickly spin up our emulation code based on the above information.

from unicorn import *
from unicorn.arm_const import *
PICO_FLASH_START = 0x10000000
PICO_FLASH_SIZE = 2 * 1024 * 1024  # 2MB

# Function addresses
FUNC_START_ADDR = 0x10000460
FUNC_END_ADDR = 0x100004c0

with open("fw.bin", "rb") as f:
	firmware_data = f.read()

# Initialize emulator
mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
# Map address to load firmware dump
mu.mem_map(PICO_FLASH_START, PICO_FLASH_SIZE)

# Write memory data
mu.mem_write(PICO_FLASH_START, firmware_data)
mu.emu_start(FUNC_START_ADDR | 1, FUNC_END_ADDR) # The "| 1" denotes switching on the thumb bit.

Now, how do we know this is working? Unicorn offers hook functionality that gets called when we start emulating. In this hook function, we can modify, print or program stuff that needs to be done when control reaches a particular address. For now, we will add ways to check if pc (program counter) is traversing the addresses correctly and whether we can read assembly operation happening at each. For this we will use capstone to read opcodes and print to us.

from capstone import *
def hook_output(mu, address, size, user_data):
md = Cs(CS_ARCH_ARM, CS_MODE_THUMB)
md.detail = True

data = bytes(mu.mem_read(address, size))
for each in md.disasm(data, address):
	inst = ("\t%s\t%s" %(each.mnemonic, each.op_str))
	print(f"Tracing>>>0x{address:X}: {inst}")

Now we add this hook just before emulation with the start and end address of rehosting function.

mu.hook_add(UC_HOOK_CODE, hook_output, begin=FUNC_START_ADDR, end=FUNC_END_ADDR)
mu.emu_start(FUNC_START_ADDR | 1, FUNC_END_ADDR)

Finally let’s run it!

Tracing>>>0x10000460: 	push	{r4, r5, r6, lr}
Tracing>>>0x10000462: 	ldr	r0, [pc, #0x60]
Tracing>>>0x10000464: 	sub	sp, #0x118
Tracing>>>0x10000466: 	bl	#0x10005344
Skipping function at address: 0x10000466
Tracing>>>0x1000046A: 	mov	r1, sp
Tracing>>>0x1000046C: 	ldr	r0, [pc, #0x58]
Tracing>>>0x1000046E: 	bl	#0x100080b0
Traceback (most recent call last):
  File "path/to/emulate.py", line 140, in <module>
    main()
  File "path/to/emulate.py", line 137, in main
    mu.emu_start(FUNC_START_ADDR | 1, FUNC_END_ADDR)
  File "venvpath/toenv/lib/python3.11/site-packages/unicorn/unicorn.py", line 547, in emu_start
    raise UcError(status)
unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)

Okay….so there is some operation that makes the emulation crash, let’s see what’s in the next address that causing this. Scroll up to see what is the operation we see that’s at that address on ghidra. Its a __wrap_printf function. Since the program is run on a emulator and we haven’t defined what it should do at printf, it crashes at that address saying UC_ERR_READ_UNMAPPED. At this point, we have 2 ways to proceed:

Since it’s a harmless printf statement which just prints “Enter your pin”, we can move forward with the second option

def hook_skip(mu, address, size, user_data):
	instructions_skip_list = [0x10000466]
	if address in instructions_skip_list:
		print("Skipping function at address:", hex(address))
		mu.reg_write(PC, address+size | 1)
	return True

And again, we add it just above emulation starts

mu.hook_add(UC_HOOK_CODE, hook_skip, begin=FUNC_START_ADDR, end=FUNC_END_ADDR)
mu.hook_add(UC_HOOK_CODE, hook_output, begin=FUNC_START_ADDR, end=FUNC_END_ADDR)
mu.emu_start(FUNC_START_ADDR | 1, FUNC_END_ADDR)

We run it again!

Tracing>>>0x10000460: 	push	{r4, r5, r6, lr}
Tracing>>>0x10000462: 	ldr	r0, [pc, #0x60]
Tracing>>>0x10000464: 	sub	sp, #0x118
Tracing>>>0x10000466: 	bl	#0x10005344
Skipping function at address: 0x10000466
Tracing>>>0x1000046A: 	mov	r1, sp
Tracing>>>0x1000046C: 	ldr	r0, [pc, #0x58]
Tracing>>>0x1000046E: 	bl	#0x100080b0
Traceback (most recent call last):
  File "path/to/emulate.py", line 140, in <module>
    main()
  File "path/to/emulate.py", line 137, in main
    mu.emu_start(FUNC_START_ADDR | 1, FUNC_END_ADDR)
  File "venvpath/toenv/lib/python3.11/site-packages/unicorn/unicorn.py", line 547, in emu_start
    raise UcError(status)
unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)

Okayy, so we know that it’s working. We gotta skip few more functions the same way until we reach the place where we need to bruteforce pins. Looking back at ghidra address, we have scanf at 0x1000046E, sleep_ms at 0x10000476 and __wrap_puts at 0x100004BA
We put all these addresses into skip_instructions array.

instructions_skip_list = [0x10000466, 0x10000476, 0x100004BA]

But hold on, if we skip scanf, how are we going to take pin as input? Now we shall create another hook which will handle data sent to scanf. Let’s re read the assembly just above scanf.

1000046a 69 46           mov        r1,sp
1000046c 16 48           ldr        r0=>s_%hd_1000d090,[DAT_100004c8]                = "%hd"
                                                                                     = 1000D090h
1000046e 07 f0 1f fe     bl         scanf                                            int scanf(char * __format, ...)

Since scanf takes 2 parameters, we see that r0 stores the modifier “%hd” and r1 stores whatever value is passed from stdin. So all we need to do is, when control reaches scanf, we store the value of pin to r1 and move ahead! Oh and, since we’re dealing in assembly, we ALWAYS send data in appropriate bytes.

PIN = 0
def hook_input(mu, address, size, user_data):
	if address == 0x1000046e:
		print("Input function called at address:", hex(address))
		r1=mu.reg_read(R1)
		mu.mem_write(r1, PIN.to_bytes(2, 'little'))
		mu.reg_write(PC, address+size | 1)

As always, we add this new hook just above emulate start and run it again.

┬─[divya@racharch:~/a/U/D/a/rehosting]─[08:33:37 PM]─[V:env]─[B:55%, 01:29:55 remaining]
╰─>λ python emulate.py
Tracing>>>0x10000460: 	push	{r4, r5, r6, lr}
Tracing>>>0x10000462: 	ldr	r0, [pc, #0x60]
Tracing>>>0x10000464: 	sub	sp, #0x118
Tracing>>>0x10000466: 	bl	#0x10005344
Skipping function at address: 0x10000466
Tracing>>>0x1000046A: 	mov	r1, sp
Tracing>>>0x1000046C: 	ldr	r0, [pc, #0x58]
Tracing>>>0x1000046E: 	bl	#0x100080b0
Input function called at address: 0x1000046e
Tracing>>>0x10000472: 	add	r5, sp, #0x14
Tracing>>>0x10000474: 	movs	r0, #0x64
Tracing>>>0x10000476: 	bl	#0x10001808
Skipping function at address: 0x10000476
Tracing>>>0x1000047A: 	movs	r2, #0x41
...Snipping...
Tracing>>>0x100004B4: 	cmp	r4, r6
Tracing>>>0x100004B6: 	bne	#0x100004aa
Tracing>>>0x100004B8: 	movs	r0, r5
Tracing>>>0x100004BA: 	bl	#0x1000509c
Skipping function at address: 0x100004ba
Tracing>>>0x100004BE: 	add	sp, #0x118

Time to start bruteforce and print the flag!

Ok now we can see the emulation works. All now we need to do is loop emulation with every possible pin value and then read the value that will be passed to __wrap_puts So the solution now is:

from unicorn import *
from unicorn.arm_const import *
from capstone import *
import struct

# Memory addresses
PICO_FLASH_START = 0x10000000
PICO_FLASH_SIZE = 2 * 1024 * 1024  # 2MB
R0 = UC_ARM_REG_R0
R1 = UC_ARM_REG_R1
PC = UC_ARM_REG_PC
SP = UC_ARM_REG_SP
PIN = 0

# Function addresses
FUNC_START_ADDR = 0x10000460
FUNC_END_ADDR = 0x100004c0

# Hook for functions to skip
def hook_skip(mu, address, size, user_data):
	instructions_skip_list = [0x10000466, 0x10000476, 0x100004BA]
	if address in instructions_skip_list:
		mu.reg_write(PC, address+size | 1)
	return True

# Hook for output functions
def hook_output(mu, address, size, user_data):
	if address == 0x100004BA:
		flag = mu.reg_read(R0)
		print(f"flag for pin {PIN}: {flag}")

# Hook for input function
def hook_input(mu, address, size, user_data):
	if address == 0x1000046e:
		print("Input function called at address:", hex(address))
		r1=mu.reg_read(R1)
		mu.mem_write(r1, PIN.to_bytes(2, 'little'))
		mu.reg_write(PC, address+size | 1)

# Main function
def main():
	global PIN
	with open("fw.bin", "rb") as f:
		firmware_data = f.read()

	# Initialize emulator
	mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
	mu.mem_map(PICO_FLASH_START, PICO_FLASH_SIZE)
	mu.reg_write(SP, PICO_RAM_START + PICO_RAM_SIZE - 0x1000)

	# Write memory data
	mu.mem_write(PICO_FLASH_START, firmware_data)

	# Add hooks
	mu.hook_add(UC_HOOK_CODE, hook_output, begin=FUNC_START_ADDR, end=FUNC_END_ADDR)
	mu.hook_add(UC_HOOK_CODE, hook_skip, begin=FUNC_START_ADDR, end=FUNC_END_ADDR)
	mu.hook_add(UC_HOOK_CODE, hook_input, begin=FUNC_START_ADDR, end=FUNC_END_ADDR)

	# Emulate rehosted function
	for pin in range(10000):
		PIN = pin
		mu.emu_start(FUNC_START_ADDR | 1, FUNC_END_ADDR)

if __name__ == "__main__":
	main()

For working purposes, lets try first with only 1 pin value.

┬─[divya@racharch:~/a/U/D/a/rehosting]─[08:33:37 PM]─[V:env]─[B:55%, 01:29:55 remaining]
╰─>λ python emulate.py
Skipping function at address: 0x10000466
Input function called at address: 0x1000046e
Skipping function at address: 0x10000476
flag for pin 0: 538963692
Skipping function at address: 0x100004ba

Huh, well how could I really think a long string such as the flag will be directly stored at a register? That would mean R0 stores the address where the value of the flag is stored. Now we quickly edit the output function.

# Hook for output functions
def hook_output(mu, address, size, user_data):
	if address == 0x100004BA:
		flag_addr = mu.reg_read(R0)
		flag = mu.mem_read(flag_addr, 20) #lets say we want to read 20 next addresses
		print(f"flag: {flag}")

Now when we run it with pin 0:

╰─>λ python emulate.py
Skipping function at address: 0x10000466
Input function called at address: 0x1000046e
Skipping function at address: 0x10000476
flag for pin 0: bytearray(b'uL\xa8\xa1\x0b\x1a\xc6\xa21G\x0f\xaa\x0e\xeb\xe4\xb8\xba\x18\xd9K')
Skipping function at address: 0x100004ba

OK! so we are infact able to see a string with the corresponding pin. Now we loop it around 0 to 9999 and redirect the output to a file. Our flag should be starting with sshs, so we can grep that out later!

Found the pin! print the entire flag!

Now since we know which pin is the correct pin for printing the flag, All we need to do is keep on printing the flag until we receive a closing braces. And voila:

╰─>λ python emulate.py
flag for 4919: bytearray(b'sshs{8ea8549aff0b37a8d1f537c65aebfe55}')

This was a fun challenge which helped to understand code emulation and assembly!