XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
"""Offline PE compatibility evidence. No attachment, execution or memory writes."""
import argparse, bisect, collections, hashlib, json, re, struct
from pathlib import Path
import pefile
from capstone import Cs, CS_ARCH_X86, CS_MODE_64, CS_GRP_JUMP, CS_GRP_CALL
from capstone.x86 import X86_OP_MEM, X86_REG_RIP

p=argparse.ArgumentParser(description=__doc__)
p.add_argument('old',type=Path);p.add_argument('new',type=Path);p.add_argument('--output',type=Path,required=True)
a=p.parse_args()
md=Cs(CS_ARCH_X86,CS_MODE_64);md.detail=True

class Image:
    def __init__(self,path):
        self.path=path;self.pe=pefile.PE(str(path),fast_load=True)
        self.pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXCEPTION']])
        self.base=self.pe.OPTIONAL_HEADER.ImageBase
        self.rows=sorted((x.struct.BeginAddress,x.struct.EndAddress) for x in self.pe.DIRECTORY_ENTRY_EXCEPTION)
        self.starts=[x[0] for x in self.rows];self.cache={}
    def data(self,rva,n):return self.pe.get_data(rva,n)
    def row(self,rva):
        i=bisect.bisect_right(self.starts,rva)-1
        if i>=0 and self.rows[i][0]<=rva<self.rows[i][1]:return self.rows[i]
        data=self.data(rva,512);used=0
        for ins in md.disasm(data,rva):
            used+=ins.size
            if ins.mnemonic in ('ret','jmp','int3'):break
        return rva,rva+used
    def norm(self,rva,end=None):
        key=(rva,end)
        if key in self.cache:return self.cache[key]
        if end is None:rva,end=self.row(rva)
        data=self.data(rva,end-rva);out=bytearray(data);refs=[];used=0
        for ins in md.disasm(data,rva):
            offset=ins.address-rva;used+=ins.size
            for op in ins.operands:
                if op.type==X86_OP_MEM and op.mem.base==X86_REG_RIP:
                    at=offset+ins.disp_offset;n=ins.disp_size
                    out[at:at+n]=b'\0'*n;refs.append((offset,ins.address+ins.size+op.mem.disp,'rip'))
                elif op.type==X86_OP_MEM and 0x100000<=op.mem.disp<self.pe.OPTIONAL_HEADER.SizeOfImage and ins.disp_size==4:
                    at=offset+ins.disp_offset;out[at:at+4]=b'\0'*4
                    refs.append((offset,op.mem.disp,'indexedImage'))
            if (ins.group(CS_GRP_JUMP) or ins.group(CS_GRP_CALL)) and ins.imm_size:
                at=offset+ins.imm_offset;n=ins.imm_size
                target=ins.operands[0].imm
                out[at:at+n]=b'\0'*n;refs.append((offset,target,'branch'))
        result=(bytes(out),refs,used==len(data));self.cache[key]=result;return result

old=Image(a.old);new=Image(a.new)
sources=list(Path('native/ManagedPlugin').glob('*.cs'))+list(Path('XFEEDToolBox.Core/Native').glob('*.cs'))
sources=[f for f in sources if 'Tests' not in f.name]
targets=collections.defaultdict(list)
for f in sources:
    for m in re.finditer(r'0x[0-9a-fA-F]+',f.read_text(encoding='utf-8-sig')):
        n=int(m[0],16)
        if 0x100000<=n<old.pe.OPTIONAL_HEADER.SizeOfImage:targets[n].append(str(f))

mapping={};evidence={};conflicts=[];ambiguous={}
def add(x,y,why):
    if x in mapping:
        if mapping[x]!=y:conflicts.append([hex(x),hex(mapping[x]),hex(y),why])
        return False
    mapping[x]=y;evidence[x]=why;return True

textold=old.pe.sections[0];textnew=new.pe.sections[0]
newbytes=textnew.get_data();textstart=textnew.VirtualAddress
def match_function(x):
    begin,end=old.row(x);norm,refs,valid=old.norm(begin,end)
    if not valid or len(norm)==0:return []
    # Find the longest unrelocated prefix anchor; then verify the entire normalized function.
    raw=old.data(begin,end-begin);chunks=[];start=0
    for i,(b,c) in enumerate(zip(raw,norm)):
        if b!=c:
            if i-start>=4:chunks.append((start,raw[start:i]))
            start=i+1
    if len(raw)-start>=4:chunks.append((start,raw[start:]))
    if not chunks:return []
    offset,needle=max(chunks,key=lambda v:len(v[1]));needle=needle[:96]
    found=[];at=max(0,begin-textstart-0x100000)
    limit=min(len(newbytes),begin-textstart+0x100000+len(raw))
    while True:
        at=newbytes.find(needle,at,limit)
        if at<0:break
        candidate=textstart+at-offset;at+=1
        if candidate<0:continue
        candidate_row=new.row(candidate)
        if candidate_row[0]!=candidate or candidate_row[1]-candidate!=len(raw):continue
        candidate_norm,_,valid=new.norm(candidate,candidate+len(raw))
        if valid and candidate_norm==norm:found.append(candidate+(x-begin))
    return sorted(set(found))

