Add function inventory (.pdata), signature generation, and export/PDB symbols

Three reversing capabilities on the win32 surface plus a pure sig-gen handler:

- vmie_win32_functions enumerates a module's functions from the exception
  directory (.pdata RUNTIME_FUNCTION), folding unwind chain continuations into
  their primary - authoritative non-leaf boundaries, not prologue heuristics.
- vmie_win32_exports resolves the export table to {name, rva, ordinal,
  forwarded}: named functions with no PDB or network. vmie_win32_pdb_ref pulls
  the CodeView/RSDS {guid, age, pdb} from the debug directory - the symbol-server
  key for any module (full PDB parsing stays out of scope).
- sig_generate (siggen.h) builds a unique masked signature for a code span,
  wildcarding the rel/RIP-relative displacement bytes the x86 decoder locates and
  growing until it matches the scope exactly once - the dual of sigscan.

The decoder now also reports disp_off/disp_len so a caller can mask the floating
bytes. The MZ/PE walk gains one shared data-directory accessor and one shared
CodeView/RSDS parser; the kernel bootstrap is moved onto both, removing its
private copies - one PE parser in the tree.
This commit is contained in:
2026-06-16 19:27:42 +03:00
parent 06230ac680
commit c4419964aa
9 changed files with 542 additions and 67 deletions
+32
View File
@@ -60,4 +60,36 @@ bool pe_section(mem_view_t v, uint64_t module_base, const char* name,
int vmie_pe_section(vmie_mem* m, uintptr_t cr3, uint64_t module_base,
const char* name, uint8_t* buf, size_t bufcap, mem_view_t* out);
/* OptionalHeader DataDirectory indices used across the engine. */
#define PE_DIR_EXPORT 0u /* IMAGE_DIRECTORY_ENTRY_EXPORT */
#define PE_DIR_DEBUG 6u /* IMAGE_DIRECTORY_ENTRY_DEBUG */
#define PE_DIR_EXCEPTION 3u /* IMAGE_DIRECTORY_ENTRY_EXCEPTION (.pdata) */
/* Read one OptionalHeader DataDirectory entry of the PE32+ image based at `base`
* in the address space `cr3`. This is the SINGLE data-directory accessor used by
* every directory walk in the engine (.pdata / export / debug) - it walks the
* DOS+NT headers from `base` once and reads DataDirectory[idx].
* idx - directory index (PE_DIR_*).
* rva - receives DataDirectory[idx].VirtualAddress (0 if the directory is
* absent); never NULL.
* size - receives DataDirectory[idx].Size; may be NULL.
* Returns 0 on success (rva/size filled), -1 if the headers are unreadable. A
* present-but-absent directory reports rva==0 with return 0. */
int pe_data_dir(vmie_mem* m, uintptr_t cr3, uint64_t base, unsigned idx,
uint32_t* rva, uint32_t* size);
/* Extract a module's CodeView RSDS reference from its debug directory. This is
* the SINGLE debug-dir/RSDS parser in the engine, shared by the kernel bootstrap
* (host.c) and the public vmie_win32_pdb_ref - there is no second copy.
* base - image base VA in `cr3`.
* guid[16] - receives the PDB GUID (in-memory byte order); never NULL.
* age - receives the PDB age; never NULL.
* name - receives the NUL-terminated PDB file name; never NULL.
* namecap - capacity of `name` (>= 1). The name is truncated to namecap-1.
* Walks PE_DIR_DEBUG for an IMAGE_DEBUG_TYPE_CODEVIEW entry whose payload starts
* with 'RSDS', then reads {guid, age, name}. Returns 0 on success, -1 if there
* is no debug directory, no CodeView/RSDS entry, or the bytes are unreadable. */
int pe_pdb_ref(vmie_mem* m, uintptr_t cr3, uint64_t base,
uint8_t guid[16], uint32_t* age, char* name, size_t namecap);
#endif /* VMIE_PE_H */
+2 -52
View File
@@ -51,11 +51,6 @@ int cr3_recover(vmie_win32* v, uint64_t va_self, uint64_t target_pa, uintptr_t*
*cr3_out = best;
return 0;
}
#define DIR_EXPORT 0u
#define DIR_DEBUG 6u
#define DBG_CODEVIEW 2u
#define CV_RSDS 0x53445352u
static int beacon_find(vmie_mem* m, uint64_t* pa, uint64_t* va) {
void *ptr = m->pa;
const void *end = m->pa + m->fsize;
@@ -74,51 +69,6 @@ static int beacon_find(vmie_mem* m, uint64_t* pa, uint64_t* va) {
return -1;
}
static int pe_datadir(vmie_mem* m, uintptr_t cr3, uint64_t base, unsigned idx, uint32_t* rva, uint32_t* size) {
uint32_t lfanew;
if (gva_read(m, cr3, base + 0x3C, &lfanew, 4)) {
return -1;
}
const uint64_t dd = base + lfanew + 0x18 + 0x70 + (uint64_t)idx*8;
if (gva_read(m, cr3, dd, rva, 4)) {
return -1;
}
return (size && gva_read(m, cr3, dd + 4, size, 4)) ? -1 : 0;
}
static int pe_pdb(vmie_mem* m, uintptr_t cr3, uint64_t base, uint8_t guid[16], uint32_t* age, char* name, size_t namecap) {
uint32_t dbg_rva, dbg_sz;
if (pe_datadir(m, cr3, base, DIR_DEBUG, &dbg_rva, &dbg_sz) || !dbg_rva) {
return -1;
}
for (uint32_t o = 0; o + 0x1C <= dbg_sz; o += 0x1C) { /* IMAGE_DEBUG_DIRECTORY[] (28B) */
uint32_t type, cv_rva, sig;
if (gva_read(m, cr3, base + dbg_rva + o + 0x0C, &type, 4)) {
return -1;
}
if (type != DBG_CODEVIEW) {
continue;
}
if (gva_read(m, cr3, base + dbg_rva + o + 0x14, &cv_rva, 4)) { /* AddressOfRawData RVA */
return -1;
}
if (gva_read(m, cr3, base + cv_rva, &sig, 4) || sig != CV_RSDS) {
return -1;
}
if (gva_read(m, cr3, base + cv_rva + 0x04, guid, 16)) {
return -1;
}
if (gva_read(m, cr3, base + cv_rva + 0x14, age, 4)) {
return -1;
}
gva_read(m, cr3, base + cv_rva + 0x18, name, namecap); /* best-effort */
name[namecap - 1] = 0;
return 0;
}
return -1;
}
static int find_ntoskrnl(vmie_mem* m, uintptr_t cr3, uint64_t* base, uint8_t guid[16], uint32_t* age) {
const uint64_t t = cr3 & PFN_MASK;
@@ -152,7 +102,7 @@ static int find_ntoskrnl(vmie_mem* m, uintptr_t cr3, uint64_t* base, uint8_t gui
if (gva_read(m, cr3, va, &mz, 2) || mz != MZ) {
continue;
}
if (pe_pdb(m, cr3, va, guid, age, pdb, sizeof pdb)) {
if (pe_pdb_ref(m, cr3, va, guid, age, pdb, sizeof pdb)) {
continue;
}
if (strncmp(pdb, "ntkrnlmp.pdb", 12) != 0) {
@@ -168,7 +118,7 @@ static int find_ntoskrnl(vmie_mem* m, uintptr_t cr3, uint64_t* base, uint8_t gui
static uint32_t ko_export_rva(vmie_mem* m, uintptr_t cr3, uint64_t kbase, const char* want) {
uint32_t exp_rva;
if (pe_datadir(m, cr3, kbase, DIR_EXPORT, &exp_rva, NULL) || !exp_rva) {
if (pe_data_dir(m, cr3, kbase, PE_DIR_EXPORT, &exp_rva, NULL) || !exp_rva) {
return 0;
}
+221
View File
@@ -122,6 +122,227 @@ int vmie_pe_section(vmie_mem* m, uintptr_t cr3, uint64_t module_base,
return 0;
}
/* ---- shared data-directory + debug-dir parse ----------------------------- *
* The single DataDirectory accessor and the single CodeView/RSDS parser of the
* engine. Both read the guest image directly under cr3 (gva_read), so they work
* for any module without first gathering the section bodies, and are reused by
* the kernel bootstrap (host.c) and the public win32 surface alike. Cold:
* one-shot header reads, not a hot loop. */
/* CodeView debug-record constants (IMAGE_DEBUG_DIRECTORY + RSDS payload). */
#define DBG_TYPE_CODEVIEW 2u
#define CV_SIG_RSDS 0x53445352u /* 'RSDS' little-endian */
#define DBG_DIR_ENTRY 0x1Cu /* sizeof(IMAGE_DEBUG_DIRECTORY) = 28 */
int pe_data_dir(vmie_mem* m, uintptr_t cr3, uint64_t base, unsigned idx,
uint32_t* rva, uint32_t* size) __attribute__((cold));
int pe_data_dir(vmie_mem* m, uintptr_t cr3, uint64_t base, unsigned idx,
uint32_t* rva, uint32_t* size) {
uint32_t lfanew;
if (gva_read(m, cr3, base + 0x3C, &lfanew, 4)) {
return -1;
}
/* NT headers at base+lfanew: Signature(4)+FileHeader(20)=0x18, then the
* PE32+ OptionalHeader; DataDirectory[] begins at OptionalHeader+0x70. */
const uint64_t dd = base + lfanew + 0x18 + 0x70 + (uint64_t)idx * 8;
if (gva_read(m, cr3, dd, rva, 4)) {
return -1;
}
return (size && gva_read(m, cr3, dd + 4, size, 4)) ? -1 : 0;
}
int pe_pdb_ref(vmie_mem* m, uintptr_t cr3, uint64_t base,
uint8_t guid[16], uint32_t* age, char* name, size_t namecap)
__attribute__((cold));
int pe_pdb_ref(vmie_mem* m, uintptr_t cr3, uint64_t base,
uint8_t guid[16], uint32_t* age, char* name, size_t namecap) {
if (namecap == 0) {
return -1;
}
uint32_t dbg_rva, dbg_sz;
if (pe_data_dir(m, cr3, base, PE_DIR_DEBUG, &dbg_rva, &dbg_sz) || !dbg_rva) {
return -1;
}
for (uint32_t o = 0; o + DBG_DIR_ENTRY <= dbg_sz; o += DBG_DIR_ENTRY) {
uint32_t type, cv_rva, sig;
if (gva_read(m, cr3, base + dbg_rva + o + 0x0C, &type, 4)) { /* Type */
return -1;
}
if (type != DBG_TYPE_CODEVIEW) {
continue;
}
if (gva_read(m, cr3, base + dbg_rva + o + 0x14, &cv_rva, 4)) { /* AddressOfRawData */
return -1;
}
if (gva_read(m, cr3, base + cv_rva, &sig, 4) || sig != CV_SIG_RSDS) {
return -1;
}
if (gva_read(m, cr3, base + cv_rva + 0x04, guid, 16)) { /* GUID */
return -1;
}
if (gva_read(m, cr3, base + cv_rva + 0x14, age, 4)) { /* Age */
return -1;
}
gva_read(m, cr3, base + cv_rva + 0x18, name, namecap); /* PdbName (best-effort) */
name[namecap - 1] = 0;
return 0;
}
return -1;
}
/* ---- public win32 surface: function inventory (.pdata) ------------------- *
* RUNTIME_FUNCTION (12 bytes): { uint32 Begin; uint32 End; uint32 UnwindInfo }.
* The exception directory is the authoritative non-leaf function table. A chain
* continuation (its UNWIND_INFO header has UNW_FLAG_CHAININFO) is NOT a function
* start - it is folded into its primary by skipping entries whose Begin falls
* inside the previous accepted [Begin, End). Cold: one-shot directory read. */
#define RTF_SIZE 12u /* sizeof(RUNTIME_FUNCTION) */
int vmie_win32_functions(vmie_win32* v, uint64_t cr3, uint64_t module_base,
func_range* out, int max) __attribute__((cold));
int vmie_win32_functions(vmie_win32* v, uint64_t cr3, uint64_t module_base,
func_range* out, int max) {
vmie_mem* m = vmie_win32_mem(v);
if (!m) { return -1; }
uint32_t pd_rva, pd_sz;
if (pe_data_dir(m, cr3, module_base, PE_DIR_EXCEPTION, &pd_rva, &pd_sz) ||
!pd_rva || pd_sz < RTF_SIZE) {
return -1;
}
int total = 0;
uint32_t prev_end = 0; /* End of the last accepted primary */
int have_prev = 0;
const uint32_t n = pd_sz / RTF_SIZE;
for (uint32_t i = 0; i < n; i++) {
uint32_t begin, end;
if (gva_read(m, cr3, module_base + pd_rva + (uint64_t)i * RTF_SIZE,
&begin, 4) ||
gva_read(m, cr3, module_base + pd_rva + (uint64_t)i * RTF_SIZE + 4,
&end, 4)) {
return -1;
}
if (end <= begin) {
continue; /* malformed / empty: skip */
}
/* Fold chain continuations: an entry whose Begin lies within the last
* accepted [Begin, End) is a continuation of that function, not a new
* start (UNW_FLAG_CHAININFO). RUNTIME_FUNCTIONs are address-sorted, so
* comparing against the previous primary's extent suffices. */
if (have_prev && begin < prev_end) {
continue;
}
if (out && total < max) {
out[total].rva = begin;
out[total].size = end - begin;
}
prev_end = end;
have_prev = 1;
total++;
}
return total;
}
/* ---- public win32 surface: exports (EAT) --------------------------------- *
* IMAGE_EXPORT_DIRECTORY (40 bytes): Base(+0x10), NumberOfFunctions(+0x14),
* NumberOfNames(+0x18), AddressOfFunctions(+0x1C), AddressOfNames(+0x20),
* AddressOfNameOrdinals(+0x24). We iterate AddressOfFunctions[] (one entry per
* exported ordinal); the name for an index is found by reverse-mapping
* AddressOfNameOrdinals[]. A function RVA that falls within the export
* directory's own [rva, rva+size) is a forwarder string, not code. Cold:
* one-shot directory read. */
#define EXP_DIR_SIZE 40u
int vmie_win32_exports(vmie_win32* v, uint64_t cr3, uint64_t module_base,
export_sym* out, int max) __attribute__((cold));
int vmie_win32_exports(vmie_win32* v, uint64_t cr3, uint64_t module_base,
export_sym* out, int max) {
vmie_mem* m = vmie_win32_mem(v);
if (!m) { return -1; }
uint32_t exp_rva, exp_sz;
if (pe_data_dir(m, cr3, module_base, PE_DIR_EXPORT, &exp_rva, &exp_sz) ||
!exp_rva) {
return -1;
}
uint8_t ed[EXP_DIR_SIZE];
if (gva_read(m, cr3, module_base + exp_rva, ed, sizeof ed)) {
return -1;
}
uint32_t ord_base, n_funcs, n_names, a_funcs, a_names, a_ords;
memcpy(&ord_base, ed + 0x10, 4);
memcpy(&n_funcs, ed + 0x14, 4);
memcpy(&n_names, ed + 0x18, 4);
memcpy(&a_funcs, ed + 0x1C, 4);
memcpy(&a_names, ed + 0x20, 4);
memcpy(&a_ords, ed + 0x24, 4);
int total = 0;
for (uint32_t i = 0; i < n_funcs; i++) {
uint32_t frva;
if (gva_read(m, cr3, module_base + a_funcs + (uint64_t)i * 4, &frva, 4)) {
return -1;
}
if (frva == 0) {
continue; /* empty export slot */
}
if (out && total < max) {
export_sym* e = &out[total];
memset(e, 0, sizeof *e);
e->rva = frva;
e->ordinal = (uint16_t)(ord_base + i);
e->forwarded = (frva >= exp_rva && frva < exp_rva + exp_sz) ? 1u : 0u;
/* reverse map: is there a name whose ordinal == i? */
for (uint32_t k = 0; k < n_names; k++) {
uint16_t ord;
if (gva_read(m, cr3, module_base + a_ords + (uint64_t)k * 2,
&ord, 2)) {
return -1;
}
if (ord != i) {
continue;
}
uint32_t nrva;
if (gva_read(m, cr3, module_base + a_names + (uint64_t)k * 4,
&nrva, 4)) {
return -1;
}
/* Read the name best-effort up to the end of its resident page:
* a 64-byte fixed read can run past the section/page even when
* the (shorter, NUL-terminated) name itself is fully resident. */
for (size_t z = sizeof e->name - 1; z > 0; z--) {
if (gva_read(m, cr3, module_base + nrva, e->name, z) == 0) {
e->name[z] = 0;
break;
}
}
e->name[sizeof e->name - 1] = 0; /* truncate long names */
break;
}
}
total++;
}
return total;
}
/* ---- public win32 surface: PDB reference (CodeView RSDS) ------------------ *
* Thin wrapper over the shared pe_pdb_ref: the same debug-dir/RSDS parser the
* kernel bootstrap uses, generalized to any module. Cold. */
int vmie_win32_pdb_ref(vmie_win32* v, uint64_t cr3, uint64_t module_base,
pdb_ref* out) __attribute__((cold));
int vmie_win32_pdb_ref(vmie_win32* v, uint64_t cr3, uint64_t module_base,
pdb_ref* out) {
vmie_mem* m = vmie_win32_mem(v);
if (!m || !out) { return -1; }
return pe_pdb_ref(m, cr3, module_base, out->guid, &out->age,
out->pdb, sizeof out->pdb);
}
/* ---- public win32 surface: section enumeration + section views ----------- *
* Cold paths (one-shot header parse / section gather, not a hot loop). They
* reuse pe_sections / the shared section-table walk above - no second parser -