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
|
#!/usr/bin/env python3
"""Port current-build function symbols onto older PSP firmware builds and
report where they DIVERGE.
Naive same-offset comparison is WRONG: code shifts between builds, so a function
at VA X in the new build sits at VA X-k in the old one, and a byte/mnemonic
compare at the same offset comes back "changed" for hundreds of functions that
are actually identical. (Observed: sos .51-vs-.54 reported 256 "LOGIC" changes;
spot-checking showed the sequences were identical, merely shifted 0x40 bytes.)
So we do alignment-independent matching:
1. index the OLD image: for every 2-byte-aligned offset in its code ranges,
hash the next N mnemonics;
2. for each named function in the NEW build, look up its first-N mnemonics to
get candidate offsets, then verify the FULL normalised mnemonic sequence.
Classification per function:
IDENT found in old build, byte-identical
RELOC found, mnemonics identical, bytes differ (immediates/addresses only)
MOVED found at a different address (still counts as unchanged logic)
DIVERGED not found anywhere -> logic really changed, or function is new
Only DIVERGED is interesting for vulnerability archaeology.
usage: vport.py <new.elf> <funcs.json> <old.elf> [old.elf ...] [--list]
"""
import json, sys, os
from collections import defaultdict
from capstone import Cs, CS_ARCH_ARM, CS_MODE_THUMB, CS_MODE_LITTLE_ENDIAN
OFF = 0x1000 # mkelf.py DATA_OFF: file offset of VA 0
NGRAM = 8 # anchor length in instructions
CODE_RANGES = [(0x0, 0x7022), (0x12000, 0x17dd0)] # sOS CODE1, CODE2 (VA)
md = Cs(CS_ARCH_ARM, CS_MODE_THUMB | CS_MODE_LITTLE_ENDIAN)
md.detail = False
def seq(buf, va, maxn=None):
out = []
for ins in md.disasm(buf, va):
out.append(ins.mnemonic)
if maxn and len(out) >= maxn:
break
return tuple(out)
def build_index(old, ranges):
idx = defaultdict(list)
for lo, hi in ranges:
for va in range(lo, hi, 2):
fo = va + OFF
if fo + 64 > len(old):
continue
s = seq(old[fo:fo + 64], va, NGRAM)
if len(s) == NGRAM:
idx[s].append(va)
return idx
def main():
new_p, funcs_p = sys.argv[1], sys.argv[2]
olds = [a for a in sys.argv[3:] if not a.startswith('--')]
show = '--list' in sys.argv
new = open(new_p, 'rb').read()
funcs = json.load(open(funcs_p))
F = sorted(((int(k, 16), v[0], v[1]) for k, v in funcs.items()))
for old_p in olds:
old = open(old_p, 'rb').read()
if old == new:
continue
idx = build_index(old, CODE_RANGES)
counts = defaultdict(int)
diverged = []
for st, nm, en in F:
size = en - st
fs = st + OFF
if fs + size > len(new):
continue
body = new[fs:fs + size]
s_new = seq(body, st)
if len(s_new) < NGRAM:
counts['TINY'] += 1
continue
anchor = s_new[:NGRAM]
hit = None
for cand in idx.get(anchor, ()):
co = cand + OFF
if co + size > len(old):
continue
if seq(old[co:co + size], cand) == s_new:
hit = cand
break
if hit is None:
counts['DIVERGED'] += 1
diverged.append((nm, st, size))
elif hit == st:
counts['IDENT' if old[fs:fs + size] == body else 'RELOC'] += 1
else:
counts['MOVED'] += 1
ver = os.path.basename(old_p)
tot = sum(counts.values())
print("=== %s (%d functions compared)" % (ver, tot))
print(" IDENT=%d RELOC=%d MOVED=%d TINY=%d **DIVERGED=%d**" %
(counts['IDENT'], counts['RELOC'], counts['MOVED'],
counts['TINY'], counts['DIVERGED']))
if show:
for nm, st, sz in sorted(diverged, key=lambda x: -x[2]):
print(" DIVERGED %-42s @0x%05x %d B" % (nm, st, sz))
print()
if __name__ == "__main__":
main()
|