Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instrumenting ARM code making use of hardfault handlers and especially CFSR and BFAR registers #1971

Open
r3-ck0 opened this issue Jul 8, 2024 · 7 comments

Comments

@r3-ck0
Copy link

r3-ck0 commented Jul 8, 2024

Hi,

During a CTF I was debugging an ARM binary that was using hardfault handlers and I was having a really difficult time, which was mostly skill issues, but I came to a spot that I think might not be my own fault anymore.

I'm providing a script that might or might not give a seasoned unicorn veteran aneurysms, sorry in advance, I hope you don't live in the US and can afford treatment. The binary in question is here: https://github.com/DownUnderCTF/Challenges_2024_Public/tree/main/hardware/crash-landing

I can run the code up to the point where the hardfault handler is triggered. The binary is then supposed to copy the value from the flag into the R3 register, based on the value in BFAR. However, when I run the below code, CFSR is empty, although it should, according to the code, contain 0x8200.

Is this a bug? Is this something on QEMU? Or is this a skill issue on my side?

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

def pretty_print_registers(uc):
    # List of ARM registers for pretty printing
    registers = [
        UC_ARM_REG_R0, UC_ARM_REG_R1, UC_ARM_REG_R2, UC_ARM_REG_R3,
        UC_ARM_REG_R4, UC_ARM_REG_R5, UC_ARM_REG_R6, UC_ARM_REG_R7,
        UC_ARM_REG_R8, UC_ARM_REG_R9, UC_ARM_REG_R10, UC_ARM_REG_R11,
        UC_ARM_REG_R12, UC_ARM_REG_SP, UC_ARM_REG_LR, UC_ARM_REG_PC,
        UC_ARM_REG_CPSR
    ]
    
    # Register names for display purposes
    register_names = [
        "R0", "R1", "R2", "R3", "R4", "R5", "R6", "R7",
        "R8", "R9", "R10", "R11", "R12", "SP", "LR", "PC",
        "CPSR"
    ]
    
    print("Current Register States:")
    for reg, name in zip(registers, register_names):
        # Read the register value
        value = uc.reg_read(reg)
        # Print the register name and its value in hexadecimal format
        print(f"{name}: 0x{value:08X}")

# code to be emulated
ARM_CODE   = None

with open("./crash_landing.bin", "rb") as f:
    ARM_CODE = f.read()


# memory address where emulation starts
ADDRESS       = 0x8000000
STACK_ADDRESS =  0x200000  # Start of stack memory
STACK_SIZE = 0x10000  # Size of stack, 64KB

mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
md = Cs(CS_ARCH_ARM, CS_MODE_THUMB)

mu.mem_map(ADDRESS, 2*1024*1024)
mu.mem_map(0x40011000, 1024)
# mu.mem_map(0xDEAD0000, 1024)
# mu.mem_map(0xBEEF0000, 1024)
mu.mem_map(0x20000000, 2*1024*1024)
mu.mem_map(0xe000e000, 4096)
mu.mem_map(STACK_ADDRESS, STACK_SIZE)
mu.reg_write(UC_ARM_REG_SP, STACK_ADDRESS + STACK_SIZE - 0x100)  # Adjust stack pointer

CFSR_ADDRESS = 0xE000ED28
BFAR_ADDRESS = 0xE000ED38

mu.mem_write(ADDRESS, ARM_CODE)

HARDFAULT_HANDLER_ADDRESS = 0x8000218

resume_addr = None

def hook_mem_invalid(uc, access, address, size, value, user_data):
    global resume_addr
    print(access)
    if access == UC_MEM_READ_UNMAPPED:
        print(f"HardFault: Invalid memory read at 0x{address:X}, size = {size}")
    elif access == UC_MEM_WRITE_UNMAPPED:
        print(f"HardFault: Invalid memory write at 0x{address:X}, size = {size}, value = 0x{value:X}")
    
    # Simulate the HardFault exception by setting the PC to the HardFault handler address
    print(f"Redirecting to HardFault handler at 0x{HARDFAULT_HANDLER_ADDRESS:X}")

    # Redirect to the HardFault handler
    # uc.reg_write(UC_ARM_REG_PC, HARDFAULT_HANDLER_ADDRESS)
    resume_addr = HARDFAULT_HANDLER_ADDRESS
    page_start = address & ~0xFFF
    print(f"{page_start:x}")

    cfsr = mu.mem_read(CFSR_ADDRESS, 4)
    bfar = mu.mem_read(BFAR_ADDRESS, 4)
    
    print("CFSR: 0x{}".format(cfsr.hex()))
    print("BFAR: 0x{}".format(bfar.hex()))

    exit()
    try:
        uc.mem_map(page_start, 4096)
    except:
        print("Nope.")

    return False  # Return True to continue the emulation

