aboutsummaryrefslogtreecommitdiffstats
path: root/fw/scan_rpc_opcode_immediates.py
blob: 1a1ce12bcad0046974c5e91c6a8d59844b5eac0b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/usr/bin/env python3
"""Candidate direct service-RPC opcode immediates in extracted signed ELF code.

This halfword-resynchronizing Thumb-2 candidate scan is NOT a control-flow
proof: inline data and instruction interiors can yield false positives, and
computed opcodes or literal-pool loads can be missed. Immediate hits are
NOT necessarily opcodes: HDCP .52 has `MOVW #0x20B` for a 523-byte message
length comparison at 0x402798, not an ASD RPC. Inspect reported sites in
IDA and trace requests before assigning producer/caller provenance.
"""
from __future__ import annotations

import argparse
import struct
from pathlib import Path

from capstone import CS_ARCH_ARM, CS_MODE_THUMB, Cs
from capstone.arm import ARM_OP_IMM

DEFAULT_OPCODE = 0x3105


def executable_spans(path: Path):
    image = path.read_bytes()
    if len(image) < 52 or image[:5] != b'\x7fELF\x01' or image[5] != 1:
        return []
    phoff = struct.unpack_from('<I', image, 28)[0]
    entsize, count = struct.unpack_from('<HH', image, 42)
    spans = []
    for i in range(count):
        at = phoff + i * entsize
        if at + 32 > len(image):
            break
        kind, off, va, _, filesz, _, flags, _ = struct.unpack_from('<8I', image, at)
        if kind == 1 and flags & 1 and off + filesz <= len(image):
            spans.append((va, image[off:off+filesz]))
    return spans


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument('paths', type=Path, nargs='+')
    ap.add_argument('--opcode', type=lambda v: int(v, 0), default=DEFAULT_OPCODE,
                    help='unsigned 32-bit opcode (default 0x3105)')
    ap.add_argument('--literals', action='store_true',
                    help='also list aligned executable-span words equal to opcode')
    args = ap.parse_args()
    opcode = args.opcode
    if not (0x100 <= opcode <= 0xffffffff):
        ap.error('opcode must fit 32 bits and exceed a Thumb movs immediate')
    files = sorted({f for p in args.paths for f in ([p] if p.is_file() else p.rglob('*.elf'))})
    md = Cs(CS_ARCH_ARM, CS_MODE_THUMB)
    md.detail = True
    for file in files:
        for va, data in executable_spans(file):
            # These opcodes cannot fit a 16-bit Thumb mov-imm. Try 32-bit
            # prefixes at *each* halfword, so invalid instructions
            # cannot truncate the scan. Candidate alignment still needs IDA.
            for off in range((-va) & 1, len(data) - 3, 2):
                if data[off+1] < 0xE8:
                    continue
                ins = next(md.disasm(data[off:off+4], va+off, count=1), None)
                if ins and ins.size == 4 and ins.mnemonic in (
                    'mov', 'movs', 'movw', 'mvn', 'addw', 'subw'
                ) and any(o.type == ARM_OP_IMM and (o.imm & 0xffffffff) == opcode
                          for o in ins.operands):
                    print(f'{file}\t0x{ins.address:x}\t{ins.mnemonic} {ins.op_str}')
            if args.literals:
                for off in range((-va) & 3, len(data) - 3, 4):
                    if struct.unpack_from('<I', data, off)[0] == opcode:
                        print(f'{file}\t0x{va+off:x}\taligned literal 0x{opcode:x} (xref unverified)')
    return 0


if __name__ == '__main__':
    raise SystemExit(main())