File size: 8,288 Bytes
9ac939d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/usr/bin/env python3
"""Bounded authentication selector, command-role and internal-mode evidence."""
import json
from pathlib import Path

from analyze_auth_announcements import recover_table
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 byte_string(address, value):
    return {address+i: b for i, b in enumerate(value+b'\0')}


def cleaned_name(ins, value):
    address = 0x10000
    t = trace_slice(ins, 70242, 70383, {}, {44: address}, limit=10000,
                    byte_memory=byte_string(address, value))
    result = []
    for at in range(address, address+len(value)+1):
        b = t['byte_memory'][at]
        if not b:
            break
        result.append(b)
    return {'input_hex': value.hex(), 'output_hex': bytes(result).hex(), 'trace': t}


def substring_trace(ins, haystack, needle):
    # The length-call results are supplied explicitly; the byte comparison runs.
    empty = not haystack or not needle or len(needle)>len(haystack)
    return trace_slice(ins, 69746, 69760 if empty else 69910, {},
                       {24: len(needle), 28: len(haystack), 72: 0x10000, 76: 0x20000},
                       limit=10000, byte_memory={**byte_string(0x10000, haystack), **byte_string(0x20000, needle)})


def main():
    qvm = json.loads(Path('docs/research/qvm/qagame.json').read_text())
    data = verified(Path(qvm['source']['member']['output']).read_bytes(), qvm['sha256'])
    h, ins = decode(data)
    rows = json.loads(Path('docs/research/game-cvars.json').read_text())
    if rows['sha256'] != qvm['sha256']:
        raise ValueError('registry provenance mismatch')
    by_name = {r['name']: r for r in rows['registry']['rows']}
    for name, address, offset in [('auth_enable',35003268,12),('dedicated',34090492,12),
                                  ('auth_groups',35001792,16),('auth_notoriety',35002084,12),
                                  ('auth_cmds_friend',35001200,16),('auth_cmds_member',35000904,16),
                                  ('auth_cmds_referee',35000608,16)]:
        if by_name[name]['mirror_address']+offset != address:
            raise ValueError('cvar mirror identity mismatch')
    classify = []
    for text in [b'',b'0',b'01',b'63',b'123',b'-1',b'+1',b' 1',b'1 ',b'1x',b'Player']:
        digit_only = all(48 <= x <= 57 for x in text)
        trace = trace_slice(ins, 253562, 253605 if digit_only else 253669, {}, {376: 0x10000},
                            limit=2048, byte_memory=byte_string(0x10000,text))
        classify.append({'input_hex':text.hex(),'digit_only':digit_only,'trace':trace})
    slots = []
    base = 0x30000
    for slot in [-1,0,3,4]:
        valid = 0 <= slot < 4
        t = trace_slice(ins,253612,253641 if valid else 253638,{34992524:4},{360:slot})
        slots.append({'slot':slot,'maxclients':4,'trace':t})
    connection = []
    for state in [-1,0,1,2]:
        slot=3;record=base+slot*6336
        t=trace_slice(ins,253641,253666 if state==0 else 253875,
                      {34992496:base,record+468:state},{352:slot})
        returned = None if state==0 else trace_slice(ins,253875,253889,{34992496:base},{88:record})
        connection.append({'state_raw':state,'slot':slot,'trace':t,'success_tail':returned})
    substrings=[]
    for haystack,needle in [(b'AlphaBravo',b'BRAVO'),(b'AlphaBravo',b'phab'),(b'Alpha',b'Gamma'),
                            (b'bigtext say',b'ay'),(b'bigtext say',b'say map'),(b'public',b'PUBLIC'),
                            (b'',b'a'),(b'abc',b''),(b'ab',b'abc')]:
        substrings.append({'haystack_hex':haystack.hex(),'needle_hex':needle.hex(),
                           'trace':substring_trace(ins,haystack,needle)})
    table=recover_table(data,h,ins,29196,2,8,167097,167279)
    table_memory={r['vm_address']:r['target_instruction'] for r in table['rows']}
    role_targets={r['value']:r['target_instruction'] for r in table['rows']}
    roles=[]
    for role in [-1,0,1,2,3,4,5,6,7,8,9]:
        target=role_targets.get(role,167275)
        t=trace_slice(ins,167148,target,{**table_memory,0x20000+520:0x30000,0x30000+3688:role},{68:0x20000})
        roles.append({'role':role,'target':target,'trace':t})
    permission_gates=[]
    for mode,entity,command,state,role in [(0,0x20000,b'whois',2,2),(1,0,b'whois',2,2),
            (1,0x20000,b'',2,2),(1,0x20000,b'whois',1,2),(1,0x20000,b'whois',2,1),
            (1,0x20000,b'whois',2,2),(3,0x20000,b'whois',2,8)]:
        stop = 167103 if mode<1 else 167131 if not entity or not command or state!=2 else 167145 if role<2 else 167148
        t=trace_slice(ins,167098,stop,{35017616:mode,entity+520:0x30000,0x30000+468:state,0x30000+3688:role},
                      {68:entity,72:0x40000},byte_memory=byte_string(0x40000,command))
        permission_gates.append({'mode':mode,'entity':entity,'command_hex':command.hex(),'state_raw':state,
                                 'role':role,'reaches_role_switch':stop==167148,'trace':t})
    routes=[]
    for route,start,stop,locals_map in [('client',189074,189083,{3348:0x20000,3120:0x40000}),
                                       ('console',263048,263055,{})]:
        routes.append({'route':route,'trace':trace_slice(ins,start,stop,{},locals_map)})
    modes=[]
    for groups,notoriety in [(0,0),(1,0),(0,5),(1,5),(0,-1)]:
        modes.append({'groups_length':groups,'notoriety':notoriety,
                      'trace':trace_slice(ins,308,389,{35002084:notoriety},{80:groups})})
    disabled=[]
    for cause,start,memory,locals_map in [
        ('enable_not_one',172,{35003268:0},{}),('bad_engine',201,{}, {68:0}),
        ('not_dedicated_two',224,{34090492:1},{}),('no_features_tail',279,{}, {76:0}),
        ('unresolved_address',469,{35017620:0},{})]:
        disabled.append({'cause':cause,'trace':trace_slice(ins,start,838,memory,locals_map)})
    refs=[];callers=[];entry=None
    for i in ins:
        at=i['index']
        if i['opcode']=='ENTER':entry=at
        if i['opcode']=='CONST' and i['operand']==35017616:
            refs.append({'instruction':at,'entry':entry,'window':ins[max(entry,at-4):at+7]})
        if i['opcode']=='CALL' and at and ins[at-1]['opcode']=='CONST' and ins[at-1]['operand']==1264:
            callers.append({'instruction':at,'entry':entry,'window':ins[at-22:at+3]})
    output={'source':qvm['source'],'sha256':qvm['sha256'],
            'functions':{str(e):partition_function(ins,e,data,h) for e in [171,253561,69723,70241,167097]},
            'digit_classification_traces':classify,'slot_bound_traces':slots,'slot_connection_traces':connection,
            'clean_name_traces':[cleaned_name(ins,s) for s in [b'^1Alpha^7',b'A^^B',b'A^xB',b'A\x01\x1f\x7f\x80B']],
            'substring_traces':substrings,'permission_role_table':table,'permission_role_traces':roles,
            'permission_gate_traces':permission_gates,
            'query_direct_callers':callers,'dispatch_traces':routes,
            'dispatch_windows':{'client_prefix':ins[188019:188059],
                                'client_precheck':ins[188059:188105],'client_match':ins[189059:189085],
                                'console_match':ins[263034:263059],'console_argv':ins[262012:262040]},
            'mode_selection_traces':modes,'disabled_mode_traces':disabled,'mode_literal_references':refs,
            'strings':{str(at):vm_string(data,h,at) for at in [77268,87076,38124,38397,38385,38139,38107,38095,
                     38081,38073,38060,38053,37794,67226,91861,91825,91796,91759,91747]},
            'limitations':'Static supplied qagame plus bounded instruction slices, not a complete QVM/runtime. '
            'All CALLs are recorded and return zero placeholders. Integer parser and substring length-call results '
            'are supplied fixture inputs; selected slices exclude preceding conditions. No complete selector, '
            'permission lifecycle, engine registration/config timing or network/runtime conformance claim.'}
    write_json(Path('docs/research/auth-queries.json'),output)
    print(f'Recovered selector helpers, role table, {len(callers)} query callers and {len(refs)} internal-mode references')


if __name__=='__main__':
    main()