etc.git


etc

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

armdis.py (8280B)

      1 #!/usr/bin/env python3
      2 """ARM32 static-analysis helper for stripped firmware ELFs. AArch64 sibling: a64dis.py.
      3 
      4 Usage (either grammar works, to match a64dis.py):
      5   armdis.py <elf> dis <hexvaddr> [count]   # disassemble, resolving PLT names and strings
      6   armdis.py <elf> str <substring>          # find a string, print its file offset and vaddr
      7   armdis.py <elf> xref <hexvaddr>          # find code that materialises that vaddr
      8   armdis.py <elf> ptr <hexvaddr>           # find pointers to that vaddr, and name the section
      9   armdis.py <elf> func <hexvaddr> [span]   # enclosing function: every call and string, in order
     10   armdis.py <elf> plt [name]               # PLT stub address, or list every stub
     11 
     12 Needs capstone and pyelftools.
     13 """
     14 import re, sys
     15 from capstone import Cs, CS_ARCH_ARM, CS_MODE_ARM
     16 from elftools.elf.elffile import ELFFile
     17 
     18 
     19 class Img:
     20     def __init__(self, path):
     21         self.f = open(path, 'rb')
     22         self.elf = ELFFile(self.f)
     23         self.secs = [(s['sh_addr'], s['sh_addr'] + s['sh_size'], s.data(), s.name)
     24                      for s in self.elf.iter_sections()
     25                      if s['sh_addr'] and s.header['sh_type'] != 'SHT_NOBITS']
     26         self.plt = {}
     27         dynsym = self.elf.get_section_by_name('.dynsym')
     28         rel = self.elf.get_section_by_name('.rel.plt')
     29         if rel is not None and dynsym is not None:
     30             base = self.elf.get_section_by_name('.plt')['sh_addr'] + 20
     31             for i, r in enumerate(rel.iter_relocations()):
     32                 self.plt[base + i * 12] = dynsym.get_symbol(r['r_info_sym']).name
     33 
     34     def va_of(self, file_off):
     35         for s in self.elf.iter_sections():
     36             a, o, n = s['sh_addr'], s['sh_offset'], s['sh_size']
     37             if a and s.header['sh_type'] != 'SHT_NOBITS' and o <= file_off < o + n:
     38                 return file_off - o + a
     39         return None
     40 
     41     def sec_of(self, file_off):
     42         for s in self.elf.iter_sections():
     43             a, o, n = s['sh_addr'], s['sh_offset'], s['sh_size']
     44             if a and s.header['sh_type'] != 'SHT_NOBITS' and o <= file_off < o + n:
     45                 return s.name
     46         return '?'
     47 
     48     def read(self, va, n):
     49         for lo, hi, data, _ in self.secs:
     50             if lo <= va < hi:
     51                 return data[va - lo: va - lo + n]
     52         return b''
     53 
     54     def cstr(self, va, n=180):
     55         b = self.read(va, n)
     56         if not b:
     57             return None
     58         b = b.split(b'\x00')[0]
     59         if len(b) >= 3 and all(0x20 <= c < 0x7f or c in b'\t\n' for c in b):
     60             return b.decode('ascii', 'replace')
     61         return None
     62 
     63 
     64 LDR_PC = re.compile(r'^(\w+), \[pc, #(-?0x[0-9a-f]+|-?\d+)\]$')
     65 ADD_PC = re.compile(r'^(\w+), pc, (\w+)$')
     66 
     67 
     68 def run(path, start, count):
     69     img = Img(path)
     70     md = Cs(CS_ARCH_ARM, CS_MODE_ARM)
     71     code = img.read(start, count * 4)
     72     reg = {}
     73     for ins in md.disasm(code, start):
     74         note = ''
     75         if ins.mnemonic in ('bl', 'blx', 'b') and ins.op_str.startswith('#'):
     76             t = int(ins.op_str[1:], 0)
     77             note = f'   -> {img.plt[t]}@plt' if t in img.plt else f'   -> sub_{t:x}'
     78         elif ins.mnemonic.startswith('ldr') and (m := LDR_PC.match(ins.op_str)):
     79             raw = img.read(ins.address + 8 + int(m.group(2), 0), 4)
     80             if len(raw) == 4:
     81                 reg[m.group(1)] = v = int.from_bytes(raw, 'little')
     82                 note = f'   = 0x{v:08x}'
     83                 if (t := img.cstr(v)):
     84                     note += f'  "{t}"'
     85         elif ins.mnemonic == 'add' and (m := ADD_PC.match(ins.op_str)):
     86             if (v := reg.get(m.group(2))) is not None:
     87                 reg[m.group(1)] = a = (v + ins.address + 8) & 0xffffffff
     88                 note = f'   = 0x{a:08x}'
     89                 if (t := img.cstr(a)):
     90                     note += f'  "{t}"'
     91         elif ins.mnemonic == 'mov' and ',' in ins.op_str:
     92             d, srcs = ins.op_str.split(', ', 1)
     93             reg[d] = reg.get(srcs)
     94         print(f'{ins.address:08x}  {ins.bytes.hex():<8}  {ins.mnemonic:<8} {ins.op_str}{note}')
     95 
     96 
     97 def func_start(img, addr, back=0x4000):
     98     """Scan back for an ARM prologue `push {..., lr}`."""
     99     for a in range(addr, addr - back, -4):
    100         w = int.from_bytes(img.read(a, 4), 'little')
    101         if (w & 0xffff0000) == 0xe92d0000 and (w & 0x4000):
    102             return a
    103     return None
    104 
    105 
    106 def summarise(path, addr, span=0x900):
    107     img = Img(path)
    108     start = func_start(img, addr)
    109     if start is None:
    110         print('no prologue found')
    111         return
    112     print(f'function 0x{start:08x}  (contains 0x{addr:08x})')
    113     md = Cs(CS_ARCH_ARM, CS_MODE_ARM)
    114     reg = {}
    115     for ins in md.disasm(img.read(start, span), start):
    116         if ins.mnemonic.startswith('ldr') and (m := LDR_PC.match(ins.op_str)):
    117             raw = img.read(ins.address + 8 + int(m.group(2), 0), 4)
    118             if len(raw) == 4:
    119                 reg[m.group(1)] = int.from_bytes(raw, 'little')
    120         elif ins.mnemonic == 'add' and (m := ADD_PC.match(ins.op_str)):
    121             if (v := reg.get(m.group(2))) is not None:
    122                 a2 = (v + ins.address + 8) & 0xffffffff
    123                 reg[m.group(1)] = a2
    124                 if (t := img.cstr(a2)):
    125                     print(f'  0x{ins.address:08x}  STR  "{t[:88]}"')
    126         elif ins.mnemonic in ('bl', 'blx') and ins.op_str.startswith('#'):
    127             t = int(ins.op_str[1:], 0)
    128             print(f'  0x{ins.address:08x}  bl   {img.plt.get(t, f"sub_{t:x}")}')
    129 
    130 
    131 def xrefs(path, target):
    132     """Find code that materialises `target` through the PIC pair."""
    133     img = Img(path)
    134     lo, _, data, _ = [x for x in img.secs if x[3] == '.text'][0]
    135     import struct
    136     hits = []
    137     for off in range(0, len(data) - 3, 4):
    138         w = struct.unpack_from('<I', data, off)[0]
    139         if (w & 0xffff0ff0) != 0xe08f0000:      # add rD, pc, rM
    140             continue
    141         rd, rm = (w >> 12) & 0xf, w & 0xf
    142         if rd != rm:
    143             continue
    144         pc = lo + off
    145         for back in range(4, 96, 4):
    146             if off - back < 0:
    147                 break
    148             pw = struct.unpack_from('<I', data, off - back)[0]
    149             if (pw & 0xffff0000) in (0xe59f0000, 0xe51f0000) and ((pw >> 12) & 0xf) == rd:  # ldr rD,[pc,#+/-imm]
    150                 delta = (pw & 0xfff) if (pw & 0xffff0000) == 0xe59f0000 else -(pw & 0xfff)
    151                 raw = img.read((lo + off - back) + 8 + delta, 4)
    152                 if len(raw) == 4 and (int.from_bytes(raw, 'little') + pc + 8) & 0xffffffff == target:
    153                     hits.append(pc)
    154                 break
    155     return hits
    156 
    157 SUBCOMMANDS = ('dis', 'str', 'xref', 'ptr', 'func', 'plt')
    158 
    159 if __name__ == '__main__':
    160     if len(sys.argv) > 2 and sys.argv[2] in SUBCOMMANDS:
    161         sys.argv[2] = '--' + sys.argv[2]
    162     if len(sys.argv) > 2 and sys.argv[2] == '--dis':
    163         del sys.argv[2]
    164     path = sys.argv[1]
    165     if sys.argv[2] == '--func':
    166         summarise(path, int(sys.argv[3], 0), int(sys.argv[4], 0) if len(sys.argv) > 4 else 0x900)
    167         sys.exit()
    168     if sys.argv[2] == '--xref':
    169         for a in xrefs(path, int(sys.argv[3], 0)):
    170             print(f'0x{a:08x}')
    171         sys.exit()
    172     if sys.argv[2] == '--str':
    173         img = Img(path)
    174         needle = sys.argv[3].encode()
    175         d = open(path, 'rb').read()
    176         i = d.find(needle)
    177         while i >= 0:
    178             va = img.va_of(i)
    179             shown = f'0x{va:08x}' if va is not None else '(not mapped)'
    180             print(f'file 0x{i:x}  vaddr {shown}  {d[i:i+70].split(bytes(1))[0].decode("latin-1")!r}')
    181             i = d.find(needle, i + 1)
    182         sys.exit()
    183     if sys.argv[2] == '--ptr':
    184         import struct as _s
    185         img = Img(path)
    186         d = open(path, 'rb').read()
    187         needle = _s.pack('<I', int(sys.argv[3], 0))
    188         i = d.find(needle)
    189         while i >= 0:
    190             va = img.va_of(i)
    191             if va is not None:
    192                 print(f'pointer at file 0x{i:x}  vaddr 0x{va:08x}  section {img.sec_of(i)}')
    193             i = d.find(needle, i + 1)
    194         sys.exit()
    195     if sys.argv[2] == '--plt':
    196         img = Img(path)
    197         want = sys.argv[3] if len(sys.argv) > 3 else None
    198         for a, n in sorted(img.plt.items()):
    199             if want is None or n == want:
    200                 print(f'0x{a:08x}  {n}')
    201         sys.exit()
    202     run(path, int(sys.argv[2], 0), int(sys.argv[3]) if len(sys.argv) > 3 else 60)