// Find the vtables of an MSVC C++ class in a game module via RTTI and list their // entries. // // node tools/rtti.mjs [entries=40] // // MSVC 32-bit RTTI: TypeDescriptor { pVFTable, spare, char name[] } with the // name ".?AV@@"; CompleteObjectLocator { signature=0, offset, cdOffset, // pTypeDescriptor, pClassHierarchyDescriptor }; vtable[-1] = &COL. // A class has one vtable per (non-virtual) base subobject; `offset` tells which. import { readFileSync } from "node:fs"; const [exePath, className, entriesText = "40"] = process.argv.slice(2); if (!exePath || !className) { console.error("usage: node rtti.mjs [entries]"); process.exit(1); } const maxEntries = Number.parseInt(entriesText, 10); const file = readFileSync(exePath); const peOffset = file.readUInt32LE(0x3c); const sectionCount = file.readUInt16LE(peOffset + 6); const optionalSize = file.readUInt16LE(peOffset + 20); const imageBase = file.readUInt32LE(peOffset + 24 + 28); const sectionTable = peOffset + 24 + optionalSize; const sections = []; for (let i = 0; i < sectionCount; i++) { const s = sectionTable + 40 * i; sections.push({ name: file.toString("latin1", s, s + 8).replace(/\0+$/, ""), va: imageBase + file.readUInt32LE(s + 12), virtualSize: file.readUInt32LE(s + 8), rawOffset: file.readUInt32LE(s + 20), rawSize: file.readUInt32LE(s + 16), }); } const section = (name) => sections.find((s) => s.name === name); const toOffset = (va) => { for (const s of sections) { if (va >= s.va && va < s.va + Math.min(s.virtualSize, s.rawSize)) return s.rawOffset + va - s.va; } return -1; }; const inSection = (va, s) => va >= s.va && va < s.va + s.virtualSize; const hex = (v) => "0x" + v.toString(16).padStart(8, "0"); // 1. TypeDescriptor: name string lives 8 bytes into it (.data). const mangled = Buffer.from(`.?AV${className}@@\0`, "latin1"); const typeDescriptors = []; for (const s of sections) { const raw = file.subarray(s.rawOffset, s.rawOffset + s.rawSize); for (let i = raw.indexOf(mangled); i !== -1; i = raw.indexOf(mangled, i + 1)) { typeDescriptors.push(s.va + i - 8); } } if (typeDescriptors.length === 0) { console.error(`no RTTI type descriptor for ${className}`); process.exit(2); } // 2. CompleteObjectLocators referencing it, 3. vtables whose [-1] is a COL. const rdata = section(".rdata"); const text = section(".text"); const rdataRaw = file.subarray(rdata.rawOffset, rdata.rawOffset + rdata.rawSize); for (const td of typeDescriptors) { console.log(`${className}: TypeDescriptor ${hex(td)}`); for (let i = 0; i + 20 <= rdataRaw.length; i += 4) { if (rdataRaw.readUInt32LE(i) !== 0 || rdataRaw.readUInt32LE(i + 12) !== td) continue; const col = rdata.va + i; const subobjectOffset = rdataRaw.readUInt32LE(i + 4); for (let j = 0; j + 4 <= rdataRaw.length; j += 4) { if (rdataRaw.readUInt32LE(j) !== col) continue; const vtable = rdata.va + j + 4; console.log(` vtable ${hex(vtable)} (subobject offset 0x${subobjectOffset.toString(16)}, COL ${hex(col)})`); for (let k = 0; k < maxEntries; k++) { const o = toOffset(vtable + 4 * k); const target = file.readUInt32LE(o); if (!inSection(target, text)) break; console.log(` [+0x${(4 * k).toString(16).padStart(2, "0")}] ${hex(target)}`); } } } }