Files
amlogic/source/bootrom_emulator/emulator.py
Eljakim Herrewijnen 1d49c6165b added rom header fuzzer
2025-01-12 22:14:25 +01:00

395 lines
14 KiB
Python

from unicorn import *
from unicorn.arm64_const import *
from ghidra_assistant.utils.utils import *
from ghidra_assistant.utils.archs.arm64.arm64_emulator import ARM64UC_Emulator
from ghidra_assistant.utils.archs.arm64.asm_arm64 import ShellcodeCrafterARM64
import sys, pathlib, struct
from add_memory_maps import *
from tools import *
from amlogic_devices import *
from amlogic_rom_db import *
def p8(v):
return struct.pack("<B", v)
ENTRY_POINT = 0xffff0000
STACK_ADDRESS = 0xfffe3800
# FASTBOOT_CMD_HANDLER = 0xffff9758
TEST_OFFSET = 0xfffa0000 + 0x8000
TEST_REQ_BUFFER = TEST_OFFSET + 0x800
TEST_CONTEXT_BUFFER = TEST_OFFSET + 0x9000
def get_local_file(name):
return pathlib.Path(pathlib.Path(__file__).parent.resolve() / name)
class Amlogic_Emulator(ARM64UC_Emulator):
def __init__(self, file = "bin/BootROM_s905x3.bin", debug=False, device="S905X3") -> None:
'''
Amlogic bootrom emulator.
'''
super().__init__(True)
self.device_offsets = amlogic_rom_db[device]
self.file = self.device_offsets['path']
self.logger = setup_logger("GSCEmulator")
self.logger.setLevel(logging.DEBUG)
self.debug = debug
self.bootrom = open(self.file, 'rb').read()
self.uc = Uc(UC_ARCH_ARM64, UC_MODE_LITTLE_ENDIAN)
self.sc = ShellcodeCrafterARM64(None, None)
self.setup_memory()
self.setup_registers()
self.setup_hooks()
self.setup_devices()
self.apply_patches()
self.debug = False
self.afl_input = []
self.em = None # By default GA not used
self.wait_for_usb = False # USB not ready
self.I2C_inject_buffer = b""
self.pc_trace = []
self.enable_trace = False
def write(self, address, data):
self.uc.mem_write(address, data)
def read(self, address, size):
return bytes(self.uc.mem_read(address, size))
def apply_patches(self):
self.write(self.device_offsets['fastboot_tx_write'], self.sc.ret_ins)
self.write(self.device_offsets['usb_ep_queue'], self.sc.ret_ins)
def setup_memory(self):
#Bootrom
self.uc.mem_map(ENTRY_POINT, len(self.bootrom), UC_PROT_EXEC | UC_PROT_READ)
self.write(ENTRY_POINT, self.bootrom)
# Timer 0xff800000|0xff800fff|rti
self.uc.mem_map(0xff800000, 0x1000, UC_PROT_READ | UC_PROT_WRITE)
# SRAM 0xfffa0000|0xfffe7fff |
self.uc.mem_map(0xfffa0000, 0x48000, UC_PROT_READ | UC_PROT_WRITE)
# ISA 0xffd0f000|0xffd0ffff|isa
self.uc.mem_map(0xffd0f000, 0x1000, UC_PROT_READ | UC_PROT_WRITE)
# eFuse 0xff630000|0xff631fff|efuse
self.uc.mem_map(0xff630000, 0x2000, UC_PROT_READ | UC_PROT_WRITE)
self.uc.mem_write(0xff630000, open(get_local_file("bin/efuse_ff630000_ff632000.bin"), 'rb').read())
# UART 0xff803000|0xff803fff|uart and add hook
self.uc.mem_map(0xff803000, 0x1000, UC_PROT_READ | UC_PROT_WRITE)
# ?? 0xffd08000|0xffd08fff|assist
self.uc.mem_map(0xffd08000, 0x1000, UC_PROT_READ | UC_PROT_WRITE)
# 0xff634400|0xff6347ff|periphs_reg
self.uc.mem_map(0xff634400, 0x400, UC_PROT_READ | UC_PROT_WRITE)
# Reset? 0xffd01000|0xffd01fff|reset
self.uc.mem_map(0xffd01000, 0x1000, UC_PROT_READ | UC_PROT_WRITE)
# RAM for USB? ff63c000
self.uc.mem_map(0xff63c000, 0x1000, UC_PROT_READ | UC_PROT_WRITE)
# UNKNOWN
self.uc.mem_map(0xffe09000, 0x2000, UC_PROT_READ | UC_PROT_WRITE)
# DMA 0xff63e000|0xff63ffff|dma
self.uc.mem_map(0xff63e000, 0x2000, UC_PROT_READ | UC_PROT_WRITE)
# 0xff63a000|0xff63bfff|usbphy21
self.uc.mem_map(0xff63a000, 0x2000, UC_PROT_READ | UC_PROT_WRITE)
# USB1 0xff400000|0xff4fffff|usb1
self.uc.mem_map(0xff400000, 0x100000, UC_PROT_READ | UC_PROT_WRITE) #1MB
# UNKNOWN 0xffd14000|0xffd14fff|spifc
self.uc.mem_map(0xffd14000, 0x1000, UC_PROT_READ | UC_PROT_WRITE) #1MB
# Flash 0xf6000000|0xf9ffffff|flash #TODO add flash device for fuzzing?
self.uc.mem_map(0xf6000000, 0xf9ffffff + 1 - 0xf6000000, UC_PROT_READ | UC_PROT_WRITE)
# Emmc 0xffe07000|0xffe08fff|emmcC TODO add eMMC device for fuzzing?
self.uc.mem_map(0xffe07000, 0x2000, UC_PROT_READ | UC_PROT_WRITE)
# 0xffe05000|0xffe06fff|emmcB
self.uc.mem_map(0xffe05000, 0x2000, UC_PROT_READ | UC_PROT_WRITE)
# FFE03000 FFE04FFF emmcA
self.uc.mem_map(0xFFE03000, 0x2000, UC_PROT_READ | UC_PROT_WRITE)
def write_ptr(self, at, ptr):
return self.uc.mem_write(at, p32(ptr))
def read_ptr(self, at):
return u32(self.uc.mem_read(at, 4))
def setup_registers(self):
self.pc = ENTRY_POINT
self.sp = STACK_ADDRESS
def setup_hooks(self):
if self.debug:
self.uc.hook_add(UC_HOOK_CODE, self.hook_code)
self.uc.hook_add(UC_HOOK_INSN_INVALID, self.hook_invalid_ins)
self.uc.hook_add(UC_HOOK_INTR, self.hook_exception)
self.uc.hook_add(UC_HOOK_MEM_READ|UC_HOOK_MEM_WRITE, self.hook_devices, None, 0xff3f0000, 0xff30cffe + 1)
#Add hook usb device
self.uc.hook_add(UC_HOOK_MEM_READ|UC_HOOK_MEM_WRITE, self.hook_devices, None, 0xff63a000, 0xff63bfff + 1)
self.uc.hook_add(UC_HOOK_MEM_READ|UC_HOOK_MEM_WRITE, self.hook_devices, None, 0xff400000, 0xff4fffff + 1)
def setup_devices(self):
self.devices = [
# USBDevice(self),
UARTDevice(self),
TimerDevice(self),
GPIODevice(self),
EfuseDevice(self),
USBPHYDevice(self),
USB1Device(self),
]
def hook_devices(self, handle, access, address, size, value, user_data):
if address == 0xff630044 and access == UC_MEM_WRITE:
pass
for device in self.devices:
if address >= device.BASE and address < device.BASE + device.SIZE:
if access == UC_MEM_READ:
device.read(address, size)
else:
device.write(address, size, value)
return True
return True
def get_device(self, name):
for dev in self.devices:
if dev.NAME == name:
return dev
return None
def run(self):
while True:
self.uc.emu_start(self.pc, self.pc + 4)
def run_until_usb(self):
info("Run until USB")
try:
while True:
self.uc.emu_start(self.pc, self.pc + 4)
if self.wait_for_usb:
return
except Exception as e:
print(bytes(self.get_device("UART").get_rx()).decode(errors="ignore"))
self.print_ctx()
print(str(e))
if e.errno == UC_ERR_READ_UNMAPPED or e.errno == UC_ERR_WRITE_UNMAPPED:
pass
# print(find_memory())
exit(0)
@property
def pc(self):
return self.uc.reg_read(UC_ARM64_REG_PC)
@pc.setter
def pc(self, value):
self.uc.reg_write(UC_ARM64_REG_PC, value)
@property
def sp(self):
return self.uc.reg_read(UC_ARM64_REG_SP)
@sp.setter
def sp(self, value):
self.uc.reg_write(UC_ARM64_REG_SP, value)
@property
def X0(self):
return self.uc.reg_read(UC_ARM64_REG_X0)
@X0.setter
def X0(self, value):
self.uc.reg_write(UC_ARM64_REG_X0, value)
@property
def X1(self):
return self.uc.reg_read(UC_ARM64_REG_X1)
@X1.setter
def X1(self, value):
self.uc.reg_write(UC_ARM64_REG_X1, value)
def pc_in_debug(self):
# for start, end in debug_functions:
# if self.pc >= start and self.pc <= end:
# return True
return False
def dump_stack(self, n_offset = 0, size = 0x200):
hexdump(bytes(self.uc.mem_read(self.sp - n_offset, size)))
def add_breakpoint(self, at):
def _breakpoint(uc : Uc, address, size, em : Amlogic_Emulator):
em.print_ctx(print)
pass
self.uc.hook_add(UC_HOOK_CODE, _breakpoint, self, at, at + 1)
def hook_code(self, uc : Uc, address, size, user_data):
'''
Hook startup code to continue boot process
'''
# if self.pc == 0xffff3520: # Reboo ttod
if self.pc == 0xffff05c4:
pass
if self.enable_trace:
self.pc_trace.append(hex(self.pc))
return
# Reset expects value in DRAM
if self.pc == 0xffff00b0:
self.write_ptr(0xff800228, uc.reg_read(UC_ARM64_REG_X1))
if self.pc == 0xffff62dc:
uc.reg_write(UC_ARM64_REG_W21, uc.reg_read(UC_ARM64_REG_W2))
# Hooks for I2c ==========
if self.pc == 0xffff569c:
# Address for HDMI i2c is 0xa4
# self.logger.debug(f"i2c reading from address: {hex(self.uc.reg_read(UC_ARM64_REG_W0))}")
self.get_device("GPIO").I2C_Address = self.uc.reg_read(UC_ARM64_REG_W0)
if self.pc == 0xffff56e8:
# self.logger.info(f"Data from i2c address={hex(self.get_device('GPIO').I2C_Address)} value={hex(self.uc.reg_read(UC_ARM64_REG_W0))}")
pass
#Dirty hook to inject I2C data, instead of implementing a full I2CDevice
if self.pc == 0xffff5760:
self.I2C_inject_buffer = b"boot@USB\x00"
self.i2c_offset = 0
if len(self.I2C_inject_buffer) > 0 and self.pc == 0xffff56d8:
self.uc.reg_write(UC_ARM64_REG_W20, self.I2C_inject_buffer[self.i2c_offset])
self.i2c_offset += 1
# Add hook for injecting i2c boot string:
if self.pc == 0xffff5790:
self.logger.info(f"I2C boot string: {self.uc.mem_read(self.sp + 0x10, 8).decode(errors='ignore')}")
# ====================
if self.get_device("UART").has_rx():
print(bytes(self.get_device("UART").get_rx()).decode(errors="ignore"), end="")
if self.pc_in_debug():
self.print_ctx()
# breakpoint()
# USB is here
if self.pc == 0xffff9d18:
self.wait_for_usb = True
uc.emu_stop()
pass
def hook_invalid_ins(self, handle, intno, user_data):
pass
def read_string(self, at):
if at == 0:
return b''
s = b''
while 1:
b = self.uc.mem_read(at, 1)
at += 12
if b == b'\0':
return s
s += b
return s
def hook_exception(self, handle : Uc, intno, user_data):
if intno == 1:
handle.reg_write(UC_ARM64_REG_PC, handle.reg_read(UC_ARM64_REG_PC) + 4)
return True
else:
# Invalid instruction
pass
def place_fastboot_command(self, input):
assert len(input) <= 0x200, "Too much data!"
# Setup fake request in X1
self.X1 = TEST_OFFSET
# Set request length to 0x200
self.write_ptr(TEST_OFFSET + 8, 0x200)
# Write input buffer
self.write_ptr(TEST_OFFSET, TEST_REQ_BUFFER)
self.write(TEST_REQ_BUFFER, input)
# Setup context
self.write_ptr(self.X1 + 0x28 + 0x4, 64)
def run_fastboot_cmd(self):
def _hook_usb_ep_que(uc : Uc, address, size, em : Amlogic_Emulator):
# em.uc.emu_stop()
return True
# self.uc.hook_add(UC_HOOK_CODE, _hook_usb_ep_que, self, 0xffff8998, 0xffff8998 + 1)
def _hook_fastboot_tx_write(uc :Uc, address, size, em : Amlogic_Emulator):
if em.debug:
hexdump(em.read(em.X0, em.X1))
# em.uc.emu_stop()
return True
self.uc.hook_add(UC_HOOK_CODE, _hook_fastboot_tx_write, self, self.device_offsets['fastboot_tx_write'], self.device_offsets['fastboot_tx_write'] + 1)
self.pc = self.device_offsets['FASTBOOT_CMD_HANDLER']
self.sp = STACK_ADDRESS
# Run
if self.debug:
self.enable_trace = True
self.uc.emu_start(self.pc, 0)
return 0
def place_bootloader(self, data=b"\xbb" * 0x40):
BL_LOCATION = 0xfffa0000 #0x40 size?
OUT_OFFSET = 0xfffa0000 + 0x1000
self.uc.mem_write(BL_LOCATION, data)
def run_check_bootloader_header(self):
BL_LOCATION = 0xfffa0000 #0x40 size?
OUT_OFFSET = 0xfffa0000 + 0x1000
VERIFY_BL = 0xffff1b2c
self.X0 = BL_LOCATION
self.X1 = OUT_OFFSET
self.pc = VERIFY_BL
SOMETHING_WRONG_AND_REBOOT = 0xffff05c4
def _hook_something_wrong(uc : Uc, address, size, em : Amlogic_Emulator):
if hasattr(em, "debug") and em.debug:
em.print_ctx()
em.something_wrong = True
uc.emu_stop()
return True
self.uc.hook_add(UC_HOOK_CODE, _hook_something_wrong, self, SOMETHING_WRONG_AND_REBOOT, SOMETHING_WRONG_AND_REBOOT + 1)
# self.enable_trace = True
self.uc.emu_start(self.pc, 0)
return 0
if __name__ == "__main__":
device = "S905X3"
# self.uc.mem_write(BL_LOCATION, b"@AML") # Magic
# self.uc.mem_write(BL_LOCATION + 0xa, p8(1)) #unk1
# self.uc.mem_write(BL_LOCATION + 0xb, p8(1)) #unk2
emulator = Amlogic_Emulator(debug=True)
# emulator.place_bootloader(open("fuzz_bl.bin", 'rb').read())
d = bytes.fromhex("4f41 4d4f 414d 104d 4c5c 414d 4c4c")
d += (0x40 - len(d)) * b"\x00"
emulator.place_bootloader(d)
emulator.run_check_bootloader_header()