mirror of
https://dev.lirent.ru/Vatrog/vm-introspection-engine.git
synced 2026-07-09 01:46:38 +03:00
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:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 -
|
||||
|
||||
Reference in New Issue
Block a user