Files
GaCy-GO/tools/disassemble_pe_range.py
T

66 lines
3.4 KiB
Python

"""Read-only disassembly of an explicit PE RVA range; no process attachment."""
import argparse
import hashlib
import pathlib
import struct
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--binary', type=pathlib.Path, required=True)
parser.add_argument('--rva', type=lambda s: int(s, 0), required=True)
parser.add_argument('--size', type=lambda s: int(s, 0), required=True)
parser.add_argument('--output', type=pathlib.Path, required=True)
parser.add_argument('--containing-function', action='store_true', help='Use the AMD64 exception-table bounds containing the selected RVA')
args = parser.parse_args()
if not 0 < args.size <= 65536:
raise SystemExit('Select a range of 1..65536 bytes')
data = args.binary.read_bytes()
if data[:2] != b'MZ':
raise SystemExit('Expected PE image')
pe = struct.unpack_from('<I', data, 0x3c)[0]
if data[pe:pe+4] != b'PE\0\0' or struct.unpack_from('<H', data, pe+4)[0] != 0x8664:
raise SystemExit('Expected AMD64 PE image')
section_count = struct.unpack_from('<H', data, pe+6)[0]
optional_size = struct.unpack_from('<H', data, pe+20)[0]
def file_offset(address, length):
for i in range(section_count):
off = pe+24+optional_size+i*40
_, rva, size, raw = struct.unpack_from('<IIII', data, off+8)
if rva <= address and address+length <= rva+size and raw+address-rva+length <= len(data):
return raw+address-rva
raise ValueError('Range is not contained in a raw section')
if args.containing_function:
if struct.unpack_from('<H', data, pe+24)[0] != 0x20b:
raise SystemExit('Expected PE32+ optional header')
exception_rva, exception_size = struct.unpack_from('<II', data, pe+24+112+3*8)
base = file_offset(exception_rva, exception_size)
matches = []
for off in range(base, base+exception_size, 12):
begin, end, _ = struct.unpack_from('<III', data, off)
if begin <= args.rva < end:
matches.append((begin, end))
if len(matches) != 1 or not 0 < matches[0][1]-matches[0][0] <= 65536:
raise SystemExit('No unique bounded exception-table function entry')
args.rva, end = matches[0]
args.size = end-args.rva
for i in range(section_count):
off = pe+24+optional_size+i*40
_, rva, size, raw = struct.unpack_from('<IIII', data, off+8)
if rva <= args.rva and args.rva+args.size <= rva+size:
start = raw+args.rva-rva
if start+args.size > len(data):
raise SystemExit('Truncated section')
decoder = Cs(CS_ARCH_X86, CS_MODE_64)
scope = 'AMD64 exception-table function bounds' if args.containing_function else 'explicit range; function boundaries unproven'
lines = [f'; SHA256 {hashlib.sha256(data).hexdigest()}',
f'; RVA {args.rva:#x}, length {args.size:#x}; {scope}']
lines += [f'{ins.address:x}: {ins.mnemonic} {ins.op_str}' for ins in decoder.disasm(data[start:start+args.size], args.rva)]
args.output.write_text('\n'.join(lines)+'\n', encoding='utf-8')
print(f'Wrote {len(lines)-2} instructions to {args.output}')
return
raise SystemExit('RVA range is not contained in a raw section')
if __name__ == '__main__':
main()