#!/usr/bin/env python3 """Reproduce static converter evidence and bounded Mono oracle probes (E02).""" import argparse import hashlib import json from pathlib import Path import platform import re import shutil import struct import subprocess from inspect_mb3dat import inspect ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / 'assets/analysis/converter' BINARIES = { '2015-10-03': '66f1e3d9d53acfbe12a2be0e2a2bde64509d61d451ba7773bda68cff9d54abe5', '2016-03-10-c': '0789c98b4150f281f92181a3368c86f5a81e033b7ecf58b8d320c5365546b5a6', } def sha(data): return hashlib.sha256(data).hexdigest() def binary(build): p = ROOT / 'assets/references/extracted' / build / 'tools/Mb3 file formats.exe' if sha(p.read_bytes()) != BINARIES[build]: raise ValueError(f'wrong converter identity: {p}') return p def pe_info(data): pe = struct.unpack_from('/', 'timeout_seconds': 10}, 'builds': {}} for build in BINARIES: version = 201501280 if build.startswith('2015') else 201512200 source = binary(build) records = [] for case in cases(version): work = ROOT / '.work/converter-oracle' / build / case['name'] dest = OUT / 'probes' / build / case['name'] work.mkdir(parents=True, exist_ok=True) dest.mkdir(parents=True, exist_ok=True) shutil.copyfile(source, work / 'converter.exe') inp, output = 'input.' + case['src'].lower(), 'output.' + case['dst'] payload = case['data'] if 'from_case' in case: previous = OUT / 'probes' / build / case['from_case'] payload = next(previous.glob('output.*')).read_bytes() (work / inp).write_bytes(payload) if case.get('missing'): (work / inp).unlink() (work / 'conversion.log').write_bytes(b'STALE LOG\n') if case.get('inplace'): output = inp elif (work / output).exists(): (work / output).unlink() if case.get('preexisting'): (work / output).write_bytes(b'PREEXISTING OUTPUT') args = case.get('arguments', [case['src'], inp, case['dst'], output, 'conversion.log']) proc = subprocess.run([mono, 'converter.exe', *args], cwd=work, capture_output=True, timeout=10) record = {k: v for k, v in case.items() if k != 'data'} record.update({'input_sha256': sha(payload), 'argv': ['mono', 'converter.exe', *args], 'binary_sha256': sha(source.read_bytes()), 'exit': proc.returncode, 'files': {}}) # Fixture payload retained even in missing-file experiments. (dest / inp).write_bytes(payload) for name, data in [('stdout.txt', proc.stdout), ('stderr.txt', proc.stderr)]: (dest / name).write_bytes(data) for name in [output, 'conversion.log']: if (work / name).exists(): shutil.copyfile(work / name, dest / name) elif (dest / name).exists(): (dest / name).unlink() if case.get('inplace'): (dest / 'before.mb3dat').write_bytes(payload) if (work / output).exists() and case['dst'] == 'mb3dat': try: record['inspection'] = inspect((work / output).read_bytes()) except ValueError as exc: record['inspection_error'] = str(exc) for p in sorted(dest.iterdir()): record['files'][p.name] = {'sha256': sha(p.read_bytes()), 'bytes': p.stat().st_size} if proc.returncode != case['expected_exit']: raise ValueError(f"{build}/{case['name']}: exit {proc.returncode}, expected {case['expected_exit']}; {proc.stderr!r}") records.append(record) report['builds'][build] = records (OUT / 'probes.json').write_text(json.dumps(report, indent=2, allow_nan=False) + '\n') print('captured', sum(map(len, report['builds'].values())), 'oracle probes') def check(): manifest = json.loads((OUT / 'static.json').read_text()) for build, info in manifest['binaries'].items(): binary(build) assert pe_info(binary(build).read_bytes())['pe_imports'] == [{'dll': 'mscoree.dll', 'symbols': ['_CorExeMain']}] for name, digest in info['outputs'].items(): assert sha((OUT / build / name).read_bytes()) == digest, (build, name) report = json.loads((OUT / 'probes.json').read_text()) for build, records in report['builds'].items(): expected_cases = cases(201501280 if build.startswith('2015') else 201512200) assert [r['name'] for r in records] == [c['name'] for c in expected_cases] for case, record in zip(expected_cases, records): payload = case['data'] if 'from_case' in case: previous = OUT / 'probes' / build / case['from_case'] payload = next(previous.glob('output.*')).read_bytes() assert record['input_sha256'] == sha(payload), record['name'] assert record['binary_sha256'] == BINARIES[build] assert record['exit'] == record['expected_exit'], record['name'] if 'inspection' in record: output_name = record['argv'][5] payload_file = OUT / 'probes' / build / record['name'] / output_name assert inspect(payload_file.read_bytes()) == record['inspection'], record['name'] for name, info in record['files'].items(): p = OUT / 'probes' / build / record['name'] / name assert sha(p.read_bytes()) == info['sha256'] and p.stat().st_size == info['bytes'], p by = {r['name']: r for r in records} version = 201501280 if build.startswith('2015') else 201512200 dest = OUT / 'probes' / build assert (dest / 'empty-false/output.mb3dat').read_bytes() == dat(version) assert (dest / 'plane-to-dat/output.mb3dat').read_bytes() == dat(version, b'\0\1' + vecs(TRIPLE), 1) assert (dest / 'dat-plane/output.mb3dat').read_bytes() == (dest / 'plane-to-dat/output.mb3dat').read_bytes() for name in ['hl1map-roundtrip', 'hl2vmf-roundtrip', 'q3map-roundtrip', 'overwrite-dat']: assert by[name]['inspection'] == by['plane-to-dat']['inspection'] assert by['patch-dat-roundtrip']['inspection'] == by['patch-true']['inspection'] assert by['patch-q3-roundtrip']['inspection']['patches'][0]['quads_u'] == 4 for record in records: log = (OUT / 'probes' / build / record['name'] / 'conversion.log').read_bytes() if record['exit'] == 0 or record['name'] in ['usage', 'bad-mode']: assert log == b'STALE LOG\n' else: assert log.startswith(b'An exception of type ') assert by['dat-trailing']['inspection']['trailing_bytes'] == 0 assert by['dat-plane']['inspection']['brushes'][0]['planes'][0]['u'] == [4, 5, 6] for n in [255, 256]: assert len(by['planes-' + str(n)]['inspection']['brushes'][0]['planes']) == n if build.startswith('2016'): d = by['properties-to-dat']['inspection'] assert d['string_properties'] == ['common', 'rare'] assert d['brushes'][0]['planes'][0]['uv']['0'] == [1, 2, 3, 6, 7, 8, 4, 5, 9, 10] for n in [255, 256]: assert by['properties-' + str(n)]['inspection']['string_properties'] == ['p' + str(i) for i in range(n)] assert by['utf8-bom-ascii-loss']['inspection']['string_properties'] == ['?'] assert by['reserved-width-3']['inspection']['flags'] == 0 assert by['truncated-patch-count']['exit'] == 0 assert by['properties-dat-roundtrip']['inspection'] == by['properties-to-dat']['inspection'] dest = OUT / 'probes' / build assert (dest / 'properties-to-txt/output.mb3txt').read_bytes() == (dest / 'properties-dat-to-txt/output.mb3txt').read_bytes() assert (dest / 'utf8-txt-repeat/output.mb3txt').read_bytes() != (dest / 'utf8-no-bom-mojibake/output.mb3txt').read_bytes() print('converter evidence identities, captured exits, and semantic assertions passed') def main(): p = argparse.ArgumentParser(description=__doc__) p.add_argument('action', choices=['decompile', 'probes', 'check']) args = p.parse_args() try: {'decompile': decompile, 'probes': probes, 'check': check}[args.action]() except (ValueError, OSError, subprocess.SubprocessError, AssertionError) as exc: p.exit(1, f'{exc}\n') if __name__ == '__main__': main()