105 lines
4.5 KiB
JavaScript
105 lines
4.5 KiB
JavaScript
// dispatch_dump.js - walks the game's msg_id -> protobuf-factory hash table
|
|
// and emits every (msg_id, factory_ptr) pair to Python.
|
|
//
|
|
// Reverse-engineered from sub_1449BD0F0 (packet reader). Lookup code:
|
|
//
|
|
// v16 = &unk_1498F2D88; // inline fallback slots
|
|
// if (qword_1498F2D90) v16 = qword_1498F2D90; // or heap index array
|
|
// idx = v16[msg_id & (dword_1498F2D98 - 1)]; // hash to bucket head
|
|
// while (idx != -1) {
|
|
// entry = qword_1498F2D50 + 32 * idx; // 32B per entry
|
|
// if (*(uint32_t*)entry == msg_id) break; // matched
|
|
// idx = *(int32_t*)(entry + 24); // chain next
|
|
// }
|
|
// factory = *(void**)(entry + 8);
|
|
// msg = (*(void**)(*factory + 16))(factory, 0); // New()
|
|
//
|
|
// Table is populated at app startup; we walk once ~2s after attach.
|
|
|
|
(function () {
|
|
'use strict';
|
|
var MOD = 'Paralogue-Win64-Shipping.exe';
|
|
var mod = Process.getModuleByName(MOD);
|
|
if (!mod) { send({t:'error', msg:'module not found'}); return; }
|
|
|
|
// Image base in IDA = 0x140000000; global addresses shown are absolute.
|
|
// Subtract that to get module-relative offsets.
|
|
var OFF = {
|
|
buckets_ptr: 0x98F2D50, // qword: &entries[0]
|
|
used_count: 0x98F2D58, // dword
|
|
empty_marker: 0x98F2D84, // dword
|
|
index_inline: 0x98F2D88, // dword[]: inline index array (small-map)
|
|
index_heap: 0x98F2D90, // qword: &heap-index-array (if grown)
|
|
hash_size: 0x98F2D98, // dword: size of index array (pow2)
|
|
};
|
|
|
|
function dump() {
|
|
try {
|
|
var bucketsBase = mod.base.add(OFF.buckets_ptr).readPointer();
|
|
var used = mod.base.add(OFF.used_count).readU32();
|
|
var empty = mod.base.add(OFF.empty_marker).readU32();
|
|
var size = mod.base.add(OFF.hash_size).readU32();
|
|
var heap = mod.base.add(OFF.index_heap).readPointer();
|
|
var indexArr = heap.isNull() ? mod.base.add(OFF.index_inline) : heap;
|
|
|
|
send({
|
|
t: 'dispatch_info',
|
|
module_base: mod.base.toString(),
|
|
module_size: mod.size,
|
|
buckets: bucketsBase.toString(),
|
|
index_arr: indexArr.toString(),
|
|
used: used, empty_marker: empty, hash_size: size,
|
|
inline: heap.isNull(),
|
|
});
|
|
|
|
if (size === 0 || size > 0x10000) {
|
|
send({t:'error', msg:'implausible hash_size=' + size});
|
|
return;
|
|
}
|
|
|
|
var seen = {};
|
|
var count = 0;
|
|
for (var i = 0; i < size; i++) {
|
|
var idx = indexArr.add(i * 4).readS32();
|
|
var hops = 0;
|
|
while (idx !== -1 && !seen[idx] && hops < 10000) {
|
|
seen[idx] = true;
|
|
var entry = bucketsBase.add(idx * 32);
|
|
var mid = entry.readU32();
|
|
var factory = entry.add(8).readPointer();
|
|
var nextIdx = entry.add(24).readS32();
|
|
// Try dereferencing factory->vtbl to confirm it's a real
|
|
// pointer. If it faults, we misunderstood the layout.
|
|
var vtbl = null, firstVtblEntry = null, rawEntry = null;
|
|
try { vtbl = factory.readPointer(); } catch (e) {}
|
|
try { firstVtblEntry = vtbl ? vtbl.readPointer() : null; }
|
|
catch (e) {}
|
|
try { rawEntry = entry.readByteArray(32); } catch (e) {}
|
|
send({
|
|
t: 'dispatch_entry',
|
|
msg_id: mid,
|
|
factory: factory.toString(),
|
|
factory_rel_base: factory.sub(mod.base).toString(),
|
|
vtbl: vtbl ? vtbl.toString() : null,
|
|
first_vtbl_entry: firstVtblEntry ? firstVtblEntry.toString() : null,
|
|
slot: idx,
|
|
bucket: i,
|
|
}, rawEntry);
|
|
count++;
|
|
idx = nextIdx;
|
|
hops++;
|
|
}
|
|
}
|
|
send({t: 'dispatch_done', total: count});
|
|
} catch (e) {
|
|
send({t:'error', msg: 'dispatch walk failed: ' + e.message + '\n' + e.stack});
|
|
}
|
|
}
|
|
|
|
// Table fills during startup; 2s delay covers dynamic registration.
|
|
setTimeout(dump, 2000);
|
|
|
|
// Also expose a manual re-dump via rpc in case we want to re-check later.
|
|
rpc.exports = { dump: dump };
|
|
})();
|