Files
fengqiaoqu 108d4833eb sdis --xref scans to the end of the PLT like --callers (#43)
A main module exports too few symbols to bound its text; max() over none raised, and PLT stubs' references were missed.
2026-10-22 08:08:00 +08:00

125 lines
6.6 KiB
Python

# Disassembly with symbols: annotates bl/b/adrp targets with dynsym defined symbols and PLT import names.
# Usage: sdis.py <original image .orig.bin> <symbol name|0xoffset> [count=200]
# sdis.py <original image> --sym 0xoffset... offset → function+offset
# sdis.py <original image> --xref 0xaddr... who references this address with adrp+add
# sdis.py <original image> --fn 0xaddr... start of the containing function (eh_frame function table)
# sdis.py <original image> --find <substring> list defined symbols whose name contains the substring
# sdis.py <original image> --callers <symbol name> list call sites that directly bl/b to this symbol
import struct, sys, bisect
from capstone import *
from capstone.arm64 import ARM64_OP_IMM
img = open(sys.argv[1], 'rb').read()
md = Cs(CS_ARCH_ARM64, CS_MODE_ARM); md.detail = True
m = struct.unpack_from('<I', img, 4)[0]; dyn = m + struct.unpack_from('<i', img, m + 4)[0]
t = {}; o = dyn
while True:
tag, v = struct.unpack_from('<qQ', img, o); o += 16
if tag == 0: break
t.setdefault(tag, v)
SYM, STR = t[6], t[5]
def cstr(off): return img[off:img.index(b'\0', off)].decode()
nsym = (STR - SYM) // 24
defs = {}; byaddr = {}
for i in range(1, nsym):
nm, info, oth, shn, val, sz = struct.unpack_from('<IBBHQQ', img, SYM + i * 24)
if shn and val:
n = cstr(STR + nm); defs[n] = (val, sz); byaddr.setdefault(val, n)
funcs = sorted(byaddr.items()); addrs = [a for a, _ in funcs]
# PLT: adrp x16 / ldr x17,[x16,#o] / add / br x17 → JUMP_SLOT name
got = {}
for i in range(0, t.get(2, 0), 24):
off, info, add = struct.unpack_from('<QQq', img, t[23] + i)
got[off] = cstr(STR + struct.unpack_from('<I', img, SYM + (info >> 32) * 24)[0])
plt = {}
def plt_scan():
lo = t[23] # The PLT is right near the end of text; scan text directly for adrp x16 sequences
for o in range(0, len(img) - 16, 4):
w = struct.unpack_from('<I', img, o)[0]
if w & 0x9F00001F != 0x90000010: continue
ins = list(md.disasm(img[o:o + 16], o))
if len(ins) == 4 and ins[1].mnemonic == 'ldr' and ins[3].mnemonic == 'br':
a = ins[0].operands[1].imm + ins[1].operands[1].mem.disp
if a in got: plt[o] = got[a]
def name(a):
if a in plt: return 'PLT:' + plt[a]
i = bisect.bisect_right(addrs, a) - 1
if i < 0: return hex(a)
return funcs[i][1] + ('' if a == funcs[i][0] else f'+{a - funcs[i][0]:#x}')
def fn_starts(): # All function starts in the eh_frame_hdr binary search table (ascending)
m0 = struct.unpack_from('<I', img, 4)[0]
ehs = m0 + struct.unpack_from('<i', img, m0 + 16)[0]
ver, penc, cenc, tenc = img[ehs:ehs + 4]
def rd(o, enc):
fmt, app = enc & 0xf, enc & 0x70
v, sz = {0x3: (struct.unpack_from('<I', img, o)[0], 4), 0xb: (struct.unpack_from('<i', img, o)[0], 4),
0x4: (struct.unpack_from('<Q', img, o)[0], 8)}[fmt]
if app == 0x10: v += o
if app == 0x30: v += ehs
return v, sz
o = ehs + 4; _, k = rd(o, penc); o += k; n, k = rd(o, cenc); o += k
out = []
for _ in range(n):
a, k = rd(o, tenc); o += k; _, k = rd(o, tenc); o += k; out.append(a)
return sorted(set(out))
def resolve(s): # 0xoffset, a defined symbol, or an imported one (its PLT stub: --callers then lists the module's calls of that import)
if s.startswith('0x'): return int(s, 16)
if s in defs: return defs[s][0]
if not plt: plt_scan()
for a, n in plt.items():
if n == s: return a
sys.exit(f'{s}: neither defined nor imported through the PLT')
if sys.argv[2] == '--sym': # sdis.py <image> --sym 0xoffset... give the containing function for each one
plt_scan()
for a in sys.argv[3:]: print(a, name(int(a, 16)))
sys.exit()
if sys.argv[2] == '--xref': # sdis.py <image> --xref 0xaddr... find the instructions that build this address with adrp+add/ldr
want = {int(a, 16) for a in sys.argv[3:]}
textend = max((v + s for v, s in defs.values() if v < 0x10000000 and s), default=0)
if not plt: plt_scan()
if plt: textend = max(textend, max(plt) + 16) # the PLT ends the text segment (a main exports too few symbols to bound it)
for o in range(0, min(len(img), textend), 4):
w = struct.unpack_from('<I', img, o)[0]
if w & 0x9F000000 != 0x90000000: continue
rd = w & 31; imm = (((w >> 5) & 0x7FFFF) << 2) | ((w >> 29) & 3)
if imm & (1 << 20): imm -= 1 << 21
page = (o & ~0xFFF) + (imm << 12)
for i in md.disasm(img[o + 4:o + 24], o + 4):
ops = i.operands
if i.mnemonic == 'add' and len(ops) == 3 and ops[1].reg and i.reg_name(ops[1].reg) == f'x{rd}' and ops[2].type == ARM64_OP_IMM:
if page + ops[2].imm in want: print(f'{i.address:#x} {name(i.address)} -> {page + ops[2].imm:#x}')
break
sys.exit()
if sys.argv[2] == '--fn': # sdis.py <image> --fn 0xaddr... start of the containing function (eh_frame)
st = fn_starts()
for a in sys.argv[3:]:
a = int(a, 16); k = bisect.bisect_right(st, a) - 1
if k < 0 or k + 1 >= len(st): print(f'{a:#x} not in the function table'); continue # Before the first function / after the last function (e.g. the rewrite area): report it on its own, without dragging down the whole batch
print(f'{a:#x} function {st[k]:#x} +{a - st[k]:#x} next function {st[k + 1]:#x}')
sys.exit()
if sys.argv[2] == '--find':
for n, (v, sz) in sorted(defs.items(), key=lambda x: x[1]):
if sys.argv[3] in n: print(f'{v:#x} {sz:#x} {n}')
sys.exit()
if sys.argv[2] == '--callers':
tgt = resolve(sys.argv[3]); textend = max((v + s for v, s in defs.values() if v < 0x10000000 and s), default=0) # Coarse upper bound
if not plt: plt_scan()
if plt: textend = max(textend, max(plt) + 16) # the PLT ends the text segment (a main exports too few symbols to bound it)
for o in range(0, min(len(img), textend), 4):
w = struct.unpack_from('<I', img, o)[0]
if w >> 26 in (0x25, 0x05) and o + ((((w & 0x3ffffff) ^ 0x2000000) - 0x2000000) * 4) == tgt:
print(f'{o:#x} {name(o)}')
sys.exit()
plt_scan()
a = resolve(sys.argv[2]); n = int(sys.argv[3]) if len(sys.argv) > 3 else 200
for ins in md.disasm(img[a:a + n * 4], a):
ann = ''
if ins.mnemonic in ('bl', 'b') or ins.mnemonic.startswith('b.') or ins.mnemonic in ('cbz', 'cbnz', 'tbz', 'tbnz', 'adr'):
op = ins.operands[-1]
if op.type == ARM64_OP_IMM: ann = ' ; ' + name(op.imm)
print(f'{ins.address:#9x} {ins.mnemonic:8s} {ins.op_str}{ann}')
if ins.mnemonic == 'ret' and n > 60 and ins.address > a + 8 and len(sys.argv) <= 3: break