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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
"""Read-only lookup of the build-pinned, original commodity market UI model.

No native calls, hooks, input, transactions, process writes or screen access.
Only exact class candidates and their bounded model headers are exported.
"""
import argparse,ctypes as c,ctypes.wintypes as w,hashlib,json,struct,time
from pathlib import Path

p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--pid',type=int,required=True)
p.add_argument('--output',type=Path,required=True)
p.add_argument('--max-megabytes',type=int,default=6144)
a=p.parse_args()
if not 1<=a.max_megabytes<=8192:raise ValueError('Bounded read budget required')
image=Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe')
if hashlib.sha256(image.read_bytes()).hexdigest()!='6891f70a021fa9f3a0e00458c284f48d1226e68ae7fc02ad48081aed47857e93':raise ValueError('Game build changed')
k=c.WinDLL('kernel32',use_last_error=True)
k.OpenProcess.argtypes=[w.DWORD,w.BOOL,w.DWORD];k.OpenProcess.restype=w.HANDLE
k.CloseHandle.argtypes=[w.HANDLE]
k.QueryFullProcessImageNameW.argtypes=[w.HANDLE,w.DWORD,w.LPWSTR,c.POINTER(w.DWORD)]
k.ReadProcessMemory.argtypes=[w.HANDLE,c.c_void_p,c.c_void_p,c.c_size_t,c.POINTER(c.c_size_t)]
class Region(c.Structure):
    _fields_=[('BaseAddress',c.c_void_p),('AllocationBase',c.c_void_p),('AllocationProtect',w.DWORD),('PartitionId',w.WORD),('RegionSize',c.c_size_t),('State',w.DWORD),('Protect',w.DWORD),('Type',w.DWORD)]
class Module(c.Structure):
    _fields_=[('dwSize',w.DWORD),('th32ModuleID',w.DWORD),('th32ProcessID',w.DWORD),('GlblcntUsage',w.DWORD),('ProccntUsage',w.DWORD),('modBaseAddr',c.c_void_p),('modBaseSize',w.DWORD),('hModule',w.HMODULE),('szModule',w.WCHAR*256),('szExePath',w.WCHAR*260)]
k.VirtualQueryEx.argtypes=[w.HANDLE,c.c_void_p,c.POINTER(Region),c.c_size_t];k.VirtualQueryEx.restype=c.c_size_t
k.CreateToolhelp32Snapshot.argtypes=[w.DWORD,w.DWORD];k.CreateToolhelp32Snapshot.restype=w.HANDLE
k.Module32FirstW.argtypes=[w.HANDLE,c.POINTER(Module)];k.Module32NextW.argtypes=[w.HANDLE,c.POINTER(Module)]
process=k.OpenProcess(0x410,False,a.pid)
if not process:raise c.WinError(c.get_last_error())
def read(at,size):
    buf=c.create_string_buffer(size);done=c.c_size_t()
    if not k.ReadProcessMemory(process,at,buf,size,c.byref(done)):return b''
    return buf.raw[:done.value]
try:
    name=c.create_unicode_buffer(32768);length=w.DWORD(len(name))
    if not k.QueryFullProcessImageNameW(process,0,name,c.byref(length)) or Path(name.value).resolve()!=image.resolve():raise ValueError('Unexpected process image')
    snap=k.CreateToolhelp32Snapshot(0x18,a.pid);m=Module();m.dwSize=c.sizeof(m);found=k.Module32FirstW(snap,c.byref(m));base=0
    while found:
        if m.szModule.lower()=='elitedangerous64.exe':base=m.modBaseAddr;break
        found=k.Module32NextW(snap,c.byref(m))
    k.CloseHandle(snap)
    if not base:raise ValueError('Game module absent')
    patterns={struct.pack('<Q',base+0x524A160):'commodity-market',struct.pack('<Q',base+0x5240AE8):'station-maintenance'}
    at=0x10000;scanned=0;regions=0;rows=[];seen=set();start=time.monotonic();budget=a.max_megabytes*1024*1024
    while at<0x7fffffffffff and scanned<budget and time.monotonic()-start<45:
        r=Region()
        if not k.VirtualQueryEx(process,at,c.byref(r),c.sizeof(r)):break
        end=r.BaseAddress+r.RegionSize;regions+=1
        if r.State==0x1000 and r.Type==0x20000 and r.Protect in (0x04,0x40):
            for pos in range(r.BaseAddress,end,1024*1024):
                if scanned>=budget or time.monotonic()-start>=45:break
                data=read(pos,min(1024*1024,end-pos,budget-scanned));scanned+=len(data)
                for pattern,label in patterns.items():
                    offset=0
                    while (offset:=data.find(pattern,offset))>=0:
                        candidate=pos+offset;offset+=8
                        if candidate%8 or candidate in seen:continue
                        seen.add(candidate);header=read(candidate,0x500)
                        if len(header)!=0x500:continue
                        pointer=lambda x:struct.unpack_from('<Q',header,x)[0]
                        if label=='station-maintenance':
                            parent=read(candidate-0x58,0x58)
                            if len(parent)==0x58 and struct.unpack_from('<Q',parent)[0]==base+0x52409B8 and struct.unpack_from('<Q',parent,8)[0]==base+0x52409E8:
                                rows.append({'kind':label,'address':hex(candidate),'header':header.hex(),'parentHeader':parent.hex(),'methods':[hex(struct.unpack('<Q',read(base+0x5240AE8+i*8,8))[0]-base) for i in range(32)]})
                            continue
                        if pointer(0x20)!=base+0x524A1E8 or pointer(0xC8)!=base+0x524A238:continue
                        rows.append({'kind':label,'address':hex(candidate),'station':hex(pointer(0x370)),
                                     'normalMarket':header[0x3C8:0x3F8].hex(),'otherMarket':header[0x3F8:0x428].hex(),
                                     'quotes':header[0x378:0x3A8].hex(),'pending':hex(pointer(0x478)),
                                     'controller':hex(pointer(0x270)),'header':header.hex()})
        at=end
    result={'pid':a.pid,'base':hex(base),'readBytes':scanned,'regions':regions,'seconds':time.monotonic()-start,'models':rows,'gameInputsSent':0,'transactionsSent':0,'processAccess':'QUERY_INFORMATION | VM_READ'}
    with a.output.open('x',encoding='utf-8') as out:json.dump(result,out,indent=2)
    print(json.dumps({key:value for key,value in result.items() if key!='models'}))
    print(json.dumps({'models':[{key:value for key,value in row.items() if key!='header'} for row in rows]}))
finally:k.CloseHandle(process)