XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
UTF-8
// Passive observation of the explicitly selected, hash-verified game process.
// No replacement callbacks, memory writes, request rewriting, or native actions.
'use strict';
const image = Process.getModuleByName('EliteDangerous64.exe').base;
const requests = new Map(), contexts = new Map(), installedCallbacks = new Set();
let serial = 0;
const maximumBody = 16 * 1024 * 1024;
const allowedHeaders = new Set(['nonce', 'uncompressedsize', 'content-length', 'content-type', 'last-modified', 'encrypted']);
function text(pointer, maximum) {
    if (pointer.isNull()) return '';
    const bytes = new Uint8Array(pointer.readByteArray(maximum));
    const end = bytes.indexOf(0);
    if (end < 0) throw new Error('unterminated bounded text');
    return String.fromCharCode.apply(null, bytes.subarray(0, end));
}
function shortText(pointer) { return pointer.isNull() ? '' : pointer.readUtf8String(); }
function emit(value, bytes) { value.utc = new Date().toISOString(); send(value, bytes); }
function request(handle) {
    const id = handle.toString();
    if (!requests.has(id)) requests.set(id, { id: ++serial, handle: id, options: new Map(), path: '', query: '', host: '', method: '', headers: {}, responseHeaders: {}, chunks: [], uploadChunks: [], uploadLength: 0, length: 0, truncated: false, finished: false });
    return requests.get(id);
}
function url(value, state) {
    const match = /^(https?):\/\/(\[[^\]]+\]|[^\/:]+)(?::([0-9]+))?([^?]*)(?:\?(.*))?$/.exec(value);
    if (!match) return;
    state.scheme = match[1]; state.host = match[2]; state.port = Number(match[3] || (match[1] === 'https' ? 443 : 80));
    state.path = match[4] || '/';
    // Authentication request credentials are unnecessary for an offline identity.
    state.query = /\/(?:user\/(?:login|logout)|comms\/login)$/.test(state.path) ? '' : match[5] || '';
}
function append(state, bytes) {
    if (state.length + bytes.byteLength > maximumBody) { state.truncated = true; return; }
    state.chunks.push(new Uint8Array(bytes)); state.length += bytes.byteLength;
}
function callback(address, kind) {
    const key = kind + ':' + address;
    if (address.isNull() || installedCallbacks.has(key)) return;
    installedCallbacks.add(key);
    Interceptor.attach(address, {
        onEnter(args) {
            this.state = contexts.get(kind + ':' + args[3]);
            this.pointer = args[0]; this.length = args[1].toUInt32() * args[2].toUInt32();
            if (!this.state || this.length > maximumBody) return;
            try {
                if (kind === 'body') append(this.state, this.pointer.readByteArray(this.length));
                if (kind === 'header') {
                    const line = this.pointer.readUtf8String(this.length).trim();
                    const status = /^HTTP\/\S+\s+([0-9]{3})/.exec(line);
                    if (status) { this.state.status = Number(status[1]); this.state.responseHeaders = {}; }
                    const split = line.indexOf(':');
                    if (split > 0 && allowedHeaders.has(line.slice(0, split).toLowerCase())) this.state.responseHeaders[line.slice(0, split).toLowerCase()] = line.slice(split + 1).trim();
                }
            } catch (error) { this.state.truncated = true; }
        },
        onLeave(result) {
            if(kind!=='upload' || !this.state)return;
            const length=result.toUInt32();
            if(length===0 || length>this.length || this.state.uploadLength+length>maximumBody)return;
            try { this.state.uploadChunks.push(new Uint8Array(this.pointer.readByteArray(length))); this.state.uploadLength+=length; }
            catch (_) { this.state.truncated=true; }
        }
    });
}
function linkCallbacks(state) {
    for (const [kind, functionOption, contextOption] of [['body',20011,10001], ['header',20079,10029], ['upload',20012,10009]]) {
        const functionPointer = state.options.get(functionOption), context = state.options.get(contextOption);
        if (functionPointer && context) { contexts.set(kind + ':' + context, state); callback(functionPointer, kind); }
    }
}
function finish(state, result) {
    if (!state || state.finished || !state.path.startsWith('/2.0/')) return;
    state.finished = true;
    if(state.uploadLength>0){
        const upload=new Uint8Array(state.uploadLength);let cursor=0;
        for(const chunk of state.uploadChunks){upload.set(chunk,cursor);cursor+=chunk.length;}
        emit({type:'http-request-body',id:state.id,path:state.path,method:state.method||'GET',headers:state.headers,query:state.query,length:state.uploadLength},upload.buffer);
    }
    const output = new Uint8Array(state.length); let offset = 0;
    for (const chunk of state.chunks) { output.set(chunk, offset); offset += chunk.length; }
    emit({ type:'http-response', id:state.id, method:state.method || 'GET', scheme:state.scheme, host:state.host, port:state.port,
        path:state.path, query:state.query, requestHeaders:state.headers, headers:state.responseHeaders,
        status:state.status || 0, curlResult:result, length:state.length, truncated:state.truncated }, output.buffer);
    state.chunks = [];
}
Interceptor.attach(image.add(0x48D5770), {
    onEnter(args) { this.state = request(args[0]); this.option = args[1].toInt32(); this.value = args[2]; },
    onLeave(result) {
        if (result.toInt32() !== 0) return;
        const state = this.state; state.options.set(this.option, this.value);
        try {
            if (this.option === 10002) { state.finished = false; state.chunks = []; state.length = 0; state.uploadChunks=[];state.uploadLength=0;state.truncated = false; state.responseHeaders = {}; url(shortText(this.value), state); }
            if (this.option === 10036) state.method = shortText(this.value);
            if (this.option === 46 && !this.value.isNull()) state.method = 'PUT';
            if (this.option === 47 && !this.value.isNull()) state.method = 'POST';
            if (this.option === 10023) {
                state.headers = {};
                let item = this.value;
                for (let index=0; !item.isNull() && index<128; index++, item=item.add(8).readPointer()) {
                    const line = shortText(item.readPointer()), split = line.indexOf(':');
                    if (split>0 && allowedHeaders.has(line.slice(0,split).toLowerCase())) state.headers[line.slice(0,split).toLowerCase()] = line.slice(split+1).trim();
                }
            }
            if ([20011,10001,20079,10029,20012,10009].includes(this.option)) linkCallbacks(state);
        } catch (error) { emit({type:'capture-error', stage:'option', error:String(error)}); }
    }
});
Interceptor.attach(image.add(0x48D6A50), { onEnter(args) {
    const state=request(args[1]);
    try{
        const body=state.options.get(10015), size=state.options.get(60);
        if(body && !body.isNull() && size && size.toInt32()>0 && size.toInt32()<=maximumBody){state.uploadChunks=[new Uint8Array(body.readByteArray(size.toInt32()))];state.uploadLength=size.toInt32();}
    }catch(_){}
    emit({type:'http-start',id:state.id,method:state.method||'GET',host:state.host,path:state.path});
} });
Interceptor.attach(image.add(0x48D6E10), { onLeave(result) {
    try { if (!result.isNull() && result.readS32()===1) finish(requests.get(result.add(8).readPointer().toString()),result.add(16).readS32()); }
    catch (error) { emit({type:'capture-error',stage:'complete',error:String(error)}); }
} });
Interceptor.attach(image.add(0x48D56E0), { onEnter(args) {
    const handle=args[0].toString(), state=requests.get(handle);
    if (state) { finish(state,-1); for(const [key,value] of contexts)if(value===state)contexts.delete(key); requests.delete(handle); }
} });
// Main game flow observations never call game methods or select menus.
let nextFlow=0, previousFlow='';
function identifier(pointer) {
    const value=pointer.readUtf8String(64).split('\0')[0];
    return /^[A-Za-z_][A-Za-z0-9_]{0,62}$/.test(value)?value:'?';
}
Interceptor.attach(image.add(0x2154C40), {
    onEnter(args) { this.flowContext=args[1]; },
    onLeave() {
        if(Date.now()<nextFlow)return; nextFlow=Date.now()+500;
        try {
            const flow=this.flowContext.sub(0x70).add(0xA0).readPointer();
            if(!flow.readPointer().equals(image.add(0x5236DB0)))return;
            const csm=flow.add(0x310).readPointer();
            if(!csm.readPointer().equals(image.add(0x5138DC0)))return;
            const from=csm.add(0x100).readPointer(), to=csm.add(0x108).readPointer();
            const current=from.isNull()?'Null':identifier(from.readPointer()), target=to.isNull()?'Null':identifier(to.readPointer());
            const value=current+'>'+target;
            if(value!==previousFlow){previousFlow=value;emit({type:'game-flow',current,target});}
        } catch (_) { }
    }
});
for(const [name,rva] of [['load-game',0x1D82470],['read-native-save',0x2404620]])
    Interceptor.attach(image.add(rva),{onEnter(){emit({type:'native-milestone',name});}});
