2026-06-15 08:20:50 +03:00
|
|
|
/* win32.h - public Windows-guest surface of the vmi-engine.
|
2026-06-14 21:47:56 +03:00
|
|
|
*
|
|
|
|
|
* The host opens a guest's RAM backing file (a flat, writable, coherent mmap),
|
|
|
|
|
* recovers the kernel address space, and reads/writes guest memory by CR3 and
|
|
|
|
|
* virtual address. Everything is CR3-keyed, never PID-keyed: a `process` already
|
|
|
|
|
* carries its own cr3, which is the key to that address space.
|
|
|
|
|
*
|
2026-06-15 02:57:46 +03:00
|
|
|
* This header is the Windows-typed surface (process/pmodule/gtext, bring-up,
|
|
|
|
|
* enumeration, the win32 scan wrappers). The OS-agnostic memory-model contract
|
|
|
|
|
* lives in memmodel.h (pulled in below); the scanners in scan.h/sigscan.h.
|
|
|
|
|
*
|
|
|
|
|
* Conventions:
|
2026-06-14 21:47:56 +03:00
|
|
|
* - `cr3` is a raw CR3 / DirectoryTableBase value; low flag bits are masked
|
|
|
|
|
* internally, so either the masked PML4 GPA or the raw register works.
|
|
|
|
|
* - Integer returns: 0 on success, negative on failure, unless stated.
|
|
|
|
|
* - The library never takes ownership of caller buffers and never retains a
|
|
|
|
|
* pointer past the call that received it, unless explicitly stated.
|
|
|
|
|
*/
|
2026-06-15 08:20:50 +03:00
|
|
|
#ifndef VMIE_WIN32_H
|
|
|
|
|
#define VMIE_WIN32_H
|
2026-06-14 21:47:56 +03:00
|
|
|
#include <stdint.h>
|
|
|
|
|
#include <stddef.h>
|
2026-06-15 02:57:46 +03:00
|
|
|
#include "memmodel.h" /* vmie_mem, vregion/VR_*, task/range, gva_read/write/ptr/regions/sweep */
|
|
|
|
|
#include "sigscan.h" /* mem_view_t, sig_pattern_t */
|
|
|
|
|
#include "scan.h" /* scan_type, scan_ptr_path, generic scan surface */
|
2026-06-14 21:47:56 +03:00
|
|
|
|
2026-06-15 08:20:50 +03:00
|
|
|
/* Opaque introspection context. Completed in src/engine/win32/engine-win32.h;
|
|
|
|
|
* callers only ever hold a pointer. Created by vmie_win32_open(), populated by
|
|
|
|
|
* host_bootstrap(), released by vmie_win32_close(). */
|
|
|
|
|
typedef struct vmie_win32 vmie_win32;
|
2026-06-14 21:47:56 +03:00
|
|
|
|
|
|
|
|
/* A guest counted string still resident in guest memory (e.g. a UNICODE_STRING
|
|
|
|
|
* buffer). Not a copy: `va` points into the guest, decode it with gva_read_text.
|
|
|
|
|
* va - guest VA of the first UTF-16LE code unit (0 if absent)
|
|
|
|
|
* len - length in BYTES (not characters); always even for UTF-16 */
|
|
|
|
|
typedef struct { uint64_t va; uint32_t len; } gtext;
|
|
|
|
|
|
|
|
|
|
/* A live process, as produced by proc_list(). Self-contained: `cr3` is all you
|
|
|
|
|
* need to read/write its user address space, `eprocess`/`peb` re-anchor it in
|
|
|
|
|
* kernel/user space without another lookup.
|
|
|
|
|
* cr3 - DirectoryTableBase (PFN-masked); key to this address space
|
|
|
|
|
* peb - PEB VA (0 for system/kernel-only processes)
|
|
|
|
|
* eprocess - _EPROCESS VA (kernel object, read under the kernel cr3)
|
|
|
|
|
* pid, ppid - process / parent ids (ppid == (uint32_t)-1 if unavailable)
|
|
|
|
|
* create_time - raw KSYSTEM_TIME / FILETIME (100 ns ticks; 0 if unavailable)
|
|
|
|
|
* name - ImageFileName, NUL-terminated ASCII (up to 15 chars)
|
|
|
|
|
* path - full image path as a guest UTF-16 string (gtext; may be empty) */
|
|
|
|
|
typedef struct {
|
|
|
|
|
uint64_t cr3;
|
|
|
|
|
uint64_t peb;
|
|
|
|
|
uint64_t eprocess;
|
|
|
|
|
uint32_t pid;
|
|
|
|
|
uint32_t ppid;
|
|
|
|
|
uint64_t create_time;
|
|
|
|
|
char name[16];
|
|
|
|
|
gtext path;
|
|
|
|
|
} process;
|
|
|
|
|
|
|
|
|
|
/* A loaded module (image) inside a process, as produced by proc_modules().
|
|
|
|
|
* pr - owning process (its cr3 is the address space these VAs live in)
|
|
|
|
|
* entry - _LDR_DATA_TABLE_ENTRY VA
|
|
|
|
|
* base - image base VA (page-aligned); pair with `size` for a MODULE scope
|
|
|
|
|
* size - image size in bytes (SizeOfImage)
|
|
|
|
|
* name - module file name (gtext UTF-16, e.g. "ntdll.dll")
|
|
|
|
|
* path - full module path (gtext UTF-16) */
|
|
|
|
|
typedef struct {
|
|
|
|
|
const process* pr;
|
|
|
|
|
uint64_t entry;
|
|
|
|
|
uint64_t base;
|
|
|
|
|
uint32_t size;
|
|
|
|
|
gtext name;
|
|
|
|
|
gtext path;
|
|
|
|
|
} pmodule;
|
|
|
|
|
|
|
|
|
|
/* ---- lifecycle ----------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
/* Open `ram_path` (the guest RAM backing file) and build a context over it.
|
|
|
|
|
* ram_path - path to a writable, share=on RAM backing file
|
|
|
|
|
* low - size in bytes of below-4G guest RAM (the PCI-hole split point);
|
|
|
|
|
* pass the value from the VM's memory layout. If total RAM <= low,
|
|
|
|
|
* the split is inert.
|
|
|
|
|
* Returns a new context (call host_bootstrap() next), or NULL on open/mmap
|
2026-06-15 08:20:50 +03:00
|
|
|
* failure. Free with vmie_win32_close(). */
|
|
|
|
|
vmie_win32* vmie_win32_open(const char* ram_path, uint64_t low);
|
2026-06-14 21:47:56 +03:00
|
|
|
|
|
|
|
|
/* Unmap, close, and free a context. Safe on NULL. After this, every pointer
|
|
|
|
|
* into guest memory obtained through this context is invalid. */
|
2026-06-15 08:20:50 +03:00
|
|
|
void vmie_win32_close(vmie_win32* v);
|
2026-06-15 02:57:46 +03:00
|
|
|
|
|
|
|
|
/* Borrow the engine's guest-memory handle for the generic address-space
|
|
|
|
|
* primitives (gva_read/gva_regions/...). The returned pointer is owned by `v`
|
2026-06-15 08:20:50 +03:00
|
|
|
* and valid until vmie_win32_close(v); do NOT free or retain it past that. NULL on
|
2026-06-15 02:57:46 +03:00
|
|
|
* NULL `v`. */
|
2026-06-15 08:20:50 +03:00
|
|
|
vmie_mem* vmie_win32_mem(vmie_win32* v);
|
2026-06-14 21:47:56 +03:00
|
|
|
|
|
|
|
|
/* One-shot bring-up: locate the guest agent beacon in physical RAM, recover a
|
|
|
|
|
* bootstrap CR3, find ntoskrnl, build the struct-offset profile, derive the
|
|
|
|
|
* permanent System DirectoryTableBase (kernel cr3) and System _EPROCESS, then
|
|
|
|
|
* ACK the agent. On success the context is ready for proc_list()/gva_read()/etc.
|
|
|
|
|
* Returns 0 on success, or a negative stage code (-1..-6) identifying the step
|
2026-06-15 08:20:50 +03:00
|
|
|
* that failed. Cold path: call once after vmie_win32_open(). */
|
|
|
|
|
int host_bootstrap(vmie_win32* v);
|
2026-06-14 21:47:56 +03:00
|
|
|
|
2026-06-15 02:57:46 +03:00
|
|
|
/* ---- guest string decode ------------------------------------------------- */
|
2026-06-14 21:47:56 +03:00
|
|
|
|
|
|
|
|
/* Read a UTF-16LE guest string and transcode it to UTF-8.
|
|
|
|
|
* va - guest VA of the first UTF-16 code unit
|
|
|
|
|
* nmemb - number of BYTES to read from the guest (rounded down to even)
|
|
|
|
|
* dst - output buffer for NUL-terminated UTF-8 (may be NULL to size only)
|
|
|
|
|
* size - capacity of `dst` in bytes
|
|
|
|
|
* Returns the number of UTF-8 bytes the full conversion needs, EXCLUDING the
|
|
|
|
|
* terminator (like snprintf): if it is >= `size`, output was truncated. When
|
|
|
|
|
* `dst` is non-NULL and `size` > 0 the result is always NUL-terminated. */
|
2026-06-15 08:20:50 +03:00
|
|
|
size_t gva_read_text(vmie_win32* v, uintptr_t cr3, uintptr_t va, size_t nmemb, char* dst, size_t size);
|
2026-06-14 21:47:56 +03:00
|
|
|
|
|
|
|
|
/* ---- enumeration --------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
/* Enumerate processes by walking ActiveProcessLinks from System.
|
|
|
|
|
* skip_system - if nonzero, omit processes with no PEB (System/kernel-only)
|
|
|
|
|
* dst - caller array receiving up to `nmax` `process` records
|
|
|
|
|
* nmax - capacity of `dst`
|
|
|
|
|
* Returns the number written (<= nmax), or negative on failure (e.g. bootstrap
|
|
|
|
|
* not completed). Enumeration stops at `nmax`; raise it to see more. */
|
2026-06-15 08:20:50 +03:00
|
|
|
int proc_list(vmie_win32* v, int skip_system, process* dst, size_t nmax);
|
2026-06-14 21:47:56 +03:00
|
|
|
|
|
|
|
|
/* Enumerate a process's loaded modules via the PEB loader InLoadOrder list.
|
|
|
|
|
* pr - process to inspect (uses pr->cr3 and pr->peb)
|
|
|
|
|
* dst - caller array receiving up to `nmax` `pmodule` records
|
|
|
|
|
* nmax - capacity of `dst`
|
|
|
|
|
* Returns the number written (<= nmax), 0 if the process has no PEB/loader. */
|
2026-06-15 08:20:50 +03:00
|
|
|
int proc_modules(vmie_win32* v, const process* pr, pmodule* dst, size_t nmax);
|
2026-06-15 02:57:46 +03:00
|
|
|
|
|
|
|
|
/* ---- win32 scan wrappers ------------------------------------------------- *
|
|
|
|
|
* Convenience entry points over the generic cr3/range scan surface (scan.h).
|
|
|
|
|
* They project a Windows `process` to its cr3, and its `pmodule[]` to a decoded
|
|
|
|
|
* `range[]` (UTF-8 names), then delegate to scan_new_cr3 / scan_pointer. */
|
|
|
|
|
|
|
|
|
|
/* Open a value-scan session over the user address space of `pr`. Equivalent to
|
|
|
|
|
* scan_new_cr3(&v->mem, pr->cr3, ...). Returns NULL on NULL pr or OOM. */
|
2026-06-15 08:20:50 +03:00
|
|
|
scan* scan_new(vmie_win32* v, const process* pr, scan_type t, const void* value,
|
2026-06-15 02:57:46 +03:00
|
|
|
int be, int aligned, uint64_t lo, uint64_t hi);
|
|
|
|
|
|
|
|
|
|
/* Pointer scan over `pr`'s user space, anchored on its loaded modules. Resolves
|
|
|
|
|
* `pr`'s module list to range[] (names engine-decoded) and delegates to
|
|
|
|
|
* scan_pointer. Returns the number of paths found, or negative on failure. */
|
2026-06-15 08:20:50 +03:00
|
|
|
int vmie_scan_pointer(vmie_win32* v, const process* pr, uint64_t target,
|
2026-06-15 02:57:46 +03:00
|
|
|
int max_depth, uint32_t max_off, scan_ptr_path* out, int max);
|
|
|
|
|
|
2026-06-16 19:06:59 +03:00
|
|
|
/* ---- PE sections + section views ----------------------------------------- *
|
|
|
|
|
* A section is a PE-image concept, so it is keyed by (vmie_win32*, cr3,
|
|
|
|
|
* module_base): the address space and where the image is based in it. The
|
|
|
|
|
* module need NOT be in the loader list - any valid PE base works, including
|
|
|
|
|
* one found by scanning for MZ/PE (a manually-mapped or hidden module). */
|
|
|
|
|
|
|
|
|
|
/* Coordinate space of a section view's mem_view_t.base_va. The pure scanners
|
|
|
|
|
* (sig_all/sig_each, x86_decode callers, ...) report every result as
|
|
|
|
|
* base_va + offset, so this enum decides what coordinate the hits/targets come
|
|
|
|
|
* back in:
|
|
|
|
|
* SECTION_LOCAL - base_va = 0 => results are section-relative
|
|
|
|
|
* offsets [0, vsize). The most stable form: independent of
|
|
|
|
|
* the image base AND of where the section sits in the image.
|
|
|
|
|
* Use when you only care about positions inside one section.
|
|
|
|
|
* MODULE_RVA - base_va = section RVA => results are module-relative (RVA).
|
|
|
|
|
* ASLR-stable across runs of the same binary; the canonical
|
|
|
|
|
* form for portable signatures and for correlating across
|
|
|
|
|
* sections of one module. Recommended for sig-gen.
|
|
|
|
|
* ABSOLUTE_VA - base_va = module_base + RVA => results are live guest
|
|
|
|
|
* virtual addresses, valid for gva_read/gva_ptr under this
|
|
|
|
|
* cr3 NOW. NOT stable across runs (ASLR). Use when you must
|
|
|
|
|
* dereference a hit immediately in the live process. */
|
|
|
|
|
typedef enum { SECTION_LOCAL, MODULE_RVA, ABSOLUTE_VA } view_base;
|
|
|
|
|
|
|
|
|
|
/* One PE section, as enumerated by vmie_win32_sections.
|
|
|
|
|
* name - section name, NUL-terminated; PE names are <= 8 bytes (name[8] is
|
|
|
|
|
* the NUL slot). Names are NOT unique in a malformed/packed image -
|
|
|
|
|
* prefer iterating by index over matching by name.
|
|
|
|
|
* rva - section RVA: byte offset of the section from the module base
|
|
|
|
|
* (so ABSOLUTE_VA = module_base + rva).
|
|
|
|
|
* vsize - virtual size in bytes (the in-memory size; may exceed the on-disk
|
|
|
|
|
* raw size). Size the buffer for vmie_win32_section_view from THIS.
|
|
|
|
|
* prot - effective protection as VR_* flags (VR_R/VR_W/VR_X), derived from
|
|
|
|
|
* the section Characteristics (IMAGE_SCN_MEM_READ/WRITE/EXECUTE).
|
|
|
|
|
* VR_U is never set: these are image semantics, not live PTE rights. */
|
|
|
|
|
typedef struct { char name[9]; uint32_t rva; uint32_t vsize; uint32_t prot; } section_desc;
|
|
|
|
|
|
|
|
|
|
/* Enumerate the sections of the PE image based at `module_base` in the address
|
|
|
|
|
* space `cr3`.
|
|
|
|
|
* v - engine handle
|
|
|
|
|
* cr3 - the process address space the module is mapped in (e.g. a
|
|
|
|
|
* process->cr3). The module need NOT be in the loader list -
|
|
|
|
|
* any valid PE base works (e.g. one found by scanning for
|
|
|
|
|
* MZ/PE, i.e. a manually-mapped / hidden module).
|
|
|
|
|
* module_base - image base VA; only the PE headers (first page) must be
|
|
|
|
|
* resident and readable under `cr3`.
|
|
|
|
|
* out, max - caller array receiving up to `max` section_desc; `out` may be
|
|
|
|
|
* NULL to count only (then `max` is ignored).
|
|
|
|
|
* Returns the TOTAL section count (may exceed `max` => enlarge and retry), or
|
|
|
|
|
* -1 if the headers are absent/unreadable or `module_base` is not a PE.
|
|
|
|
|
*
|
|
|
|
|
* The returned `rva`/`vsize` are ASLR-independent (image-relative): stable
|
|
|
|
|
* across runs of the same binary. The absolute placement is module_base + rva.
|
|
|
|
|
*
|
|
|
|
|
* Example - list the sections of the first module of a process:
|
|
|
|
|
* pmodule m; proc_modules(v, pr, &m, 1);
|
|
|
|
|
* section_desc s[32];
|
|
|
|
|
* int n = vmie_win32_sections(v, pr->cr3, m.base, s, 32);
|
|
|
|
|
* for (int i = 0; i < n && i < 32; i++)
|
|
|
|
|
* printf("%-8s rva=%#x vsize=%#x %c%c%c\n", s[i].name, s[i].rva, s[i].vsize,
|
|
|
|
|
* (s[i].prot & VR_R) ? 'R' : '-', (s[i].prot & VR_W) ? 'W' : '-',
|
|
|
|
|
* (s[i].prot & VR_X) ? 'X' : '-'); */
|
|
|
|
|
int vmie_win32_sections(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
|
|
|
|
section_desc* out, int max);
|
|
|
|
|
|
|
|
|
|
/* Gather a section's bytes from the live process into `buf` and return a flat
|
|
|
|
|
* mem_view_t over them in the coordinate space chosen by `mode`. This is the
|
|
|
|
|
* "section memory, addressed from 0" entry point.
|
|
|
|
|
* v, cr3, module_base - as in vmie_win32_sections.
|
|
|
|
|
* sec - the section to open (from vmie_win32_sections; carries
|
|
|
|
|
* rva/vsize). Must be non-NULL.
|
|
|
|
|
* mode - view_base: sets out->base_va (see view_base).
|
|
|
|
|
* SECTION_LOCAL => 0, MODULE_RVA => sec->rva,
|
|
|
|
|
* ABSOLUTE_VA => module_base + sec->rva.
|
|
|
|
|
* buf, bufcap - caller-owned destination; size it to sec->vsize (from
|
|
|
|
|
* enumeration). If bufcap < sec->vsize the section is TRUNCATED:
|
|
|
|
|
* out->size = bufcap (this is NOT an error). The returned view
|
|
|
|
|
* BORROWS buf - it is valid only while `buf` lives and is left
|
|
|
|
|
* unmodified; the library retains no pointer past this call.
|
|
|
|
|
* out - on success: out->data = buf, out->size = min(sec->vsize,
|
|
|
|
|
* bufcap), out->base_va per `mode`. Must be non-NULL.
|
|
|
|
|
* Returns 0 on success, or -1 if `sec`/`buf`/`out` is NULL, the headers are
|
|
|
|
|
* unreadable, or the section bytes are not fully resident (paged out / sparse)
|
|
|
|
|
* so the read fails.
|
|
|
|
|
*
|
|
|
|
|
* Reversing nuance: .text/.rdata are normally fully resident; a section that is
|
|
|
|
|
* partly paged out yields -1 - re-try when resident, or sweep the live VA range
|
|
|
|
|
* with gva_sweep instead. The base-mode also picks the coordinate stability:
|
|
|
|
|
* SECTION_LOCAL/MODULE_RVA are ASLR-stable (offset / RVA), ABSOLUTE_VA is the
|
|
|
|
|
* live VA for this run only.
|
|
|
|
|
*
|
|
|
|
|
* Example - find an IDA pattern in .text as RVAs (ASLR-stable across runs):
|
|
|
|
|
* section_desc s[32];
|
|
|
|
|
* int n = vmie_win32_sections(v, cr3, base, s, 32);
|
|
|
|
|
* for (int i = 0; i < n && i < 32; i++) if (!strcmp(s[i].name, ".text")) {
|
|
|
|
|
* uint8_t* buf = malloc(s[i].vsize);
|
|
|
|
|
* mem_view_t tv;
|
|
|
|
|
* if (vmie_win32_section_view(v, cr3, base, &s[i], MODULE_RVA,
|
|
|
|
|
* buf, s[i].vsize, &tv) == 0) {
|
|
|
|
|
* sig_pattern_t p; sig_parse_ida("48 8B 05 ? ? ? ?", &p);
|
|
|
|
|
* uint64_t rvas[64];
|
|
|
|
|
* int h = sig_all(tv, &p, rvas, 64); // rvas[] are module RVAs, stable
|
|
|
|
|
* sig_free(&p);
|
|
|
|
|
* }
|
|
|
|
|
* free(buf);
|
|
|
|
|
* }
|
|
|
|
|
*
|
|
|
|
|
* Example - step instructions section-locally (offset 0 == section start):
|
|
|
|
|
* mem_view_t tv;
|
|
|
|
|
* vmie_win32_section_view(v, cr3, base, &text, SECTION_LOCAL,
|
|
|
|
|
* buf, text.vsize, &tv);
|
|
|
|
|
* for (size_t off = 0; off < tv.size; ) {
|
|
|
|
|
* x86_insn in;
|
|
|
|
|
* int len = x86_decode(tv.data + off, tv.size - off, &in); // off == section-local addr
|
|
|
|
|
* if (len <= 0) { off++; continue; }
|
|
|
|
|
* off += (size_t)len;
|
|
|
|
|
* } */
|
|
|
|
|
int vmie_win32_section_view(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
|
|
|
|
const section_desc* sec, view_base mode,
|
|
|
|
|
uint8_t* buf, size_t bufcap, mem_view_t* out);
|
|
|
|
|
|
2026-06-16 19:27:42 +03:00
|
|
|
/* ---- function inventory / exports / PDB reference ------------------------ *
|
|
|
|
|
* Authoritative module metadata recovered from the PE directories, keyed by
|
|
|
|
|
* (vmie_win32*, cr3, module_base) like the section surface. All RVAs are
|
|
|
|
|
* image-relative and therefore ASLR-independent (absolute VA = module_base +
|
|
|
|
|
* rva); only the headers and the relevant directory need be resident. */
|
|
|
|
|
|
|
|
|
|
/* One function extent from the module's exception directory (.pdata
|
|
|
|
|
* RUNTIME_FUNCTION).
|
|
|
|
|
* rva - function start RVA (BeginAddress). Absolute VA = module_base + rva.
|
|
|
|
|
* size - EndAddress - BeginAddress, in bytes.
|
|
|
|
|
* Only NON-LEAF functions appear in .pdata (leaf functions with no unwind data
|
|
|
|
|
* are absent) - authoritative where present, but not a complete function list.
|
|
|
|
|
* rva/size are ASLR-independent. */
|
|
|
|
|
typedef struct { uint32_t rva; uint32_t size; } func_range;
|
|
|
|
|
|
|
|
|
|
/* Enumerate functions of the module at `module_base` (cr3 address space) from
|
|
|
|
|
* .pdata. Chain continuations (UNWIND_INFO with UNW_FLAG_CHAININFO) are folded
|
|
|
|
|
* into their primary - one entry per function start. Returns TOTAL count
|
|
|
|
|
* (out=NULL => count only), or -1 if no exception directory / unreadable.
|
|
|
|
|
*
|
|
|
|
|
* Example - list the first 64 functions of a module as ASLR-stable RVAs:
|
|
|
|
|
* func_range fr[64];
|
|
|
|
|
* int n = vmie_win32_functions(v, pr->cr3, m.base, fr, 64);
|
|
|
|
|
* for (int i = 0; i < n && i < 64; i++)
|
|
|
|
|
* printf("sub_%x (%u bytes)\n", fr[i].rva, fr[i].size); */
|
|
|
|
|
int vmie_win32_functions(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
|
|
|
|
func_range* out, int max);
|
|
|
|
|
|
2026-06-16 19:52:25 +03:00
|
|
|
/* One call-graph edge, with both endpoints as RVAs relative to the module base
|
|
|
|
|
* (absolute VA = module_base + rva).
|
|
|
|
|
* from - RVA of the function that contains the call/jmp site (a .pdata
|
|
|
|
|
* function start)
|
|
|
|
|
* to - RVA of the branch target (inside the same module image)
|
|
|
|
|
* kind - 0 = call (E8 / direct CALL), 1 = direct jmp (E9/EB, including a tail
|
|
|
|
|
* call to another function). */
|
|
|
|
|
typedef struct { uint32_t from; uint32_t to; uint8_t kind; } call_edge;
|
|
|
|
|
|
|
|
|
|
/* Build the intra-module call graph of the image at `module_base` (in the `cr3`
|
|
|
|
|
* address space). Reuses the existing primitives - vmie_win32_functions to
|
|
|
|
|
* enumerate the .pdata function starts, vmie_win32_section_view to gather the
|
|
|
|
|
* .text bytes, and x86_decode to step each function - and emits one edge for
|
|
|
|
|
* every DIRECT call/jmp (has_rel) whose resolved target lands inside the module
|
|
|
|
|
* image [module_base, module_base + SizeOfImage). `from` is the containing
|
|
|
|
|
* function's RVA, `to` is the target's RVA.
|
|
|
|
|
*
|
|
|
|
|
* INDIRECT calls/jmps (through a register or memory, e.g. `call [rip+disp]` or
|
|
|
|
|
* `jmp rax`) are SKIPPED here - they carry no static rel target. Resolve those
|
|
|
|
|
* separately: switch tables via gva_jumptable, import thunks via the IAT (a
|
|
|
|
|
* wave-2 concern). A direct branch whose target falls OUTSIDE the image (an
|
|
|
|
|
* inter-module jmp/call) is also skipped - the graph is intra-module by
|
|
|
|
|
* construction.
|
|
|
|
|
*
|
|
|
|
|
* Writes up to `max` edges to `out` (NULL to count only) and returns the TOTAL
|
|
|
|
|
* edge count, or -1 if the .pdata/.text directory is missing or unreadable.
|
|
|
|
|
* Edges are grouped by source function (all of one function's edges are
|
|
|
|
|
* contiguous), in ascending function order.
|
|
|
|
|
*
|
|
|
|
|
* Example - out-degree of each function:
|
|
|
|
|
* call_edge e[4096];
|
|
|
|
|
* int n = vmie_win32_callgraph(v, pr->cr3, m.base, e, 4096);
|
|
|
|
|
* // group by e[i].from to get each function's callees */
|
|
|
|
|
int vmie_win32_callgraph(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
|
|
|
|
call_edge* out, int max);
|
|
|
|
|
|
2026-06-16 20:03:49 +03:00
|
|
|
/* One import: a function this module pulls from another DLL, recovered from the
|
|
|
|
|
* import directory (the INT/IAT pair of an IMAGE_IMPORT_DESCRIPTOR).
|
|
|
|
|
* iat_rva - RVA of the IAT slot that holds the resolved function pointer at
|
|
|
|
|
* run time (absolute VA = module_base + iat_rva). A call through
|
|
|
|
|
* this import is `call qword [rip+disp]` whose target lands on this
|
|
|
|
|
* slot - so iat_rva is exactly what vmie_win32_func_imports reports;
|
|
|
|
|
* correlate the two to name a function's API calls.
|
|
|
|
|
* dll - the exporting DLL name as written in the descriptor, NUL-
|
|
|
|
|
* terminated, TRUNCATED to 31 chars (e.g. "KERNEL32.dll"). A name
|
|
|
|
|
* longer than 31 bytes is cut; this is the documented limit.
|
|
|
|
|
* name - the imported function name, NUL-terminated, TRUNCATED to 63 chars
|
|
|
|
|
* (long C++ mangled names are cut); "" for a by-ordinal import.
|
|
|
|
|
* ordinal - the import ordinal for a by-ordinal import (name[0]=='\0'), else
|
|
|
|
|
* 0. By-ordinal imports set the high bit in the thunk and carry no
|
|
|
|
|
* name in the image. */
|
|
|
|
|
typedef struct { uint32_t iat_rva; char dll[32]; char name[64]; uint16_t ordinal; } import_sym;
|
|
|
|
|
|
|
|
|
|
/* Enumerate the module's imports from its import directory (IMAGE_DIRECTORY_
|
|
|
|
|
* ENTRY_IMPORT). For each IMAGE_IMPORT_DESCRIPTOR it reads the DLL name, then
|
|
|
|
|
* walks the parallel INT (OriginalFirstThunk: the name/ordinal hints) and IAT
|
|
|
|
|
* (FirstThunk: the resolved-pointer slots) in lockstep so every entry carries
|
|
|
|
|
* its own IAT-slot RVA. A by-name thunk points at an IMAGE_IMPORT_BY_NAME
|
|
|
|
|
* (hint+NUL-terminated name); a by-ordinal thunk has its top bit set and yields
|
|
|
|
|
* an ordinal instead. The INT is preferred when present (it survives binding);
|
|
|
|
|
* the IAT is the fallback.
|
|
|
|
|
*
|
|
|
|
|
* Returns the TOTAL number of imports (out=NULL => count only, so size then
|
|
|
|
|
* fill), or -1 if there is no import directory or the headers/directory are
|
|
|
|
|
* unreadable. Entries are reported descriptor by descriptor, and within a
|
|
|
|
|
* descriptor in thunk order.
|
|
|
|
|
*
|
|
|
|
|
* Example - list a module's imports and where each resolves:
|
|
|
|
|
* import_sym im[512];
|
|
|
|
|
* int n = vmie_win32_imports(v, pr->cr3, m.base, im, 512);
|
|
|
|
|
* for (int i = 0; i < n && i < 512; i++)
|
|
|
|
|
* if (im[i].name[0])
|
|
|
|
|
* printf("%s!%s -> IAT %#x\n", im[i].dll, im[i].name, im[i].iat_rva);
|
|
|
|
|
* else
|
|
|
|
|
* printf("%s!#%u -> IAT %#x\n", im[i].dll, im[i].ordinal, im[i].iat_rva); */
|
|
|
|
|
int vmie_win32_imports(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
|
|
|
|
import_sym* out, int max);
|
|
|
|
|
|
|
|
|
|
/* One inline-hook finding: a function whose FIRST instruction is a direct
|
|
|
|
|
* jmp/call leaving the module image - the classic detour / trampoline shape.
|
|
|
|
|
* func_rva - the hooked function's RVA (a .pdata function start). Absolute VA
|
|
|
|
|
* = module_base + func_rva.
|
|
|
|
|
* target - the absolute VA the entry redirects to. It lies OUTSIDE the
|
|
|
|
|
* module image [module_base, module_base + SizeOfImage); that is
|
|
|
|
|
* exactly what makes it a cross-module hook rather than an ordinary
|
|
|
|
|
* intra-module branch. */
|
|
|
|
|
typedef struct { uint32_t func_rva; uint64_t target; } inline_hook;
|
|
|
|
|
|
|
|
|
|
/* Detect inline (entry-redirect) hooks. For each function from .pdata
|
|
|
|
|
* (vmie_win32_functions) it decodes the FIRST instruction with x86_decode; if
|
|
|
|
|
* that instruction is a DIRECT jmp/call (has_rel) whose resolved target
|
|
|
|
|
* (x86_branch_target) lands OUTSIDE the module image
|
|
|
|
|
* [module_base, module_base + SizeOfImage), it records {func_rva, target}. An
|
|
|
|
|
* un-hooked function begins with its real prologue (push/sub/mov/endbr64...) or
|
|
|
|
|
* branches inside its own image, so it is not reported.
|
|
|
|
|
*
|
|
|
|
|
* Returns the TOTAL number of hooked functions (out=NULL => count only), or -1
|
|
|
|
|
* if the .pdata/.text directory or headers are missing/unreadable.
|
|
|
|
|
*
|
|
|
|
|
* Scope: this finds INLINE hooks (the function body's entry is patched). IAT
|
|
|
|
|
* hooks - an import SLOT redirected to point outside its resolving module - are
|
|
|
|
|
* a different shape that needs cross-module pointer resolution and are NOT
|
|
|
|
|
* covered here.
|
|
|
|
|
*
|
|
|
|
|
* Example - report any patched function entries in a module:
|
|
|
|
|
* inline_hook hk[64];
|
|
|
|
|
* int n = vmie_win32_inline_hooks(v, pr->cr3, m.base, hk, 64);
|
|
|
|
|
* for (int i = 0; i < n && i < 64; i++)
|
|
|
|
|
* printf("sub_%x hooked -> %#llx\n", hk[i].func_rva,
|
|
|
|
|
* (unsigned long long)hk[i].target); */
|
|
|
|
|
int vmie_win32_inline_hooks(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
|
|
|
|
inline_hook* out, int max);
|
|
|
|
|
|
|
|
|
|
/* Recover which IAT slots a function calls, in call order - the function's
|
|
|
|
|
* API-call sequence / behavioral fingerprint. It steps `func_rva`'s body with
|
|
|
|
|
* x86_decode and, for every `call/jmp qword [rip+disp]` (an indirect branch
|
|
|
|
|
* through memory: has_riprel) whose resolved memory target (x86_riprel_target)
|
|
|
|
|
* is an IAT slot of THIS module's import directory, it records that slot's RVA.
|
|
|
|
|
* Correlate the returned RVAs with vmie_win32_imports (same iat_rva) to turn the
|
|
|
|
|
* sequence into named API calls (e.g. CreateFileW, WriteFile, CloseHandle).
|
|
|
|
|
*
|
|
|
|
|
* func_rva - the function to analyze, as an RVA (e.g. from
|
|
|
|
|
* vmie_win32_functions or an export). Absolute VA = module_base +
|
|
|
|
|
* func_rva.
|
|
|
|
|
* iat_rvas - caller array receiving up to `max` IAT-slot RVAs in the order
|
|
|
|
|
* the calls appear; NULL to count only.
|
|
|
|
|
*
|
|
|
|
|
* Returns the TOTAL number of IAT-slot calls in the function (out=NULL =>
|
|
|
|
|
* count), or -1 if the headers / import directory / function bytes are
|
|
|
|
|
* unreadable. v1 resolves call/jmp THROUGH the IAT (rip-relative onto an import
|
|
|
|
|
* slot); other indirect forms are out of scope.
|
|
|
|
|
*
|
|
|
|
|
* Example - print the API sequence of a function:
|
|
|
|
|
* uint32_t slots[128];
|
|
|
|
|
* int n = vmie_win32_func_imports(v, pr->cr3, m.base, fn_rva, slots, 128);
|
|
|
|
|
* import_sym im[512];
|
|
|
|
|
* int ni = vmie_win32_imports(v, pr->cr3, m.base, im, 512);
|
|
|
|
|
* for (int i = 0; i < n && i < 128; i++)
|
|
|
|
|
* for (int j = 0; j < ni && j < 512; j++)
|
|
|
|
|
* if (im[j].iat_rva == slots[i]) { puts(im[j].name); break; } */
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
/* Devirtualization (C++ vtables) needs NO dedicated symbol - it is a
|
|
|
|
|
* COMPOSITION of primitives the engine already exposes:
|
|
|
|
|
* - a vtable at `vtable_va` is an array of code pointers, so its METHODS are
|
|
|
|
|
* gva_jumptable(mem, cr3, vtable_va, ...) (codeanalysis.h) - the same
|
|
|
|
|
* code-pointer-array walk that recovers switch tables;
|
|
|
|
|
* - its live INSTANCES are pmap_referrers(pm, vtable_va, ...) (pmap.h),
|
|
|
|
|
* because an object's first qword is its vtable pointer (who-points-here on
|
|
|
|
|
* the vtable VA enumerates the objects).
|
|
|
|
|
* Recover the method RVAs with gva_jumptable, then func_hash (codeanalysis.h)
|
|
|
|
|
* can name each method body against a known-hash table. No new call is added
|
|
|
|
|
* for this on purpose. */
|
|
|
|
|
|
2026-06-16 19:27:42 +03:00
|
|
|
/* One exported symbol from the module export directory (EAT).
|
|
|
|
|
* rva - export target RVA (absolute VA = module_base + rva). Forwarder
|
|
|
|
|
* exports report the forwarder-string RVA; see `forwarded`.
|
|
|
|
|
* ordinal - export ordinal (biased value as exported).
|
|
|
|
|
* name - export name, NUL-terminated, TRUNCATED to 63 chars (long C++
|
|
|
|
|
* mangled names are cut; "" for by-ordinal-only exports).
|
|
|
|
|
* forwarded - nonzero if this is a forwarder (rva points into the export
|
|
|
|
|
* section, not code - e.g. "NTDLL.RtlAllocateHeap"). */
|
|
|
|
|
typedef struct { uint32_t rva; uint16_t ordinal; uint8_t forwarded; char name[64]; } export_sym;
|
|
|
|
|
|
|
|
|
|
/* Enumerate the module's exports (named functions, no PDB/network needed).
|
|
|
|
|
* Returns TOTAL count (out=NULL => count), or -1 if no export directory /
|
|
|
|
|
* unreadable. Entries are reported in export-table order; by-ordinal-only
|
|
|
|
|
* exports (no name) carry name[0]=='\0'.
|
|
|
|
|
*
|
|
|
|
|
* Example - print a module's named exports:
|
|
|
|
|
* export_sym es[256];
|
|
|
|
|
* int n = vmie_win32_exports(v, pr->cr3, m.base, es, 256);
|
|
|
|
|
* for (int i = 0; i < n && i < 256; i++)
|
|
|
|
|
* printf("%-40s rva=%#x ord=%u%s\n", es[i].name, es[i].rva,
|
|
|
|
|
* es[i].ordinal, es[i].forwarded ? " (forwarder)" : ""); */
|
|
|
|
|
int vmie_win32_exports(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
|
|
|
|
export_sym* out, int max);
|
|
|
|
|
|
|
|
|
|
/* CodeView PDB reference from the module debug directory (RSDS). The
|
|
|
|
|
* symbol-server lookup key.
|
|
|
|
|
* guid - PDB GUID (16 bytes, in-memory byte order, as the symbol server path
|
|
|
|
|
* uses).
|
|
|
|
|
* age - PDB age.
|
|
|
|
|
* pdb - PDB file name, NUL-terminated, truncated to 63 chars (e.g.
|
|
|
|
|
* "ntdll.pdb").
|
|
|
|
|
* Use {guid, age, pdb} to fetch the PDB out-of-band; PARSING the PDB for
|
|
|
|
|
* internal symbol names is OUT OF SCOPE here (it needs the external file). */
|
|
|
|
|
typedef struct { uint8_t guid[16]; uint32_t age; char pdb[64]; } pdb_ref;
|
|
|
|
|
|
|
|
|
|
/* Extract the module's PDB reference. Returns 0 on success, -1 if no debug
|
|
|
|
|
* directory / not RSDS / unreadable. Generalizes the kernel bootstrap's GUID
|
|
|
|
|
* resolve to any module.
|
|
|
|
|
*
|
|
|
|
|
* Example - format the symbol-server path component for a module:
|
|
|
|
|
* pdb_ref pr_;
|
|
|
|
|
* if (vmie_win32_pdb_ref(v, pr->cr3, m.base, &pr_) == 0) {
|
|
|
|
|
* char g[33];
|
|
|
|
|
* for (int i = 0; i < 16; i++) sprintf(g + i*2, "%02X", pr_.guid[i]);
|
|
|
|
|
* printf("%s/%s%X/%s\n", pr_.pdb, g, pr_.age, pr_.pdb);
|
|
|
|
|
* } */
|
|
|
|
|
int vmie_win32_pdb_ref(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
|
|
|
|
pdb_ref* out);
|
|
|
|
|
|
2026-06-15 08:20:50 +03:00
|
|
|
#endif /* VMIE_WIN32_H */
|