Download scripts/analyze_chat_paths.py from nmarshall/urt-rs: direct link, hf CLI and curl.
- Browser
- Download file 7.62 kB
-
https://huggingface.co/nmarshall/urt-rs/resolve/2d840a0fc49ec00f91cf1f8384861985afa519af/scripts/analyze_chat_paths.py
- Command line
-
hf download hf://nmarshall/urt-rs@2d840a0fc49ec00f91cf1f8384861985afa519af/scripts/analyze_chat_paths.py
-
curl -L -o analyze_chat_paths.py https://huggingface.co/nmarshall/urt-rs/resolve/2d840a0fc49ec00f91cf1f8384861985afa519af/scripts/analyze_chat_paths.py
7.62 kB
| #!/usr/bin/env python3 | |
| """Recover bounded chat assembly/recipient gates and native command metadata.""" | |
| import gzip | |
| import json | |
| from pathlib import Path | |
| import re | |
| import struct | |
| from analyze_deferred_auth import partition_function, trace_slice | |
| from analyze_engine_control import control_flow | |
| from analyze_event_queues import symbol_record | |
| from analyze_frame_timing import read_address | |
| from analyze_game_cvars import vm_string | |
| from analyze_native import elf | |
| from analyze_qvm import decode, write_json | |
| from analyze_vm_extensions import native_functions, verified | |
| def native_string(data, sections, address, limit=512): | |
| result=bytearray() | |
| for at in range(address,address+limit): | |
| b,_=read_address(data,sections,at,1) | |
| if b==b'\0':return result.decode('ascii') | |
| result.extend(b) | |
| raise ValueError('unterminated native string') | |
| def main(): | |
| q=json.loads(Path('docs/research/qvm/qagame.json').read_text()) | |
| d=verified(Path(q['source']['member']['output']).read_bytes(),q['sha256']);h,ins=decode(d) | |
| registry=json.loads(Path('docs/research/game-cvars.json').read_text()) | |
| if registry['sha256']!=q['sha256']:raise ValueError('registry provenance mismatch') | |
| row=next(r for r in registry['registry']['rows'] | |
| if r['name']=='g_allowChat') | |
| if row['mirror_address']+12!=34077172:raise ValueError('chat cvar identity changed') | |
| joins=[] | |
| for used,length,index,argc in [(0,0,0,1),(0,5,0,2),(1021,1,0,2),(1022,0,0,2),(1022,1,0,2),(0,1023,0,1)]: | |
| accepted=used+length<1023 | |
| t=trace_slice(ins,180896,180950 if accepted else 180955,{}, {20:used,28:length,24:index,1056:argc}) | |
| after=t['memory'][0x40000000+20] | |
| end=trace_slice(ins,180955,180962,{}, {20:after},capture_stack=True) | |
| joins.append({'used':used,'token_length':length,'index':index,'argc':argc,'accepted':accepted, | |
| 'trace':t,'terminator_trace':end}) | |
| gates=[] | |
| for allow in [0,1,2,3]: | |
| for mode in [0,1,2,4]: | |
| accepted=allow!=0 and (mode!=0 or allow>=2) | |
| t=trace_slice(ins,186750,186764 if accepted else 187379,{34077172:allow},{124:mode}) | |
| gates.append({'allow_chat':allow,'mode':mode,'accepted':accepted,'trace':t}) | |
| sender,recipient,srecord,rrecord,base=0x10000,0x20000,0x30000,0x40000,0x30000-2*6336 | |
| mute=[] | |
| for deadline in [999,1000,1001]: | |
| mute.append({'deadline':deadline,'now':1000,'trace':trace_slice(ins,186737,186750 if deadline<=1000 else 187379, | |
| {sender+520:srecord,srecord+3128:deadline,34992532:1000},{116:sender})}) | |
| ignores=[] | |
| for value in [0,1,-1]: | |
| ignores.append({'sender_slot':2,'ignore_word':value,'trace':trace_slice(ins,186788,186813 if value==0 else 187379, | |
| {sender+520:srecord,recipient+520:rrecord,34992496:base,rrecord+3780+2*4:value}, | |
| {116:sender,120:recipient})}) | |
| states=[] | |
| for state in [0,1,2,3]: | |
| states.append({'state_raw':state,'trace':trace_slice(ins,186851,186863 if state==2 else 187379, | |
| {recipient+520:rrecord,rrecord+468:state},{120:recipient})}) | |
| teams=[] | |
| for team_sender,team_recipient,self_target,team_mode_result in [(1,1,False,1),(1,2,False,1),(1,1,True,0)]: | |
| target=sender if self_target else recipient | |
| tr=srecord if self_target else rrecord | |
| accepted=team_sender==team_recipient or self_target | |
| # Result of team-mode helper 33715 is an explicit fixture input here. | |
| memory={sender+520:srecord,target+520:tr,srecord+3096:team_sender,tr+3096:team_recipient} | |
| teams.append({'same_object':self_target,'sender_team':team_sender,'recipient_team':team_recipient, | |
| 'team_mode_helper_result':team_mode_result, | |
| 'trace':trace_slice(ins,186888,186916 if accepted else 187379,memory,{76:team_mode_result,116:sender,120:target})}) | |
| output={'source':q['source'],'sha256':q['sha256'],'allow_chat_registry':row,'join_traces':joins, | |
| 'allow_chat_traces':gates,'mute_traces':mute,'ignore_traces':ignores,'recipient_state_traces':states, | |
| 'team_comparison_traces':teams,'functions':{str(e):partition_function(ins,e,d,h) for e in [180858,186721,187381]}, | |
| 'chat_tail_window':ins[189262:189342], | |
| 'strings':{str(at):vm_string(d,h,at) for at in [77124,77117,77706,77676,77599,77828,77805,77728]}, | |
| 'native':{},'limitations':'Static QVM/Linux ELF64 and bounded synthetic slices. Calls are recorded, ' | |
| 'not entered; zero return values are placeholders. Join lengths/team-helper results are supplied inputs. ' | |
| 'Native command tables and constants are file-backed metadata; graphs are syntactic, not execution. ' | |
| 'Remaining recipient/deadchat/filter/format/queue/runtime paths are not a complete chat contract.'} | |
| for label in ['linux--Quake3-UrT-Ded.x86_64','linux--Quake3-UrT.x86_64']: | |
| r=json.loads(Path(f'docs/research/native/{label}.json').read_text()) | |
| data=verified((Path('.cache/UrbanTerror43')/r['source']['path']).read_bytes(),r['source']['sha256']) | |
| metadata,symbols=elf(data);sections=metadata['sections'];a=r['artifacts']['disassembly.txt'] | |
| fs=native_functions(verified(gzip.open(a['path'],'rb').read(),a['uncompressed_sha256']).decode()) | |
| names=['SV_ExecuteClientCommand','Cmd_Args_Sanitize','SV_ClientCommand'] | |
| graphs={name:control_flow(fs[name]) for name in names if name in fs} | |
| table=symbol_record(symbols,'ucmds') | |
| if table['bytes']%16 or table['bytes']>4096:raise ValueError('invalid ucmd table size') | |
| raw,offset=read_address(data,sections,table['address'],table['bytes']) | |
| entries=[] | |
| for i in range(0,len(raw),16): | |
| name,handler=struct.unpack_from('<2Q',raw,i) | |
| entries.append({'index':i//16,'name_address':name,'handler_address':handler, | |
| 'name':native_string(data,sections,name) if name else None, | |
| 'handler_symbols':[s['name'] for s in symbols if s['address']==handler and not s['undefined']] if handler else []}) | |
| # Retain direct immediate strings only at actual Q_stricmp setup sites. | |
| strings=[];g=graphs['SV_ExecuteClientCommand'];ii=g['instructions'] | |
| for n,x in enumerate(ii): | |
| if '<Q_stricmp>' not in x['operands']:continue | |
| for prev in ii[max(0,n-4):n]: | |
| match=re.match(r'\$0x([0-9a-f]+), %edi',prev['operands']) | |
| if prev['opcode']=='movl' and match: | |
| addr=int(match[1],16);strings.append({'call_address':x['address'],'address':addr,'text':native_string(data,sections,addr)}) | |
| sanitizer=graphs['Cmd_Args_Sanitize'];addresses=[] | |
| for x in sanitizer['instructions']: | |
| match=re.search(r'#\s*0x([0-9a-f]+)',x['operands']) | |
| if x['opcode']=='movzbl' and match: | |
| at=int(match[1],16);b,fo=read_address(data,sections,at,1) | |
| addresses.append({'instruction':x['address'],'address':at,'file_offset':fo,'byte':b[0]}) | |
| output['native'][label]={'source':r['source'],'disassembly_sha256':a['uncompressed_sha256'],'graphs':graphs, | |
| 'builtin_table':{**table,'file_offset':offset,'entries':entries},'limit_command_strings':strings, | |
| 'sanitizer_constant_bytes':addresses,'argv_symbol':symbol_record(symbols,'cmd_argv')} | |
| write_json(Path('docs/research/chat-paths.json'),output) | |
| print('Recovered chat join/recipient gates and both Linux ELF64 command tables') | |
| if __name__=='__main__':main() | |