etc.git


etc

~ scripts and random things
git clone https://github.com/djnnvx/etc.git
log | files | refs | archive

commit 6bc4dcd5d4b5ac820b9fcf4cc640f30c59614f76
parent dfafd1909a887588eebe34e8d7c1e8a19e296f62
Author: djnn <[email protected]>
Date: Fri, 18 Sep 2026 20:32:37 +0200

tiny disassemblers

Diffstat:
Mscripts/a64dis.py | 7++-----
Ascripts/armdis.py | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

2 files changed, 204 insertions(+), 5 deletions(-)

diff --git a/scripts/a64dis.py b/scripts/a64dis.py
@@ -5,10 +5,9 @@ Usage:
   a64dis.py <elf> str <substring>         # find string + its file vaddr(s)
   a64dis.py <elf> xref <hexvaddr>         # find ADRP+ADD code refs to a vaddr
   a64dis.py <elf> dis <hexvaddr> [count]  # disassemble count insns at vaddr
-  a64dis.py <elf> plt                     # map .rela.plt slots -> import names
-No rebase: addresses are file vaddrs (p_vaddr based), matching Ghidra after you
-add its image base. Resolves adrp+add string loads.
+Addresses are file vaddrs (p_vaddr based), not rebased. Add Ghidra's image base to match.
 """
+
 import sys, struct
 from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN
 from elftools.elf.elffile import ELFFile
@@ -44,7 +43,6 @@ def find_str(e,sub):
         while True:
             j=data.find(sub,i)
             if j<0: break
-            # back up to string start
             k=j
             while k>0 and 0x20<=data[k-1]<0x7f: k-=1
             end=data.find(b'\x00',j)
@@ -59,7 +57,6 @@ def disasm(e,va,count):
         print(f"0x{ins.address:x}:  {ins.mnemonic:8s} {ins.op_str}")
 
 def xref(e,target):
-    # scan executable segments for adrp+add producing target page+off
     md=Cs(CS_ARCH_ARM64,CS_MODE_LITTLE_ENDIAN)
     for s in e.iter_segments():
         if s['p_type']!='PT_LOAD' or not (s['p_flags']&1): continue
diff --git a/scripts/armdis.py b/scripts/armdis.py
@@ -0,0 +1,202 @@
+#!/usr/bin/env python3
+"""ARM32 static-analysis helper for stripped firmware ELFs. AArch64 sibling: a64dis.py.
+
+Usage (either grammar works, to match a64dis.py):
+  armdis.py <elf> dis <hexvaddr> [count]   # disassemble, resolving PLT names and strings
+  armdis.py <elf> str <substring>          # find a string, print its file offset and vaddr
+  armdis.py <elf> xref <hexvaddr>          # find code that materialises that vaddr
+  armdis.py <elf> ptr <hexvaddr>           # find pointers to that vaddr, and name the section
+  armdis.py <elf> func <hexvaddr> [span]   # enclosing function: every call and string, in order
+  armdis.py <elf> plt [name]               # PLT stub address, or list every stub
+
+Needs capstone and pyelftools.
+"""
+import re, sys
+from capstone import Cs, CS_ARCH_ARM, CS_MODE_ARM
+from elftools.elf.elffile import ELFFile
+
+
+class Img:
+    def __init__(self, path):
+        self.f = open(path, 'rb')
+        self.elf = ELFFile(self.f)
+        self.secs = [(s['sh_addr'], s['sh_addr'] + s['sh_size'], s.data(), s.name)
+                     for s in self.elf.iter_sections()
+                     if s['sh_addr'] and s.header['sh_type'] != 'SHT_NOBITS']
+        self.plt = {}
+        dynsym = self.elf.get_section_by_name('.dynsym')
+        rel = self.elf.get_section_by_name('.rel.plt')
+        if rel is not None and dynsym is not None:
+            base = self.elf.get_section_by_name('.plt')['sh_addr'] + 20
+            for i, r in enumerate(rel.iter_relocations()):
+                self.plt[base + i * 12] = dynsym.get_symbol(r['r_info_sym']).name
+
+    def va_of(self, file_off):
+        for s in self.elf.iter_sections():
+            a, o, n = s['sh_addr'], s['sh_offset'], s['sh_size']
+            if a and s.header['sh_type'] != 'SHT_NOBITS' and o <= file_off < o + n:
+                return file_off - o + a
+        return None
+
+    def sec_of(self, file_off):
+        for s in self.elf.iter_sections():
+            a, o, n = s['sh_addr'], s['sh_offset'], s['sh_size']
+            if a and s.header['sh_type'] != 'SHT_NOBITS' and o <= file_off < o + n:
+                return s.name
+        return '?'
+
+    def read(self, va, n):
+        for lo, hi, data, _ in self.secs:
+            if lo <= va < hi:
+                return data[va - lo: va - lo + n]
+        return b''
+
+    def cstr(self, va, n=180):
+        b = self.read(va, n)
+        if not b:
+            return None
+        b = b.split(b'\x00')[0]
+        if len(b) >= 3 and all(0x20 <= c < 0x7f or c in b'\t\n' for c in b):
+            return b.decode('ascii', 'replace')
+        return None
+
+
+LDR_PC = re.compile(r'^(\w+), \[pc, #(-?0x[0-9a-f]+|-?\d+)\]$')
+ADD_PC = re.compile(r'^(\w+), pc, (\w+)$')
+
+
+def run(path, start, count):
+    img = Img(path)
+    md = Cs(CS_ARCH_ARM, CS_MODE_ARM)
+    code = img.read(start, count * 4)
+    reg = {}
+    for ins in md.disasm(code, start):
+        note = ''
+        if ins.mnemonic in ('bl', 'blx', 'b') and ins.op_str.startswith('#'):
+            t = int(ins.op_str[1:], 0)
+            note = f'   -> {img.plt[t]}@plt' if t in img.plt else f'   -> sub_{t:x}'
+        elif ins.mnemonic.startswith('ldr') and (m := LDR_PC.match(ins.op_str)):
+            raw = img.read(ins.address + 8 + int(m.group(2), 0), 4)
+            if len(raw) == 4:
+                reg[m.group(1)] = v = int.from_bytes(raw, 'little')
+                note = f'   = 0x{v:08x}'
+                if (t := img.cstr(v)):
+                    note += f'  "{t}"'
+        elif ins.mnemonic == 'add' and (m := ADD_PC.match(ins.op_str)):
+            if (v := reg.get(m.group(2))) is not None:
+                reg[m.group(1)] = a = (v + ins.address + 8) & 0xffffffff
+                note = f'   = 0x{a:08x}'
+                if (t := img.cstr(a)):
+                    note += f'  "{t}"'
+        elif ins.mnemonic == 'mov' and ',' in ins.op_str:
+            d, srcs = ins.op_str.split(', ', 1)
+            reg[d] = reg.get(srcs)
+        print(f'{ins.address:08x}  {ins.bytes.hex():<8}  {ins.mnemonic:<8} {ins.op_str}{note}')
+
+
+def func_start(img, addr, back=0x4000):
+    """Scan back for an ARM prologue `push {..., lr}`."""
+    for a in range(addr, addr - back, -4):
+        w = int.from_bytes(img.read(a, 4), 'little')
+        if (w & 0xffff0000) == 0xe92d0000 and (w & 0x4000):
+            return a
+    return None
+
+
+def summarise(path, addr, span=0x900):
+    img = Img(path)
+    start = func_start(img, addr)
+    if start is None:
+        print('no prologue found')
+        return
+    print(f'function 0x{start:08x}  (contains 0x{addr:08x})')
+    md = Cs(CS_ARCH_ARM, CS_MODE_ARM)
+    reg = {}
+    for ins in md.disasm(img.read(start, span), start):
+        if ins.mnemonic.startswith('ldr') and (m := LDR_PC.match(ins.op_str)):
+            raw = img.read(ins.address + 8 + int(m.group(2), 0), 4)
+            if len(raw) == 4:
+                reg[m.group(1)] = int.from_bytes(raw, 'little')
+        elif ins.mnemonic == 'add' and (m := ADD_PC.match(ins.op_str)):
+            if (v := reg.get(m.group(2))) is not None:
+                a2 = (v + ins.address + 8) & 0xffffffff
+                reg[m.group(1)] = a2
+                if (t := img.cstr(a2)):
+                    print(f'  0x{ins.address:08x}  STR  "{t[:88]}"')
+        elif ins.mnemonic in ('bl', 'blx') and ins.op_str.startswith('#'):
+            t = int(ins.op_str[1:], 0)
+            print(f'  0x{ins.address:08x}  bl   {img.plt.get(t, f"sub_{t:x}")}')
+
+
+def xrefs(path, target):
+    """Find code that materialises `target` through the PIC pair."""
+    img = Img(path)
+    lo, _, data, _ = [x for x in img.secs if x[3] == '.text'][0]
+    import struct
+    hits = []
+    for off in range(0, len(data) - 3, 4):
+        w = struct.unpack_from('<I', data, off)[0]
+        if (w & 0xffff0ff0) != 0xe08f0000:      # add rD, pc, rM
+            continue
+        rd, rm = (w >> 12) & 0xf, w & 0xf
+        if rd != rm:
+            continue
+        pc = lo + off
+        for back in range(4, 96, 4):
+            if off - back < 0:
+                break
+            pw = struct.unpack_from('<I', data, off - back)[0]
+            if (pw & 0xffff0000) in (0xe59f0000, 0xe51f0000) and ((pw >> 12) & 0xf) == rd:  # ldr rD,[pc,#+/-imm]
+                delta = (pw & 0xfff) if (pw & 0xffff0000) == 0xe59f0000 else -(pw & 0xfff)
+                raw = img.read((lo + off - back) + 8 + delta, 4)
+                if len(raw) == 4 and (int.from_bytes(raw, 'little') + pc + 8) & 0xffffffff == target:
+                    hits.append(pc)
+                break
+    return hits
+
+SUBCOMMANDS = ('dis', 'str', 'xref', 'ptr', 'func', 'plt')
+
+if __name__ == '__main__':
+    if len(sys.argv) > 2 and sys.argv[2] in SUBCOMMANDS:
+        sys.argv[2] = '--' + sys.argv[2]
+    if len(sys.argv) > 2 and sys.argv[2] == '--dis':
+        del sys.argv[2]
+    path = sys.argv[1]
+    if sys.argv[2] == '--func':
+        summarise(path, int(sys.argv[3], 0), int(sys.argv[4], 0) if len(sys.argv) > 4 else 0x900)
+        sys.exit()
+    if sys.argv[2] == '--xref':
+        for a in xrefs(path, int(sys.argv[3], 0)):
+            print(f'0x{a:08x}')
+        sys.exit()
+    if sys.argv[2] == '--str':
+        img = Img(path)
+        needle = sys.argv[3].encode()
+        d = open(path, 'rb').read()
+        i = d.find(needle)
+        while i >= 0:
+            va = img.va_of(i)
+            shown = f'0x{va:08x}' if va is not None else '(not mapped)'
+            print(f'file 0x{i:x}  vaddr {shown}  {d[i:i+70].split(bytes(1))[0].decode("latin-1")!r}')
+            i = d.find(needle, i + 1)
+        sys.exit()
+    if sys.argv[2] == '--ptr':
+        import struct as _s
+        img = Img(path)
+        d = open(path, 'rb').read()
+        needle = _s.pack('<I', int(sys.argv[3], 0))
+        i = d.find(needle)
+        while i >= 0:
+            va = img.va_of(i)
+            if va is not None:
+                print(f'pointer at file 0x{i:x}  vaddr 0x{va:08x}  section {img.sec_of(i)}')
+            i = d.find(needle, i + 1)
+        sys.exit()
+    if sys.argv[2] == '--plt':
+        img = Img(path)
+        want = sys.argv[3] if len(sys.argv) > 3 else None
+        for a, n in sorted(img.plt.items()):
+            if want is None or n == want:
+                print(f'0x{a:08x}  {n}')
+        sys.exit()
+    run(path, int(sys.argv[2], 0), int(sys.argv[3]) if len(sys.argv) > 3 else 60)