#!/usr/bin/env python3 """sbl_uni.py — быстрый harness SBL1 на Unicorn (без пересборки QEMU). Карта RAM как в saimaa; ELF-сегменты; UARTDM/TF в stdout; MMIO->0. Использование: ./tools/sbl_uni.py fw/img/patch/img0_b17.elf --thumb 0x802f63f [--regs r0=0x220000,...] [--fill 0x085F0000:0x10000:0x08006831] [--poke 0xADDR=0xVAL] [--limit 1000000] Печатает: выполнено инстр., последний PC, fault-адрес, UART. """ import struct import sys from unicorn import (Uc, UC_ARCH_ARM, UC_MODE_THUMB, UC_HOOK_MEM_UNMAPPED, UC_HOOK_MEM_WRITE, UC_HOOK_CODE) from unicorn.arm_const import (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_SP, UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_CPSR) _REGS = (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) RAMS = [ (0x00000000, 0x00001000), # zero page (QEMU-compat: [0] writes succeed) (0x00200000, 0x00100000), # OCIMEM (0x00400000, 0x07400000), # RPM range (heap-ish) (0x08000000, 0x00600000), # DDRLOW (0x08600000, 0x00010000), # IMEM (0x80000000, 0x07C00000), # DDRHIGH_A (0x87C00000, 0x00800000), # SMEM(+SBL BSS) (0x078AF000, 0x00001000), # UARTDM page (0x13000000, 0x00002000), # UEFI tables (already above, keep) ] UART_TF = 0x078AF070 UART_SR = 0x078AF008 def load_elf(mu, path): d = open(path, "rb").read() assert d[:4] == b"\x7fELF" entry = struct.unpack(" maze mu.reg_write(UC_ARM_REG_CPSR, 0x13) if "--fill" in sys.argv: for spec in sys.argv[sys.argv.index("--fill") + 1].split(","): a, sz, v = spec.split(":") a, sz, v = int(a, 0), int(sz, 0), int(v, 0) mu.mem_write(a, struct.pack("mid-fn->maze). # After K, redirect once to poll resumption (fresh bls unfreeze lr). _fmlr = [None] _fmct = [0] _FMK = 50 _FMPC = 0x8007779 _traps = [] _trap_n = [0] if "--trap" in sys.argv: _traps = [int(x, 0) for x in sys.argv[sys.argv.index("--trap") + 1].split(",")] if "--trapn" in sys.argv: _trap_n = [int(sys.argv[sys.argv.index("--trapn") + 1])] _spmax = [0x085FFF00] _splog = [] if "--spytrack" in sys.argv else None elog = [] if "--entrylog" in sys.argv else None _prev = [0] _eranges = [] _inrng = [] if "--entrylog" in sys.argv: for _spec in sys.argv[sys.argv.index("--entrylog") + 1].split(","): _lo, _sz = _spec.split(":") _lo, _sz = int(_lo, 0), int(_sz, 0) _eranges.append((_lo, _lo + _sz)) _inrng.append(False) from unicorn.arm_const import UC_ARM_REG_CPSR as _CPSR def hook_code(mu, addr, size, data): count[0] += 1 last[0] = addr _pp = _prev[0] _prev[0] = addr if addr == _pp + 2 or addr == _pp + 4: if _sled_n[0] == 0: _sled_start[0] = _pp _sled_n[0] += 1 else: if _sled_n[0] >= _SLED_TH and len(_sled_log) < 20: _sled_log.append((count[0], _sled_start[0], _pp, _sled_n[0])) _sled_n[0] = 0 if 0x08006B8C <= addr < 0x08006B9E: try: _lr = mu.reg_read(_LR) except Exception: _lr = None if _lr == _fmlr[0]: _fmct[0] += 1 if _fmct[0] >= _FMK: _fmct[0] = 0 _fmlr[0] = None mu.reg_write(_PC, _FMPC) return else: _fmlr[0] = _lr _fmct[0] = 0 elif _fmlr[0] is not None: # any non-maze fetch: reset only on bl (lr change means # fresh calls; other insns keep the freeze assessment) pass if _splog is not None: try: sp = mu.reg_read(UC_ARM_REG_SP) except Exception: sp = 0 if sp > _spmax[0] and len(_splog) < 40: _spmax[0] = sp _splog.append((count[0], addr, sp)) if _dirty[0]: # deferred restore: MEM_WRITE hooks fire BEFORE the write lands, # so in-hook restore gets overwritten by the instruction itself _inww[0] = True try: for a, b in _dirty[1]: mu.mem_write(a, stub_snap[a - STUB_LO:b - STUB_LO]) finally: _inww[0] = False _dirty[0] = False del _dirty[1][:] try: thumb = (mu.reg_read(_CPSR) >> 5) & 1 except Exception: thumb = 1 hist.append((addr, thumb)) if _traps and addr in _traps: try: vals = [mu.reg_read(_REGS[i]) for i in range(8)] except Exception: vals = [] print(f"TRAP {addr:#x} insn#{count[0]} r0-r7={[hex(x) for x in vals]} sp={mu.reg_read(UC_ARM_REG_SP):#x} lr={mu.reg_read(_LR):#x}") try: print(f" [73A100]={bytes(mu.mem_read(0x073A100, 4)).hex()}") except Exception as e: print(f" [73A100] unreadable: {e}") try: from capstone import Cs, CS_ARCH_ARM, CS_MODE_THUMB, CS_MODE_ARM md_t = Cs(CS_ARCH_ARM, CS_MODE_THUMB) md_a = Cs(CS_ARCH_ARM, CS_MODE_ARM) for _pc, _th in list(hist)[-60:]: try: _b = _pc & ~1 _c = mu.mem_read(_b, 8 if _th else 12) for _ins in (md_t if _th else md_a).disasm(_c, _b): print(f" hist {_b:#x}{'t' if _th else 'a'}: {_ins.mnemonic} {_ins.op_str}") break except Exception: print(f" hist {_pc:#x}: ") except ImportError: pass _trap_n[0] -= 1 if _trap_n[0] <= 0 and "--trapn" in sys.argv: mu.emu_stop() return if "--trapn" not in sys.argv: mu.emu_stop() return if qlog is not None and addr in (0x8017EA4, 0x8017C60): try: vals = [mu.reg_read(_REGS[i]) for i in range(8)] except Exception: vals = [] qlog.append((addr, vals, mu.reg_read(UC_ARM_REG_SP), mu.reg_read(UC_ARM_REG_LR))) if elog is not None: for _i, (_elo, _ehi) in enumerate(_eranges): inside = _elo <= addr < _ehi if inside and not _inrng[_i]: if len(elog) < 20000: try: _th = (mu.reg_read(_CPSR) >> 5) & 1 except Exception: _th = -1 elog.append((count[0], _prev[0], addr, _th)) if not inside and _inrng[_i]: if len(elog) < 20000: elog.append((count[0], _prev[0], -addr, 2)) _inrng[_i] = inside if calls is not None and size in (2, 4): try: if size == 2: h = struct.unpack("> 3) & 7 if is_blx else -1 # bx Rm (jump, no link): 010001110 Rm:000 is_bx = (h & 0xFF80) == 0x4700 if is_bx: bxrm = (h >> 3) & 15 try: if bxrm < 8: bxt = mu.reg_read(_REGS[bxrm]) elif bxrm == 14: bxt = mu.reg_read(_LR) else: bxt = -1 except Exception: bxt = -1 if len(calls) < 8000: calls.append((addr, 100 + bxrm, bxt)) # pop {..,pc} (0xBD00+): log stacked target if calls is not None and (h & 0xFF00) == 0xBD00: try: _sp = mu.reg_read(UC_ARM_REG_SP) _tgt = struct.unpack("> 16) & 0xFFFF if (h0 & 0xF800) == 0xF000 and (h1 & 0xE000) == 0xE000: S = (h0 >> 10) & 1 J1 = (h1 >> 13) & 1 J2 = (h1 >> 11) & 1 I1 = 1 - (J1 ^ S) I2 = 1 - (J2 ^ S) off = (I1 << 23) | (I2 << 22) | ((h0 & 0x3FF) << 12) | ((h1 & 0x7FF) << 1) if S: off -= 0x1000000 if len(calls) < 8000: calls.append((addr, 200, (addr + 4 + off) & 0xFFFFFFFF)) if rets is not None and 0x8017E00 <= addr < 0x8018000: # pop.w {...,pc}: 0xE8BDxxxx with bit15 set if (w & 0xFFFF8000) == 0xE8BD8000: try: rv = mu.reg_read(_REGS[0]) except Exception: rv = -1 if len(rets) < 200: rets.append((addr, rv)) if is_blx: try: tgt = mu.reg_read(_REGS[rm]) except Exception: tgt = -1 if len(calls) < 4000: calls.append((addr, rm, tgt)) except Exception: return mu.hook_add(UC_HOOK_CODE, hook_code) # null-fetch redirect: branch-to-0 lands in maze (movs r0,#0; bx lr), # immune to [0]-writes that clobber a static maze copy from unicorn.arm_const import UC_ARM_REG_PC as _PC _nulllog = [] _halt_on_null = "--haltonnull" in sys.argv def hook_null(mu, addr, size, data): if len(_nulllog) < 10: try: r = [mu.reg_read(_REGS[i]) for i in range(4)] lr = mu.reg_read(_LR) sp = mu.reg_read(UC_ARM_REG_SP) except Exception: r, lr, sp = [], -1, -1 # walk back: find the branch source from hist _nulllog.append((count[0], r, lr, sp)) if _halt_on_null and len(_nulllog) == 1: return False mu.reg_write(_PC, STUB + 1) mu.hook_add(UC_HOOK_CODE, hook_null, begin=0x0, end=0xFFF) if "--watchcode" in sys.argv: from unicorn.arm_const import UC_ARM_REG_PC as _PC2 wlog = [] def hook_ww(mu, access, addr, size, value, data): if len(wlog) < 30: try: pc = mu.reg_read(_PC2) except Exception: pc = -1 wlog.append((pc, addr, size, value)) mu.hook_add(UC_HOOK_MEM_WRITE, hook_ww, begin=0x8005800, end=0x8039254) import atexit def _dump(): print(f"code-writes: {len(wlog)}") for pc, a, sz, v in wlog[:30]: print(f" pc={pc:#x} [{a:#x}] sz={sz} val={v:#x}") atexit.register(_dump) # --- machine scaffold parity (saimaa.c values, verbatim) --- def W32(a, v): mu.mem_write(a, struct.pack(" restart 0x8013ba5) or done # (restore r4/r5/sl/fp, return to sbl_main 0x802f681). mu.mem_write(0x08006C10, bytes.fromhex( "ff2c13d940f20004c0f2220440f20005c0f2220540f2000a" "c0f6060a41f2000bc0f6060bdff800f081f60208dff800f0" "a53b0108")) # S9 stublet (62B @0x08006C48, b22): labyrinth-epilogue exit router. # ldr r3,[sp,#0x14]: nonzero -> original pop.w (legit return); # zero (empty scaffold frames) -> restore r0/r4/r5/sl/fp, reset sp # to 0x085FFF00 (fresh STUBV-fill stack), jump to sbl_main 0x802f69d # via ip (no literal loads). mu.mem_write(0x08006C48, bytes.fromhex( "059b0bb1bde8f081002040f20004c0f2220440f20005c0f2" "220540f2000ac0f6060a41f2000bc0f6060b4ff60073c0f6" "5f039d464ff29d6cc0f6020c6047")) # S10 stublet (10B @0x08006C88, b23): rollback-exit router. # movw/movt ip + bx ip to sbl_main 0x802f6a3. mu.mem_write(0x08006C88, bytes.fromhex("4ff2a36cc0f6020c6047")) # S18 stublet (10B @0x08006D22, b29): poll-epilogue router. # movw/movt ip + bx ip to sbl_main 0x802f6a9 (post-dispatcher). mu.mem_write(0x08006D22, bytes.fromhex("4ff2a96cc0f6020c6047")) # S21 stublet (32B @0x08006D5A, b38): table-walker router. # [sp+0x24] valid-code? => original pop.w (legit return); # else b.w S19 (sbl_main post-logger restart, bounded table work). mu.mem_write(0x08006D5A, bytes.fromhex( "099b13f0010f09d01a0e082a01d1bde8f09f1a0f082a" "01d1bde8f09ffff7d9bf")) # S20 stublet (34B @0x08006D38, b37): clock-dispatcher router. # [sp+0x2C] valid-code? => original pop.w (legit return); # else r0=sl (PBL tables) + b.w dispatcher re-run (idempotent math). mu.mem_write(0x08006D38, bytes.fromhex( "0b9b13f0010f09d01a0e082a01d1bde8fe8f1a0f082a" "01d1bde8fe8f504607f013bf")) # S17 safe-logger (50B @0x08006CF0, b26): print r1-string via # UARTDM-TF only if r1 in rodata [0x804B800,0x8058000); pop-return. mu.mem_write(0x08006CF0, bytes.fromhex( "4bf60003c0f60403994211d348f20003c0f605039942" "0bd210b54ff20003c0f28a730c4614f8010b08b11867fae710bd7047")) # PBL version-table fabrication (b23): [0x080528D0] = STUBV so the # rollback query's blx calls maze (returns 0 = versions OK). W32(0x080528D0, STUBV) # PBL dispatcher-table fabrication (b33): [0x80164F0] (literal pool, # image default points at hash blob!) retargeted to scratch struct # at 0x08024000 (OCIMEM, out of stack-march paths!) with [0x18]=2 # (ready-flag -> world-switch dispatch; TZ deferred via OCIMEM stub). W32(0x08024000, 0x08024000) W32(0x08024014, 0x00000002) W32(0x080164F0, 0x08024000) # HW-ready bit fabrication (b25): [0x073A100] bits 0,1,2 = peripheral # status done-bits (poll#1 checks bit2 (lsls#29), poll#2 bit1 (lsls#30), # post-poll gate bit0 (lsls#31)). SBL only reads it, never writes. W32(0x073A100, 0x00000007) # PBL world-switch Thumb-stub (b24): the ARM trampoline at 0x221EF8 # is entered in Thumb (mode confusion at dispatch); real ARM would # save regs/CPS to MON and call TZ (not loaded yet). Stub returns 0 # to the dispatcher via stale lr (TZ bringup deferred). # NOTE: overwrites 4B of real ARM trampoline (recoverable from ELF). mu.mem_write(0x00221EF8, bytes([0x00, 0x20, 0x70, 0x47])) # S12-stub (b27, 100B @0x08006964, ARM): blx-to-padding becomes # smart dispatcher: ldr ip,[sp,#4] (=pushed lr); ip==STUBV/even/ # non-code => ip=poll resumption 0x8007779; else keep pushed-lr # (NO add-sp: marches on mid-entries! un-leak dropped, helper-leak # down is harmless); restore r7; ACK peripheral bit1; bx lr. # PBL would place the real ARM helper here. mu.mem_write(0x08006964, bytes.fromhex( "04c09de58d3b06e3003840e303005ce10900000a0100" "1ce30700000a2c3ca0e1080053e30000001a050000ea" "2c3ea0e1080053e30000001a010000ea79c707e300c8" "40e300700ae3737040e30c309fe5002093e5022082e3" "002083e51eff2fe100a17300")) # S16-stublet (b27, 64B @0x08006CC0): smart return for B66 bx ip. # ip==STUBV (fill/maze, never a legit return) => default; # ip odd + top 8 (code) => add sp,#8 (pop the helper frame B66 # skipped!) + bx ip (legit pushed-lr return, un-leaks the stack); # else default (restore r7, jump poll resumption 0x8007779). mu.mem_write(0x08006CC0, bytes.fromhex( "ddf804c046f68d33c0f600039c450ed01cf0010f0bd0" "5fea1c63082b01d102b060475fea1c73082b01d102b0" "60474af20007c0f2730747f2797cc0f6000c6047")) # stack zone fill (STUBV odd) mu.mem_write(0x085F0000, struct.pack("= STUB_HI: return _swn[0] += 1 if len(_swlog) < 8: try: _pc = mu.reg_read(_PC) except Exception: _pc = -1 _swlog.append((_pc, addr, size)) # record for deferred restore (see hook_code); in-hook restore # would be overwritten by the faulting instruction itself a = max(addr, STUB_LO) b = min(addr + size, STUB_HI) if b > a and len(_dirty[1]) < 16: _dirty[1].append((a, b)) _dirty[0] = True #hook all DDRLOW, filter in python (catches writes starting below LO) mu.hook_add(UC_HOOK_MEM_WRITE, hook_stubw, begin=0x08000000, end=0x085FFFFF) if "--watchmaze" in sys.argv: from unicorn.arm_const import UC_ARM_REG_PC as _PC3 mzlog = [] def hook_mz(mu, access, addr, size, value, data): if len(mzlog) < 40: try: pc = mu.reg_read(_PC3) except Exception: pc = -1 mzlog.append((pc, addr, size, value)) mu.hook_add(UC_HOOK_MEM_WRITE, hook_mz, begin=0x08006B8C, end=0x08006B9D) import atexit as _ax def _dmz(): print(f"maze-writes: {len(mzlog)}") for pc, a, sz, v in mzlog[:40]: print(f" pc={pc:#x} [{a:#x}] sz={sz} val={v:#x}") _ax.register(_dmz) if "--watchlow" in sys.argv: _spec = sys.argv[sys.argv.index("--watchlow") + 1].split(":") _wlo, _whi = int(_spec[0], 0), int(_spec[0], 0) + int(_spec[1], 0) from unicorn.arm_const import UC_ARM_REG_PC as _PC4 wlolog = [] def hook_wlo(mu, access, addr, size, value, data): if len(wlolog) < 60: try: pc = mu.reg_read(_PC4) except Exception: pc = -1 wlolog.append((pc, addr, size, value)) mu.hook_add(UC_HOOK_MEM_WRITE, hook_wlo, begin=0x00200000, end=0x085FFFFF) import atexit as _ax2 def _dwlo(): inn = [(pc, a, sz, v) for pc, a, sz, v in wlolog if not (a + sz <= _wlo or a >= _whi)] print(f"low-writes-in-range: {len(inn)}") for pc, a, sz, v in inn[:60]: print(f" pc={pc:#x} [{a:#x}] sz={sz} val={v:#x}") _ax2.register(_dwlo) limit = 2000000 if "--limit" in sys.argv: limit = int(sys.argv[sys.argv.index("--limit") + 1]) try: mu.emu_start(thumb | 1, 0xFFFFFFFF, 0, limit) except Exception as e: print("EMU-EXC:", str(e)[:120]) print("insns:", count[0], "last-pc:", hex(last[0])) print("unmapped:", unmapped["n"], unmapped["first"][:4]) print("regs:", " ".join(f"r{i}={mu.reg_read(_REGS[i]):#x}" for i in range(8)), f"sp={mu.reg_read(UC_ARM_REG_SP):#x}", f"lr={mu.reg_read(UC_ARM_REG_LR):#x}", f"pc={mu.reg_read(UC_ARM_REG_PC):#x}") try: from capstone import Cs, CS_ARCH_ARM, CS_MODE_THUMB, CS_MODE_ARM md_t = Cs(CS_ARCH_ARM, CS_MODE_THUMB) md_a = Cs(CS_ARCH_ARM, CS_MODE_ARM) real = [(pc, th) for pc, th in hist if not (0 <= (pc & ~1) < 0x1000)][-48:] for pc, thumb in real: try: base = pc & ~1 code = mu.mem_read(base, 8 if thumb else 12) md = md_t if thumb else md_a for ins in md.disasm(code, base): print(f" {base:#x}{'t' if thumb else 'a'}: {ins.mnemonic} {ins.op_str}") break except Exception: print(f" {pc:#x}: ") except ImportError: print("hist:", [hex(pc) for pc, _ in list(hist)[-12:]]) if _sled_log or _sled_n[0] >= _SLED_TH: if _sled_n[0] >= _SLED_TH: _sled_log.append((count[0], _sled_start[0], last[0], _sled_n[0])) print(f"sleds: {len(_sled_log)}") for n, s, e, c in _sled_log: print(f" insn#{n} [{s:#x}..{e:#x}] len={c}") if elog is not None: print(f"entries: {len(elog)}") for n, prev, cur, th in elog[:10]: if cur < 0: print(f" FIRST EXIT insn#{n} {prev:#x} -> {(-cur):#x}") else: print(f" FIRST insn#{n} {prev:#x} -> {cur:#x}{'t' if th == 1 else ('a' if th == 0 else '?')}") for n, prev, cur, th in elog[-10:]: if cur < 0: print(f" LAST EXIT insn#{n} {prev:#x} -> {(-cur):#x}") else: print(f" LAST insn#{n} {prev:#x} -> {cur:#x}{'t' if th == 1 else ('a' if th == 0 else '?')}") if _splog is not None: print(f"sp-highs: {len(_splog)}") for n, pc, sp in _splog: print(f" insn#{n} {pc:#x} sp={sp:#x}") if _nulllog: print(f"null-entries: {len(_nulllog)}") for n, r, lr, sp in _nulllog: print(f" insn#{n} r0-r3={[hex(x) for x in r]} lr={lr:#x} sp={sp:#x}") if qlog is not None: print(f"querylog: {len(qlog)}") for pc, r, sp, lr in qlog[:20]: print(f" {pc:#x} r0-r7={[hex(x) for x in r]} sp={sp:#x} lr={lr:#x}") if rets is not None: print(f"rets-in-range: {len(rets)}") for pc, rv in rets[-20:]: print(f" ret {pc:#x} r0={rv:#x}" if rv >= 0 else f" ret {pc:#x} r0=?") if calls is not None: print(f"blx-calls: {len(calls)}") bls = [(pc, tgt) for pc, rm, tgt in calls if rm == 200] print(f"bl-immed: {len(bls)}") for pc, tgt in bls[-30:]: print(f" {pc:#x}: bl #{tgt:#x}") pops = [(pc, tgt) for pc, rm, tgt in calls if rm == 300] print(f"pop-pc: {len(pops)}") for pc, tgt in pops[:15]: print(f" FIRST {pc:#x}: pop-> {tgt:#x}" if tgt >= 0 else f" FIRST {pc:#x}: pop-> ?") for pc, tgt in pops[-15:]: print(f" LAST {pc:#x}: pop-> {tgt:#x}" if tgt >= 0 else f" LAST {pc:#x}: pop-> ?") for pc, rm, tgt in calls[-12:]: if rm == 200: continue tag = f"bx r{rm - 100}" if rm >= 100 else f"blx r{rm}" print(f" {pc:#x}: {tag} -> {tgt:#x}" if tgt >= 0 else f" {pc:#x}: {tag} -> ?") if "--dumpmem" in sys.argv: for spec in sys.argv[sys.argv.index("--dumpmem") + 1].split(","): a, sz = spec.split(":") a, sz = int(a, 0), int(sz, 0) print(f"mem[{a:#x}+{sz:#x}]:") print(bytes(mu.mem_read(a, sz)).hex()) print("UART:", "".join(uart_out)[:160]) print(f"stubguard-hits: {_swn[0]}") for _pc, _a, _s in _swlog: print(f" guarded pc={_pc:#x} [{_a:#x}] sz={_s}") if __name__ == "__main__": main()