diff --git a/CMakeLists.txt b/CMakeLists.txt index df56684..5abb9c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,17 +11,19 @@ option(VMIE_LTO "Enable LTO" OFF) # build-only; shipped default is -O2, no add_library(vmie STATIC src/core/gpa.c src/engine/gva.c - src/engine/host.c - src/engine/pe.c - src/engine/proc.c - src/engine/profile.c - src/engine/text.c + src/engine/sigphys.c + src/engine/win32/host.c + src/engine/win32/pe.c + src/engine/win32/proc.c + src/engine/win32/profile.c + src/engine/win32/text.c src/handlers/scan.c src/handlers/sigscan.c) target_include_directories(vmie PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # public API: include/*.h PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/core/include # private: core.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/engine/include) # private: engine.h, contract.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/engine/include # private: engine-arch.h, pe.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/engine/win32) # private: engine-win32.h, contract.h target_compile_options(vmie PRIVATE -O2 -Wall -Wextra) if(VMIE_LTO) target_compile_options(vmie PRIVATE -flto) @@ -39,10 +41,10 @@ set(VMIE_STARTUP ${CMAKE_CURRENT_BINARY_DIR}/vmie-startup.exe) add_custom_command( OUTPUT ${VMIE_STARTUP} COMMAND ${MINGW_CC} -O2 -Wall -Wextra -static -s - -I${CMAKE_CURRENT_SOURCE_DIR}/src/engine/include - -o ${VMIE_STARTUP} ${CMAKE_CURRENT_SOURCE_DIR}/src/engine/guest.c - DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/engine/guest.c - ${CMAKE_CURRENT_SOURCE_DIR}/src/engine/include/contract.h + -I${CMAKE_CURRENT_SOURCE_DIR}/src/engine/win32 + -o ${VMIE_STARTUP} ${CMAKE_CURRENT_SOURCE_DIR}/src/engine/win32/guest.c + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/engine/win32/guest.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/engine/win32/contract.h COMMENT "Cross-compiling vmie-startup.exe (mingw-w64, x86-64)" VERBATIM) add_custom_target(vmie-startup ALL DEPENDS ${VMIE_STARTUP}) diff --git a/include/memmodel.h b/include/memmodel.h index 7a2d142..6bc6512 100644 --- a/include/memmodel.h +++ b/include/memmodel.h @@ -27,6 +27,40 @@ * pass it, with a cr3, to the address-space primitives below. */ typedef struct vmie_mem vmie_mem; +/* One contiguous GPA window backed by a file span: GPA [gpa, gpa+len) maps 1:1 + * onto file offset [file_off, file_off+len). A POD descriptor, promoted here so + * an explicit-segment dump can be opened through the public surface below; the + * full vmie_mem (which embeds an array of these) is defined in core.h. */ +#ifndef VMIE_GPA_SEG_DEFINED +#define VMIE_GPA_SEG_DEFINED +typedef struct gpa_seg { + uint64_t gpa; + uint64_t len; + uint64_t file_off; +} gpa_seg; +#endif + +/* ---- dump source lifecycle ----------------------------------------------- * + * A vmie_mem is the universal memory source: a live win32 physical image and an + * on-disk dump are both vmie_mem. These open a dump (or any flat/segmented RAM + * image) as a heap-owned vmie_mem for the source-agnostic physical scanners + * (scan.h: sig_scan_mem/sig_scan_sources). No paging/cr3: a dump supports the + * physical signature scan only. The win32 engine produces its vmie_mem through + * the win32 surface (win32.h) instead. */ + +/* Open `path` as a single-`low` image (the classic QEMU split; low >= file size + * => one inert identity segment, i.e. a flat dump). Returns a heap-owned handle, + * or NULL on open/mmap failure. Release with vmie_mem_close(). */ +vmie_mem* vmie_mem_open(const char* path, uint64_t low); + +/* Open `path` with an explicit segment map (`nseg` entries; see gpa_seg). The + * map must be well-formed against the file size (dense, sorted, in-file). + * Returns a heap-owned handle, or NULL on failure. Release with vmie_mem_close(). */ +vmie_mem* vmie_mem_open_segs(const char* path, const gpa_seg* segs, int nseg); + +/* Unmap, close, and free a handle from vmie_mem_open*. Safe on NULL. */ +void vmie_mem_close(vmie_mem* m); + /* ---- flat memory view (single owner) ------------------------------------- * * A contiguous view of memory. * data - host pointer to the bytes (borrowed; not owned by the view) diff --git a/include/scan.h b/include/scan.h index 68ae0bc..ffd56d9 100644 --- a/include/scan.h +++ b/include/scan.h @@ -9,7 +9,7 @@ * memory and feed them to the signature matcher. * * The Windows-typed convenience entry points (scan_new(process*), - * vmie_scan_pointer(process*)) live in the win32 surface (vmie.h). + * vmie_scan_pointer(process*)) live in the win32 surface (win32.h). */ #ifndef VMIE_SCAN_H #define VMIE_SCAN_H @@ -61,7 +61,23 @@ int gva_sig_first(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi, int gva_sig_rip (vmie_mem* m, uintptr_t cr3, uint64_t hit_va, size_t disp_off, size_t instr_len, uint64_t* target); -/* gva_sig_phys (scan the raw physical image) needs the core segment map, so it - * is an engine bridge, declared in engine.h - not part of the handler surface. */ +/* ---- physical-image signature scan (OS-agnostic engine bridge) ----------- * + * Scan the raw physical image (the core segment map) for a signature, without a + * cr3 or page tables: each seg is one mem_view_t over its file span, fed to the + * pure matcher. This is the dump path - a dump (vmie_mem_open*) supports the + * physical scan only. Keyed by vmie_mem*, like the rest of this header. */ + +/* Attributed hit from a multi-source scan: which source matched, and where. */ +typedef struct { int source; uint64_t gpa; } sig_hit_src; + +/* Scan one physical image for `p`. Writes up to `max` GPA hits to `out` (NULL to + * count only) and returns the TOTAL number of hits, or -1 on a bad pattern. */ +int sig_scan_mem (vmie_mem* m, const sig_pattern_t* p, uint64_t* out, int max); + +/* Scan `nsrc` physical images for `p`, tagging each hit with its source index. + * Writes up to `max` attributed hits to `out` (NULL to count only) and returns + * the TOTAL across all sources, or -1 on a bad pattern. */ +int sig_scan_sources(vmie_mem* const* srcs, int nsrc, const sig_pattern_t* p, + sig_hit_src* out, int max); #endif /* VMIE_SCAN_H */ diff --git a/include/sigscan.h b/include/sigscan.h index 575ca99..3115a37 100644 --- a/include/sigscan.h +++ b/include/sigscan.h @@ -36,6 +36,12 @@ bool sig_parse_ida(const char* ida, sig_pattern_t* out); * false on NULL args or an empty mask. */ bool sig_parse_mask(const uint8_t* bytes, const char* mask, sig_pattern_t* out); +/* Build an exact (no-wildcard) pattern from `len` raw bytes: every byte must + * match. A thin wrapper over sig_parse_mask with an all-'x' mask, so the result + * is released with sig_free() like any other pattern. Returns true on success, + * false on NULL args, a zero length, or OOM. Touches no vmie_mem (pure). */ +bool sig_from_bytes(const uint8_t* bytes, size_t len, sig_pattern_t* out); + /* Release a pattern produced by sig_parse_*. Safe on NULL and on an * already-freed pattern (it is zeroed). */ void sig_free(sig_pattern_t* p); diff --git a/include/vmie.h b/include/win32.h similarity index 84% rename from include/vmie.h rename to include/win32.h index 4c6617e..cf0faf0 100644 --- a/include/vmie.h +++ b/include/win32.h @@ -1,4 +1,4 @@ -/* vmie.h - public Windows-guest surface of the vmi-engine. +/* 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 @@ -16,18 +16,18 @@ * - The library never takes ownership of caller buffers and never retains a * pointer past the call that received it, unless explicitly stated. */ -#ifndef VMIE_VMIE_H -#define VMIE_VMIE_H +#ifndef VMIE_WIN32_H +#define VMIE_WIN32_H #include #include #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/include/engine.h; - * callers only ever hold a pointer. Created by vmie_open(), populated by - * host_bootstrap(), released by vmie_close(). */ -typedef struct vmie vmie; +/* 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. @@ -80,26 +80,26 @@ typedef struct { * 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_close(). */ -vmie* vmie_open(const char* ram_path, uint64_t low); + * failure. Free with vmie_win32_close(). */ +vmie_win32* vmie_win32_open(const char* ram_path, 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_close(vmie* v); +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_close(v); do NOT free or retain it past that. NULL on + * and valid until vmie_win32_close(v); do NOT free or retain it past that. NULL on * NULL `v`. */ -vmie_mem* vmie_memory(vmie* 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_open(). */ -int host_bootstrap(vmie* v); + * that failed. Cold path: call once after vmie_win32_open(). */ +int host_bootstrap(vmie_win32* v); /* ---- guest string decode ------------------------------------------------- */ @@ -111,7 +111,7 @@ int host_bootstrap(vmie* v); * 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* v, uintptr_t cr3, uintptr_t va, size_t nmemb, char* dst, size_t size); +size_t gva_read_text(vmie_win32* v, uintptr_t cr3, uintptr_t va, size_t nmemb, char* dst, size_t size); /* ---- enumeration --------------------------------------------------------- */ @@ -121,14 +121,14 @@ size_t gva_read_text(vmie* v, uintptr_t cr3, uintptr_t va, size_t nmemb, char* d * 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* v, int skip_system, process* dst, size_t nmax); +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* v, const process* pr, pmodule* dst, size_t nmax); +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). @@ -137,13 +137,13 @@ int proc_modules(vmie* v, const process* pr, pmodule* dst, size_t nmax); /* 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* v, const process* pr, scan_type t, const void* value, +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* v, const process* pr, uint64_t target, +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); -#endif /* VMIE_VMIE_H */ +#endif /* VMIE_WIN32_H */ diff --git a/src/cli.c b/src/cli.c index df8307b..f6e0c52 100644 --- a/src/cli.c +++ b/src/cli.c @@ -2,8 +2,8 @@ * * Opens a guest RAM backing file, brings up the VMI context, lists processes, * and for the first user process dumps its loaded modules and mapped regions. - * Public surface only (include/vmie.h); the region walk takes a vmie_mem*, - * borrowed from the engine via vmie_memory(). + * Public surface only (include/win32.h); the region walk takes a vmie_mem*, + * borrowed from the engine via vmie_win32_mem(). * * argv[1] path to the guest RAM backing file * argv[2] `low` - size in bytes of below-4G guest RAM (strtoull, base 0) @@ -14,7 +14,7 @@ #include #include #include -#include "vmie.h" +#include "win32.h" #define DEFAULT_NMAX 512 #define MOD_CAP 256 @@ -41,7 +41,7 @@ static void decode_prot(uint32_t prot, char out[5]) { out[4] = 0; } -static void dump_modules(vmie* ctx, const process* pr) { +static void dump_modules(vmie_win32* ctx, const process* pr) { pmodule mods[MOD_CAP]; const int nm = proc_modules(ctx, pr, mods, MOD_CAP); if (nm <= 0) { @@ -68,12 +68,12 @@ static void dump_modules(vmie* ctx, const process* pr) { } } -static void dump_regions(vmie* ctx, const process* pr) { +static void dump_regions(vmie_win32* ctx, const process* pr) { vregion* rg = malloc((size_t)RGN_CAP * sizeof *rg); if (!rg) { return; } - const int total = gva_regions(vmie_memory(ctx), pr->cr3, 0, ~0ull, 0, rg, RGN_CAP); + const int total = gva_regions(vmie_win32_mem(ctx), pr->cr3, 0, ~0ull, 0, rg, RGN_CAP); const int shown = total < 0 ? 0 : (total < RGN_CAP ? total : RGN_CAP); for (int i = 0; i < shown; i++) { char prot[5]; @@ -103,7 +103,7 @@ int main(int argc, char** argv) { } } - vmie* ctx = vmie_open(ram_path, low); + vmie_win32* ctx = vmie_win32_open(ram_path, low); if (!ctx) { fprintf(stderr, "error: cannot open RAM backing file '%s'\n", ram_path); return 1; @@ -112,14 +112,14 @@ int main(int argc, char** argv) { const int rc = host_bootstrap(ctx); if (rc != 0) { fprintf(stderr, "error: bootstrap failed (%d): %s\n", rc, bootstrap_stage(rc)); - vmie_close(ctx); + vmie_win32_close(ctx); return 1; } process* procs = malloc(nmax * sizeof *procs); if (!procs) { fprintf(stderr, "error: out of memory\n"); - vmie_close(ctx); + vmie_win32_close(ctx); return 1; } @@ -127,7 +127,7 @@ int main(int argc, char** argv) { if (np < 0) { fprintf(stderr, "error: proc_list failed (%d)\n", np); free(procs); - vmie_close(ctx); + vmie_win32_close(ctx); return 1; } @@ -157,6 +157,6 @@ int main(int argc, char** argv) { } free(procs); - vmie_close(ctx); + vmie_win32_close(ctx); return 0; } diff --git a/src/core/gpa.c b/src/core/gpa.c index 849f129..0e3c823 100644 --- a/src/core/gpa.c +++ b/src/core/gpa.c @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -147,3 +148,43 @@ void gpa_close(vmie_mem* m) { clean_ctx(m); } + +/* ---- public dump source (heap-owned vmie_mem) ---------------------------- * + * Thin wrappers over gpa_open*: heap-allocate a vmie_mem and open into it, so a + * dump (or any flat/segmented RAM image) is a first-class memory source for the + * physical scanners without exposing the win32 engine. */ + +__attribute__((cold)) +vmie_mem* vmie_mem_open(const char* path, uint64_t low) { + vmie_mem* m = calloc(1, sizeof *m); + if (!m) { + return NULL; + } + if (gpa_open(m, path, (uintptr_t)low)) { + free(m); + return NULL; + } + return m; +} + +__attribute__((cold)) +vmie_mem* vmie_mem_open_segs(const char* path, const gpa_seg* segs, int nseg) { + vmie_mem* m = calloc(1, sizeof *m); + if (!m) { + return NULL; + } + if (gpa_open_segs(m, path, segs, nseg)) { + free(m); + return NULL; + } + return m; +} + +__attribute__((cold)) +void vmie_mem_close(vmie_mem* m) { + if (!m) { + return; + } + gpa_close(m); + free(m); +} diff --git a/src/core/include/core.h b/src/core/include/core.h index 8d83ff8..bc6f89d 100644 --- a/src/core/include/core.h +++ b/src/core/include/core.h @@ -2,17 +2,14 @@ #define VMIE_CORE_H #include #include +#include "memmodel.h" /* gpa_seg (POD, promoted public) + vmie_mem forward decl */ #define VMIE_MAX_SEGS 8 -/* One contiguous GPA window backed by a file span: GPA [gpa, gpa+len) maps 1:1 - * onto file offset [file_off, file_off+len). The classic single-`low` guest is - * two segs ({0,low,0} below the 4 GiB hole, {4G,fsize-low,low} above it). */ -typedef struct gpa_seg { - uint64_t gpa; - uint64_t len; - uint64_t file_off; -} gpa_seg; +/* gpa_seg (the GPA<->file-span descriptor) is defined in memmodel.h, included + * above: one contiguous GPA window mapping 1:1 onto a file span. The classic + * single-`low` guest is two segs ({0,low,0} below the 4 GiB hole, {4G,fsize-low, + * low} above it). */ /* Flat RW mmap of the guest RAM backing file. The GPA<->file-offset map is the * sorted, dense, in-file segment table seg[0..nseg): each seg is one contiguous diff --git a/src/engine/gva.c b/src/engine/gva.c index 0604e9b..518359f 100644 --- a/src/engine/gva.c +++ b/src/engine/gva.c @@ -2,7 +2,7 @@ #include #include #include -#include "engine.h" +#include "engine-arch.h" /* PTE permission bits we propagate down the walk. */ #define PTE_RW (1ull << 1) @@ -45,7 +45,15 @@ static int gva_gpa(vmie_mem* m, uintptr_t cr3, uintptr_t va, return 0; } -/* zero-copy borrowed read: leaf-bounded host pointer at `va` (see engine.h). */ +/* cold extern translate: GPA of `va` under `cr3`, or -1. Wraps the hot static + * gva_gpa for cold callers outside this TU (win32 bring-up) without exposing the + * inlinable hot primitive. Declared in engine-arch.h. */ +__attribute__((cold)) +int gva_translate(vmie_mem* m, uintptr_t cr3, uintptr_t va, uintptr_t* gpa) { + return gva_gpa(m, cr3, va, gpa, NULL); +} + +/* zero-copy borrowed read: leaf-bounded host pointer at `va` (see memmodel.h). */ __attribute__((hot)) const void* gva_ptr(vmie_mem* m, uintptr_t cr3, uintptr_t va, size_t* avail) { uintptr_t gpa; size_t leaf; @@ -91,47 +99,6 @@ int khalf_score(const vmie_mem* m, uint64_t pml4) { return n; } -__attribute__((cold)) -int cr3_recover(vmie* v, uint64_t va_self, uint64_t target_pa, uintptr_t* cr3_out) { - vmie_mem* m = &v->mem; - int best_score = -1; uint64_t best = 0; - for (size_t off = 0; off + 0x1000 <= m->fsize; off += 0x1000) { - const uintptr_t cand = offset_gpa(m, off); - uintptr_t gpa; - if (gva_gpa(m, cand, va_self, &gpa, NULL)) continue; - if ((gpa & ~0xFFFull) != (target_pa & ~0xFFFull)) continue; - const int score = khalf_score(m, cand); - if (score > best_score) { best_score = score; best = cand; } - } - if (best_score < 0) return -1; - *cr3_out = best; - return 0; -} - -/* ---- lifecycle (cold) ---------------------------------------------------- */ - -__attribute__((cold)) -vmie* vmie_open(const char* ram_path, uint64_t low) { - vmie* v = calloc(1, sizeof *v); - if (!v) { - return NULL; - } - if (gpa_open(&v->mem, ram_path, low)) { - free(v); - return NULL; - } - return v; -} - -__attribute__((cold)) -void vmie_close(vmie* v) { - if (!v) { - return; - } - gpa_close(&v->mem); - free(v); -} - /* ---- region enumeration -------------------------------------------------- */ struct rgn_acc { @@ -302,26 +269,3 @@ int gva_sweep(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi, free(rg); free(buf); return rc; } - -/* ---- physical-image signature bridge ------------------------------------- * - * Iterates the core segment map (each seg is one mem_view_t over its file span) - * and runs the pure matcher. Reaches into vmie_mem, so it lives engine-side. */ -struct physcb { uint64_t* out; int max, n; }; -static int phys_hit(void* u, uint64_t gpa) { - struct physcb* c = u; - if (c->out && c->n < c->max) c->out[c->n] = gpa; - c->n++; - return 0; -} - -int gva_sig_phys(vmie_mem* m, const sig_pattern_t* p, uint64_t* out, int max) { - if (!p || p->len == 0) return -1; - struct physcb c = { out, max, 0 }; - - for (int i = 0; i < m->nseg; i++) { - const gpa_seg* s = &m->seg[i]; - const mem_view_t v = { (const uint8_t*)m->pa + s->file_off, (size_t)s->len, s->gpa }; - sig_each(v, p, phys_hit, &c); - } - return c.n; -} diff --git a/src/engine/include/engine-arch.h b/src/engine/include/engine-arch.h new file mode 100644 index 0000000..a323e65 --- /dev/null +++ b/src/engine/include/engine-arch.h @@ -0,0 +1,35 @@ +#ifndef VMIE_ENGINE_ARCH_H +#define VMIE_ENGINE_ARCH_H +#include +#include +#include "core.h" +#include "memmodel.h" /* vmie_mem, vregion/VR_*, gva_read/write/ptr/regions/sweep */ + +/* x86-64 long-mode paging bits, shared by every PT-walking TU. */ +#define PFN_MASK (0xFFFFFFFFFFull << 12) +#define PG_P 0x1ull +#define PG_PS 0x80ull + +/* sign-extend a 48-bit canonical VA */ +#define VA_CANON(v) (((v) & (1ull << 47)) ? ((v) | 0xFFFF000000000000ull) : (v)) + +/* USER_MIN/USER_MAX/KERN_MIN (the canonical VA-window bounds) live in + * memmodel.h (handler-visible), pulled in above. */ + +/* gva_ptr is declared in memmodel.h; the engine marks its definition hot. */ + +/* gva_read/gva_write/gva_regions/gva_sweep + gva_sweep_cb and vregion/VR_* + * are the OS-agnostic contract: declared in memmodel.h, pulled in above. */ + +/* paging heuristic, shared by the arch walker and the win32 bring-up. Counts + * present kernel-half PML4 entries under `pml4` (an address-space liveness + * score). OS-agnostic: const vmie_mem*, no profile/struct vmie. */ +int khalf_score(const vmie_mem* m, uint64_t pml4) __attribute__((cold)); + +/* cold extern wrapper over the hot static page-table walk: translate `va` under + * `cr3` to a GPA (no leaf length). Returns 0 on success, -1 if not present. For + * cold callers outside gva.c (win32 bring-up); the hot inlinable primitive stays + * private to gva.c. */ +int gva_translate(vmie_mem* m, uintptr_t cr3, uintptr_t va, uintptr_t* gpa) __attribute__((cold)); + +#endif /* VMIE_ENGINE_ARCH_H */ diff --git a/src/engine/include/engine.h b/src/engine/include/engine.h deleted file mode 100644 index 77bcd04..0000000 --- a/src/engine/include/engine.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef VMIE_ENGINE_H -#define VMIE_ENGINE_H -#include -#include -#include "core.h" -#include "memmodel.h" /* vmie_mem, vregion/VR_*, gva_read/write/ptr/regions/sweep */ -#include "sigscan.h" /* sig_pattern_t (for gva_sig_phys) */ -#include "pe.h" /* PE image parsing + vmie_pe_section (engine-private) */ - -/* x86-64 long-mode paging bits, shared by every PT-walking TU. */ -#define PFN_MASK (0xFFFFFFFFFFull << 12) -#define PG_P 0x1ull -#define PG_PS 0x80ull - -/* sign-extend a 48-bit canonical VA */ -#define VA_CANON(v) (((v) & (1ull << 47)) ? ((v) | 0xFFFF000000000000ull) : (v)) - -/* USER_MIN/USER_MAX/KERN_MIN (the canonical VA-window bounds) live in - * memmodel.h (handler-visible), pulled in above. */ - -typedef struct { - uint8_t guid[16]; /* ntoskrnl CodeView GUID (in-memory byte order) */ - uint32_t age; /* CodeView age */ - /* _EPROCESS (read under kcr3) */ - uint16_t ep_dtb; /* Pcb.DirectoryTableBase (cr3) */ - uint16_t ep_pid; /* UniqueProcessId */ - uint16_t ep_ppid; /* InheritedFromUniqueProcessId (0=unknown) */ - uint16_t ep_links; /* ActiveProcessLinks */ - uint16_t ep_name; /* ImageFileName (char[15], ANSI) */ - uint16_t ep_peb; /* Peb (0=unknown) */ - uint16_t ep_createtime; /* CreateTime (FILETIME, 0=unknown) */ - uint16_t ep_imgpath; /* ImageFilePathHint (UNICODE_STRING, 0=unk)*/ - /* user-side PEB chain (read under process cr3) */ - uint16_t peb_ldr; /* PEB.Ldr */ - uint16_t ldr_loadlist; /* PEB_LDR_DATA.InLoadOrderModuleList */ - uint16_t lde_base, lde_size, lde_name; /* LDR_DATA_TABLE_ENTRY */ - uint16_t lde_fullname; /* LDR_DATA_TABLE_ENTRY.FullDllName */ -} profile; - -/* sysproc = System _EPROCESS VA: the ActiveProcessLinks ring anchor, captured at - * bootstrap so enumeration needs no export re-resolve. mem is the FIRST member - * so a vmie* aliases a vmie_mem*. prof carried by value. */ -typedef struct vmie { - vmie_mem mem; - uint64_t kcr3; - uint64_t kbase; - uint64_t sysproc; - profile prof; -} vmie; - -int profile_build(vmie* v, uintptr_t cr3, uint64_t sys_ep, const uint8_t guid[16], uint32_t age); - -/* gva_ptr is declared in memmodel.h; the engine marks its definition hot. */ - -/* bootstrap helpers (gva.c) */ -int khalf_score(const vmie_mem* m, uint64_t pml4) __attribute__((cold)); -int cr3_recover(vmie* v, uint64_t va_self, uint64_t target_pa, uintptr_t* cr3_out) __attribute__((cold)); - -/* gva_read/gva_write/gva_regions/gva_sweep + gva_sweep_cb and vregion/VR_* - * are the OS-agnostic contract: declared in memmodel.h, pulled in above. */ - -/* Scan the raw physical image for a signature, iterating the core segment map - * (each seg is one mem_view_t over its file span). Reaches into vmie_mem, so it - * is an engine bridge, not a handler. Returns the number of GPA hits (writes up - * to `max` to `out`; -1 on a bad pattern). */ -int gva_sig_phys(vmie_mem* m, const sig_pattern_t* p, uint64_t* out, int max); - -#endif /* VMIE_ENGINE_H */ diff --git a/src/engine/sigphys.c b/src/engine/sigphys.c new file mode 100644 index 0000000..e335531 --- /dev/null +++ b/src/engine/sigphys.c @@ -0,0 +1,62 @@ +/* sigphys.c - physical-image signature bridge (OS-agnostic engine bridge). + * + * Iterates the core segment map (each seg is one mem_view_t over its file span) + * and runs the pure matcher. Reaches into vmie_mem (the segment table), so it is + * an engine bridge, not a handler - but it names no Windows concept and depends + * on no paging/profile, so it lives engine-side, outside src/engine/win32/. The + * dump-only scan set is { core/gpa.c, handlers/sigscan.c, this }, no win32. */ +#include +#include +#include "core.h" +#include "sigscan.h" +#include "scan.h" + +struct physcb { uint64_t* out; int max, n; }; +static int phys_hit(void* u, uint64_t gpa) { + struct physcb* c = u; + if (c->out && c->n < c->max) c->out[c->n] = gpa; + c->n++; + return 0; +} + +int sig_scan_mem(vmie_mem* m, const sig_pattern_t* p, uint64_t* out, int max) { + if (!p || p->len == 0) return -1; + struct physcb c = { out, max, 0 }; + + for (int i = 0; i < m->nseg; i++) { + const gpa_seg* s = &m->seg[i]; + const mem_view_t v = { (const uint8_t*)m->pa + s->file_off, (size_t)s->len, s->gpa }; + sig_each(v, p, phys_hit, &c); + } + return c.n; +} + +/* Tagging callback for the multi-source scan: stamps the current source index + * onto each hit and writes attributed records up to capacity. */ +struct srccb { sig_hit_src* out; int max, n, source; }; +static int src_hit(void* u, uint64_t gpa) { + struct srccb* c = u; + if (c->out && c->n < c->max) { + c->out[c->n].source = c->source; + c->out[c->n].gpa = gpa; + } + c->n++; + return 0; +} + +int sig_scan_sources(vmie_mem* const* srcs, int nsrc, const sig_pattern_t* p, + sig_hit_src* out, int max) { + if (!p || p->len == 0) return -1; + struct srccb c = { out, max, 0, 0 }; + + for (int si = 0; si < nsrc; si++) { + vmie_mem* m = srcs[si]; + c.source = si; + for (int i = 0; i < m->nseg; i++) { + const gpa_seg* s = &m->seg[i]; + const mem_view_t v = { (const uint8_t*)m->pa + s->file_off, (size_t)s->len, s->gpa }; + sig_each(v, p, src_hit, &c); + } + } + return c.n; +} diff --git a/src/engine/include/contract.h b/src/engine/win32/contract.h similarity index 100% rename from src/engine/include/contract.h rename to src/engine/win32/contract.h diff --git a/src/engine/win32/engine-win32.h b/src/engine/win32/engine-win32.h new file mode 100644 index 0000000..392bc23 --- /dev/null +++ b/src/engine/win32/engine-win32.h @@ -0,0 +1,44 @@ +#ifndef VMIE_ENGINE_WIN32_H +#define VMIE_ENGINE_WIN32_H +#include +#include +#include "engine-arch.h" /* paging macros, khalf_score, gva_* contract */ +#include "contract.h" /* guest-agent beacon layout */ +#include "pe.h" /* PE image parsing + vmie_pe_section (engine-private) */ + +typedef struct { + uint8_t guid[16]; /* ntoskrnl CodeView GUID (in-memory byte order) */ + uint32_t age; /* CodeView age */ + /* _EPROCESS (read under kcr3) */ + uint16_t ep_dtb; /* Pcb.DirectoryTableBase (cr3) */ + uint16_t ep_pid; /* UniqueProcessId */ + uint16_t ep_ppid; /* InheritedFromUniqueProcessId (0=unknown) */ + uint16_t ep_links; /* ActiveProcessLinks */ + uint16_t ep_name; /* ImageFileName (char[15], ANSI) */ + uint16_t ep_peb; /* Peb (0=unknown) */ + uint16_t ep_createtime; /* CreateTime (FILETIME, 0=unknown) */ + uint16_t ep_imgpath; /* ImageFilePathHint (UNICODE_STRING, 0=unk)*/ + /* user-side PEB chain (read under process cr3) */ + uint16_t peb_ldr; /* PEB.Ldr */ + uint16_t ldr_loadlist; /* PEB_LDR_DATA.InLoadOrderModuleList */ + uint16_t lde_base, lde_size, lde_name; /* LDR_DATA_TABLE_ENTRY */ + uint16_t lde_fullname; /* LDR_DATA_TABLE_ENTRY.FullDllName */ +} profile; + +/* sysproc = System _EPROCESS VA: the ActiveProcessLinks ring anchor, captured at + * bootstrap so enumeration needs no export re-resolve. mem is the FIRST member + * so a vmie_win32* aliases a vmie_mem*. prof carried by value. */ +typedef struct vmie_win32 { + vmie_mem mem; + uint64_t kcr3; + uint64_t kbase; + uint64_t sysproc; + profile prof; +} vmie_win32; + +int profile_build(vmie_win32* v, uintptr_t cr3, uint64_t sys_ep, const uint8_t guid[16], uint32_t age); + +/* bootstrap helper (win32 host bring-up) */ +int cr3_recover(vmie_win32* v, uint64_t va_self, uint64_t target_pa, uintptr_t* cr3_out) __attribute__((cold)); + +#endif /* VMIE_ENGINE_WIN32_H */ diff --git a/src/engine/guest.c b/src/engine/win32/guest.c similarity index 100% rename from src/engine/guest.c rename to src/engine/win32/guest.c diff --git a/src/engine/host.c b/src/engine/win32/host.c similarity index 78% rename from src/engine/host.c rename to src/engine/win32/host.c index 43ebc3f..34db062 100644 --- a/src/engine/host.c +++ b/src/engine/win32/host.c @@ -1,10 +1,56 @@ #include #include -#include "vmie.h" -#include "contract.h" -#include "engine.h" +#include +#include "win32.h" +#include "engine-win32.h" #define MZ 0x5A4Du + +/* ---- lifecycle (cold) ---------------------------------------------------- */ + +__attribute__((cold)) +vmie_win32* vmie_win32_open(const char* ram_path, uint64_t low) { + vmie_win32* v = calloc(1, sizeof *v); + if (!v) { + return NULL; + } + if (gpa_open(&v->mem, ram_path, low)) { + free(v); + return NULL; + } + return v; +} + +__attribute__((cold)) +void vmie_win32_close(vmie_win32* v) { + if (!v) { + return; + } + gpa_close(&v->mem); + free(v); +} + +/* ---- bootstrap CR3 recovery (cold) --------------------------------------- * + * Scan the image for a candidate PML4 that translates the agent's self-VA to its + * known physical anchor, scored by kernel-half liveness. The page-table walk is + * borrowed from the arch layer via gva_translate (cold), so the hot path stays + * private to gva.c. */ +__attribute__((cold)) +int cr3_recover(vmie_win32* v, uint64_t va_self, uint64_t target_pa, uintptr_t* cr3_out) { + vmie_mem* m = &v->mem; + int best_score = -1; uint64_t best = 0; + for (size_t off = 0; off + 0x1000 <= m->fsize; off += 0x1000) { + const uintptr_t cand = offset_gpa(m, off); + uintptr_t gpa; + if (gva_translate(m, cand, va_self, &gpa)) continue; + if ((gpa & ~0xFFFull) != (target_pa & ~0xFFFull)) continue; + const int score = khalf_score(m, cand); + if (score > best_score) { best_score = score; best = cand; } + } + if (best_score < 0) return -1; + *cr3_out = best; + return 0; +} #define DIR_EXPORT 0u #define DIR_DEBUG 6u #define DBG_CODEVIEW 2u @@ -162,12 +208,12 @@ static void beacon_ack(vmie_mem* m, uint64_t anchor_pa) { gpa_write(m, anchor_pa + offsetof(contract, ack), &ack, 8); } -vmie_mem* vmie_memory(vmie* v) { +vmie_mem* vmie_win32_mem(vmie_win32* v) { return v ? &v->mem : NULL; } __attribute__((cold)) -int host_bootstrap(vmie* v) { +int host_bootstrap(vmie_win32* v) { vmie_mem* m = &v->mem; uint64_t anchor_pa, va_self; uintptr_t cr3boot; diff --git a/src/engine/pe.c b/src/engine/win32/pe.c similarity index 100% rename from src/engine/pe.c rename to src/engine/win32/pe.c diff --git a/src/engine/proc.c b/src/engine/win32/proc.c similarity index 91% rename from src/engine/proc.c rename to src/engine/win32/proc.c index f775871..22d93b7 100644 --- a/src/engine/proc.c +++ b/src/engine/win32/proc.c @@ -2,15 +2,15 @@ #include #include #include -#include "engine.h" -#include "vmie.h" +#include "engine-win32.h" +#include "win32.h" #define pr_(v) ((v)->prof) #define RING_GUARD 100000u #define MOD_GUARD 4096u -static void grab_ustr(vmie* v, uintptr_t cr3, uint64_t va, gtext* out) { +static void grab_ustr(vmie_win32* v, uintptr_t cr3, uint64_t va, gtext* out) { vmie_mem* m = &v->mem; uint16_t len = 0; uint64_t buf = 0; @@ -23,7 +23,7 @@ static void grab_ustr(vmie* v, uintptr_t cr3, uint64_t va, gtext* out) { out->len = len; } -int proc_list(vmie* v, int skip_system, process* dst, size_t nmax) { +int proc_list(vmie_win32* v, int skip_system, process* dst, size_t nmax) { vmie_mem* m = &v->mem; const profile* p = &pr_(v); const uint64_t kcr3 = v->kcr3; @@ -74,7 +74,7 @@ int proc_list(vmie* v, int skip_system, process* dst, size_t nmax) { return (int)n; } -int proc_modules(vmie* v, const process* pr, pmodule* dst, size_t nmax) { +int proc_modules(vmie_win32* v, const process* pr, pmodule* dst, size_t nmax) { vmie_mem* m = &v->mem; const profile* p = &pr_(v); const uint64_t cr3 = pr->cr3; @@ -119,7 +119,7 @@ int proc_modules(vmie* v, const process* pr, pmodule* dst, size_t nmax) { * Project a Windows process/module list onto the generic cr3/range surface and * delegate to the OS-agnostic scanners (scan.h). */ -scan* scan_new(vmie* v, const process* pr, scan_type t, const void* value, +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) { if (!pr) { return NULL; @@ -129,7 +129,7 @@ scan* scan_new(vmie* v, const process* pr, scan_type t, const void* value, #define PTR_MOD_CAP 1024u -int vmie_scan_pointer(vmie* v, const process* pr, uint64_t target, +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) { if (!pr) { return -1; diff --git a/src/engine/profile.c b/src/engine/win32/profile.c similarity index 90% rename from src/engine/profile.c rename to src/engine/win32/profile.c index 57b1647..c5232cc 100644 --- a/src/engine/profile.c +++ b/src/engine/win32/profile.c @@ -1,10 +1,10 @@ #include #include -#include "engine.h" +#include "engine-win32.h" #define pr_(v) ((v)->prof) -#define RING_CAP 4096 /* USER_MIN/USER_MAX/KERN_MIN come from engine.h */ +#define RING_CAP 4096 /* USER_MIN/USER_MAX/KERN_MIN come from engine-arch.h */ #define SCAN_MAX 1024 #define FT_LO 0x01D0000000000000ll #define FT_HI 0x01F0000000000000ll @@ -14,7 +14,7 @@ static int canon_ok(uint64_t p, int kernel) { } /* Circular LIST_ENTRY walker (Flink at node+0); one primitive for both rings. */ -static int list_ring_ok(vmie* v, uintptr_t cr3, uint64_t head, int kernel) { +static int list_ring_ok(vmie_win32* v, uintptr_t cr3, uint64_t head, int kernel) { vmie_mem* m = &v->mem; uint64_t node; if (gva_read(m, cr3, head, &node, 8)) { @@ -32,7 +32,7 @@ static int list_ring_ok(vmie* v, uintptr_t cr3, uint64_t head, int kernel) { } /* Pass 1: ep_name/ep_pid/ep_links/ep_dtb from the System _EPROCESS. */ -static int discover_core(vmie* v, uintptr_t cr3, uint64_t sys_ep) { +static int discover_core(vmie_win32* v, uintptr_t cr3, uint64_t sys_ep) { vmie_mem* m = &v->mem; profile* p = &pr_(v); uint8_t buf[0x800]; @@ -88,7 +88,7 @@ static int discover_core(vmie* v, uintptr_t cr3, uint64_t sys_ep) { } /* Transient snapshot of (eprocess, pid, cr3) over the active ring. */ -static int collect_procs(vmie* v, uintptr_t cr3, uint64_t sys_ep, uint64_t* eps, uint32_t* pids, uint64_t* cr3s, int cap) { +static int collect_procs(vmie_win32* v, uintptr_t cr3, uint64_t sys_ep, uint64_t* eps, uint32_t* pids, uint64_t* cr3s, int cap) { vmie_mem* m = &v->mem; const profile* p = &pr_(v); int n = 0; @@ -112,7 +112,7 @@ static int collect_procs(vmie* v, uintptr_t cr3, uint64_t sys_ep, uint64_t* eps, } /* Pass 2a: ep_ppid by population (creator PID). Best-effort. */ -static void discover_ppid(vmie* v, uintptr_t cr3, const uint64_t* eps, const uint32_t* pids, int n) { +static void discover_ppid(vmie_win32* v, uintptr_t cr3, const uint64_t* eps, const uint32_t* pids, int n) { vmie_mem* m = &v->mem; int best_off = -1, best_hits = 0; for (int o = 0x100; o <= 0x600; o += 8) { @@ -137,7 +137,7 @@ static void discover_ppid(vmie* v, uintptr_t cr3, const uint64_t* eps, const uin } /* Pass 2b: ep_createtime (CreateTime, FILETIME) -- every sample in boot range, System earliest. Best-effort. */ -static void discover_createtime(vmie* v, uintptr_t cr3, const uint64_t* eps, int n) { +static void discover_createtime(vmie_win32* v, uintptr_t cr3, const uint64_t* eps, int n) { vmie_mem* m = &v->mem; for (int o = 0x140; o <= 0x600; o += 8) { int64_t sysv = 0; @@ -160,7 +160,7 @@ static void discover_createtime(vmie* v, uintptr_t cr3, const uint64_t* eps, int /* Pass 2c: ep_imgpath (ImageFilePathHint) -- UNICODE_STRING whose tail equals the * process's untruncated ImageFileName; probe short-named (<15) procs only. Best-effort. */ -static void discover_imgpath(vmie* v, uintptr_t cr3, const uint64_t* eps, const uint64_t* cr3s, int n) { +static void discover_imgpath(vmie_win32* v, uintptr_t cr3, const uint64_t* eps, const uint64_t* cr3s, int n) { vmie_mem* m = &v->mem; profile* p = &pr_(v); for (int i = 0; i < n; i++) { @@ -202,7 +202,7 @@ static void discover_imgpath(vmie* v, uintptr_t cr3, const uint64_t* eps, const /* Pass 2d: ep_peb + user PEB/Ldr chain; commits the x64-invariant LDR offsets * (incl. FullDllName) after validating them on the live first entry. */ -static int discover_user_chain(vmie* v, uintptr_t cr3, const uint64_t* eps, const uint64_t* cr3s, int n) { +static int discover_user_chain(vmie_win32* v, uintptr_t cr3, const uint64_t* eps, const uint64_t* cr3s, int n) { vmie_mem* m = &v->mem; profile* p = &pr_(v); @@ -260,7 +260,7 @@ static int discover_user_chain(vmie* v, uintptr_t cr3, const uint64_t* eps, cons } __attribute__((cold)) -int profile_build(vmie* v, uintptr_t cr3, uint64_t sys_ep, const uint8_t guid[16], uint32_t age) { +int profile_build(vmie_win32* v, uintptr_t cr3, uint64_t sys_ep, const uint8_t guid[16], uint32_t age) { memset(&pr_(v), 0, sizeof(pr_(v))); memcpy(pr_(v).guid, guid, 16); pr_(v).age = age; diff --git a/src/engine/text.c b/src/engine/win32/text.c similarity index 92% rename from src/engine/text.c rename to src/engine/win32/text.c index 7a50cb2..3965d1a 100644 --- a/src/engine/text.c +++ b/src/engine/win32/text.c @@ -1,7 +1,7 @@ #include #include -#include "engine.h" -#include "vmie.h" +#include "engine-win32.h" +#include "win32.h" static void utf8_emit(uint32_t cp, char* dst, size_t size, size_t* need, size_t* wrote) { uint8_t b[4]; size_t k; @@ -16,7 +16,7 @@ static void utf8_emit(uint32_t cp, char* dst, size_t size, size_t* need, size_t* *need += k; } -size_t gva_read_text(vmie* v, uintptr_t cr3, uintptr_t va, size_t nmemb, char* dst, size_t size) { +size_t gva_read_text(vmie_win32* v, uintptr_t cr3, uintptr_t va, size_t nmemb, char* dst, size_t size) { vmie_mem* m = &v->mem; size_t need = 0, wrote = 0; uint16_t stage[256]; diff --git a/src/handlers/sigscan.c b/src/handlers/sigscan.c index db368a6..9d6f8e1 100644 --- a/src/handlers/sigscan.c +++ b/src/handlers/sigscan.c @@ -66,6 +66,19 @@ bool sig_parse_mask(const uint8_t* b, const char* m, sig_pattern_t* out) { return true; } +bool sig_from_bytes(const uint8_t* bytes, size_t len, sig_pattern_t* out) { + if (!bytes || !out || len == 0) return false; + + char* mask = malloc(len + 1); /* all-'x' (exact, no wildcard) */ + if (!mask) return false; + memset(mask, 'x', len); + mask[len] = 0; + + const bool ok = sig_parse_mask(bytes, mask, out); + free(mask); + return ok; +} + void sig_free(sig_pattern_t* p) { if (!p) return; free(p->bytes); free(p->mask);