mirror of
https://dev.lirent.ru/Vatrog/vm-introspection-engine.git
synced 2026-07-09 01:46:38 +03:00
Add imports, inline-hook detection, function hashing, per-function imports
Wave 2 of the code-analysis layer:
- vmie_win32_imports resolves the import directory (INT/IAT) to {iat_rva, dll,
name, ordinal} - named APIs, walking the name and slot thunks in lockstep so
every import carries the IAT slot a call lands on.
- vmie_win32_inline_hooks decodes each .pdata function's entry and reports any
whose first instruction is a direct jmp/call leaving the module image - the
detour/trampoline shape.
- vmie_win32_func_imports records, in order, the IAT slots a function calls
through (call qword [rip+disp] onto an import slot): the function's API-call
sequence, named by correlating with vmie_win32_imports.
- func_hash (codeanalysis.h) hashes a function position-independently, zeroing
the displacement bytes the decoder locates - one primitive for fingerprinting
known code and for detecting a changed body across snapshots.
Devirtualization needs no new call and is documented as a composition: a
vtable's methods are gva_jumptable(vtable_va), its instances are
pmap_referrers(vtable_va), and func_hash names each method. Imports reuse the
shared data-directory accessor; the analyses reuse the function/section/decode
primitives - no second PE or instruction parser.
This commit is contained in:
@@ -62,6 +62,7 @@ int vmie_pe_section(vmie_mem* m, uintptr_t cr3, uint64_t module_base,
|
||||
|
||||
/* OptionalHeader DataDirectory indices used across the engine. */
|
||||
#define PE_DIR_EXPORT 0u /* IMAGE_DIRECTORY_ENTRY_EXPORT */
|
||||
#define PE_DIR_IMPORT 1u /* IMAGE_DIRECTORY_ENTRY_IMPORT */
|
||||
#define PE_DIR_DEBUG 6u /* IMAGE_DIRECTORY_ENTRY_DEBUG */
|
||||
#define PE_DIR_EXCEPTION 3u /* IMAGE_DIRECTORY_ENTRY_EXCEPTION (.pdata) */
|
||||
|
||||
|
||||
@@ -516,3 +516,262 @@ int vmie_win32_callgraph(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
free(tbuf);
|
||||
return total;
|
||||
}
|
||||
|
||||
/* ---- public win32 surface: imports (import directory INT/IAT) ------------- *
|
||||
* IMAGE_IMPORT_DESCRIPTOR (20 bytes): OriginalFirstThunk(+0, the INT RVA),
|
||||
* TimeDateStamp(+4), ForwarderChain(+8), Name(+12, the DLL-name RVA),
|
||||
* FirstThunk(+16, the IAT RVA). The array ends at an all-zero descriptor. Each
|
||||
* descriptor's INT and IAT are parallel arrays of 8-byte thunks: a by-ordinal
|
||||
* thunk has bit 63 set (ordinal in the low 16 bits); a by-name thunk is the RVA
|
||||
* of an IMAGE_IMPORT_BY_NAME { uint16 Hint; char Name[]; }. We prefer the INT
|
||||
* for the name/ordinal (it survives binding) and always take the slot RVA from
|
||||
* the IAT position. Cold: one-shot directory read, reusing pe_data_dir. */
|
||||
|
||||
#define IMPDESC_SIZE 20u /* sizeof(IMAGE_IMPORT_DESCRIPTOR) */
|
||||
#define IMPDESC_OFT_OFF 0u /* OriginalFirstThunk (INT) RVA */
|
||||
#define IMPDESC_NAME_OFF 12u /* Name (DLL name) RVA */
|
||||
#define IMPDESC_FT_OFF 16u /* FirstThunk (IAT) RVA */
|
||||
#define IMP_ORDINAL_FLAG 0x8000000000000000ull /* by-ordinal thunk bit 63 */
|
||||
#define IMP_MAX_DESC 4096u /* descriptor-walk guard (malformed) */
|
||||
#define IMP_MAX_THUNK 65536u /* per-descriptor thunk guard */
|
||||
|
||||
/* Read a NUL-terminated ASCII string from guest VA into dst[cap], truncating to
|
||||
* cap-1 and best-effort over a non-fully-resident tail (shrink the read until it
|
||||
* succeeds), mirroring the export-name read. */
|
||||
static void imp_read_str(vmie_mem* m, uintptr_t cr3, uint64_t va,
|
||||
char* dst, size_t cap) {
|
||||
if (cap == 0) { return; }
|
||||
for (size_t z = cap - 1; z > 0; z--) {
|
||||
if (gva_read(m, cr3, va, dst, z) == 0) { dst[z] = 0; break; }
|
||||
}
|
||||
dst[cap - 1] = 0;
|
||||
}
|
||||
|
||||
int vmie_win32_imports(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
import_sym* out, int max) __attribute__((cold));
|
||||
int vmie_win32_imports(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
import_sym* out, int max) {
|
||||
vmie_mem* m = vmie_win32_mem(v);
|
||||
if (!m) { return -1; }
|
||||
|
||||
uint32_t imp_rva, imp_sz;
|
||||
if (pe_data_dir(m, cr3, module_base, PE_DIR_IMPORT, &imp_rva, &imp_sz) ||
|
||||
!imp_rva) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int total = 0;
|
||||
for (uint32_t d = 0; d < IMP_MAX_DESC; d++) {
|
||||
const uint64_t desc = module_base + imp_rva + (uint64_t)d * IMPDESC_SIZE;
|
||||
uint32_t oft, name_rva, ft;
|
||||
if (gva_read(m, cr3, desc + IMPDESC_OFT_OFF, &oft, 4) ||
|
||||
gva_read(m, cr3, desc + IMPDESC_NAME_OFF, &name_rva, 4) ||
|
||||
gva_read(m, cr3, desc + IMPDESC_FT_OFF, &ft, 4)) {
|
||||
return -1;
|
||||
}
|
||||
if (oft == 0 && name_rva == 0 && ft == 0) {
|
||||
break; /* zero terminator descriptor */
|
||||
}
|
||||
if (ft == 0) {
|
||||
continue; /* no IAT: nothing to report */
|
||||
}
|
||||
|
||||
char dll[32];
|
||||
dll[0] = 0;
|
||||
if (name_rva) {
|
||||
imp_read_str(m, cr3, module_base + name_rva, dll, sizeof dll);
|
||||
}
|
||||
|
||||
/* INT (OriginalFirstThunk) carries names even after binding; fall back to
|
||||
* the IAT (FirstThunk) when there is no INT. The slot RVA always comes
|
||||
* from the IAT position so it matches a `call qword [rip]` target. */
|
||||
const uint32_t int_rva = oft ? oft : ft;
|
||||
for (uint32_t t = 0; t < IMP_MAX_THUNK; t++) {
|
||||
uint64_t thunk;
|
||||
if (gva_read(m, cr3, module_base + int_rva + (uint64_t)t * 8,
|
||||
&thunk, 8)) {
|
||||
return -1;
|
||||
}
|
||||
if (thunk == 0) {
|
||||
break; /* end of this thunk array */
|
||||
}
|
||||
const uint32_t iat_rva = ft + t * 8;
|
||||
if (out && total < max) {
|
||||
import_sym* s = &out[total];
|
||||
memset(s, 0, sizeof *s);
|
||||
s->iat_rva = iat_rva;
|
||||
memcpy(s->dll, dll, sizeof s->dll);
|
||||
if (thunk & IMP_ORDINAL_FLAG) {
|
||||
s->ordinal = (uint16_t)(thunk & 0xFFFFu); /* by-ordinal */
|
||||
} else {
|
||||
/* by-name: thunk is the RVA of IMAGE_IMPORT_BY_NAME; the name
|
||||
* begins at +2 (after the uint16 Hint). */
|
||||
imp_read_str(m, cr3, module_base + (uint32_t)thunk + 2,
|
||||
s->name, sizeof s->name);
|
||||
}
|
||||
}
|
||||
total++;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/* ---- public win32 surface: inline-hook detection ------------------------- *
|
||||
* For each .pdata function, decode the FIRST instruction; if it is a DIRECT
|
||||
* jmp/call (has_rel) whose target leaves the module image, it is a detour. Reuses
|
||||
* vmie_win32_functions (.pdata starts) and x86_decode - no second parser. The
|
||||
* entry bytes are read directly under cr3 (a 16-byte window covers any single
|
||||
* x86 instruction). Cold: one-shot directory read + per-function entry decode. */
|
||||
|
||||
#define HOOK_ENTRY_BYTES 16u /* max length of one x86 instruction */
|
||||
|
||||
int vmie_win32_inline_hooks(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
inline_hook* out, int max) __attribute__((cold));
|
||||
int vmie_win32_inline_hooks(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
inline_hook* out, int max) {
|
||||
vmie_mem* m = vmie_win32_mem(v);
|
||||
if (!m) { return -1; }
|
||||
|
||||
/* image bounds [module_base, module_base + SizeOfImage) (as in callgraph). */
|
||||
uint32_t lfanew;
|
||||
if (gva_read(m, cr3, module_base + 0x3C, &lfanew, 4)) { return -1; }
|
||||
uint32_t size_of_image;
|
||||
if (gva_read(m, cr3, module_base + lfanew + 0x18 + OPT_SIZEOFIMAGE_OFF,
|
||||
&size_of_image, 4)) {
|
||||
return -1;
|
||||
}
|
||||
const uint64_t img_lo = module_base;
|
||||
const uint64_t img_hi = module_base + (uint64_t)size_of_image; /* exclusive */
|
||||
|
||||
/* function inventory: count, then gather (stack common case, heap overflow)
|
||||
* so every function entry is examined, none silently dropped. */
|
||||
const int nfn = vmie_win32_functions(v, cr3, module_base, NULL, 0);
|
||||
if (nfn < 0) { return -1; }
|
||||
func_range stack_fr[256];
|
||||
func_range* fr = stack_fr;
|
||||
func_range* heap_fr = NULL;
|
||||
if (nfn > (int)(sizeof stack_fr / sizeof stack_fr[0])) {
|
||||
heap_fr = malloc((size_t)nfn * sizeof *heap_fr);
|
||||
if (!heap_fr) { return -1; }
|
||||
fr = heap_fr;
|
||||
}
|
||||
const int got = vmie_win32_functions(v, cr3, module_base, fr, nfn);
|
||||
if (got < 0) { free(heap_fr); return -1; }
|
||||
|
||||
int total = 0;
|
||||
for (int f = 0; f < got; f++) {
|
||||
const uint64_t fn_va = module_base + fr[f].rva;
|
||||
uint8_t entry[HOOK_ENTRY_BYTES];
|
||||
if (gva_read(m, cr3, fn_va, entry, sizeof entry)) {
|
||||
continue; /* entry not resident: skip */
|
||||
}
|
||||
x86_insn in;
|
||||
const int ilen = x86_decode(entry, sizeof entry, &in);
|
||||
if (ilen <= 0) {
|
||||
continue; /* undecodable entry: skip */
|
||||
}
|
||||
if (!in.has_rel || (in.flow != X86_JMP && in.flow != X86_CALL)) {
|
||||
continue; /* not a direct jmp/call */
|
||||
}
|
||||
const uint64_t tgt = x86_branch_target(fn_va, &in);
|
||||
if (tgt >= img_lo && tgt < img_hi) {
|
||||
continue; /* stays inside the image */
|
||||
}
|
||||
if (out && total < max) {
|
||||
out[total].func_rva = fr[f].rva;
|
||||
out[total].target = tgt;
|
||||
}
|
||||
total++;
|
||||
}
|
||||
|
||||
free(heap_fr);
|
||||
return total;
|
||||
}
|
||||
|
||||
/* ---- public win32 surface: per-function imports (call/jmp through IAT) ---- *
|
||||
* Step one function and, for each `call/jmp qword [rip+disp]` (has_riprel) whose
|
||||
* memory target is an IAT slot of this module's import directory, record the slot
|
||||
* RVA - the function's API-call sequence. Reuses vmie_win32_functions (to bound
|
||||
* the body), vmie_win32_section_view (to gather .text), and the decoder. Cold. */
|
||||
|
||||
int vmie_win32_func_imports(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
uint32_t func_rva, uint32_t* iat_rvas, int max)
|
||||
__attribute__((cold));
|
||||
int vmie_win32_func_imports(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
uint32_t func_rva, uint32_t* iat_rvas, int max) {
|
||||
vmie_mem* m = vmie_win32_mem(v);
|
||||
if (!m) { return -1; }
|
||||
|
||||
/* the IAT-slot window of this module: [imp_lo, imp_hi). A rip-relative target
|
||||
* landing in this set and 8-aligned is an import-thunk call. */
|
||||
import_sym tmp[1];
|
||||
const int nimp = vmie_win32_imports(v, cr3, module_base, tmp, 0);
|
||||
if (nimp < 0) { return -1; }
|
||||
uint32_t imp_lo = 0xFFFFFFFFu, imp_hi = 0; /* IAT-slot RVA bounds */
|
||||
if (nimp > 0) {
|
||||
import_sym* im = malloc((size_t)nimp * sizeof *im);
|
||||
if (!im) { return -1; }
|
||||
const int gi = vmie_win32_imports(v, cr3, module_base, im, nimp);
|
||||
if (gi < 0) { free(im); return -1; }
|
||||
const int use = gi < nimp ? gi : nimp;
|
||||
for (int i = 0; i < use; i++) {
|
||||
if (im[i].iat_rva < imp_lo) { imp_lo = im[i].iat_rva; }
|
||||
if (im[i].iat_rva + 8 > imp_hi) { imp_hi = im[i].iat_rva + 8; }
|
||||
}
|
||||
free(im);
|
||||
}
|
||||
|
||||
/* locate the function's extent from .pdata (count then gather). */
|
||||
const int nfn = vmie_win32_functions(v, cr3, module_base, NULL, 0);
|
||||
if (nfn < 0) { return -1; }
|
||||
func_range stack_fr[256];
|
||||
func_range* fr = stack_fr;
|
||||
func_range* heap_fr = NULL;
|
||||
if (nfn > (int)(sizeof stack_fr / sizeof stack_fr[0])) {
|
||||
heap_fr = malloc((size_t)nfn * sizeof *heap_fr);
|
||||
if (!heap_fr) { return -1; }
|
||||
fr = heap_fr;
|
||||
}
|
||||
const int got = vmie_win32_functions(v, cr3, module_base, fr, nfn);
|
||||
if (got < 0) { free(heap_fr); return -1; }
|
||||
uint32_t fn_size = 0;
|
||||
for (int f = 0; f < got; f++) {
|
||||
if (fr[f].rva == func_rva) { fn_size = fr[f].size; break; }
|
||||
}
|
||||
free(heap_fr);
|
||||
if (fn_size == 0) { return -1; } /* not a known function start */
|
||||
|
||||
/* gather the function body addressed at its absolute VA so a rip-relative
|
||||
* target is directly an absolute VA. */
|
||||
uint8_t* fb = malloc(fn_size);
|
||||
if (!fb) { return -1; }
|
||||
if (gva_read(m, cr3, module_base + func_rva, fb, fn_size)) {
|
||||
free(fb);
|
||||
return -1;
|
||||
}
|
||||
const uint64_t fn_va = module_base + func_rva;
|
||||
|
||||
int total = 0;
|
||||
for (size_t off = 0; off < fn_size; ) {
|
||||
x86_insn in;
|
||||
const int ilen = x86_decode(fb + off, fn_size - off, &in);
|
||||
if (ilen <= 0) { break; } /* desync: stop this function */
|
||||
if (in.has_riprel && (in.flow == X86_CALL || in.flow == X86_JMP)) {
|
||||
const uint64_t tgt = x86_riprel_target(fn_va + off, &in);
|
||||
if (tgt >= module_base) {
|
||||
const uint64_t tgt_rva = tgt - module_base;
|
||||
if (tgt_rva >= imp_lo && tgt_rva < imp_hi &&
|
||||
(tgt_rva & 7u) == 0) {
|
||||
if (iat_rvas && total < max) {
|
||||
iat_rvas[total] = (uint32_t)tgt_rva;
|
||||
}
|
||||
total++;
|
||||
}
|
||||
}
|
||||
}
|
||||
off += (size_t)ilen;
|
||||
}
|
||||
|
||||
free(fb);
|
||||
return total;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user