Download scripts/analyze_message_strings.py from nmarshall/urt-rs: direct link, hf CLI and curl.
- Browser
- Download file 11.3 kB
-
https://huggingface.co/nmarshall/urt-rs/resolve/8fb42db136c756d4a8a8cc79dbf1dfb2bf24cc2a/scripts/analyze_message_strings.py
- Command line
-
hf download hf://nmarshall/urt-rs@8fb42db136c756d4a8a8cc79dbf1dfb2bf24cc2a/scripts/analyze_message_strings.py
-
curl -L -o analyze_message_strings.py https://huggingface.co/nmarshall/urt-rs/resolve/8fb42db136c756d4a8a8cc79dbf1dfb2bf24cc2a/scripts/analyze_message_strings.py
11.3 kB
| #!/usr/bin/env python3 | |
| """Bounded OOB ReadData and string codec instruction compositions.""" | |
| import copy | |
| import gzip | |
| import itertools | |
| import json | |
| from pathlib import Path | |
| import re | |
| from analyze_area_queries import site | |
| from analyze_bsp_loading import read_address | |
| from analyze_bsp_references import global_address, trace_fields | |
| from analyze_collision_composition import get_word, put_word | |
| from analyze_engine_control import control_flow | |
| from analyze_message_byte_codec import MSG, DATA | |
| from analyze_message_widths import reader, run, writer | |
| from analyze_message_wrappers import message_memory, SOURCE, WRAPPER_STACK | |
| from analyze_native import elf | |
| from analyze_qvm import write_json | |
| from analyze_vm_extensions import native_functions, verified | |
| DEST=0xe0000 | |
| def string_graph(g): | |
| g=copy.deepcopy(g);omitted=[] | |
| for x in g['instructions']: | |
| # Explicit frame-save/restore omission, including the line reader's | |
| # tail restores which precede its final terminator store. | |
| if x['opcode'] in ['pushq','popq'] or (x['opcode']=='movq' and '(%rsp)' in x['operands']): | |
| omitted.append(x['address']);x['opcode']='nop';x['operands']='' | |
| return g,omitted | |
| def read_string(graphs,name,source,size): | |
| original=graphs[name];g,omitted=string_graph(original);rows=g['instructions'];calls={x['address']:i for i,x in enumerate(rows) if x['opcode']=='callq'};end=next(x['address'] for x in rows if x['opcode']=='retq') | |
| zero=next(x for x in original['instructions'] if x['opcode']=='movb' and x['operands'].startswith('$0x0,'));base=int(re.search(r', (0x[0-9a-f]+)',zero['operands'])[1],16);capacity=8192 if name=='MSG_ReadBigString' else 1024 | |
| m=message_memory(16384) | |
| for i,v in enumerate(source):m[DATA+i]=v | |
| m[DATA+len(source)]=0 | |
| for i in range(capacity):m[base+i]=0xa5 | |
| m[base-1]=m[base+capacity]=0x7b;put_word(m,MSG+28,size) | |
| regs={'%rdi':MSG,'%rbx':MSG,'%rsp':WRAPPER_STACK};pc=rows[0]['address'];trace=[];count=0 | |
| while True: | |
| t=run(g,pc,list(calls)+[end],m,regs);m=t['memory'];regs=t['registers'];trace+=t['trace'] | |
| if t['stop']==end:break | |
| i=calls[t['stop']] | |
| if '<MSG_ReadBits>' not in rows[i]['operands'] or [regs['rdi'],regs['rsi']]!=[MSG,8]:raise ValueError('unexpected string read request') | |
| m,value,error,child=reader(graphs,m,8);trace+=child | |
| if error:raise ValueError('unexpected OOB read error') | |
| regs['rax']=value;count+=1;pc=rows[i+1]['address'] | |
| if count>capacity:raise ValueError('string fixture request bound') | |
| result=bytes(m[base+i] for i in range(capacity));length=result.index(0) | |
| return {'function':name,'source_hex':bytes(source).hex(),'size':size,'capacity':capacity,'read_requests':count,'read_count':get_word(m,MSG+32),'bit':get_word(m,MSG+36),'result_hex':result[:length+1].hex(),'tail_unchanged':all(v==0xa5 for v in result[length+1:]),'returned_pointer':regs['rax'],'buffer_address':base,'canaries':[m[base-1],m[base+capacity]],'omitted_frame_instructions':omitted,**trace_fields({'trace':trace},False)} | |
| def write_string(graphs,name,source,capacity,constant,strlen_override=None): | |
| g=graphs[name];rows=g['instructions'];start=site(g,'testq','%rsi, %rsi');end=next(x['address'] for x in rows if x['opcode']=='movq' and re.match(r'0x(?:400|2000)\(%rsp\), %rbx',x['operands']));calls={x['address']:i for i,x in enumerate(rows) if x['opcode']=='callq'} | |
| m=message_memory(capacity);put_word(m,constant[0],constant[1],1) | |
| if source is not None: | |
| for i,v in enumerate(source+[0]):m[SOURCE+i]=v | |
| regs={'%rsi':SOURCE if source is not None else 0,'%rbx':SOURCE if source is not None else 0,'%r12':MSG,'%rsp':WRAPPER_STACK};pc=start;trace=[];values=[];copy_requests=[];prints=0 | |
| for x in graphs['MSG_WriteBits']['instructions']: | |
| if x['opcode']=='addl' and '(%rip)' in x['operands']:put_word(m,global_address(x),0) | |
| while True: | |
| t=run(g,pc,list(calls)+[end],m,regs);m=t['memory'];regs=t['registers'];trace+=t['trace'] | |
| if t['stop']==end:break | |
| i=calls[t['stop']];target=rows[i]['operands'] | |
| if '<strlen@plt>' in target: | |
| if regs['rdi']!=SOURCE:raise ValueError('strlen pointer') | |
| regs['rax']=len(source) if strlen_override is None else strlen_override # Explicit supplied library return. | |
| elif '<Q_strncpyz>' in target: | |
| args=[regs[k] for k in ['rdi','rsi','rdx']];copy_requests.append(args) | |
| if args[:2]!=[WRAPPER_STACK,SOURCE] or len(source)>=args[2]:raise ValueError('copy fixture') | |
| for j,v in enumerate(source+[0]):m[WRAPPER_STACK+j]=v # Supplied copy/NUL effects. | |
| elif '<Com_Printf>' in target:prints+=1 # Logging callee remains unexecuted. | |
| elif '<MSG_WriteBits>' in target: | |
| if [regs['rdi'],regs['rdx']]!=[MSG,8]:raise ValueError('string writer request') | |
| values.append(regs['rsi']);m,requests,error,child=writer(graphs,m,8,regs['rsi']);trace+=child | |
| if requests or error:raise ValueError('unexpected OOB write') | |
| else:raise ValueError('unexpected string helper '+target) | |
| pc=rows[i+1]['address'] | |
| if len(values)>256:raise ValueError('string writer fixture bound') | |
| size=get_word(m,MSG+28) | |
| return {'function':name,'source_hex':None if source is None else bytes(source).hex(),'capacity':capacity,'supplied_strlen_word':None if source is None else len(source) if strlen_override is None else strlen_override,'requested_hex':bytes(values).hex(),'copy_requests':copy_requests,'printf_requests':prints,'payload_hex':bytes(m[DATA+i] for i in range(size)).hex(),'size':size,'bit':get_word(m,MSG+36),'overflow_flag':get_word(m,MSG+4),'source_unchanged':source is None or all(m[SOURCE+i]==v for i,v in enumerate(source+[0])),'canaries':[m[DATA-1],m[DATA+64]],**trace_fields({'trace':trace},False)} | |
| def main(): | |
| out={'native':{},'limitations':'Synthetic Linux ELF64 OOB instruction compositions; original processes and native callees never execute. ReadData and strings compose ReadBits/WriteBits with explicit caller-register retention and virtual frames. String strlen return and Q_strncpyz copy/NUL effects are supplied, not native library execution. Frame saves/restores/PUSH/POP excluded/stopped. Padded backing storage is distinct from cursize. No compressed strings/data, actual packets, alias/lifecycle/capacity-wrap/ABI/runtime/all-platform conformance.'} | |
| 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']);meta,_=elf(data);a=r['artifacts']['disassembly.txt'];fs=native_functions(verified(gzip.open(a['path'],'rb').read(),a['uncompressed_sha256']).decode());names=['MSG_ReadData','MSG_ReadString','MSG_ReadBigString','MSG_ReadStringLine','MSG_WriteString','MSG_WriteBigString','MSG_ReadBits','MSG_WriteBits'];graphs={n:control_flow(fs[n]) for n in names} | |
| data_probes=[];g=graphs['MSG_ReadData'];rows=g['instructions'];start=site(g,'testl','%edx, %edx');end=site(g,'addq','$0x8, %rsp');calls={x['address']:i for i,x in enumerate(rows) if x['opcode']=='callq'} | |
| for length in [-1,0,1,2,3,4,5,6,7,8,9,16,17,31,32]: | |
| for size in sorted({0,1,max(0,length-1),max(0,length)}): | |
| source=[(129+37*i)&255 for i in range(40)];m=message_memory(64) | |
| for i,v in enumerate(source):m[DATA+i]=v | |
| for i in range(40):m[DEST+i]=0xa5 | |
| m[DEST-1]=m[DEST+40]=0x7b;put_word(m,MSG+28,size) | |
| regs={'%r15':length&0xffffffff,'%r14':DEST,'%rbp':MSG,'%rdx':length&0xffffffff,'%rdi':MSG,'%rsp':WRAPPER_STACK};pc=start;trace=[];count=0 | |
| while True: | |
| t=run(g,pc,list(calls)+[end],m,regs);m=t['memory'];regs=t['registers'];trace+=t['trace'] | |
| if t['stop']==end:break | |
| i=calls[t['stop']] | |
| if '<MSG_ReadBits>' not in rows[i]['operands'] or [regs['rdi'],regs['rsi']]!=[MSG,8]:raise ValueError('data read request') | |
| m,value,error,child=reader(graphs,m,8);trace+=child | |
| if error:raise ValueError('data OOB read') | |
| regs['rax']=value;count+=1;pc=rows[i+1]['address'] | |
| if count>32:raise ValueError('data read fixture bound') | |
| data_probes.append({'length':length,'size':size,'read_requests':count,'read_count':get_word(m,MSG+32),'bit':get_word(m,MSG+36),'source_hex':bytes(source).hex(),'destination_hex':bytes(m[DEST+i] for i in range(40)).hex(),'canaries':[m[DEST-1],m[DEST+40]],**trace_fields({'trace':trace},False)}) | |
| reads=[] | |
| sequences=[[0],[65,66,67,0],[37,128,255,65,10,0],[13,10,65,0],list(range(1,256))+[0]] | |
| for name,source in itertools.product(['MSG_ReadString','MSG_ReadBigString','MSG_ReadStringLine'],sequences): | |
| for size in sorted({0,len(source)-1,len(source)}):reads.append(read_string(graphs,name,source,size)) | |
| for name in ['MSG_ReadString','MSG_ReadBigString','MSG_ReadStringLine']: | |
| capacity=8192 if name=='MSG_ReadBigString' else 1024;source=[65]*capacity+[0];reads.append(read_string(graphs,name,source,len(source))) | |
| writes=[];guards=[];constants={} | |
| patterns=[None,[],[37,128,255,65,10]]+[[128 if i%2 else 65 for i in range(length)] for length in [1,2,3,4,5,6,7,8,9,17,31]] | |
| for name in ['MSG_WriteString','MSG_WriteBigString']: | |
| g=graphs[name];rows=g['instructions'];x=next(x for x in rows if x['opcode']=='movzbl' and '(%rip)' in x['operands']);address=global_address(x);value,offset=read_address(data,meta['sections'],address,1);constant=(address,value[0]);constants[name]={'address':address,'file_offset':offset,'byte':value[0]} | |
| for source,capacity in itertools.product(patterns,[4,64]):writes.append(write_string(graphs,name,source,capacity,constant)) | |
| ci=next(i for i,x in enumerate(rows) if '<strlen@plt>' in x['operands']);stops=[x['address'] for x in rows if x['opcode']=='callq' and ('<Q_strncpyz>' in x['operands'] or '<Com_Printf>' in x['operands'])];limit=8192 if name=='MSG_WriteBigString' else 1024 | |
| for length in [limit,limit+1]:writes.append(write_string(graphs,name,[],64,constant,strlen_override=length)) | |
| for length in [0,limit-1,limit,limit+1,0xffffffff,0x80000000]: | |
| t=run(g,rows[ci+1]['address'],stops,{}, {'%rax':length,'%rbx':SOURCE,'%rsp':WRAPPER_STACK});target=next(x['operands'] for x in rows if x['address']==t['stop']);guards.append({'function':name,'supplied_strlen_word':length,'outcome':'copy_request' if '<Q_strncpyz>' in target else 'printf_request','copy_arguments':[t['registers'][k] for k in ['rdi','rsi','rdx']] if '<Q_strncpyz>' in target else None,**trace_fields(t,False)}) | |
| out['native'][label]={'source':r['source'],'disassembly_sha256':a['uncompressed_sha256'],'graphs':graphs,'data_reads':data_probes,'string_reads':reads,'string_writes':writes,'writer_length_guards':guards,'writer_empty_constants':constants} | |
| write_json(Path('docs/research/message-strings.json'),out) | |
| print('Recovered OOB data reads and string read/write boundary compositions') | |
| if __name__=='__main__':main() | |