Skip to content

Instantly share code, notes, and snippets.

@19h
Last active April 21, 2025 16:57
Show Gist options
  • Save 19h/4e2d084db883ae6a920a15be900f2c8a to your computer and use it in GitHub Desktop.
Save 19h/4e2d084db883ae6a920a15be900f2c8a to your computer and use it in GitHub Desktop.
Highfly PE32-64 vtable dumper
/***************************************************************************************************
* vtable_scanner.cpp - Clang-style v-table enumerator (RTTI-stripped)
* ================================================================================================
*
* Version: 2.3 (Refined based on internal review)
* Date: 21. April 2025
* Author: Kenan Sulayman
*
* Overview
* --------
* This program implements a scanner designed to identify potential C++ virtual function tables
* (v-tables) within the memory space of the currently running executable module on Windows.
* Its primary characteristic is the ability to function even when standard C++ Run-Time Type
* Information (RTTI) has been disabled or stripped from the binary, a common occurrence in
* release builds or for obfuscation purposes.
*
* The scanner operates by analyzing the Portable Executable (PE) structure of the target module
* to locate executable code sections (typically `.text`) and initialized data sections
* (e.g., `.data`, `.rdata`). It then iterates through the data sections, searching for sequences
* of pointers that exclusively point into the identified code sections. These sequences are
* considered candidate v-tables based on specific heuristics.
*
* Key Features
* ------------
* - RTTI-Independent: Does not rely on compiler-generated RTTI structures (like type_info).
* - Heuristic-Based: Uses patterns (sequences of code pointers in data sections, preceded by
* non-code-pointers) to infer the location of v-tables.
* - PE Section Analysis: Parses the module's PE header to accurately identify and merge code
* and initialized data memory ranges.
* - 32-bit and 64-bit Support: Automatically detects the architecture of the target module.
* - Configurable Filtering: Allows setting a minimum number of virtual function slots required
* for a candidate sequence to be considered a valid v-table, reducing false positives.
* - JSON Output: Produces a structured JSON report containing the identified v-tables.
* - RVA and EA Reporting: For each v-table and virtual function slot, the output includes both
* the Relative Virtual Address (RVA, offset from the module base) and the Effective Address
* (EA, the absolute memory address).
* - DLL Implementation: Packaged as a DLL designed to be injected into a target process.
* The scan is initiated automatically upon DLL attachment via `DllMain`.
* - Logging: Utilizes the spdlog library for detailed logging of the scanning process to a file.
*
* Motivation & Background
* -----------------------
* Reverse engineering, security analysis, and interoperability often require understanding the
* object layout and virtual function dispatch mechanisms of C++ applications. V-tables are
* central to polymorphism in C++. However, RTTI, which provides explicit type information and
* v-table pointers, is often disabled for performance or size reasons, or intentionally stripped
* to hinder analysis.
*
* This scanner provides a method to recover v-table locations based on their structural properties
* in memory: they are typically arrays of function pointers located in data sections. By finding
* these arrays where all elements point to valid code addresses, we can heuristically identify
* v-tables without relying on RTTI metadata. The inclusion of both RVA and EA is crucial for
* different analysis tools and contexts (RVA for static analysis and rebasing, EA for dynamic
* analysis and debugging).
*
* Assumptions
* -----------
* - The target process is a standard Windows PE executable (EXE or DLL).
* - V-tables reside within sections marked as initialized data (e.g., `.data`, `.rdata`) and
* are not located in executable or uninitialized data sections.
* - V-tables consist of contiguous arrays of pointers (4 bytes in 32-bit, 8 bytes in 64-bit).
* - Pointers within a v-table point directly to the beginning of functions within an executable
* section (e.g., `.text`). Thunks or jumps outside merged executable sections might not be
* correctly handled by the `text_.contains` check.
* - A potential v-table is likely preceded by data that is *not* a code pointer. This helps
* distinguish v-tables from simple arrays of function pointers that might be part of a
* larger structure or follow another v-table immediately.
* - The minimum v-table size heuristic (`minSlots`) is sufficient to filter out most non-v-table
* arrays of function pointers.
*
* Usage
* -----
* Compile this code as a DLL. Inject the DLL into the target process. Upon injection, the
* `DllMain` function will trigger, create a new thread, and execute the `RunVTableScan` function.
* The scan results will be written to `C:\vtable_dump.json`, and logging information will be
* written to `C:\vtable_scanner.log`. The `RunVTableScan` function also returns a pointer to a
* static string containing the JSON dump (use with caution regarding lifetime and threading).
*
* Dependencies
* ------------
* - Windows API (windows.h, psapi.h)
* - C++ Standard Library (<cstdint>, <vector>, <set>, <algorithm>, <fstream>, <iomanip>, <sstream>)
* - spdlog library (for logging)
* - nlohmann/json library (for JSON generation)
*
***************************************************************************************************/
// Reduce the number of included Windows headers for faster compilation and smaller footprint.
// Excludes things like GDI, RPC, Winsock, etc., which are not needed here.
#define WIN32_LEAN_AND_MEAN
#include <windows.h> // Core Windows API functions (GetModuleHandleW, CreateThread, PE structures, etc.)
#include <psapi.h> // Process Status API (Used here indirectly via PE header parsing)
#include <cstdint> // For fixed-width integer types like uint8_t, uint32_t, uint64_t, uintptr_t
#include <vector> // For dynamic arrays (used to store SlotInfo and VTableInfo)
#include <set> // For storing seen v-table RVAs to prevent duplicates
#include <algorithm> // For std::min, std::max
#include <fstream> // For file output (writing JSON dump and logs)
#include <iomanip> // For std::hex, std::uppercase (used in hex formatting)
#include <sstream> // For std::ostringstream (used in hex formatting)
#include <string> // For std::string usage
#include <stdexcept> // For standard exceptions
// External dependency: spdlog for flexible and efficient logging
#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h> // For logging to a file (thread-safe version used later)
// External dependency: nlohmann/json for easy JSON creation and manipulation
#include <nlohmann/json.hpp>
// Alias for convenience to avoid typing nlohmann::json repeatedly
using json = nlohmann::json;
/* -----------------------------------------------------------------------------------------------
* Hexadecimal Formatting Helpers
* -----------------------------------------------------------------------------------------------
* These inline functions provide a consistent way to format 32-bit and 64-bit unsigned integers
* as hexadecimal strings with a "0x" prefix and uppercase letters (e.g., "0xFF", "0x1A2B").
* They omit leading zeros after the prefix for brevity.
* ----------------------------------------------------------------------------------------------- */
/**
* @brief Formats a 32-bit unsigned integer as a hexadecimal string.
* @param v The 32-bit value to format.
* @return A std::string representing the value in uppercase hex format (e.g., "0x123ABC").
*/
static inline std::string hex32(uint32_t v) {
std::ostringstream o;
o << "0x" << std::uppercase << std::hex << v;
return o.str();
}
/**
* @brief Formats a 64-bit unsigned integer as a hexadecimal string.
* @param v The 64-bit value to format.
* @return A std::string representing the value in uppercase hex format (e.g., "0x123456789ABCDEF0").
*/
static inline std::string hex64(uint64_t v) {
std::ostringstream o;
o << "0x" << std::uppercase << std::hex << v;
return o.str();
}
/* -----------------------------------------------------------------------------------------------
* Simple Memory Range Representation
* -----------------------------------------------------------------------------------------------
* The `Range` class provides a basic abstraction for a contiguous block of memory, defined by
* its start and end addresses. It's used to represent merged PE sections (like .text, .data)
* and offers a method (`contains`) to check if a given memory address falls within the range.
* ----------------------------------------------------------------------------------------------- */
class Range {
// Private members storing the begin and end pointers of the memory range.
uint8_t* b_{nullptr}; // Pointer to the first byte of the range.
uint8_t* e_{nullptr}; // Pointer to one byte *past* the last byte of the range.
public:
/**
* @brief Default constructor. Creates an empty range.
*/
Range() = default;
/**
* @brief Constructs a Range object representing a memory block.
* @param b Pointer to the starting byte of the memory block.
* @param n The size (number of bytes) of the memory block.
*/
Range(uint8_t* b, size_t n): b_(b), e_(b+n) {}
/**
* @brief Gets a pointer to the beginning of the range.
* @return Pointer to the first byte. Returns nullptr for default-constructed ranges.
*/
uint8_t* begin() const { return b_; }
/**
* @brief Gets a pointer to one byte past the end of the range.
* @return Pointer indicating the end boundary. Returns nullptr for default-constructed ranges.
*/
uint8_t* end() const { return e_; }
/**
* @brief Calculates the size of the range in bytes.
* @return The size of the range. Returns 0 for default-constructed ranges.
*/
size_t size() const { return (b_ && e_) ? static_cast<size_t>(e_ - b_) : 0; }
/**
* @brief Checks if a given memory address (or a small block starting at that address)
* is fully contained within this range.
* @param p A void pointer to the memory address to check.
* @param n The number of bytes (starting from p) that must be contained. Defaults to 1.
* @return `true` if the address range [`p`, `p + n`) falls entirely within [`b_`, `e_`),
* `false` otherwise, including if the range is invalid or `p` is null.
*/
bool contains(const void* p, size_t n=1) const {
// Ensure the range itself is valid and the pointer `p` is not null.
if (!b_ || !e_ || !p || n == 0) {
return false;
}
// Cast pointers to uintptr_t for address arithmetic.
uintptr_t a = reinterpret_cast<uintptr_t>(p);
uintptr_t lo = reinterpret_cast<uintptr_t>(b_);
uintptr_t hi = reinterpret_cast<uintptr_t>(e_); // `hi` points one byte *past* the end.
// Perform the containment check:
// 1. Is the start address `a` greater than or equal to the range start `lo`?
// 2. Is the start address `a` strictly less than the range end `hi`?
// 3. Does the end address `a + n` not exceed the range end `hi`?
// This check ensures the entire block of `n` bytes fits within the range.
// Checking `(a + n) <= hi` avoids potential integer overflow if `a` is very large.
return (a >= lo) && (a < hi) && ((a + n) <= hi);
}
};
/* -----------------------------------------------------------------------------------------------
* Data Structures for Scan Results
* -----------------------------------------------------------------------------------------------
* These structures define how the information about discovered v-tables and their contents
* (virtual function slots) is organized and stored.
* ----------------------------------------------------------------------------------------------- */
/**
* @brief Represents a single slot (entry) within a virtual function table.
* Stores the index of the slot and the RVA of the function it points to.
*/
struct SlotInfo {
uint32_t idx; // The zero-based index of this slot within its v-table.
uint32_t fnRva; // The Relative Virtual Address (RVA) of the virtual function. RVA is the offset from the module's base address.
};
/**
* @brief Represents a discovered virtual function table (v-table).
* Stores the RVA of the v-table, the number of slots, and details of each slot.
*/
struct VTableInfo {
uint32_t rva; // The RVA of the start of this v-table within the module.
uint32_t slotCount; // The total number of contiguous function pointer slots detected.
std::vector<SlotInfo> slots; // Details of each individual slot in the v-table.
};
/**
* @brief Configuration settings for the VTableScanner.
* Allows customization of the scanning process.
*/
struct ScanCfg {
bool verbose = true; // If true, log informational messages during the scan.
uint32_t minSlots = 3; // Minimum number of consecutive valid function pointers required
// to classify a sequence as a potential v-table. Helps filter out
// false positives. A value of 3 is common as v-tables often have
// at least a destructor plus other functions.
};
/* -----------------------------------------------------------------------------------------------
* V-Table Scanner Class
* -----------------------------------------------------------------------------------------------
* Encapsulates the state and logic for scanning a module for v-tables. Handles PE parsing,
* memory iteration, candidate validation, and result storage.
* ----------------------------------------------------------------------------------------------- */
class VTableScanner {
public:
/**
* @brief Constructs and initializes the VTableScanner for a specific module.
* Parses PE headers, determines architecture, and identifies code/data sections.
* @param mod Handle to the loaded module (e.g., obtained via GetModuleHandleW).
* @param cfg Scan configuration settings.
* @throws std::runtime_error if the module handle is invalid or PE headers are malformed.
*/
explicit VTableScanner(HMODULE mod, ScanCfg cfg);
/**
* @brief Performs the core v-table scanning process based on the initialized configuration
* and identified memory sections. Populates the internal list of found v-tables.
*/
void scan();
/**
* @brief Generates a JSON representation of the discovered v-tables.
* @return A nlohmann::json object containing an array of v-table information.
* Includes RVA and EA for v-tables and function slots.
*/
json dumpJSON() const;
private:
/**
* @brief Parses the PE header of the module to identify and store the memory ranges
* corresponding to executable code sections and initialized data sections.
* Merges multiple sections of the same type into single contiguous ranges.
* @throws std::runtime_error if PE headers are inaccessible or malformed.
*/
void collectSections();
// Member Variables: State storage for the scanner.
HMODULE hMod_; // Handle to the target module being scanned.
uintptr_t base_; // Base address (Effective Address) where the module is loaded.
bool is64_; // Flag indicating if the module is 64-bit (true) or 32-bit (false).
Range text_; // Merged memory range for all executable sections.
Range data_; // Merged memory range for all initialized, non-executable data sections.
ScanCfg cfg_; // Copy of the scan configuration settings.
std::vector<VTableInfo> vt_; // Stores the details of validated v-tables found.
std::set<uint32_t> seen_; // Tracks RVAs of found v-tables to prevent duplicates.
};
/* -----------------------------------------------------------------------------------------------
* VTableScanner Constructor Implementation
* ----------------------------------------------------------------------------------------------- */
VTableScanner::VTableScanner(HMODULE m, ScanCfg c) : hMod_(m), cfg_(c), base_(0), is64_(false) {
// Validate the module handle.
if (!hMod_) {
throw std::runtime_error("Invalid module handle provided to VTableScanner.");
}
// Obtain the base address where the module 'm' is loaded.
base_ = reinterpret_cast<uintptr_t>(hMod_);
// --- PE Header Parsing ---
// Access PE headers carefully, checking for potential null pointers or invalid offsets.
try {
auto* dos = reinterpret_cast<IMAGE_DOS_HEADER*>(base_);
// Check DOS signature
if (dos->e_magic != IMAGE_DOS_SIGNATURE) {
throw std::runtime_error("Invalid DOS signature found.");
}
// Check PE signature offset validity
if (dos->e_lfanew <= 0 || dos->e_lfanew > 1024) { // Basic sanity check on offset
throw std::runtime_error("Invalid e_lfanew offset in DOS header.");
}
auto* nt = reinterpret_cast<IMAGE_NT_HEADERS*>(base_ + dos->e_lfanew);
// Check NT signature
if (nt->Signature != IMAGE_NT_SIGNATURE) {
throw std::runtime_error("Invalid NT signature found.");
}
// --- Architecture Detection ---
// Determine if the module is 32-bit (PE32) or 64-bit (PE32+).
if (nt->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
is64_ = true;
} else if (nt->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
is64_ = false;
} else {
throw std::runtime_error("Invalid OptionalHeader Magic value.");
}
} catch (const std::exception& e) {
// Catch exceptions during header parsing (e.g., access violations if base_ is wrong)
throw std::runtime_error("Failed to parse PE headers: " + std::string(e.what()));
} catch (...) {
// Catch potential memory access violations if the module handle is valid but points to unloadable memory.
throw std::runtime_error("Memory access violation during PE header parsing.");
}
// --- Section Identification ---
// Collect and merge section information. This can also throw if headers are bad.
collectSections();
// Log the identified ranges and configuration.
spdlog::info("VTableScanner Initialized:");
spdlog::info(" Module Base: {}", hex64(base_));
spdlog::info(" Architecture: {}", is64_ ? "64-bit" : "32-bit");
spdlog::info(" Text Section(s) Range: {} - {} (Size: {} bytes)",
text_.begin() ? hex64(reinterpret_cast<uintptr_t>(text_.begin())) : "N/A",
text_.end() ? hex64(reinterpret_cast<uintptr_t>(text_.end())) : "N/A",
text_.size());
spdlog::info(" Data Section(s) Range: {} - {} (Size: {} bytes)",
data_.begin() ? hex64(reinterpret_cast<uintptr_t>(data_.begin())) : "N/A",
data_.end() ? hex64(reinterpret_cast<uintptr_t>(data_.end())) : "N/A",
data_.size());
spdlog::info(" Config: Verbose={}, MinSlots={}", cfg_.verbose, cfg_.minSlots);
}
/* -----------------------------------------------------------------------------------------------
* VTableScanner::collectSections Implementation
* ----------------------------------------------------------------------------------------------- */
void VTableScanner::collectSections() {
// Access PE headers again (assuming base_ is valid from constructor).
auto* dos = reinterpret_cast<IMAGE_DOS_HEADER*>(base_);
auto* nt = reinterpret_cast<IMAGE_NT_HEADERS*>(base_ + dos->e_lfanew); // Already validated in ctor
// Get the number of sections and pointer to the first section header.
WORD n = nt->FileHeader.NumberOfSections;
auto* sec = IMAGE_FIRST_SECTION(nt);
// --- Iterate Through Sections ---
for (WORD i = 0; i < n; ++i, ++sec) {
// Get section characteristics and calculate its memory range.
DWORD ch = sec->Characteristics;
uint8_t* p = reinterpret_cast<uint8_t*>(base_ + sec->VirtualAddress);
// Use VirtualSize primarily, but ensure SizeOfRawData is considered if VirtualSize is zero.
size_t sz = sec->Misc.VirtualSize;
if (sz == 0 && (ch & (IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_CNT_CODE))) {
// For code/data sections, if VirtualSize is 0, SizeOfRawData might be more appropriate.
// However, typically VirtualSize reflects the actual memory footprint.
// Using max ensures we cover at least the initialized part.
sz = std::max(sz, static_cast<size_t>(sec->SizeOfRawData));
}
if (sz == 0) continue; // Skip sections with zero size.
// --- Classify and Merge Section ---
if (ch & IMAGE_SCN_MEM_EXECUTE) {
// Section contains executable code. Merge it into the `text_` range.
if (!text_.size()) {
text_ = Range(p, sz); // First executable section found.
} else {
// Merge with existing text range.
uint8_t* lo = std::min(text_.begin(), p);
uint8_t* hi = std::max(text_.end(), p + sz);
text_ = Range(lo, static_cast<size_t>(hi - lo));
}
} else if ((ch & IMAGE_SCN_CNT_INITIALIZED_DATA) && !(ch & IMAGE_SCN_MEM_EXECUTE)) {
// Section contains initialized data and is not executable. Merge into `data_` range.
if (!data_.size()) {
data_ = Range(p, sz); // First data section found.
} else {
// Merge with existing data range.
uint8_t* lo = std::min(data_.begin(), p);
uint8_t* hi = std::max(data_.end(), p + sz);
data_ = Range(lo, static_cast<size_t>(hi - lo));
}
}
// Other section types (uninitialized data, resources, relocations, etc.) are ignored.
}
// Final check if essential sections were found.
if (!text_.size()) {
spdlog::warn("No executable sections found or merged.");
// Depending on requirements, could throw an error here.
}
if (!data_.size()) {
spdlog::warn("No initialized data sections found or merged.");
// Depending on requirements, could throw an error here.
}
}
/* -----------------------------------------------------------------------------------------------
* VTableScanner::scan Implementation
* ----------------------------------------------------------------------------------------------- */
void VTableScanner::scan() {
// Ensure data and text sections are valid before scanning.
if (!data_.begin() || !data_.size() || !text_.begin() || !text_.size()) {
spdlog::error("Cannot scan: Data or Text section range is invalid or empty.");
return;
}
// Determine the size of a pointer based on the module's architecture.
const size_t stride = is64_ ? sizeof(uint64_t) : sizeof(uint32_t);
// Counters for logging statistics.
uint64_t cand = 0; // Total number of potential v-table starting pointers encountered.
uint64_t acc = 0; // Total number of candidate sequences accepted as v-tables.
spdlog::info("Starting v-table scan within data range {}...", hex64(reinterpret_cast<uintptr_t>(data_.begin())));
// --- Iterate through the Data Section ---
// Loop pointer by pointer through the merged data range.
// `p` points to the potential start of a v-table slot.
// `data_.end() - stride` ensures we don't read past the end when dereferencing the last possible pointer.
for (uint8_t* p = data_.begin(); p <= data_.end() - stride; p += stride) {
// --- Read Potential Function Pointer ---
// Read the pointer value at the current address `p`.
uintptr_t fn = is64_ ? *reinterpret_cast<uint64_t*>(p)
: *reinterpret_cast<uint32_t*>(p);
// --- Validate Pointer Target (Rule 1: Points to Code) ---
// Check if the value `fn` points into the merged executable `text_` section.
if (!text_.contains(reinterpret_cast<void*>(fn), 1)) {
continue; // Not a code pointer, move to the next address.
}
// --- Validate Preceding Pointer (Rule 2: Not a Code Pointer) ---
// Heuristic: V-tables are often preceded by non-code-pointer data (like object headers).
// Check only if `p` is not the very beginning of the data section.
if (p != data_.begin()) {
// Read the pointer value at the *previous* position (`p - stride`).
uintptr_t prev = is64_ ? *reinterpret_cast<uint64_t*>(p - stride)
: *reinterpret_cast<uint32_t*>(p - stride);
// If the *previous* pointer *also* points to code, assume this isn't the start of a v-table.
if (text_.contains(reinterpret_cast<void*>(prev), 1)) {
continue; // Likely continuation of another sequence, skip.
}
}
// If rules 1 and 2 pass, `p` is a candidate for the start of a v-table.
++cand;
// --- Scan Forward for Consecutive Code Pointers ---
// Determine the length of the sequence of valid code pointers starting at `p`.
uint8_t* q = p; // Lookahead pointer.
uint32_t slots = 0; // Count of consecutive code pointers.
// Loop while `q` is within the bounds of the data section.
while (q <= data_.end() - stride) {
// Read the pointer value at the current lookahead position `q`.
uintptr_t f = is64_ ? *reinterpret_cast<uint64_t*>(q)
: *reinterpret_cast<uint32_t*>(q);
// Check if this pointer `f` points into the text section.
if (!text_.contains(reinterpret_cast<void*>(f), 1)) {
break; // Sequence broken, stop counting.
}
// Increment slot count and move lookahead pointer forward.
++slots;
q += stride;
}
// --- Filter by Minimum Size (Rule 3: minSlots) ---
if (slots < cfg_.minSlots) {
continue; // Sequence too short, discard.
}
// --- Check for Duplicates and Store Valid V-Table ---
// Calculate the RVA of the potential v-table start `p`.
uint32_t rva = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(p) - base_);
// Use the `seen_` set to prevent recording duplicates.
if (!seen_.insert(rva).second) {
continue; // RVA already processed, skip.
}
// --- Create and Populate VTableInfo ---
// Sequence is long enough and not a duplicate, record it.
VTableInfo t;
t.rva = rva;
t.slotCount = slots;
t.slots.reserve(slots); // Reserve space for efficiency.
// Populate the slot details.
for (uint32_t i = 0; i < slots; ++i) {
uint8_t* slot_addr = p + (i * stride);
uintptr_t f = is64_ ? *reinterpret_cast<uint64_t*>(slot_addr)
: *reinterpret_cast<uint32_t*>(slot_addr);
uint32_t frva = static_cast<uint32_t>(f - base_);
t.slots.push_back({i, frva}); // Add {index, function_rva}
}
// Add the validated v-table information to the results vector.
vt_.push_back(std::move(t));
++acc; // Increment accepted counter.
// Log the found v-table if verbose mode is enabled.
if (cfg_.verbose) {
spdlog::info(" Found VTBL @ RVA {}, EA {} ({} slots)", hex32(rva), hex64(reinterpret_cast<uintptr_t>(p)), slots);
}
// --- Advance Main Loop Pointer ---
// Advance `p` past the v-table we just processed to avoid redundant checks.
// The loop's `p += stride` will handle the final increment.
p += (slots - 1) * stride;
} // End of the main `for` loop iterating through the data section.
// Log the final scan statistics.
spdlog::info("Scan complete: {} candidates checked, {} v-tables accepted.", cand, acc);
}
/* -----------------------------------------------------------------------------------------------
* VTableScanner::dumpJSON Implementation
* ----------------------------------------------------------------------------------------------- */
json VTableScanner::dumpJSON() const {
// Create the top-level JSON array.
json arr = json::array();
// Iterate through each found VTableInfo.
for (auto const& t : vt_) {
// Create a JSON object for the current v-table.
json jt;
jt["rva"] = hex32(t.rva); // V-table RVA (hex string)
jt["ea"] = hex64(base_ + t.rva); // V-table EA (hex string)
jt["slotCount"] = t.slotCount; // Number of slots
jt["slots"] = json::array(); // Initialize slots array
// Iterate through each SlotInfo within the v-table.
for (auto const& s : t.slots) {
// Create a JSON object for the current slot and add it to the slots array.
jt["slots"].push_back({
{"idx", s.idx}, // Slot index (integer)
{"fnRva", hex32(s.fnRva)}, // Function RVA (hex string)
{"fnEa", hex64(base_ + s.fnRva)} // Function EA (hex string)
});
}
// Add the completed v-table JSON object to the main array.
arr.push_back(std::move(jt));
}
// Return the JSON array containing all v-table information.
return arr;
}
/* -----------------------------------------------------------------------------------------------
* DLL Export and Entry Point Logic
* -----------------------------------------------------------------------------------------------
* Contains the exported function `RunVTableScan` to trigger the scan, a worker thread
* function, and the `DllMain` entry point for DLL injection scenarios.
* ----------------------------------------------------------------------------------------------- */
// Global static string to hold the JSON dump result.
// WARNING: Using a static variable makes `RunVTableScan` non-reentrant. If the DLL could be
// called concurrently, external synchronization or a different result passing mechanism
// (e.g., caller-provided buffer) would be required. The returned `c_str()` points to this
// static buffer and is only valid until the next call modifies `dump`.
static std::string g_jsonDumpResult;
/**
* @brief Exported C-style function to initiate the v-table scan on the host process's main module.
* Configures the scanner, runs the scan, saves the JSON output to a file, stores the
* result in a static string, and returns a pointer to that string.
* @return const char* Pointer to a statically allocated C-string containing the JSON dump,
* or a JSON error message if scanning failed. The pointer's lifetime is limited.
* Returns NULL only in catastrophic early failure before logging is set up.
*/
extern "C" __declspec(dllexport) const char* RunVTableScan() {
try {
// Define the scanner configuration.
ScanCfg cfg;
cfg.verbose = true; // Log details during scan.
cfg.minSlots = 3; // Minimum v-table size.
// Get a handle to the main executable module of the current process.
HMODULE hTargetModule = GetModuleHandleW(nullptr);
if (!hTargetModule) {
// Logging might not be set up yet if called outside DllMain context,
// but attempt anyway.
spdlog::error("RunVTableScan: Failed to get module handle for the host process.");
g_jsonDumpResult = "{\"error\": \"Failed to get host module handle.\"}";
// Attempt to write error to file as well, best effort.
std::ofstream errFile("C:\\vtable_dump.json", std::ios::trunc);
if (errFile) errFile << g_jsonDumpResult;
return g_jsonDumpResult.c_str();
}
spdlog::info("RunVTableScan: Starting scan on host module handle {}", fmt::ptr(hTargetModule));
// Create the scanner instance. Constructor handles PE parsing and section collection.
// This might throw std::runtime_error if PE parsing fails.
VTableScanner sc(hTargetModule, cfg);
// Execute the scanning process.
sc.scan();
// Generate the JSON output (pretty-printed with indent 2).
g_jsonDumpResult = sc.dumpJSON().dump(2);
// Attempt to write the JSON dump to a predefined file path.
std::ofstream outFile("C:\\vtable_dump.json", std::ios::trunc);
if (outFile) {
outFile << g_jsonDumpResult;
spdlog::info("JSON dump successfully written to C:\\vtable_dump.json");
} else {
spdlog::error("Failed to open C:\\vtable_dump.json for writing.");
// JSON result is still available via the return pointer.
}
// Return a C-style string pointer to the static result variable.
return g_jsonDumpResult.c_str();
} catch (const std::exception& e) {
// Catch standard exceptions during scanner setup or scanning.
spdlog::critical("RunVTableScan: Exception caught: {}", e.what());
g_jsonDumpResult = "{\"error\": \"Exception during scan. Check C:\\\\vtable_scanner.log for details.\", \"details\": \"" + std::string(e.what()) + "\"}";
// Attempt to write error to file as well.
std::ofstream errFile("C:\\vtable_dump.json", std::ios::trunc);
if (errFile) errFile << g_jsonDumpResult;
return g_jsonDumpResult.c_str();
} catch (...) {
// Catch any other unknown exceptions.
spdlog::critical("RunVTableScan: Unknown exception caught.");
g_jsonDumpResult = "{\"error\": \"Unknown exception during scan. Check C:\\\\vtable_scanner.log for details.\"}";
// Attempt to write error to file as well.
std::ofstream errFile("C:\\vtable_dump.json", std::ios::trunc);
if (errFile) errFile << g_jsonDumpResult;
return g_jsonDumpResult.c_str();
}
}
/**
* @brief Worker thread function that executes the main scanning logic.
* This function is run in a separate thread created by DllMain to avoid
* performing complex operations within the loader lock.
* @param lpParam Thread parameter (unused).
* @return DWORD Exit code for the thread (0 indicates success).
*/
DWORD WINAPI ScannerWorkerThread(LPVOID lpParam) {
spdlog::info("ScannerWorkerThread started.");
// Call the exported function to perform the scan and output generation.
RunVTableScan();
spdlog::info("ScannerWorkerThread finished.");
// Return 0 to indicate successful completion of the thread's task.
return 0;
}
/**
* @brief DLL Entry Point (DllMain).
* Handles DLL attachment/detachment events. On process attach, it sets up logging
* and creates a worker thread to perform the v-table scan.
* @param h HMODULE handle of the DLL instance.
* @param r DWORD reason code for the call (e.g., DLL_PROCESS_ATTACH).
* @param lpReserved Reserved parameter.
* @return BOOL TRUE indicates success. FALSE on attach indicates a critical failure preventing load.
*/
BOOL WINAPI DllMain(HINSTANCE h, DWORD r, LPVOID lpReserved) {
// Handle different reasons for DllMain being called.
switch (r) {
case DLL_PROCESS_ATTACH:
{
// Executed when the DLL is first loaded into the process.
// Optimization: Disable DLL_THREAD_ATTACH and DLL_THREAD_DETACH notifications.
DisableThreadLibraryCalls(h);
// --- Setup Logging ---
// Use a try-catch for robustness during logger initialization.
bool logInitialized = false;
try {
// Configure spdlog to write to a thread-safe file sink. Truncate existing file.
auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("C:\\vtable_scanner.log", true);
auto lg = std::make_shared<spdlog::logger>("vtlog", sink);
spdlog::set_default_logger(lg);
spdlog::set_level(spdlog::level::info); // Log info and higher levels.
spdlog::flush_on(spdlog::level::info); // Flush logs immediately for info level and above.
logInitialized = true;
spdlog::info("VTable Scanner DLL Attached. Logging initialized.");
} catch (const spdlog::spdlog_ex& ex) {
// Log initialization failed (e.g., file permissions). Use fallback debug output.
OutputDebugStringA("VTable Scanner: CRITICAL - Failed to initialize spdlog file sink: ");
OutputDebugStringA(ex.what());
OutputDebugStringA("\n");
// Cannot use spdlog here. Proceed without file logging.
} catch (...) {
OutputDebugStringA("VTable Scanner: CRITICAL - Unknown error during logging setup.\n");
// Proceed without file logging.
}
// --- Start Scanner Thread ---
// Create a separate thread to run the scan, avoiding blocking DllMain.
HANDLE hThread = CreateThread(
nullptr, // Default security attributes
0, // Default stack size
ScannerWorkerThread, // Thread function
nullptr, // Parameter to thread function
0, // Run thread immediately
nullptr // Thread ID (not needed)
);
if (hThread == NULL) {
// Thread creation failed. This is a significant issue.
if (logInitialized) {
spdlog::critical("Failed to create scanner worker thread. Error code: {}", GetLastError());
} else {
OutputDebugStringA("VTable Scanner: CRITICAL - Failed to create scanner worker thread.\n");
}
// Consider returning FALSE to indicate DLL load failure, though the process state might be complex.
// For simplicity here, we log the error and continue (DLL loads, but scan won't run).
} else {
// Thread created successfully.
if (logInitialized) {
spdlog::info("Scanner worker thread created successfully.");
}
// Close the handle as we don't need to manage the thread further.
CloseHandle(hThread);
}
break; // End of DLL_PROCESS_ATTACH case
}
case DLL_PROCESS_DETACH:
{
// Executed when the DLL is being unloaded.
// Perform cleanup if necessary.
// Check lpReserved: if non-NULL, process is terminating; if NULL, FreeLibrary was called.
if (lpReserved == nullptr) {
// DLL unloaded via FreeLibrary
spdlog::info("VTable Scanner DLL Detached (FreeLibrary).");
} else {
// Process is terminating
spdlog::info("VTable Scanner DLL Detached (Process Terminating).");
}
// Shutdown the logger to ensure all buffered messages are flushed.
spdlog::shutdown();
break; // End of DLL_PROCESS_DETACH case
}
case DLL_THREAD_ATTACH:
// Disabled by DisableThreadLibraryCalls.
break;
case DLL_THREAD_DETACH:
// Disabled by DisableThreadLibraryCalls.
break;
}
// Return TRUE to indicate successful handling of the DllMain call.
return TRUE;
}
┌─────────────────────────────────────────────────────┐
│ Host Process MEMORY │
└─────────────────────────────────────────────────────┘
▲ ▲
│ │
DLL injected EA (abs addr) RVA (rel addr)
──────────── │ │
│ │
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ vtable_scanner.dll (v2.3) │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│ Load (within loader‑lock) ┌──────────────────────────────────────────────┐
│ │ DLL_PROCESS_ATTACH │
│ ├──────────────────────────────────────────────┤
│ │ DisableThreadLibraryCalls(h) │
│ │ init‐spdlog → C:\vtable_scanner.log │
│ │ CreateThread(ScannerWorkerThread) │
│ └──────────────────────────────────────────────┘
╔════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ ScannerWorkerThread (own stack) ║
╚════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
│ 1. `RunVTableScan()` ─────► (high‑level flow) ────────────────────────────────────────────────────┐
│ writes result + returns char * │
│ │
│ ▼
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ VTableScanner ctor (PE introspection) │
│ • validate DOS ⟶ PE headers │
│ • detect arch (PE32 / PE32+) → is64_ → stride │
│ • collectSections() │
│ – iterate IMAGE_SECTION_HEADER │
│ – merge EXEC → text_ (code Range) │
│ – merge DATA → data_ (init‑data Range) │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ scan() loop │
│ for p ∈ data_[0..N‑stride] step stride │
│ if *p ∉ text_ → continue (rule 1) │
│ if *(p‑stride) ∈ text_ → continue (rule 2) │
│ count consecutive pointers q ┌───────────────┐ │
│ while *q ∈ text_ slots++ │ data section │ │
│ │┌───────────┐│ │
│ if slots < minSlots → continue (rule 3) ││ v‑table ││ │
│ rva = p‑base_; if rva ∈ seen_ → continue │└───────────┘│ │
│ push_back {rva,slotCount,[SlotInfo…]} └───────────────┘ │
│ p += (slots‑1)*stride (skip over table we just stored) │
│ │
│ • cand / acc counters logged │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ dumpJSON() │
│ JSON array ← for each VTableInfo │
│ { "rva": "0x....", "ea": "0x....", "slotCount": n, "slots":[ {idx,fnRva,fnEa}, … ] } │
│ pretty‑print 2‑space indent │
│ g_jsonDumpResult = string │
│ fwrite → C:\vtable_dump.json │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Return pointer → host (for ImGui overlay, debugger console, etc.) │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────┘
╔════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ Thread exits → CloseHandle ║
╚════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
(later) FreeLibrary / process termination
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ DLL_PROCESS_DETACH │
│ spdlog::shutdown(); (guaranteed flush) │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Legend
──────
* **EXEC code** : merged `.text` ± sister sections containing executable bytes
* **DATA (init)** : merged `.data`, `.rdata`, etc.; scan domain
* **stride** : `8` on x64, `4` on x86
* **Rule 1** : current pointer must land inside EXEC
* **Rule 2** : previous pointer must *not* land inside EXEC
* **Rule 3 (minSlots)** : sequence length ≥ `cfg.minSlots` (default 3)
* **EA** : effective (absolute) address = base + RVA
* **seen_** : `std::set` of table RVAs to suppress duplicates
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment