File size: 4,690 Bytes
fd99160 | 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 77 78 79 80 81 82 83 84 | #!/usr/bin/env python3
"""Recover absolute x86_64 ELF syscall switch tables, retaining case evidence."""
import gzip
import hashlib
import json
from pathlib import Path
import re
import struct
from analyze_native import parse, take
from analyze_qvm import write_json
def recover(data, disassembly, symbol):
layout, _ = parse(data)
if layout['format'] != 'ELF' or layout['bits'] != 64:
raise ValueError('this switch-table recognizer supports ELF64 only')
function = re.search(r'(?m)^([0-9a-f]+) <'+re.escape(symbol)+r'>:\n(.*?)(?=\n[0-9a-f]+ <|\Z)', disassembly, re.S)
if not function:
raise ValueError(f'missing disassembly symbol {symbol}')
prologue = '\n'.join(function[2].splitlines()[:24])
maximum = re.search(r'cmpq\s+\$0x([0-9a-f]+), %(\w+)', prologue)
table = re.search(r'jmpq\s+\*0x([0-9a-f]+)\(,%(\w+),8\)', prologue)
if not maximum or not table or maximum[2] != table[2]:
raise ValueError(f'unrecognized bounds/table pattern in {symbol}')
maximum, table_address = int(maximum[1], 16), int(table[1], 16)
if maximum > 4096:
raise ValueError('unreasonable syscall switch size')
table_section = next((s for s in layout['sections'] if not s['zero_fill'] and s['address'] <= table_address and table_address+(maximum+1)*8 <= s['address']+s['bytes']), None)
if table_section is None:
raise ValueError('switch table outside a file-backed section')
offset = table_section['file_offset']+table_address-table_section['address']
targets = struct.unpack('<'+'Q'*(maximum+1), take(data, offset, (maximum+1)*8))
lines = []
for line in disassembly.splitlines():
match = re.match(r'\s*([0-9a-f]+):\s', line)
if match:
lines.append((int(match[1], 16), line.strip()))
indexes = {address: n for n, (address, _) in enumerate(lines)}
groups = {}
for number, target in enumerate(targets):
if not any(s['executable'] and s['address'] <= target < s['address']+s['bytes'] for s in layout['sections']) or target not in indexes:
raise ValueError('switch target is not a decoded executable instruction')
groups.setdefault(target, []).append(number)
cases = []
for target, numbers in sorted(groups.items()):
snippet = []
for _, line in lines[indexes[target]:indexes[target]+24]:
snippet.append(line)
if re.search(r'\t(?:retq?|jmpq?)\s', line):
break
names = sorted(set(re.findall(r'\b(?:callq?|jmpq?)\s+[^\n<]*<([^>+]+)(?:\+[^>]*)?>', '\n'.join(snippet))))
cases.append({'target_address': target, 'numbers_sharing_target': numbers,
'first_instructions': snippet, 'direct_call_or_jump_labels': names})
return {'symbol': symbol, 'entry_address': int(function[1], 16), 'maximum_case_number': maximum,
'table_address': table_address, 'table_file_offset': offset,
'case_target_count': len(groups), 'cases': cases,
'limitations': 'Recognized bounds-check and absolute pointer-table pattern. Shared targets can be default/aliases; 24-instruction snippets are evidence, not complete case semantics or syscall signatures.'}
def main():
output = []
for label in ('linux--Quake3-UrT-Ded.x86_64', 'linux--Quake3-UrT.x86_64'):
report = json.loads(Path(f'docs/research/native/{label}.json').read_text())
path = Path('.cache/UrbanTerror43')/report['source']['path']
data = path.read_bytes()
if hashlib.sha256(data).hexdigest() != report['source']['sha256']:
raise ValueError('native snapshot mismatch')
artifact = report['artifacts']['disassembly.txt']
disassembly = gzip.open(artifact['path'], 'rb').read()
if hashlib.sha256(disassembly).hexdigest() != artifact['uncompressed_sha256']:
raise ValueError('disassembly artifact changed')
symbols = ['SV_GameSystemCalls'] if 'Ded' in label else ['SV_GameSystemCalls', 'CL_CgameSystemCalls', 'CL_UISystemCalls']
rows = [recover(data, disassembly.decode(), symbol) for symbol in symbols]
result = {'source': report['source'], 'dispatches': rows}
write_json(Path(f'docs/research/native/{label}.dispatch.json'), result)
output.extend({'image': label, 'symbol': r['symbol'], 'maximum': r['maximum_case_number'],
'table_address': r['table_address'], 'distinct_targets': r['case_target_count']} for r in rows)
write_json(Path('docs/research/native/dispatch-summary.json'), output)
print('Recovered four native syscall dispatch tables with address-keyed case snippets')
if __name__ == '__main__':
main()
|