for x in targets:
    if x>=0x4DB6000:continue
    hits=match_function(x)
    if len(hits)==1:add(x,hits[0],'unique full normalized function')
    else:ambiguous[x]=hits

# Resolve globals through original RIP references, accepting only whole matching functions.
oldtext=textold.get_data();referenced_functions=set()
pattern=rb'[\x40-\x4f]?(?:[\x8b\x8d\x89\x3b\x39\x85\x83\x80\xc7\xff]|\x0f[\xb6\xb7\xbe\xbf\x10\x11\x28\x29\x2e\x2f])[\x05\x0d\x15\x1d\x25\x2d\x35\x3d]....'
for m in re.finditer(pattern,oldtext,re.S):
    rva=textold.VirtualAddress+m.start();instructions=list(md.disasm(old.data(rva,15),rva,count=1))
    if not instructions:continue
    ins=instructions[0]
    if not any(op.type==X86_OP_MEM and op.mem.base==X86_REG_RIP and ins.address+ins.size+op.mem.disp in targets for op in ins.operands):continue
    begin,end=old.row(rva)
    if begin==rva and end-begin<=0:continue
    if begin in referenced_functions:continue
    referenced_functions.add(begin)
    hits=match_function(begin)
    if len(hits)==1:add(begin,hits[0],'unique full function containing original RIP reference')
for m in re.finditer(rb'[\xe8\xe9]....',oldtext,re.S):
    at=textold.VirtualAddress+m.start();target=at+5+struct.unpack_from('<i',m[0],1)[0]
    if target not in targets or target in mapping:continue
    begin,end=old.row(at)
    if begin in referenced_functions:continue
    referenced_functions.add(begin);hits=match_function(begin)
    if len(hits)==1:add(begin,hits[0],'unique full original caller function')

# Mapped instruction references also verify static global addresses and leaf functions.
queue=list(mapping);done=set()
while queue:
    x=queue.pop()
    if x in done or x>=0x4DB6000:continue
    done.add(x);y=mapping[x];ob,oe=old.row(x);nb,ne=new.row(y)
    on,orr,ok=old.norm(ob,oe);nn,nrr,nok=new.norm(nb,ne)
    if not(ok and nok and on==nn):continue
    for (off,ot,kind),(nf,nt,nkind) in zip(orr,nrr):
        if off!=nf or kind!=nkind or ot not in targets:continue
        if add(ot,nt,f'{kind} reference in mapped function {ob:#x} at +{off:#x}'):queue.append(ot)

# Vtables must match at least eight consecutive entries, after applying verified code mappings.
for x in targets:
    if x<0x4DB6000 or x>=0x5E66000 or x in mapping:continue
    oldvalues=struct.unpack('<16Q',old.data(x,128));anchors=[]
    for i,value in enumerate(oldvalues):
        rva=value-old.base
        if rva not in mapping and 0x1000<=rva<0x4DB6000:
            hits=match_function(rva)
            if len(hits)==1:add(rva,hits[0],'unique vtable entry full normalized function')
        if rva in mapping:anchors.append((i,new.base+mapping[rva]))
    if len(anchors)<2:continue
    i,value=anchors[0];rdata=new.pe.sections[1];rd=rdata.get_data();at=0;hits=[]
    while True:
        at=rd.find(struct.pack('<Q',value),at)
        if at<0:break
        candidate=rdata.VirtualAddress+at-i*8;at+=1
        if candidate<rdata.VirtualAddress:continue
        if all(struct.unpack('<Q',new.data(candidate+j*8,8))[0]==v for j,v in anchors):hits.append(candidate)
    if len(hits)==1:add(x,hits[0],f'unique vtable match using {len(anchors)} independently mapped entries')

