99 lines
3.4 KiB
Python
Executable File
99 lines
3.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""xref + slack scanner: LE32 pointers into code/rodata, NUL strings, zero gaps.
|
|
|
|
Usage: lofi_xref.py "Lofi-12 XT.bin" [--find TEXT] [--strings-min 6]
|
|
"""
|
|
import argparse, os, struct, sys
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from lofi_image import parse_image, find_sect_by_role
|
|
|
|
def le_words(payload):
|
|
for i in range(0, len(payload) - 3, 4):
|
|
yield i, struct.unpack('<I', payload[i:i+4])[0]
|
|
|
|
def zero_runs(payload, minlen=32):
|
|
out, i, n = [], 0, len(payload)
|
|
while i < n:
|
|
if payload[i] == 0:
|
|
j = i
|
|
while j < n and payload[j] == 0:
|
|
j += 1
|
|
if j - i >= minlen:
|
|
out.append((i, j - i))
|
|
i = j
|
|
else:
|
|
i += 1
|
|
return out
|
|
|
|
def c_strings(payload, minlen=6):
|
|
out, i, n = [], 0, len(payload)
|
|
while i < n:
|
|
if 32 <= payload[i] < 127:
|
|
j = i
|
|
while j < n and 32 <= payload[j] < 127:
|
|
j += 1
|
|
if j - i >= minlen and j < n and payload[j] == 0:
|
|
out.append((i, payload[i:j].decode()))
|
|
i = max(j, i + 1)
|
|
else:
|
|
i += 1
|
|
return out
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument('image')
|
|
ap.add_argument('--find', default=None)
|
|
ap.add_argument('--strings-min', type=int, default=6)
|
|
ap.add_argument('--slack-min', type=int, default=64)
|
|
a = ap.parse_args()
|
|
d = open(a.image, 'rb').read()
|
|
info = parse_image(d)
|
|
code = find_sect_by_role(info, 'code')
|
|
ro = find_sect_by_role(info, 'rodata')
|
|
print(f"code: sect{code['index']} {code['addr']:08X}..{code['end']:08X} len={code['len']}")
|
|
print(f"rodata: sect{ro['index']} {ro['addr']:08X}..{ro['end']:08X} len={ro['len']}")
|
|
# pointers in rodata -> code, and rodata -> self
|
|
to_code = to_self = 0
|
|
code_hits = []
|
|
for off, w in le_words(ro['payload']):
|
|
if code['addr'] <= w < code['end']:
|
|
to_code += 1
|
|
code_hits.append((off, w))
|
|
elif ro['addr'] <= w < ro['end']:
|
|
to_self += 1
|
|
print(f"rodata xrefs: {to_code} -> code, {to_self} -> self")
|
|
gaps = zero_runs(ro['payload'], a.slack_min)
|
|
gaps.sort(key=lambda t: -t[1])
|
|
print(f"top zero gaps in rodata (min {a.slack_min}):")
|
|
for off, ln in gaps[:10]:
|
|
print(f" +{off:#x} (file {ro['off']+12+off:#x}) len={ln}")
|
|
if a.find:
|
|
needle = a.find.encode()
|
|
hits = []
|
|
p = ro['payload']
|
|
s = 0
|
|
while True:
|
|
i = p.find(needle, s)
|
|
if i < 0:
|
|
break
|
|
hits.append(i)
|
|
s = i + 1
|
|
print(f"'{a.find}': {len(hits)} hit(s) in rodata")
|
|
for h in hits:
|
|
faddr = ro['addr'] + h
|
|
refs = [off for off, w in code_hits if False] # placeholder
|
|
# find pointers TO this string: scan rodata words == faddr
|
|
# (vtable-adjacent tables) — cheap exact scan
|
|
ptrs = []
|
|
for off, w in le_words(ro['payload']):
|
|
if w == faddr:
|
|
ptrs.append(ro['off'] + 12 + off)
|
|
print(f" +{h:#x} file={ro['off']+12+h:#x} load={faddr:08X} "
|
|
f"referenced_by_{len(ptrs)}={['%#x' % x for x in ptrs[:8]]}")
|
|
else:
|
|
strs = c_strings(ro['payload'], a.strings_min)
|
|
print(f"NUL strings len>={a.strings_min} in rodata: {len(strs)}")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|