# Callback function for CPU exceptions
def hook_intr(uc, intno, user_data):
    pc = uc.reg_read(UC_ARM_REG_PC)
    print(f"CPU exception {intno} at PC = 0x{pc:X}")
    return True  # Continue emulation

# Callback function for tracing instructions
def hook_code(uc, address, size, user_data):
    # print("Executing at 0x{:X}: ".format(address), end='')
    # Read the instruction bytes
    instruction_bytes = uc.mem_read(address, size)
    #  print(' '.join(format(x, '02x') for x in instruction_bytes))

    if address == 0x800041e or address == 0x8000426:
        # print(f"0x{uc.reg_read(UC_ARM_REG_R0):x}")
        uc.mem_write(uc.reg_read(UC_ARM_REG_R0), b"\xff\xff\xff\xff")
        #  print(f"Read: {uc.mem_read(uc.reg_read(UC_ARM_REG_R0), 4)}")
    
    if address == 0x8000346:
        uc.emu_stop()

    if address == 0x800034a:
        print(f"Data: {uc.mem_read(0x20000000, 256)}")
        pretty_print_registers(uc)

    # Disassemble the instruction
    # for i in md.disasm(instruction_bytes, address):
    #     print(f"{i.mnemonic} {i.op_str} (bytes: {' '.join(format(x, '02x') for x in instruction_bytes)})")

mu.hook_add(UC_HOOK_CODE, hook_code)
mu.hook_add(UC_HOOK_MEM_INVALID, hook_mem_invalid)
mu.hook_add(UC_HOOK_INTR, hook_intr)
# mu.hook_add(UC_HOOK_CODE, lambda *a: mu.emu_stop())

PC = ADDRESS + 0x434
while True:
    print(f"0x{PC:x}")
    mu.emu_start(PC ^ 1, 0xFFFFFFFF, count=1)
    PC = mu.reg_read(UC_ARM_REG_PC)

    if resume_addr is not None:
        PC = resume_addr
        resum_addr = None

    if PC == 0x80001FA:
        break
@wtdcode
Copy link
Member

wtdcode commented Jul 9, 2024

I suspect it was fixed in dev branch, could you have a try?

@r3-ck0
Copy link
Author

r3-ck0 commented Jul 9, 2024

Thanks for coming back to me this quickly, will look at it when I get home.

@gerph
Copy link
Contributor

gerph commented Jul 9, 2024 via email

@r3-ck0
Copy link
Author

r3-ck0 commented Jul 10, 2024

Thank you for your input. I tried to changing to dev-branch by building it and then moving .so.2 and .a files to my python site-packages directory - I hope that's the correct way. It did not change the problem.

I also updated the code as such:

mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
+ mu.ctl_set_cpu_model(UC_CPU_ARM_CORTEX_M3)
md = Cs(CS_ARCH_ARM, CS_MODE_THUMB)

But to no avail, the values stay

CFSR: 0x00000000
BFAR: 0x00000000

@r3-ck0
Copy link
Author

r3-ck0 commented Jul 14, 2024

Any other ideas about this?

@BitMaskMixer
Copy link
Contributor

Hi @r3-ck0,

I was curious in this issue and played a little bit with your python script around.
Your script was not working directly, so I had to tweak it a little bit, but I am able to reproduce the issue.
Since you are accessing "magic" values (most likley some registers), I searched for them in the unicorn-source code.

Since there are no direct hits, I dug a little bit deeper and tried to understand what unicorn do, so I wrote a small c sample application to have proper gdb debugger support to debug unicorn itself. (I will create a PR for that in the future I think)

It seems you are out of luck here, as you are accessing peripherals which are not implemented in unicorn.

You could implement an emulation of the device if you have the technical reference, to get the right behaviour.
However, you might want to give qemu a second try, as you must emulate more parts of the board I guess (like feeding watchdog..)

I did not read anything about the CTF itself, so I might be wrong here, but I guess the "medium" CTFs are written to be emulated with tools, without excessive programming.

@r3-ck0
Copy link
Author

r3-ck0 commented Jul 22, 2024

Hey @BitMaskMixer !

Thanks for looking into it - I was feeling the same way. It might be a great chance to look into the depths of unicorn. I understand that this is not the goal of the CTF, I was just curious and wanted to see if I can push the boundaries a bit :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants