#!/usr/bin/env python3 """Annotated arm64 disassembly of functions in a stripped shared object. Used to learn the signatures of SteamVR's private interfaces from the headset's own binaries. Resolves adrp+add/ldr pairs to strings or data, PLT stubs to imported symbol names, and RELATIVE relocations in data. Usage: disasm.py LIB ADDR [ADDR ...] [--max N] [--until ADDR] disasm.py --xref LIB TARGET [TARGET ...] Addresses are hex. Disassembles linearly from ADDR until a `ret` that is not skipped over by an earlier forward branch, or N instructions. """ import re import struct import subprocess import sys import capstone from capstone import arm64_const as A class Elf: def __init__(self, path): self.path = path self.data = open(path, "rb").read() self.sections = [] out = subprocess.run(["readelf", "-SW", path], capture_output=True, text=True).stdout for m in re.finditer(r"\]\s+(\S+)\s+\S+\s+([0-9a-f]+)\s+([0-9a-f]+)\s+([0-9a-f]+)", out): name, addr, off, size = m.group(1), int(m.group(2), 16), int(m.group(3), 16), int(m.group(4), 16) self.sections.append((name, addr, off, size)) self.relative = {} self.symbolic = {} out = subprocess.run(["readelf", "-rW", path], capture_output=True, text=True).stdout for line in out.splitlines(): m = re.match(r"\s*([0-9a-f]+)\s+[0-9a-f]+\s+(R_AARCH64_\w+)\s+(\S+)?\s*(.*)", line) if not m: continue where, kind = int(m.group(1), 16), m.group(2) if kind == "R_AARCH64_RELATIVE": self.relative[where] = int(m.group(3), 16) elif kind in ("R_AARCH64_JUMP_SLOT", "R_AARCH64_GLOB_DAT", "R_AARCH64_ABS64"): rest = (m.group(4) or "").strip() sym = rest.split("+")[0].strip() if rest else "" self.symbolic[where] = sym.split("@")[0] self.plt = self._map_plt() def section_of(self, addr): for name, a, off, size in self.sections: if a and a <= addr < a + size: return name, a, off return None def read(self, addr, n): s = self.section_of(addr) if not s or s[0] == ".bss": return None return self.data[addr - s[1] + s[2]: addr - s[1] + s[2] + n] def cstring(self, addr, limit=160): raw = self.read(addr, limit) if not raw: return None end = raw.find(b"\0") if end <= 0: return None text = raw[:end] if all(32 <= c < 127 or c in (9, 10) for c in text): return text.decode() return None def u64(self, addr): raw = self.read(addr, 8) return struct.unpack(" 1: got = page + ins.operands[1].mem.disp sym = self.symbolic.get(got) if sym: plt[ins.address - 4] = sym page = None return plt def describe(self, addr): """Best-effort label for an address used as data.""" if addr in self.plt: return "plt:" + self.plt[addr] s = self.cstring(addr) if s: return repr(s) if addr in self.relative: target = self.relative[addr] s = self.cstring(target) return f"-> {target:#x}" + (f" {s!r}" if s else "") if addr in self.symbolic: return "got:" + self.symbolic[addr] sec = self.section_of(addr) return sec[0] if sec else None def disassemble(elf, start, max_ins, until=None): md = capstone.Cs(capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM) md.detail = True code = elf.read(start, max_ins * 4) regs_page = {} furthest = start for ins in md.disasm(code, start): note = "" ops = ins.operands if ins.id == A.ARM64_INS_ADRP: regs_page[ops[0].reg] = ops[1].imm elif ins.id == A.ARM64_INS_ADD and len(ops) == 3 and ops[1].reg in regs_page and ops[2].type == A.ARM64_OP_IMM: target = regs_page.pop(ops[1].reg) + ops[2].imm note = f"{target:#x} {elf.describe(target) or ''}" elif ins.id in (A.ARM64_INS_LDR, A.ARM64_INS_STR) and len(ops) > 1 and ops[1].type == A.ARM64_OP_MEM \ and ops[1].mem.base in regs_page: target = regs_page[ops[1].mem.base] + ops[1].mem.disp note = f"[{target:#x}] {elf.describe(target) or ''}" elif ins.id in (A.ARM64_INS_BL, A.ARM64_INS_B) and ops and ops[0].type == A.ARM64_OP_IMM: target = ops[0].imm label = elf.plt.get(target) note = f"-> {label}" if label else "" if ins.id == A.ARM64_INS_B and target > furthest: furthest = target elif ins.group(capstone.CS_GRP_JUMP) and ops and ops[-1].type == A.ARM64_OP_IMM: if ops[-1].imm > furthest: furthest = ops[-1].imm print(f" {ins.address:#x}: {ins.mnemonic:8} {ins.op_str:40} {note}".rstrip()) if until is not None: if ins.address + 4 >= until: break continue if ins.id == A.ARM64_INS_RET and ins.address >= furthest: break if ins.id == A.ARM64_INS_B and ops and ops[0].type == A.ARM64_OP_IMM and ins.address >= furthest \ and ops[0].imm < ins.address: break def xrefs(elf, target): """Addresses in .text whose adrp+add (or adrp+ldr) pair forms `target`.""" name, addr, off, size = [s for s in elf.sections if s[0] == ".text"][0] words = struct.unpack_from(f"<{size // 4}I", elf.data, off) hits = [] page_of = {} for i, w in enumerate(words): pc = addr + i * 4 if (w & 0x9F000000) == 0x90000000: # adrp rd = w & 0x1F immlo = (w >> 29) & 3 immhi = (w >> 5) & 0x7FFFF imm = (immhi << 2) | immlo if imm & (1 << 20): imm -= 1 << 21 page_of[rd] = ((pc & ~0xFFF) + (imm << 12), pc) elif (w & 0xFF800000) == 0x91000000: # add (immediate, 64-bit, no shift) rn = (w >> 5) & 0x1F if rn in page_of and pc - page_of[rn][1] < 64: if page_of[rn][0] + ((w >> 10) & 0xFFF) == target: hits.append(pc) return hits def main(): args = sys.argv[1:] if args and args[0] == "--xref": elf = Elf(args[1]) for a in args[2:]: print(a, [hex(h) for h in xrefs(elf, int(a, 16))]) return max_ins = 400 until = None if "--until" in args: i = args.index("--until") until = int(args[i + 1], 16) del args[i:i + 2] if "--max" in args: i = args.index("--max") max_ins = int(args[i + 1]) del args[i:i + 2] elf = Elf(args[0]) for a in args[1:]: start = int(a, 16) print(f"== {start:#x}") disassemble(elf, start, max_ins if until is None else (until - start) // 4 + 1, until) if __name__ == "__main__": main()