Last active
April 21, 2025 16:57
-
-
Save 19h/4e2d084db883ae6a920a15be900f2c8a to your computer and use it in GitHub Desktop.
Highfly PE32-64 vtable dumper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*************************************************************************************************** | |
* 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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
┌─────────────────────────────────────────────────────┐ | |
│ 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