unicorn-engine / unicorn

Unicorn CPU emulator framework (ARM, AArch64, M68K, Mips, Sparc, PowerPC, RiscV, S390x, TriCore, X86)
http://www.unicorn-engine.org
GNU General Public License v2.0
7.47k stars 1.33k forks source link

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

Open r3-ck0 opened 1 month ago

r3-ck0 commented 1 month ago

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 commented 1 month ago

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

r3-ck0 commented 1 month ago

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

gerph commented 1 month ago

On Mon, 8 Jul 2024, r3-ck0 wrote:

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.

I suspect this is related to the CPU you're using. The default CPU has the MMU, whereas the registers you're trying to access only exist on the M-variant ARMs without a MMU. I don't see anywhere that you select the CPU type, so you need to select an M-variant CPU in order to even have those registers available.

-- Charles Justin Ferguson [ My thoughts are my own. And sometimes they're not even sane. ]

r3-ck0 commented 1 month ago

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 commented 1 month ago

Any other ideas about this?

BitMaskMixer commented 1 month ago

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 commented 1 month ago

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 :)