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())
|