File size: 7,220 Bytes
b1c5e8e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/usr/bin/env python3
"""Recover bounded qagame cvar change paths without executing any callees."""
import json
from pathlib import Path

from analyze_deferred_auth import partition_function, trace_slice
from analyze_game_cvars import vm_string
from analyze_qvm import decode, write_json
from analyze_vm_extensions import verified


def direct_registrations(ins, data, header):
    """Index adjacent trap-3 calls; immediate argument candidates are syntactic."""
    result, entry = [], None
    for i in ins:
        at = i['index']
        if i['opcode'] == 'ENTER':
            entry = at
        if i['opcode'] != 'CALL' or at == 0 or ins[at-1]['opcode'] != 'CONST' or ins[at-1]['operand'] != -4:
            continue
        start = max(entry, at-28)
        previous_calls = [j for j in range(start, at) if ins[j]['opcode'] == 'CALL']
        if previous_calls:
            start = previous_calls[-1]+1
        candidates = {}
        for j in range(start, at-1):
            if ins[j]['opcode'] == 'CONST' and ins[j+1]['opcode'] == 'ARG':
                arg, value = ins[j+1]['operand'], ins[j]['operand']
                item = {'instruction': j, 'value': value}
                if arg in (12, 16) and 0 <= value < header['data_length']+header['literal_length']:
                    item['string'] = vm_string(data, header, value)
                candidates[str(arg)] = item
        result.append({'function_entry': entry, 'call_instruction': at,
                       'immediate_argument_candidates': candidates, 'window': ins[start:at+2]})
    return result


def main():
    source = json.loads(Path('docs/research/qvm/qagame.json').read_text())
    data = verified(Path(source['source']['member']['output']).read_bytes(), source['sha256'])
    header, ins = decode(data)
    registry = json.loads(Path('docs/research/game-cvars.json').read_text())
    if registry['sha256'] != source['sha256']:
        raise ValueError('registry provenance mismatch')
    rows = registry['registry']['rows']
    for name, address in [('g_gametype', 34090788), ('g_stratTime', 34077764)]:
        if len([r for r in rows if r['name'] == name and r['mirror_address']+12 == address]) != 1:
            raise ValueError('strategy setting mirror identity changed')
    row, mirror = 0x10000, 0x20000
    traces = {}
    # Each fixture follows actual instructions, recording rather than entering calls.
    traces['cache_changed'] = trace_slice(ins, 2974, 2997, {row: mirror, row+16: 7, mirror+4: 8}, {20: row})
    traces['cache_unchanged'] = trace_slice(ins, 2974, 3834, {row: mirror, row+16: 8, mirror+4: 8}, {20: row})
    traces['null_mirror'] = trace_slice(ins, 2962, 3834, {row: 0}, {20: row})
    traces['update_call'] = trace_slice(ins, 2962, 2974, {row: mirror}, {20: row})
    traces['notifications_disabled'] = trace_slice(ins, 2997, 3123, {row+20: 0}, {20: row})
    notifications = []
    for comparison, flags in [(0, 0), (1, 0), (1, 32), (1, 33)]:
        model = trace_slice(ins, 3018, 3123, {row: mirror, row+4: 0x30000, row+12: flags, mirror+8: 0x3f800000},
                            {20: row, 40: comparison})
        notifications.append({'name_comparison_result': comparison, 'raw_flags': flags, 'trace': model})
    word24 = []
    for value in (0, 1, -1):
        for phase, start, stop, pointer_local, flag_local in [('register', 2764, 2774, 24, 32),
                                                            ('update', 3123, 3133, 20, 28)]:
            model = trace_slice(ins, start, stop, {row+24: value}, {pointer_local: row, flag_local: 0})
            word24.append({'phase': phase, 'word24': value, 'flag_local': flag_local, 'trace': model})
    settings = []
    for name, address, start, stop, values in [
            ('g_bombPlantTime', 34080428, 3219, 3260, [-1, 0, 2, 3, 5]),
            ('g_funstuff', 34063556, 3585, 3630, [-1, 0, 1, 2]),
            ('g_inactivity', 34085460, 3648, 3668, [-1, 0, 1, 9, 10])]:
        match = [r for r in rows if r['name'] == name and r['mirror_address']+12 == address]
        if len(match) != 1:
            raise ValueError('setting mirror identity changed')
        for value in values:
            settings.append({'name': name, 'input': value, 'integer_address': address,
                             'trace': trace_slice(ins, start, stop, {address: value}, {})})
    for gametype in (0, 4, 8):
        for value in (-1, 0, 1, 2, 10, 11):
            settings.append({'name': 'g_stratTime', 'input': value, 'gametype_raw': gametype,
                             'integer_address': 34077764,
                             'trace': trace_slice(ins, 3777, 3834, {34077764: value, 34090788: gametype}, {})})
    strings = {str(at): vm_string(data, header, at) for at in
               [36187, 36145, 36111, 39360, 36382, 36358, 36345, 37632, 35998, 38919, 38546, 39625, 38431,
                39367, 39400, 39982, 39731, 39293]}
    sites = direct_registrations(ins, data, header)
    constant_traces = []
    for site in sites:
        at = site['call_instruction']
        start = at-9
        sequence = ins[start:at+2]
        expected = ['CONST', 'ARG']*4+['CONST', 'CALL', 'POP']
        if [i['opcode'] for i in sequence] != expected or [sequence[j]['operand'] for j in (1, 3, 5, 7)] != [8, 12, 16, 20]:
            continue
        constant_traces.append({'call_instruction': at, 'trace': trace_slice(ins, start, at+2, {}, {})})
    output = {'source': source['source'], 'sha256': source['sha256'],
              'row_word20_nonzero_names': [r['name'] for r in rows if r['word20_initial']],
              'row_word24_nonzero_names': [r['name'] for r in rows if r['word24_initial']],
              'cache_traces': traces, 'notification_traces': notifications, 'word24_traces': word24,
              'setting_traces': settings, 'strings': strings,
              'direct_registration_sites': sites, 'constant_registration_traces': constant_traces,
              'post_table_registration_trace': trace_slice(ins, 2791, 2813, {}, {}),
              'registration_wrapper_trace': trace_slice(ins, 16095, 16124, {},
                                                       {32: 0x20000, 36: 0x30000, 40: 0x40000, 44: 80}),
              'support_functions': {str(e): partition_function(ins, e, data, header) for e in [2589, 16094, 34364, 69386]},
              'word24_call_windows': {str(at): ins[at-5:at+3] for at in [2818, 3873]},
              'limitations': 'Static supplied qagame and bounded synthetic memory only. Calls are not entered; '
              'zero call results are placeholders, including formatted command pointers. Notification comparison '
              'results are explicit fixture inputs. Setting slices exclude surrounding name/change gates. Immediate '
              'registration argument candidates are syntactic, not proven CFG argument flow; computed calls excluded. '
              'Trap cvar-set calls do not mutate synthetic mirrors. No effective runtime/config/permissions claim.'}
    write_json(Path('docs/research/cvar-changes.json'), output)
    print(f'Recovered {len(output["direct_registration_sites"])} direct registration sites and {len(settings)} setting fixtures')


if __name__ == '__main__':
    main()