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