// The verified Powerplay JSON reader also receives cached plaintext on a cold
// start. Observing it distinguishes a valid cached catalogue from an HTTP 304.
Interceptor.attach(image.add(0x1418580), {onEnter(args){
    try {
        const value=args[1].readPointer();
        if(value.isNull())return;
        const length=value.readU64().toNumber();
        if(length<=0 || length>maximumBody)return;
        emit({type:'native-data-document',id:++serial,path:'/2.0/elite/powerplay2/powers/list',source:'native-plaintext-reader-cache-or-http',length},value.add(0x14).readByteArray(length));
    }catch(error){emit({type:'capture-error',stage:'powerplay-plaintext-reader',error:String(error)});}
}});
const winsock=Process.getModuleByName('ws2_32.dll');
const getLocal=new NativeFunction(winsock.getExportByName('getsockname'),'int',['pointer','pointer','pointer']);
const getRemote=new NativeFunction(winsock.getExportByName('getpeername'),'int',['pointer','pointer','pointer']);
const addressBuffer=Memory.alloc(128), addressLength=Memory.alloc(4), observedEndpoints=new Set();
function endpoint(socket,remote){
    addressLength.writeU32(128);
    if((remote?getRemote:getLocal)(socket,addressBuffer,addressLength)!==0)return null;
    const family=addressBuffer.readU16();
    if(family!==2 && family!==23)return null;
    const port=addressBuffer.add(2).readU8()*256+addressBuffer.add(3).readU8();
    const bytes=new Uint8Array(addressBuffer.add(family===2?4:8).readByteArray(family===2?4:16));
    const ip=family===2?Array.from(bytes).join('.'):Array.from({length:8},(_,i)=>(bytes[i*2]*256+bytes[i*2+1]).toString(16)).join(':');
    return{ip,port,family};
}
for(const name of ['send','recv','sendto','recvfrom','WSASend','WSARecv','WSASendTo','WSARecvFrom','connect','bind']) {
    const entry=winsock.findExportByName(name); if(!entry)continue;
    Interceptor.attach(entry,{onEnter(args){
        const previousError=this.lastError;
        try{
            const local=endpoint(args[0],false), remote=endpoint(args[0],true), key=args[0]+JSON.stringify(local)+JSON.stringify(remote);
            if(local && !observedEndpoints.has(key)){observedEndpoints.add(key);emit({type:'socket-endpoint',api:name,local,remote});}
        }catch(_){}finally{this.lastError=previousError;}
    }});
}
emit({type:'ready',pid:Process.id,mode:'passive',nativeGameHashVerified:true,networkRequestsChanged:false});