File size: 5,418 Bytes
9038fbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""Recover bounded radio parser states, key comparisons and substitution branches."""
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

BASE=0x40000000


def compare(ins,left,right,bound=99999):
    a,b=0x10000,0x20000
    memory={a+i:v for i,v in enumerate(left+b'\0')}
    memory.update({b+i:v for i,v in enumerate(right+b'\0')})
    return trace_slice(ins,69387,69525,{}, {48:a,52:b,56:bound},
                       byte_memory=memory,limit=4096,capture_stack=True,halt_on_leave=True)


def main():
    q=json.loads(Path('docs/research/qvm/qagame.json').read_text())
    data=verified(Path(q['source']['member']['output']).read_bytes(),q['sha256']);h,ins=decode(data)
    functions={str(e):partition_function(ins,e,data,h) for e in [69386,185212,186435]}
    keys=[]
    for i in range(185212,186435):
        if ins[i]['opcode']=='CONST' and ins[i]['operand']==69386 and ins[i+1]['opcode']=='CALL':
            literal=ins[i-5]
            if literal['opcode']!='CONST' or ins[i-4]['opcode']!='ARG' or ins[i-4]['operand']!=12:
                raise ValueError('key comparison pattern changed')
            keys.append({'key':vm_string(data,h,literal['operand']),'address':literal['operand'],
                         'compare_call':i+1,'bound':ins[i-3]['operand'],'window':ins[i-11:i+8]})
    comparisons=[]
    for k in keys:
        key=k['key'].encode('ascii')
        for candidate in [key,key.upper(),key[:-1],key+b'x']:
            t=compare(ins,candidate,key)
            comparisons.append({'key':k['key'],'candidate':candidate.decode('ascii'),'trace':t})
    # Per-character slices: no copy/append/lookup callee is entered.
    chars=[]
    for marker,key,ch in [(0,'',ord('x')),(0,'',36),(36,'foo',35),(35,'',ord('a')),
                           (36,'h',ord('P')),(36,'foo',ord('1')),(36,'foo',32),
                           (36,'foo',255),(0,'',0)]:
        bm={BASE+32:ch,BASE+182:marker}
        bm.update({BASE+183+i:v for i,v in enumerate(key.encode()+b'\0')})
        t=trace_slice(ins,186481,186676,{}, {28:0,336:len(key),376:0x50000,380:0x60000},byte_memory=bm)
        chars.append({'pending_marker':marker,'pending_key':key,'byte':ch,'trace':t})
    lookup_results=[]
    for result in [0,0x70000]:
        t=trace_slice(ins,186609,186676,{}, {340:result,380:0x60000},byte_memory={BASE+182:36})
        lookup_results.append({'supplied_lookup_return':result,'trace':t})
    tails=[]
    for marker in [0,35,36]:
        t=trace_slice(ins,186693,186719,{}, {380:0x60000},byte_memory={BASE+182:marker})
        tails.append({'pending_marker':marker,'trace':t})
    lengths=[]
    for length in [149,150,151]:
        t=trace_slice(ins,186449,186719,{}, {344:length,380:0x60000},
                       byte_memory={BASE+182:0},limit=256)
        lengths.append({'supplied_source_length':length,'trace':t})
    entity,record=0x10000,0x20000
    health=[]
    for value in [-1,0,24,25,49,50,74,75,100]:
        for state,override in [(0,0),(3,0),(4,0),(0,1)]:
            t=trace_slice(ins,185237,186431,{entity+684:value,entity+520:record,
                        record+4:state,record+4324:override},{1144:entity})
            health.append({'raw_health':value,'raw_state':state,'override':override,'trace':t})
    hp=[]
    for state,override in [(0,0),(3,0),(4,0),(0,1)]:
        t=trace_slice(ins,185350,186431,{entity+684:42,entity+520:record,record+4:state,
                        record+4324:override},{1144:entity})
        hp.append({'raw_state':state,'override':override,'trace':t})
    side_effects=[]
    for pointer,now in [(0,1000),(0x30000,1000),(0x30000,2147483640)]:
        t=trace_slice(ins,185987,186031,{entity+520:record,record+6168:pointer,34992532:now},
                      {1144:entity},byte_memory={},capture_stack=True,halt_on_leave=True)
        side_effects.append({'object_pointer':pointer,'now':now,'trace':t})
    addresses=[77862,77857,77850,77847,78120,78109,78095,78087,78079,78074,78068,
               78066,78037,78014,78009,78000,77991,77972,77959,77936]
    addresses.extend(k['address'] for k in keys)
    strings={str(a):vm_string(data,h,a) for a in addresses}
    out={'source':q['source'],'sha256':q['sha256'],'functions':functions,'keys':keys,
         'comparison_traces':comparisons,'character_traces':chars,'lookup_result_traces':lookup_results,
         'pending_tail_traces':tails,'source_length_traces':lengths,'health_traces':health,'hp_traces':hp,
         'momo_traces':side_effects,'strings':strings,
         'limitations':'Static partitions and bounded synthetic instruction slices. No original process or full '
         'QVM executes. CALLs return zero placeholders; source lengths and isolated lookup returns are supplied '
         'inputs, not executed callees. No composed parser output or formatting/copy/append execution. '
         'Timer, weapon, location, leader and crosshair bodies are retained but not fully evaluated; field '
         'identities, ownership, marker lifetime and original runtime/platform behavior remain open.'}
    write_json(Path('docs/research/chat-expansion.json'),out)
    print(f'Recovered {len(keys)} substitution keys, parser state and health/momo slices')


if __name__=='__main__':main()