#!/usr/bin/env python3 """Bounded Mach-O STABS and PE resource recovery; no native code execution.""" from collections import Counter import hashlib import json from pathlib import Path import struct from analyze_native import parse, take, unpack, cstring from analyze_qvm import write_json, write_rows from analyze_vm_extensions import verified STABS = {0x20: 'N_GSYM', 0x22: 'N_FNAME', 0x24: 'N_FUN', 0x26: 'N_STSYM', 0x28: 'N_LCSYM', 0x2e: 'N_BNSYM', 0x32: 'N_AST', 0x3c: 'N_OPT', 0x40: 'N_RSYM', 0x44: 'N_SLINE', 0x4e: 'N_ENSYM', 0x60: 'N_SSYM', 0x64: 'N_SO', 0x66: 'N_OSO', 0x80: 'N_LSYM', 0x82: 'N_BINCL', 0x84: 'N_SOL', 0x86: 'N_PARAMS', 0x88: 'N_VERSION', 0x8a: 'N_OLEVEL', 0xa0: 'N_PSYM', 0xa2: 'N_EINCL', 0xa4: 'N_ENTRY', 0xc0: 'N_LBRAC', 0xc2: 'N_EXCL', 0xe0: 'N_RBRAC', 0xe2: 'N_BCOMM', 0xe4: 'N_ECOMM'} def stab_records(data, layout): if layout['format'] != 'Mach-O' or layout['bits'] != 64: raise ValueError('STABS reader supports thin Mach-O64 only') cmd = next(c for c in layout['load_commands'] if c['command'] == '0x2') offset, count, strings_at, strings_bytes = unpack('<4I', data, cmd['file_offset']+8) strings = take(data, strings_at, strings_bytes) take(data, offset, count*16) rows = [] for i in range(count): name, typ, section, desc, value = unpack(' 8 or nodes > 65536: raise ValueError('cyclic or excessive resource tree') header = unpack('expected else None} def reconstruct_icon(data, leaves, group): rows = icon_group(take(data, group['file_offset'], group['bytes'])) header = bytearray(struct.pack('<3H', 0, 1, len(rows))) images = [] offset = 6+16*len(rows) for row in rows: matches = [r for r in leaves if r['path'] == [3, row['resource_id'], group['path'][2]]] if len(matches) != 1 or matches[0]['bytes'] != row['bytes']: raise ValueError('missing/ambiguous icon language or mismatched image size') leaf = matches[0] image = take(data, leaf['file_offset'], leaf['bytes']) header.extend(struct.pack('<4B2H2I', row['width'] % 256, row['height'] % 256, row['colors'], 0, row['planes'], row['bits'], len(image), offset)) images.append(image) offset += len(image) return bytes(header)+b''.join(images), rows def main(): summaries = [] for label in ('macos--libSDL-1.2.0.dylib', 'macos--Quake3-UrT.x86_64', 'windows--Quake3-UrT.exe', 'windows--Quake3-UrT-Ded.exe'): report = json.loads(Path(f'docs/research/native/{label}.json').read_text()) data = verified((Path('.cache/UrbanTerror43')/report['source']['path']).read_bytes(), report['source']['sha256']) layout, _ = parse(data) output = {'source': report['source']} if layout['format'] == 'Mach-O': rows = stab_records(data, layout) output.update(decode_stabs(data, layout, rows)) artifact = Path(f'assets/analysis/native/{label}.stabs.jsonl.gz') write_rows(artifact, rows) output['raw_records'] = str(artifact) summaries.append({'image': label, 'stabs': len(rows), 'function_ranges': len(output['functions']), 'objects': len(output['objects']), 'unresolved': len(output['unresolved'])}) else: leaves = resource_leaves(data, layout) root = Path(f'assets/analysis/native/resources/{label}') icons = [] for i, leaf in enumerate(leaves): path = root/f'{i:06d}.bin' path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(take(data, leaf['file_offset'], leaf['bytes'])) leaf['output'] = str(path) if len(leaf['path']) == 3 and leaf['path'][0] == 3: leaf['dib'] = icon_dib(take(data, leaf['file_offset'], leaf['bytes'])) if len(leaf['path']) == 3 and leaf['path'][0] == 6: leaf['strings'] = string_table(take(data, leaf['file_offset'], leaf['bytes']), leaf['path'][1]) if len(leaf['path']) == 3 and leaf['path'][0] == 14: ico, entries = reconstruct_icon(data, leaves, leaf) path = root/f'{i:06d}.ico' path.write_bytes(ico) icons.append({'group_path': leaf['path'], 'entries': entries, 'output': str(path), 'bytes': len(ico), 'sha256': hashlib.sha256(ico).hexdigest()}) output.update({'leaves': leaves, 'icon_groups': icons, 'limitations': 'All leaves are preserved. Shipped icon groups, DIB headers/extents and string tables are decoded. ' 'Pixel/mask behavior and surplus icon bytes remain unclassified; other resource types are not decoded.'}) summaries.append({'image': label, 'resource_leaves': len(leaves), 'type_counts': dict(Counter(str(r['path'][0]) for r in leaves)), 'icon_groups': len(icons)}) write_json(Path(f'docs/research/native/{label}.metadata.json'), output) write_json(Path('docs/research/native/metadata-summary.json'), summaries) print(json.dumps(summaries, indent=2)) if __name__ == '__main__': main()