Files
2026-10-30 23:54:07 +07:00

146 lines
5.8 KiB
JavaScript

import * as e from './common.mjs';
const {
fs,
path,
fail,
sha256,
matches,
escapeRE,
atomic_json
} =e;
export const align=(v, b)=>Math.ceil(v/b)*b;
export function build(client, dump, classes, output){
const text=e.readText(dump),
methods=new Map();
for(const name of classes){
const m=text.match(new RegExp('// Full Name: [^\\n]*\\.'+escapeRE(name)+'\\n(.*?)(?=\\n// TypeDefIndex:|$)', 's'));
if(!m)fail('Missing native metadata class '+name);
for(const line of m[1].split('\n')){
const mm=line.match(/\b([\w.<>]+)\([^\n]*RVA: (0x[\da-fA-F]+)/);
if(!mm)continue;
const rva=Number(mm[2]);
if(rva&&!methods.has(rva))methods.set(rva, (name+'_'+mm[1]).replace(/[^A-Za-z0-9_]/g, '_'));
}
}
if(!methods.size)fail('No methods selected');
const starts=[...methods.keys()].sort((a, b)=>a-b),
all=[...new Set(matches(text, /RVA: (0x[\da-fA-F]+)/g).map(m=>Number(m[1])).filter(Boolean))].sort((a, b)=>a-b),
ends=new Map(starts.map(r=>[r, all.find(v=>v>r)??r+0x1000])),
windows=[];
for(const rva of starts){
if(windows.length&&rva<windows.at(-1)[1]+0x1000)windows.at(-1)[1]=align(rva+0x1000, 0x1000);
else windows.push([Math.floor(rva/0x1000)*0x1000, align(rva+0x1000, 0x1000)]);
}
const blob=fs.readFileSync(path.join(client, 'GameAssembly.dll')),
pe=blob.readUInt32LE(0x3c);
if(blob.subarray(pe, pe+4).toString('hex')!=='50450000'||blob.readUInt16LE(pe+4)!==0x8664)fail('Expected x64 PE source');
const opt=blob.readUInt16LE(pe+20);
if(opt!==240||blob.readUInt16LE(pe+24)!==0x20b)fail('Expected PE32+ optional header');
const ss=pe+24+opt,
sections=Array.from({
length:blob.readUInt16LE(pe+6)
}, (_, i)=>{
const a=ss+i*40;
return {
vs:blob.readUInt32LE(a+8),
rva:blob.readUInt32LE(a+12),
size:blob.readUInt32LE(a+16),
raw:blob.readUInt32LE(a+20)
};
}),
segments=windows.map(([start, end])=>{
const s=sections.find(s=>s.rva<=start&&end<=s.rva+Math.min(s.vs, s.size));
if(!s)fail('Code window exceeds original file-backed section');
const offset=s.raw+start-s.rva;
return {
rva:start,
source_offset:offset,
bytes:blob.subarray(offset, offset+end-start)
};
}),
base=blob.readBigUInt64LE(pe+48),
nt=Buffer.from(blob.subarray(pe, pe+24+opt)),
exportRva=align(Math.max(...segments.map(s=>s.rva+s.bytes.length)), 0x1000),
count=starts.length,
order=Array.from({
length:count
}, (_, i)=>i).sort((a, b)=>{
const x=methods.get(starts[a])+'_'+starts[a].toString(16),
y=methods.get(starts[b])+'_'+starts[b].toString(16);
return x<y?-1:x>y?1:0;
});
let exports=Buffer.alloc(40+count*10);
const names=[];
for(const i of order){
names.push(exports.length);
exports=Buffer.concat([exports, Buffer.from(methods.get(starts[i])+'_'+starts[i].toString(16)+'\0')]);
}
const dllName=exports.length;
exports=Buffer.concat([exports, Buffer.from('cw-native-analysis-only.dll\0')]);
for(const [at, v]of [[0, 0], [4, 0], [12, exportRva+dllName], [16, 1], [20, count], [24, count], [28, exportRva+40], [32, exportRva+40+count*4], [36, exportRva+40+count*8]])exports.writeUInt32LE(v, at);
for(let i=0; i<count; i++){
exports.writeUInt32LE(starts[i], 40+i*4);
exports.writeUInt32LE(exportRva+names[i], 40+count*4+i*4);
exports.writeUInt16LE(order[i], 40+count*8+i*2);
}
const dest=segments.map((s, i)=>({
name:'cw'+i,
rva:s.rva,
data:s.bytes,
flags:0x60000020
}));
dest.push({
name:'.edata',
rva:exportRva,
data:exports,
flags:0x40000040
});
const headerSize=align(0x80+nt.length+dest.length*40, 0x200);
nt.writeUInt16LE(dest.length, 6);
nt.writeUInt16LE(nt.readUInt16LE(22)&~2, 22);
for(const [at, v]of [[40, 0], [44, segments[0].rva], [56, 0x1000], [60, 0x200], [80, align(exportRva+exports.length, 0x1000)], [84, headerSize], [88, 0]])nt.writeUInt32LE(v, at);
nt.fill(0, 136, 264);
nt.writeUInt32LE(exportRva, 136);
nt.writeUInt32LE(exports.length, 140);
let carrier=Buffer.alloc(headerSize);
carrier.write('MZ');
carrier.writeUInt32LE(0x80, 0x3c);
nt.copy(carrier, 0x80);
for(let i=0; i<dest.length; i++){
const s=dest[i],
at=0x80+nt.length+i*40,
rawOffset=carrier.length;
carrier.write(s.name, at, 8, 'ascii');
for(const [off, v]of [[8, s.data.length], [12, s.rva], [16, align(s.data.length, 0x200)], [20, rawOffset], [36, s.flags]])carrier.writeUInt32LE(v, at+off);
carrier=Buffer.concat([carrier, s.data, Buffer.alloc(align(s.data.length, 0x200)-s.data.length)]);
}
e.replace(output, carrier);
const hex=v=>'0x'+v.toString(16),
manifest={
source_sha256:sha256(blob),
dump_sha256:e.sha256_file(dump),
carrier_sha256:sha256(carrier),
image_base:hex(base),
classes,
methods:e.dict(starts.map(r=>[hex(base+BigInt(r)), methods.get(r)])),
method_ends:e.dict(starts.map(r=>[hex(base+BigInt(r)), hex(base+BigInt(ends.get(r)))])),
ranges:segments.map(s=>({
rva:hex(s.rva),
source_offset:s.source_offset,
size:s.bytes.length,
sha256:sha256(s.bytes)
})),
limitations:['Derived analysis carrier; never execute it.', 'External functions, original data and dynamic runtime state are absent.', 'Exports are analysis annotations derived from the matching metadata dump.', 'Method ends are metadata boundaries; compiler cold tails may live elsewhere.', 'Runtime hotfix overrides are not established by static analysis.']
};
const ext=path.extname(output);
atomic_json(ext?output.slice(0, -ext.length)+'.manifest.json':output+'.manifest.json', manifest, 2, true);
console.log(`${methods.size} methods, ${segments.length} exact code windows, ${carrier.length} bytes`);
return manifest;
}
export function main(){
const a=e.cli(['client', 'dump', 'class', 'output'], {
}, [], ['class']);
build(a.client, a.dump, a.class, a.output);
}
e.runMain(import.meta.url, main);