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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
|
#!/usr/bin/env python3
"""DMCUB (Xtensa) static analysis: functions, call graph, and annotated listings.
capstone-6 decodes Xtensa mnemonics/operands, but its branch and call operands
are unreliable (`op_str` shows a displacement and `operands[].imm` is wrong), so
call/jump targets are decoded here from the raw encodings:
CALLn op0=5, n=bits5:4, imm18=bits23:6 target = (PC&~3) + (imm18<<2) + 4
J op0=6, n=0, imm18=bits23:6 target = PC + 4 + imm18
B*(RRI8) op0=6/7 family, imm8=bits23:16 target = PC + 4 + imm8
Requires /home/user/.venvs/cs6/bin/python (capstone >= 6).
usage:
dmcub_dis.py <image> funcs list functions with call counts
dmcub_dis.py <image> hot [N] hottest call targets
dmcub_dis.py <image> dis <va> [<va>...] annotated listing of a function
dmcub_dis.py <image> xrefs <va> callers of a function
dmcub_dis.py <image> grep <regex> functions containing a mnemonic/op pattern
"""
import collections
import importlib.util
import os
import re
import struct
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
_s = importlib.util.spec_from_file_location('dmcub_cfg', os.path.join(HERE, 'dmcub_cfg.py'))
cfg = importlib.util.module_from_spec(_s)
_s.loader.exec_module(cfg)
def w24(p, i):
return p[i] | (p[i + 1] << 8) | (p[i + 2] << 16)
class NarrowBranch:
"""capstone 6 cannot decode Xtensa's density-option branches BEQZ.N/BNEZ.N
(op0=0xC with r >= 8) -- ~700 of them in a DCN 3.2 image, i.e. a large slice
of the control flow, including the `beqz.n a9` that selects
DMUB_CMD__ABM_INIT_CONFIG. This shim decodes them.
RI6: word = b0 | b1<<8 ; op0 = word & 0xF = 0xC
r = (word >> 4) & 0xF s = (word >> 8) & 0xF imm4 = word >> 12
imm6 = ((r & 3) << 4) | imm4 target = PC + 4 + imm6
r in 8..B -> beqz.n, r in C..F -> bnez.n (r < 8 is movi.n)
"""
__slots__ = ('address', 'size', 'bytes', 'mnemonic', 'op_str', 'operands', 'target_va')
def __init__(self, off_bytes, va):
word = off_bytes[0] | (off_bytes[1] << 8)
r = (word >> 4) & 0xF
s = (word >> 8) & 0xF
imm4 = (word >> 12) & 0xF
imm6 = ((r & 3) << 4) | imm4
self.address = va
self.size = 2
self.bytes = bytes(off_bytes[:2])
self.mnemonic = 'beqz.n' if (r & 0xC) == 0x8 else 'bnez.n'
self.target_va = va + 4 + imm6
self.op_str = 'a%d, . 0x%08x' % (s, self.target_va)
self.operands = []
@staticmethod
def match(p, i):
return (p[i] & 0xF) == 0xC and ((p[i] >> 4) & 0xF) >= 8
def sext(v, bits):
return v - (1 << bits) if v >> (bits - 1) else v
class Analysis:
"""Function set + call graph.
The function set starts from the dense run of `entry` prologues (the main code
region) and is then grown to a fixpoint with every call target that also has
an `entry` prologue. The growth step matters: some images keep code *below*
the dense run -- on dcn_3_2_1 `memcpy` itself lives there, and a single-cutoff
heuristic dropped it along with its 51 call sites. Stray `36 41 xx` matches
inside literal pools are not call targets, so they stay excluded.
"""
def __init__(self, path):
self.img = cfg.Image(path)
p = self.img.payload
all_ents = self.img.entries()
self.code_lo = self.img.code_start(all_ents)
self.ent_set = set(all_ents)
self._dis = {}
self._reach = {}
ents = [e for e in all_ents if e >= self.code_lo]
for _ in range(8):
self._set_funcs(ents, len(p))
self._build_calls()
extra = set()
for t in self.calls:
off = t - self.img.base
if off in self.ent_set and t not in self.funcs:
extra.add(off)
if not extra:
break
ents = sorted(set(ents) | extra)
self._dis = {}
def _set_funcs(self, ents, end):
self.ents = ents
self.funcs = {}
for i, s in enumerate(ents):
e = ents[i + 1] if i + 1 < len(ents) else end
self.funcs[self.img.va(s)] = (s, e)
def _reset_calls(self):
self.calls = collections.Counter()
self.callers = collections.defaultdict(set)
self.callsites = collections.defaultdict(list)
# ---- decoding -------------------------------------------------------
STOP = ('retw.n', 'retw', 'ret.n', 'ret', 'j', 'jx', 'ill')
def _decode_one(self, off, end):
p = self.img.payload
if off >= end:
return None
ins = next(self.img.md.disasm(p[off:min(off + 3, end)], self.img.va(off), 1), None)
if ins is not None and ins.mnemonic not in ('.byte', '.db'):
return ins
if off + 1 < end and NarrowBranch.match(p, off):
return NarrowBranch(p[off:off + 2], self.img.va(off))
return ins
def dis(self, fva):
"""Decode a function by **recursive descent**, then linear-fill the rest.
Three problems forced this shape, each of which silently corrupted
earlier results:
1. capstone's `skipdata` advances one byte on an unknown opcode, so one
undecodable instruction desynchronises the rest of the function (41%
of the code, most of the call graph). Xtensa length depends only on
op0, so a failed decode advances architecturally.
2. A *successful* decode can still be garbage that straddles a basic-block
boundary: an alignment byte before a switch case label gets absorbed
into a bogus 3-byte instruction, eating the real first instruction of
the case. That is how the ABM handler's `addi.n a10, a2, 4` vanished,
which in turn hid a known finding from the taint analysis. Branch,
call and jump-table targets are therefore used as decode anchors.
3. Decoding past the end of the body into data invents `jx` sites, so a
decoded `ill` terminates a path.
"""
if fva in self._dis:
return self._dis[fva]
s, e = self.funcs[fva]
base = self.img.base
p = self.img.payload
found = {}
skipped = set()
work = [s]
while work:
off = work.pop()
if off in found or off in skipped or not (s <= off < e):
continue
ins = self._decode_one(off, e)
if ins is None or ins.mnemonic in ('.byte', '.db'):
# Step over an opcode capstone does not know and keep walking:
# stopping here truncated the command dispatcher after 20
# instructions and hid every case stub from the taint analysis.
skipped.add(off)
work.append(off + (2 if 8 <= (p[off] & 0xF) <= 0xD else 3))
continue
found[off] = ins
m = ins.mnemonic
if m == 'ill':
del found[off]
continue
# branch/jump targets inside this function are anchors
t = self.target(ins)
if t is not None and s <= t - base < e and not m.startswith('call'):
work.append(t - base)
if m == 'jx':
for tbl in self._jx_table_targets(found, off, s, e):
work.append(tbl - base)
if m not in ('j', 'jx', 'retw.n', 'retw', 'ret.n', 'ret'):
work.append(off + ins.size)
# Instructions proven reachable by the descent, kept apart from the
# linear fill below: only these are real code. Audits that must not be
# fooled by data (jump-table scans, taint) use dis_reachable().
self._reach[fva] = set(found)
# linear fill of bytes no path reached (jump-table bodies, cold code)
i = s
while i < e:
if i in found:
i += found[i].size
continue
ins = self._decode_one(i, e)
if ins is not None and ins.mnemonic not in ('.byte', '.db', 'ill'):
nxt = i + ins.size
if not any(k in found for k in range(i, nxt)):
found[i] = ins
i = nxt
else:
i += 2 if 8 <= (p[i] & 0xF) <= 0xD else 3
out = [found[k] for k in sorted(found)]
self._dis[fva] = out
return out
def dis_reachable(self, fva):
"""Only the instructions the recursive descent actually reached."""
ins = self.dis(fva)
reach = self._reach.get(fva, set())
return [x for x in ins if (x.address - self.img.base) in reach]
def indirect_targets(self, fva):
"""callx site VA -> candidate callees.
Sub-command dispatch in this firmware goes through tables of function
pointers (`l32r base; l32i fp, base[+k]; callx8 fp`), e.g. the PSR op
table at 0x6001a620 and the Replay one at 0x6001a680. Without resolving
these, a handler's reachable set is just its own stub and an audit of it
is worthless.
Resolution: nearest preceding `l32r` literal is the table base; a constant
`l32i` displacement selects one entry, otherwise every consecutive entry
that is a known function start is a candidate.
"""
out = {}
ins = self.dis_reachable(fva)
for i, x in enumerate(ins):
if not x.mnemonic.startswith('callx'):
continue
reg = x.op_str.strip()
base = None
disp = None
track = {reg}
for y in range(i - 1, max(-1, i - 30), -1):
z = ins[y]
p = [q.strip() for q in z.op_str.split(',')]
if not p or p[0] not in track:
continue
if z.mnemonic in ('l32i', 'l32i.n') and len(p) == 3:
try:
disp = int(p[2], 0) if disp is None else disp
except ValueError:
pass
track.add(p[1])
elif z.mnemonic == 'l32r':
base = self.lit(z)
break
elif z.mnemonic in ('mov', 'mov.n') and len(p) == 2:
track.add(p[1])
elif z.mnemonic == 'addx4' and len(p) == 3:
track.add(p[2])
disp = None # variable index -> whole table
if base is None:
continue
cands = []
if disp:
v = self.img.word(base + disp)
if v in self.funcs:
cands = [v]
if not cands:
for n in range(64):
v = self.img.word(base + 4 * n)
if v is None:
break
if v in self.funcs:
cands.append(v)
elif v != 0:
break
if cands:
out[x.address] = cands
return out
def _jx_table_targets(self, found, jx_off, s, e):
"""Entries of the jump table feeding a `jx`, so case bodies get anchored."""
base = None
for k in sorted(found, reverse=True):
if k >= jx_off:
continue
if found[k].mnemonic == 'l32r':
base = self.lit(found[k])
break
if base is None:
return []
out = []
for n in range(256):
v = self.img.word(base + 4 * n)
if v is None or not (self.img.base <= v < self.img.base + len(self.img.payload)):
break
out.append(v)
return out
def coverage(self):
"""Decode success over the code region: bytes consumed by successfully
decoded instructions vs bytes stepped over.
Reported this way rather than as a fraction of function spans, because
function spans run to the next `entry` prologue and therefore include
inter-function data, which makes a span-based figure meaningless.
"""
p = self.img.payload
dec = tot = 0
i = self.code_lo
end = len(p)
while i < end - 3:
ins = self._decode_one(i, end)
if ins is not None and ins.mnemonic not in ('.byte', '.db'):
dec += ins.size
tot += ins.size
i += ins.size
else:
step = 2 if 8 <= (p[i] & 0xF) <= 0xD else 3
tot += step
i += step
return dec, tot
def target(self, ins):
"""Resolved branch/call target VA, or None."""
if isinstance(ins, NarrowBranch):
return ins.target_va
b = ins.bytes
if not b:
return None
op0 = b[0] & 0xF
pc = ins.address
if ins.mnemonic.startswith('call') and not ins.mnemonic.startswith('callx'):
if op0 != 5 or len(b) < 3:
return None
imm18 = sext((w24(b, 0) >> 6) & 0x3FFFF, 18)
return (pc & ~3) + (imm18 << 2) + 4
if ins.mnemonic == 'j' or ins.mnemonic.startswith('b'):
# Xtensa has two wide branch encodings -- RRI8 (imm8 at bits 23:16,
# e.g. BEQI/BGEU) and BRI12 (imm12 at bits 23:12, e.g. BEQZ/BNEZ) --
# and decoding every branch as RRI8 sent the recursive descent to
# wrong addresses, which silently truncated big handlers (PSR: 59 of
# ~300 instructions reached). capstone prints the displacement
# relative to the instruction address for both forms, so use it.
m = re.search(r'\.\s*([+-])\s*(0x[0-9a-fA-F]+|\d+)\s*$', ins.op_str)
if m:
disp = int(m.group(2), 0)
return pc + (disp if m.group(1) == '+' else -disp)
m = re.search(r'0x([0-9a-fA-F]{8})\s*$', ins.op_str)
if m:
return int(m.group(1), 16)
return None
return None
def _build_calls(self):
self._reset_calls()
for fva in self.funcs:
for x in self.dis(fva):
if x.mnemonic.startswith('call') and not x.mnemonic.startswith('callx'):
t = self.target(x)
if t is None:
continue
self.calls[t] += 1
self.callers[t].add(fva)
self.callsites[t].append((fva, x.address))
# ---- helpers --------------------------------------------------------
def lit(self, ins):
"""Value loaded by an l32r, or None."""
if ins.mnemonic != 'l32r':
return None
try:
la = int(ins.op_str.split()[-1], 0)
except ValueError:
return None
return self.img.word(la)
def func_of(self, va):
best = None
for f, (s, e) in self.funcs.items():
if f <= va < self.img.va(e):
if best is None or f > best:
best = f
return best
def listing(self, fva, annotate=True):
out = []
for x in self.dis(fva):
note = ''
t = self.target(x)
if t is not None:
note = ' -> 0x%08x%s' % (t, ' [fn]' if t in self.funcs else '')
v = self.lit(x)
if v is not None:
note = ' = 0x%08x' % v
out.append('%08x %-14s %-9s %-28s%s' % (
x.address, x.bytes.hex(), x.mnemonic, x.op_str, note if annotate else ''))
return out
def main():
path, cmd = sys.argv[1], sys.argv[2]
a = Analysis(path)
if cmd == 'funcs':
print('%d functions, code region from payload 0x%x' % (len(a.funcs), a.code_lo))
for f in sorted(a.funcs):
s, e = a.funcs[f]
print(' 0x%08x size=%-6d calls_in=%-4d' % (f, e - s, a.calls[f]))
elif cmd == 'hot':
n = int(sys.argv[3]) if len(sys.argv) > 3 else 20
for t, c in a.calls.most_common(n):
s = a.funcs.get(t)
print(' 0x%08x calls=%-5d callers=%-4d size=%s' % (
t, c, len(a.callers[t]), (s[1] - s[0]) if s else 'not-a-known-entry'))
elif cmd == 'dis':
for v in sys.argv[3:]:
fva = int(v, 0)
if fva not in a.funcs:
fva2 = a.func_of(fva)
print('# 0x%x is inside 0x%08x' % (fva, fva2))
fva = fva2
s, e = a.funcs[fva]
print('=== 0x%08x size=%d callers=%d' % (fva, e - s, len(a.callers[fva])))
print('\n'.join(a.listing(fva)))
elif cmd == 'xrefs':
t = int(sys.argv[3], 0)
for f, site in sorted(a.callsites[t]):
print(' 0x%08x called from 0x%08x (fn 0x%08x)' % (t, site, f))
elif cmd == 'grep':
pat = re.compile(sys.argv[3])
for f in sorted(a.funcs):
for x in a.dis(f):
line = '%s %s' % (x.mnemonic, x.op_str)
if pat.search(line):
print(' fn 0x%08x @0x%08x %s' % (f, x.address, line))
else:
print(__doc__)
if __name__ == '__main__':
main()
|