Files
2026-04-15 22:24:44 -07:00

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 };
})();