1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
|
#!/usr/bin/env python3
"""Boot sOS, then run the stage-2 payload task -- the sOS-side component loader.
`bl_main` spawns exactly one task via `SVC 0x51` (entry VA 0x204895). IDA had no
function defined there at all, because nothing statically references it; it is
reachable only through the task spawn. It is named `bl_stage2_task` in the IDB
now, and it runs:
bl_stage2_init -> bl_init_obj_slots -> bl_load_ta_image
i.e. it is the sOS firmware-authenticity path. Reaching it required fixing four
separate things, each of which failed silently:
1. **The MMU is ON.** sOS sets SCTLR.M during boot (SCTLR=0xC5107D,
TTBR0=0xFC48, TTBCR=0x24). Unicorn is QEMU and implements ARMv7 translation
faithfully, so after boot every access goes through sOS's page tables and
`uc.mem_map()` only decides what physical memory exists behind them.
2. **`SVC 0x79` (VA->PA translate) cannot be stubbed to 0.** `bl_main` calls it
to obtain the physical address of its own code, then passes that to
`SVC 0x62` to re-map it. With the stub returning 0, bl_main re-mapped
VA 0x200000 to *physical 0* and destroyed the CODE2 mapping: VA 0x203b5a
went from PA 0x15B5A (correct, = VA-0x1EE000) to PA 0x3B5A, after which the
task executed unrelated bytes. Nothing reported an error.
3. **`SVC 0x62` (map pages) must really edit the page tables.** Returning 0
claims success while creating no translation, and the VA faults later.
4. **CPU mode.** Boot ends in User mode (CPSR=0x30). Kernel pages are
privileged-only, so calling kernel code from User mode takes a permission
fault.
And the trap that made all of the above expensive to diagnose:
**Once the MMU is on, `count=` does NOT bound emu_start.** A faulting access
retires zero instructions, so the counter never decrements and the abort
loop runs forever -- `count=1000` hangs exactly as long as `count=200000`.
Use `timeout=` (wall clock) and `armmmu.require_mapped()` instead.
Current state: the task reaches `bl_load_ta_image` and then stalls inside
`bl_memcpy`, because no TA image is staged at the physical address it reads
(from 0x2060E8+0x38). Staging a real signed container there is the next step.
"""
import sys, os, time, json
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import unicorn
from unicorn import UC_PROT_ALL, UC_HOOK_BLOCK
from unicorn.arm_const import (UC_ARM_REG_SP, UC_ARM_REG_PC, UC_ARM_REG_LR,
UC_ARM_REG_CPSR, UC_ARM_REG_R0, UC_ARM_REG_R1,
UC_ARM_REG_R2)
import importlib.util
_s = importlib.util.spec_from_file_location(
"bootmod", os.path.join(os.path.dirname(__file__), "boot_sos_full.py"))
B = importlib.util.module_from_spec(_s); _s.loader.exec_module(B)
from pspemu.ccp import CCP, CCP_MMIO_SIZE
from pspemu import armmmu
from pspemu.memmap import MAGIC_RET
STAGE2_TASK = 0x204894 # bl_stage2_task (thumb)
BL_MAIN = 0x203A30
CODE2_BIAS = 0x1EE000
def build(verbose=True):
c, img = B.load_sos()
for s in range(0x40, 0xC0):
c.svc.register(s, lambda comp, n: 0)
for a in B.CP15_SKIP:
c.write(a, B.THUMB_NOP2)
for base, size in B.AXI_MAPS_CCP:
try:
c.uc.mem_map(base, size, UC_PROT_ALL)
except unicorn.UcError:
pass
ccp = CCP(c, base=B.CCP_WINDOW_VA, size=CCP_MMIO_SIZE)
c.uc.reg_write(UC_ARM_REG_SP, 0x3F000)
c.uc.emu_start(0x98, 0xFF0000, count=100000) # -> bl_main idle loop
assert 0x203B5A <= c.uc.reg_read(UC_ARM_REG_PC) < 0x203B70, "boot did not idle"
pt = armmmu.PageTableEditor(c.uc)
assert pt.selftest(0x203B5A), "page-table walk selftest failed"
def svc79(comp, n): # VA -> PA
_kind, pa = pt.describe(comp.uc.reg_read(UC_ARM_REG_R0))
return pa or 0
def svc62(comp, n): # map pages
va = comp.uc.reg_read(UC_ARM_REG_R0)
pa = comp.uc.reg_read(UC_ARM_REG_R1)
np = comp.uc.reg_read(UC_ARM_REG_R2)
if np == 0 or np > 0x4000 or pa == 0:
return 0
pt.ensure_backed(pa, np * 0x1000)
try:
pt.map_range(va, pa, np)
return 0
except Exception:
return 1
c.svc.register(0x79, svc79)
c.svc.register(0x62, svc62)
# Re-run bl_main so its mapping SVCs take effect for real this time.
c.uc.reg_write(UC_ARM_REG_SP, 0x3F000)
c.uc.emu_start(BL_MAIN | 1, 0xFF0000, count=400000)
# INVARIANT: bl_main's own re-mapping must not have moved its code.
kind, pa = pt.describe(0x203B5A)
assert pa == 0x203B5A - CODE2_BIAS, (
"CODE2 mapping corrupted: VA 0x203b5a -> PA %#x, expected %#x "
"(this is what a stubbed SVC 0x79 does)" % (pa or 0, 0x203B5A - CODE2_BIAS))
pt.ensure_backed(MAGIC_RET, 0x1000)
pt.map_page(MAGIC_RET, MAGIC_RET)
if verbose:
print("boot OK; CODE2 mapping intact (VA 0x203b5a -> PA %#x)" % pa)
return c, ccp, pt
def stage_image(c, pt, path, pa=0x00A00000):
"""Stage a real signed $PS1 container where bl_stage2_task looks for it.
The task loads a DRIVER image: `sys_drv.bin` carries 'DR' (0x5244) at
body+0x14, with body+0x0c = 0xEB6020 and body+0x18 = 0x27000, matching
SYS_DRV's known layout. So stage 2 is loading SYS_DRV.
Two addresses matter:
* the container must be visible at VA 0x20A000 (its 0x100-byte signed
header) / 0x20A100 (the TA header), where bl_load_ta_image reads it;
* its physical address must be published at VA 0x2060E8+0x38, which
bl_stage2_task reads with `LDRD R1,R0,[R9,#0x38]`.
NOTE: `uc.mem_read/mem_write` are PHYSICAL and bypass the MMU. Writing a
guest global by its VA silently lands somewhere else -- VA 0x206120 is
PA 0x18120. Always translate first.
"""
img = open(path, "rb").read()
pt.ensure_backed(pa, len(img) + 0x2000)
c.uc.mem_write(pa, img)
pt.map_range(0x20A000, pa, (len(img) + 0xFFF) // 0x1000)
def vwr(va, val):
_k, p = pt.describe(va)
c.uc.mem_write(p, int(val).to_bytes(4, "little"))
vwr(0x206120, pa) # pa_lo
vwr(0x206124, 0) # pa_hi
return img
def install_svc64(c, pt, slot=0):
"""SVC 0x64 = create process. An OUT-PARAMETER call: it must write a slot
index to *R1. Left as a stub it returns 0 ("success") while the caller's
pre-set 0xFF remains, and bl_load_ta_image rejects with 0x25 because
`ta_slot >= 0x20`.
WARNING: this minimal model is NOT sufficient. It gets past the slot check,
but real SVC 0x64 also builds the new process's address space and PCB;
without that the loader spins forever re-clearing PCB state (24M blocks /
120s with only 70 distinct blocks -- an infinite loop, not slow progress).
Faithful process creation is the next piece of work.
"""
from unicorn.arm_const import UC_ARM_REG_R1
def svc64(comp, n):
_k, pa = pt.describe(comp.uc.reg_read(UC_ARM_REG_R1))
if pa is not None:
comp.uc.mem_write(pa, bytes([slot]))
return 0
c.svc.register(0x64, svc64)
def run_task(c, pt, seconds=8):
pt.require_mapped(pc=STAGE2_TASK, sp=0x287928, ret=MAGIC_RET)
blocks = []
c.uc.hook_add(UC_HOOK_BLOCK, lambda uc, a, s, u: blocks.append(a))
c.uc.reg_write(UC_ARM_REG_SP, 0x287928)
c.uc.reg_write(UC_ARM_REG_LR, MAGIC_RET | 1)
t0 = time.time()
try:
# timeout=, NOT count= : count does not bound a fault loop under the MMU
c.uc.emu_start(STAGE2_TASK | 1, MAGIC_RET, timeout=seconds * 1_000_000)
except Exception as e:
return blocks, str(e), time.time() - t0
return blocks, None, time.time() - t0
def diagnose_stall(c, pt, blocks):
"""Distinguish "still working" from "wedged in an unbounded MMU abort loop".
FIDELITY-NOTES describes the trap: an access to a VA with no guest
translation retires ZERO instructions, so the faulting block restarts
forever. It presents as "the emulator is slow", and the diagnostic pair is
* `c.faults` EMPTY -- UC_HOOK_MEM_INVALID never fires, because the HOST
memory is mapped; it is the GUEST translation that is missing; and
* a tiny number of distinct blocks executing enormously many times.
This harness used to report that state as the bland "timeout (still
running)", which reads like progress. Measured here: 571k blocks, 32
distinct, PC pinned at 0x200170 inside the ARM memcpy -- and the memcpy was
entered exactly ONCE (len 0x20, from LR 0x204061), so it is not a long copy,
it is one copy that can never complete.
Returns a dict describing the stall, or None if the run looks live.
"""
from unicorn.arm_const import (UC_ARM_REG_PC, 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)
if not blocks:
return None
distinct = len(set(blocks))
# A live run keeps discovering blocks; a wedged one cycles a handful.
if distinct > 64 or len(blocks) < 10000 or c.faults:
return None
pc = c.uc.reg_read(UC_ARM_REG_PC)
regs = {"PC": pc, "SP": c.uc.reg_read(UC_ARM_REG_SP),
"LR": c.uc.reg_read(UC_ARM_REG_LR)}
for i, r in enumerate((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)):
regs["R%d" % i] = c.uc.reg_read(r)
unmapped = []
for name, va in regs.items():
if va < 0x1000:
continue
try:
kind, pa = pt.describe(va)
except Exception:
continue
if pa is None:
unmapped.append((name, va, kind))
# Only call it an ABORT LOOP when an unmapped VA is actually present.
# Measured counter-example: after hand-mapping VA 0x20A000 the task moves on
# and then spins at 0x205DC0 with 51 distinct blocks and NO unmapped
# register -- a busy-wait, not a fault loop. Block RATE separates them too:
# the abort loop retires zero instructions (542k blocks in 8s) while the
# busy-wait runs flat out (15.1M in 8s). Reporting both as "abort loop"
# would be a confident wrong diagnosis.
kind = "abort_loop" if unmapped else "spin"
return dict(pc=pc, blocks=len(blocks), distinct=distinct,
unmapped=unmapped, kind=kind)
def main():
c, ccp, pt = build()
blocks, err, dt = run_task(c, pt)
print("bl_stage2_task: %d blocks, %d distinct, pc=%#x, %.1fs"
% (len(blocks), len(set(blocks)), c.uc.reg_read(UC_ARM_REG_PC), dt))
stall = diagnose_stall(c, pt, blocks)
if stall is None:
print(" stop reason: %s" % (err or "timeout (still running)"))
else:
if stall["kind"] == "abort_loop":
print(" stop reason: STALLED -- unbounded MMU abort loop, not progress")
else:
print(" stop reason: spinning (busy-wait), not an abort loop")
print(" PC pinned at %#x, %d blocks but only %d distinct, c.faults empty"
% (stall["pc"], stall["blocks"], stall["distinct"]))
if stall["unmapped"]:
for name, va, kind in stall["unmapped"]:
print(" %-3s = %#010x has NO guest translation (%s)"
% (name, va, kind))
print(" -> the faulting access can never retire, so the block")
print(" restarts forever. Map that VA (or model the SVC that")
print(" should have mapped it) rather than raising the timeout.")
else:
print(" no register holds an unmapped VA, so this is NOT a"
" translation fault -- it is a genuine busy-wait")
print(" ccp: %s errors=%d" % (dict(ccp.stats()), len(ccp.errors)))
json.dump(sorted(set(blocks)), open("/tmp/task_blocks.json", "w"))
# success == we got into the loader, not merely that something ran
reached_loader = any(0x203F24 <= b < 0x204100 for b in blocks) # bl_load_ta_image
print(" reached bl_load_ta_image: %s" % reached_loader)
return 0 if reached_loader else 1
if __name__ == "__main__":
sys.exit(main())
|