mirror of
https://dev.lirent.ru/Vatrog/vm-introspection-engine.git
synced 2026-07-08 23:46:36 +03:00
5ea9a3785f
vmie_mem could only be built from a path. Add a second input: an
already-open file descriptor, dup()'d internally (borrowed - the
caller's fd stays valid, core owns and closes its copy).
- vmie_mem_from_fd / vmie_mem_from_fd_segs (read-write, as the path
constructors: PROT_RW, MAP_SHARED)
- vmie_mem_from_ro_fd / vmie_mem_from_ro_fd_segs (read-only: map
PROT_READ, mark the source ro; gpa_write/gva_write return -1, every
read path is unchanged; accepts an O_RDONLY fd)
- vmie_win32_open_fd (the win32 context over an fd backing file)
Factor the mmap/validate tail and the single-low segment map out of the
path constructors into shared helpers, so the path and fd inputs go
through one mmap site and one map builder each.
512 lines
28 KiB
C
512 lines
28 KiB
C
/* win32.h - public Windows-guest surface of the vmi-engine.
|
|
*
|
|
* 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.
|
|
*
|
|
* 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:
|
|
* - `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.
|
|
*/
|
|
#ifndef VMIE_WIN32_H
|
|
#define VMIE_WIN32_H
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#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 */
|
|
|
|
/* 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;
|
|
|
|
/* 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
|
|
* failure. Free with vmie_win32_close(). */
|
|
vmie_win32* vmie_win32_open(const char* ram_path, uint64_t low);
|
|
|
|
/* As vmie_win32_open, but over an already-open file descriptor `fd` for the RAM
|
|
* backing file instead of a path. `fd` is BORROWED: it is dup()'d internally, so
|
|
* the context owns its own copy and the caller's `fd` stays open and valid after
|
|
* this call AND after vmie_win32_close(). `fd` must reference an mmap-able,
|
|
* read-write object (regular file / memfd_create / POSIX shm); its file position
|
|
* is not read or changed. Returns a new context (call host_bootstrap() next), or
|
|
* NULL on dup/fstat/mmap failure. Free with vmie_win32_close(). */
|
|
vmie_win32* vmie_win32_open_fd(int fd, uint64_t low);
|
|
|
|
/* Unmap, close, and free a context. Safe on NULL. After this, every pointer
|
|
* into guest memory obtained through this context is invalid. */
|
|
void vmie_win32_close(vmie_win32* v);
|
|
|
|
/* Borrow the engine's guest-memory handle for the generic address-space
|
|
* primitives (gva_read/gva_regions/...). The returned pointer is owned by `v`
|
|
* and valid until vmie_win32_close(v); do NOT free or retain it past that. NULL on
|
|
* NULL `v`. */
|
|
vmie_mem* vmie_win32_mem(vmie_win32* v);
|
|
|
|
/* 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
|
|
* that failed. Cold path: call once after vmie_win32_open(). */
|
|
int host_bootstrap(vmie_win32* v);
|
|
|
|
/* ---- guest string decode ------------------------------------------------- */
|
|
|
|
/* 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. */
|
|
size_t gva_read_text(vmie_win32* v, uintptr_t cr3, uintptr_t va, size_t nmemb, char* dst, size_t size);
|
|
|
|
/* ---- 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. */
|
|
int proc_list(vmie_win32* v, int skip_system, process* dst, size_t nmax);
|
|
|
|
/* 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. */
|
|
int proc_modules(vmie_win32* v, const process* pr, pmodule* dst, size_t nmax);
|
|
|
|
/* ---- 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. */
|
|
scan* scan_new(vmie_win32* v, const process* pr, scan_type t, const void* value,
|
|
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. */
|
|
int vmie_scan_pointer(vmie_win32* v, const process* pr, uint64_t target,
|
|
int max_depth, uint32_t max_off, scan_ptr_path* out, int max);
|
|
|
|
/* ---- 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);
|
|
|
|
/* ---- 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);
|
|
|
|
/* 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);
|
|
|
|
/* 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. */
|
|
|
|
/* 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);
|
|
|
|
#endif /* VMIE_WIN32_H */
|