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)