urt-rs / scripts /analyze_auth_queries.py
nmarshall's picture
Recover authentication selectors command checks and mode paths
9ac939d
Raw History Blame Contribute Delete
8.29 kB
#!/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()