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