"""Compare exact original appearance bodies, including their embedded RVA tables.
This produces offline evidence only. It changes no native profile, hash guard,
running process, or original client file.
"""
import hashlib,json,struct
from pathlib import Path
import pefile
from capstone import Cs,CS_ARCH_X86,CS_MODE_64,CS_OP_MEM,CS_OP_IMM
from capstone.x86 import X86_REG_RIP,X86_REG_R11
old_path=Path('artifacts/ship-model-reference/client-copy/EliteDangerous64.exe')
new_path=Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe')
old=pefile.PE(str(old_path),fast_load=True);new=pefile.PE(str(new_path),fast_load=True)
cs=Cs(CS_ARCH_X86,CS_MODE_64);cs.detail=True
rows=[]
for before,after,size,code_size in [(0x3C3F440,0x3C3F3A0,0x158,0x138),(0x3C3F5A0,0x3C3F500,0x204,0x204)]:
buffers=[bytearray(old.get_data(before,size)),bytearray(new.get_data(after,size))]
instructions=[list(cs.disasm(buffers[0][:code_size],before)),list(cs.disasm(buffers[1][:code_size],after))]
if len(instructions[0])!=len(instructions[1]):raise ValueError('Changed actual instruction count')
changes=[]
for left,right in zip(*instructions):
if left.address-before!=right.address-after or left.size!=right.size or left.mnemonic!=right.mnemonic:
raise ValueError('Changed original instruction shape')
start=left.address-before
if left.bytes==right.bytes:continue
kind=None;targets=None
if any(op.type==CS_OP_MEM and op.mem.base==X86_REG_RIP for op in left.operands):
kind='rip-relative-address'
targets=[hex(i.address+i.size+i.disp) for i in (left,right)]
for data,instruction in zip(buffers,(left,right)):
data[start+instruction.disp_offset:start+instruction.disp_offset+instruction.disp_size]=b'\0'*instruction.disp_size
elif left.mnemonic in ('call','jmp') or left.mnemonic.startswith('j'):
kind='relative-control-flow';targets=[hex(i.operands[0].imm) for i in (left,right)]
for data,instruction in zip(buffers,(left,right)):
data[start+instruction.imm_offset:start+instruction.imm_offset+instruction.imm_size]=b'\0'*instruction.imm_size
elif left.disp_size==4 and right.disp_size==4:
if (left.disp,right.disp) not in ((0x3C3F578,0x3C3F4D8),(0x5539360,0x5539330),(0x5539350,0x5539320)):
raise ValueError('Unreviewed image-base displacement change')
kind='image-base-indexed-address';targets=[hex(left.disp),hex(right.disp)]
for data,instruction in zip(buffers,(left,right)):
data[start+instruction.disp_offset:start+instruction.disp_offset+4]=b'\0'*4
if kind is None:raise ValueError('Unclassified changed instruction')
changes.append(dict(offset=hex(start),kind=kind,targets=targets,old=left.mnemonic+' '+left.op_str,new=right.mnemonic+' '+right.op_str))
jump_entries=[]
for offset in range(code_size,size,4):
a=struct.unpack_from('<I',buffers[0],offset)[0];b=struct.unpack_from('<I',buffers[1],offset)[0]
if a-before!=b-after or not 0<=a-before<code_size:raise ValueError('Changed original switch destination')
jump_entries.append(dict(old=hex(a),new=hex(b),functionOffset=hex(a-before)))
for data in buffers:struct.pack_into('<I',data,offset,a-before)
if buffers[0]!=buffers[1]:raise ValueError('Unexplained body bytes still differ')
rows.append(dict(oldRva=hex(before),newRva=hex(after),totalBytes=size,actualCodeBytes=code_size,
actualInstructions=len(instructions[0]),normalizedIdentical=True,normalizedSha256=hashlib.sha256(buffers[0]).hexdigest(),
changedOperands=changes,switchEntries=jump_entries))
result=dict(scope='Offline verified operand/table relocation only; not a loadable native profile or live verification',
old=dict(path=str(old_path),sha256=hashlib.sha256(old_path.read_bytes()).hexdigest()),
new=dict(path=str(new_path),sha256=hashlib.sha256(new_path.read_bytes()).hexdigest()),functions=rows,
unchangedRuntimeGlobals=['0x5fdf100','0x5fdf190','0x5fdf2a0','0x5fdf330','0x5fdf3e0','0x5fdf460','0x5fdf4e0','0x5fdf560',
'0x5fdf490','0x5fdf494','0x5fdf498','0x5fdf4a0','0x5fdf4c8'],
changedStaticGlobals={'0x5539360':'0x5539330','0x5539350':'0x5539320'})
out=Path('artifacts/galaxy-client-compatibility/appearance-manual-triage.json')
out.write_text(json.dumps(result,indent=2)+'\n',encoding='utf-8')
print(json.dumps(dict(output=str(out),functions=[{k:r[k] for k in ('oldRva','newRva','totalBytes','actualCodeBytes','actualInstructions','normalizedIdentical')} for r in rows]),indent=2))
"""Compare exact original appearance bodies, including their embedded RVA tables.
This produces offline evidence only. It changes no native profile, hash guard,
running process, or original client file.
"""
import hashlib,json,struct
from pathlib import Path
import pefile
from capstone import Cs,CS_ARCH_X86,CS_MODE_64,CS_OP_MEM,CS_OP_IMM
from capstone.x86 import X86_REG_RIP,X86_REG_R11
old_path=Path('artifacts/ship-model-reference/client-copy/EliteDangerous64.exe')
new_path=Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe')
old=pefile.PE(str(old_path),fast_load=True);new=pefile.PE(str(new_path),fast_load=True)
cs=Cs(CS_ARCH_X86,CS_MODE_64);cs.detail=True
rows=[]
for before,after,size,code_size in [(0x3C3F440,0x3C3F3A0,0x158,0x138),(0x3C3F5A0,0x3C3F500,0x204,0x204)]:
buffers=[bytearray(old.get_data(before,size)),bytearray(new.get_data(after,size))]
instructions=[list(cs.disasm(buffers[0][:code_size],before)),list(cs.disasm(buffers[1][:code_size],after))]
if len(instructions[0])!=len(instructions[1]):raise ValueError('Changed actual instruction count')
changes=[]
for left,right in zip(*instructions):
if left.address-before!=right.address-after or left.size!=right.size or left.mnemonic!=right.mnemonic:
raise ValueError('Changed original instruction shape')
start=left.address-before
if left.bytes==right.bytes:continue
kind=None;targets=None
if any(op.type==CS_OP_MEM and op.mem.base==X86_REG_RIP for op in left.operands):
kind='rip-relative-address'
targets=[hex(i.address+i.size+i.disp) for i in (left,right)]
for data,instruction in zip(buffers,(left,right)):
data[start+instruction.disp_offset:start+instruction.disp_offset+instruction.disp_size]=b'\0'*instruction.disp_size
elif left.mnemonic in ('call','jmp') or left.mnemonic.startswith('j'):
kind='relative-control-flow';targets=[hex(i.operands[0].imm) for i in (left,right)]
for data,instruction in zip(buffers,(left,right)):
data[start+instruction.imm_offset:start+instruction.imm_offset+instruction.imm_size]=b'\0'*instruction.imm_size
elif left.disp_size==4 and right.disp_size==4:
if (left.disp,right.disp) not in ((0x3C3F578,0x3C3F4D8),(0x5539360,0x5539330),(0x5539350,0x5539320)):
raise ValueError('Unreviewed image-base displacement change')
kind='image-base-indexed-address';targets=[hex(left.disp),hex(right.disp)]
for data,instruction in zip(buffers,(left,right)):
data[start+instruction.disp_offset:start+instruction.disp_offset+4]=b'\0'*4
if kind is None:raise ValueError('Unclassified changed instruction')
changes.append(dict(offset=hex(start),kind=kind,targets=targets,old=left.mnemonic+' '+left.op_str,new=right.mnemonic+' '+right.op_str))
jump_entries=[]
for offset in range(code_size,size,4):
a=struct.unpack_from('<I',buffers[0],offset)[0];b=struct.unpack_from('<I',buffers[1],offset)[0]
if a-before!=b-after or not 0<=a-before<code_size:raise ValueError('Changed original switch destination')
jump_entries.append(dict(old=hex(a),new=hex(b),functionOffset=hex(a-before)))
for data in buffers:struct.pack_into('<I',data,offset,a-before)
if buffers[0]!=buffers[1]:raise ValueError('Unexplained body bytes still differ')
rows.append(dict(oldRva=hex(before),newRva=hex(after),totalBytes=size,actualCodeBytes=code_size,
actualInstructions=len(instructions[0]),normalizedIdentical=True,normalizedSha256=hashlib.sha256(buffers[0]).hexdigest(),
changedOperands=changes,switchEntries=jump_entries))
result=dict(scope='Offline verified operand/table relocation only; not a loadable native profile or live verification',
old=dict(path=str(old_path),sha256=hashlib.sha256(old_path.read_bytes()).hexdigest()),
new=dict(path=str(new_path),sha256=hashlib.sha256(new_path.read_bytes()).hexdigest()),functions=rows,
unchangedRuntimeGlobals=['0x5fdf100','0x5fdf190','0x5fdf2a0','0x5fdf330','0x5fdf3e0','0x5fdf460','0x5fdf4e0','0x5fdf560',
'0x5fdf490','0x5fdf494','0x5fdf498','0x5fdf4a0','0x5fdf4c8'],
changedStaticGlobals={'0x5539360':'0x5539330','0x5539350':'0x5539320'})
out=Path('artifacts/galaxy-client-compatibility/appearance-manual-triage.json')
out.write_text(json.dumps(result,indent=2)+'\n',encoding='utf-8')
print(json.dumps(dict(output=str(out),functions=[{k:r[k] for k in ('oldRva','newRva','totalBytes','actualCodeBytes','actualInstructions','normalizedIdentical')} for r in rows]),indent=2))