# Short identical leaf routines are distinguished by their original vtable neighbourhood.
rdold=old.pe.sections[1].get_data();rdnew=new.pe.sections[1].get_data()
for x in list(targets):
    if x in mapping or x>=0x4db6000:continue
    pointer=struct.pack('<Q',old.base+x);at=0;candidate_values=set()
    while True:
        at=rdold.find(pointer,at)
        if at<0:break
        here=at;at+=1;anchors=[]
        for off in range(-64,65,8):
            if off==0 or here+off<0 or here+off+8>len(rdold):continue
            val=struct.unpack_from('<Q',rdold,here+off)[0]-old.base
            if val in mapping:anchors.append((off,new.base+mapping[val]))
        if len(anchors)<3:continue
        off,val=anchors[0];pos=0
        while True:
            pos=rdnew.find(struct.pack('<Q',val),pos)
            if pos<0:break
            origin=pos-off;pos+=1
            if origin<64 or origin+72>len(rdnew):continue
            if all(struct.unpack_from('<Q',rdnew,origin+o)[0]==v for o,v in anchors):
                candidate_values.add(struct.unpack_from('<Q',rdnew,origin)[0]-new.base)
    if len(candidate_values)==1:
        y=next(iter(candidate_values));ob,oe=old.row(x);nb,ne=new.row(y)
        on,_,ok=old.norm(ob,oe);nn,_,nok=new.norm(nb,ne)
        if ok and nok and on==nn:add(x,y,'original vtable neighbourhood plus identical complete normalized leaf')
        else:ambiguous[x]=[y]
for table,offset,target in [(0x5165e48,0x30,0xeacb30)]:
    if table not in mapping:continue
    assert struct.unpack('<Q',old.data(table+offset,8))[0]==old.base+target
    y=struct.unpack('<Q',new.data(mapping[table]+offset,8))[0]-new.base
    if old.norm(target)[0]==new.norm(y)[0]:add(target,y,'verified mapped owning vtable slot and full normalized function')
if 0x8ae6f0 not in mapping:
    ins=list(md.disasm(old.data(0x8ae6f0,12),0x8ae6f0))[0]
    global_id=ins.address+ins.size+ins.operands[1].mem.disp
    if global_id in mapping:
        hits=[]
        for y in ambiguous.get(0x8ae6f0,[]):
            newins=list(md.disasm(new.data(y,12),y))[0]
            if newins.address+newins.size+newins.operands[1].mem.disp==mapping[global_id]:hits.append(y)
        if len(hits)==1:add(0x8ae6f0,hits[0],'identical complete leaf and independently verified component identifier global')
if 0x8ae6f0 in mapping:
    add(0x8ae6f6,mapping[0x8ae6f0]+6,'end of verified getter RIP-relative instruction')
if 0x5fdf490 in mapping and 0x5fdf494 in targets:
    add(0x5fdf494,mapping[0x5fdf490]+4,'second uint32 of the verified packed stellar identifier pair')
# The radius getter includes a two-entry image-relative jump table after its RET.
# Verify all code and both table targets rather than disassembling the table as code.
radius=0x3cf04b0
if radius in targets and radius not in mapping:
    body_pointer=struct.pack('<Q',old.base+0x3ceb400);index=rdold.find(body_pointer)
    if index>=0 and rdold.find(body_pointer,index+1)<0:
        oldtable=old.pe.sections[1].VirtualAddress+index-0x60
        newptr=struct.pack('<Q',new.base+mapping[0x3ceb400]);ni=rdnew.find(newptr)
        if ni>=0 and rdnew.find(newptr,ni+1)<0:
            newtable=new.pe.sections[1].VirtualAddress+ni-0x60
            candidate=struct.unpack('<Q',new.data(newtable+0xd0,8))[0]-new.base
            assert struct.unpack('<Q',old.data(oldtable+0xd0,8))[0]-old.base==radius
            same=old.norm(radius,radius+0xbf)[0]==new.norm(candidate,candidate+0xbf)[0]
            ot=struct.unpack('<II',old.data(radius+0xc0,8));nt=struct.unpack('<II',new.data(candidate+0xc0,8))
            same= same and all(oa-radius==na-candidate for oa,na in zip(ot,nt))
            same= same and old.data(radius+0xc8,17)==new.data(candidate+0xc8,17)
            if same:add(radius,candidate,'unique body-service vtable; complete code, relative switch targets and 17 case indices verified')

result={'oldSha256':hashlib.sha256(a.old.read_bytes()).hexdigest(),'newSha256':hashlib.sha256(a.new.read_bytes()).hexdigest(),
 'targets':len(targets),'mappedTargets':sum(x in mapping for x in targets),'conflicts':conflicts,
 'mapping':[{ 'old':hex(x),'new':hex(mapping[x]),'evidence':evidence[x],'sources':targets[x]} for x in sorted(targets) if x in mapping],
 'unresolved':[{'old':hex(x),'candidates':[hex(y) for y in ambiguous.get(x,[])],'sources':targets[x]} for x in sorted(targets) if x not in mapping]}
a.output.parent.mkdir(parents=True,exist_ok=True);a.output.write_text(json.dumps(result,indent=2),encoding='utf-8')
print(json.dumps({k:v for k,v in result.items() if k not in ('mapping','unresolved')},indent=2))
print('Unresolved:',', '.join(x['old'] for x in result['unresolved']))