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.
235 lines
13 KiB
C
235 lines
13 KiB
C
/* memmodel.h - the OS-agnostic, x86-64 memory-model contract (the middle layer).
|
|
*
|
|
* This is the shared vocabulary between the ENGINE (which turns guest-physical
|
|
* RAM into a usable virtual memory model via x86-64 paging + Windows bring-up)
|
|
* and the HANDLERS (scanners that consume that model). It names no Windows
|
|
* concept: a handler compiled against this header literally cannot mention an
|
|
* _EPROCESS, a PEB, or an LDR entry.
|
|
*
|
|
* OS-agnostic, but architecture-bound: the address-space key is the x86-64 CR3
|
|
* (the PML4 base), shared by any guest OS on x86-64 - it is not portable to an
|
|
* ISA with a different paging root (e.g. ARM64 TTBR0/1).
|
|
*
|
|
* Everything here is keyed by a `vmie_mem*` (the opaque physical/paging
|
|
* substrate) plus a `cr3` (the address space). The engine handle is never
|
|
* handed to a handler - only `vmie_mem*` + `cr3`.
|
|
*
|
|
* 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.
|
|
* - A "VA" is a 64-bit canonical guest virtual address. Reads/writes that
|
|
* cross a page boundary are handled internally (per-page translation).
|
|
* - Integer returns: 0 on success, negative on failure, unless stated.
|
|
*/
|
|
#ifndef VMIE_MEMMODEL_H
|
|
#define VMIE_MEMMODEL_H
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
|
|
/* Opaque guest-physical memory handle (the mmap'd RAM backing file + segment
|
|
* map). Defined in src/core/include/core.h; handlers hold only a pointer and
|
|
* 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);
|
|
|
|
/* Build a source from an already-open file descriptor `fd` instead of a path,
|
|
* with the single-`low` map (as vmie_mem_open). `fd` is BORROWED: it is dup()'d
|
|
* internally, so the source owns its own copy and the caller's `fd` stays open
|
|
* and valid both after this call AND after vmie_mem_close(). `fd` must reference
|
|
* an mmap-able, read-write object (a regular file, memfd_create, or POSIX shm) -
|
|
* a pipe/socket/tty is not mmap-able and yields NULL. The fd's file position is
|
|
* not read or changed (the mapping starts at offset 0). Returns a heap-owned
|
|
* handle, or NULL on dup/fstat/mmap failure. Release with vmie_mem_close(). */
|
|
vmie_mem* vmie_mem_from_fd(int fd, uint64_t low);
|
|
|
|
/* As vmie_mem_from_fd, but with an explicit segment map (`nseg` entries; see
|
|
* gpa_seg), well-formed against the file size. Same dup-borrow semantics: the
|
|
* caller's `fd` stays valid after this call and after vmie_mem_close(); `fd`
|
|
* must be an mmap-able RW object and its position is untouched. Returns a
|
|
* heap-owned handle, or NULL on failure. Release with vmie_mem_close(). */
|
|
vmie_mem* vmie_mem_from_fd_segs(int fd, const gpa_seg* segs, int nseg);
|
|
|
|
/* Read-only twin of vmie_mem_from_fd: build a source from `fd` with the
|
|
* single-`low` map, but map it PROT_READ and mark it read-only. On the returned
|
|
* handle gpa_write/gva_write (and any write through the engine) return -1; all
|
|
* reads - gpa_read/gva_read, gpa_ptr/gva_ptr, gva_regions, sig_scan_mem - behave
|
|
* exactly as on an RW source. Because only read access is needed, `fd` may be
|
|
* opened O_RDONLY (an O_RDWR fd is also accepted). Same dup-borrow semantics: the
|
|
* fd is dup()'d internally, so the caller's `fd` stays valid after this call AND
|
|
* after vmie_mem_close(); its file position is not read or changed. NOTE: writing
|
|
* THROUGH a pointer returned by gpa_ptr/gva_ptr on a read-only source is a
|
|
* contract violation (the pages are read-only and the store will fault) - use
|
|
* such pointers for reading only. Returns a heap-owned handle, or NULL on
|
|
* dup/fstat/mmap failure. Release with vmie_mem_close(). */
|
|
vmie_mem* vmie_mem_from_ro_fd(int fd, uint64_t low);
|
|
|
|
/* As vmie_mem_from_ro_fd, but with an explicit segment map (`nseg` entries; see
|
|
* gpa_seg), well-formed against the file size. Same read-only contract
|
|
* (writes -> -1; no writes through gpa_ptr/gva_ptr) and dup-borrow semantics
|
|
* (caller's `fd` stays valid after this call and after vmie_mem_close(); accepts
|
|
* an O_RDONLY or O_RDWR fd; position untouched). Returns a heap-owned handle, or
|
|
* NULL on failure. Release with vmie_mem_close(). */
|
|
vmie_mem* vmie_mem_from_ro_fd_segs(int fd, const gpa_seg* segs, int nseg);
|
|
|
|
/* Unmap, close, and free a handle from vmie_mem_open*, vmie_mem_from_fd*, or
|
|
* vmie_mem_from_ro_fd*. 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)
|
|
* size - number of valid bytes at `data`
|
|
* base_va - address that data[0] corresponds to (guest VA, or GPA for a
|
|
* physical view). All matches are reported as base_va + offset. */
|
|
typedef struct {
|
|
const uint8_t* data;
|
|
size_t size;
|
|
uint64_t base_va;
|
|
} mem_view_t;
|
|
|
|
/* ---- region map ---------------------------------------------------------- *
|
|
* A vregion is one run of VA-contiguous, present guest pages sharing the same
|
|
* effective protection. It is the unit of "what is mapped, and how" and the
|
|
* scoping primitive for the scanners (see scan.h).
|
|
*
|
|
* x86-64 has no read bit: a present page is readable, so VR_R is always set on a
|
|
* returned region. Write/execute/user are the EFFECTIVE rights along the whole
|
|
* page-table path (RW & US are AND-ed across levels, NX is OR-ed), not just the
|
|
* leaf entry, so they reflect what the guest CPU actually enforces. */
|
|
#ifndef VMIE_VREGION_DEFINED
|
|
#define VMIE_VREGION_DEFINED
|
|
#define VR_R 0x1u /* readable (present => always set) */
|
|
#define VR_W 0x2u /* writable (RW bit set at every level) */
|
|
#define VR_X 0x4u /* executable(NX clear at every level) */
|
|
#define VR_U 0x8u /* user-accessible (US bit set at every level) */
|
|
|
|
typedef struct {
|
|
uint64_t va; /* run start VA (clamped into the requested [lo,hi] window) */
|
|
uint64_t len; /* run length in bytes */
|
|
uint32_t prot; /* OR of VR_* flags */
|
|
} vregion;
|
|
#endif
|
|
|
|
/* Canonical VA-window bounds of the memory model, shared by every scanning TU.
|
|
* These describe the address space the contract operates over (the [lo,hi]
|
|
* windows of gva_regions/gva_sweep), so they are handler-visible.
|
|
* USER_MIN is 0x10000: the low 64 KiB is reserved, so no live user pointer
|
|
* targets below it - starting there drops a class of false positives. */
|
|
#define USER_MIN 0x0000000000010000ull
|
|
#define USER_MAX 0x00007FFFFFFFFFFFull
|
|
#define KERN_MIN 0xFFFF800000000000ull
|
|
|
|
/* ---- generic boundary types (replace the Windows-typed process/pmodule) --- *
|
|
* A schedulable address space, decoded by the engine from whatever the guest
|
|
* OS calls one. `cr3` is all a handler needs to read/write its memory.
|
|
* cr3 - DirectoryTableBase (PFN-masked); key to this address space
|
|
* pid, ppid - process / parent ids (ppid == (uint64_t)-1 if unavailable)
|
|
* name - short image name, NUL-terminated UTF-8 (engine-decoded) */
|
|
typedef struct {
|
|
uint64_t cr3;
|
|
uint64_t pid;
|
|
uint64_t ppid;
|
|
char name[16];
|
|
} task;
|
|
|
|
/* A named, contiguous VA range (e.g. a loaded module image), the anchor a
|
|
* pointer scan walks back to. The engine decodes the name; no LDR entry VA.
|
|
* base - range base VA (page-aligned)
|
|
* size - range length in bytes
|
|
* name - decoded UTF-8 name (e.g. "ntdll.dll"), NUL-terminated */
|
|
typedef struct {
|
|
uint64_t base;
|
|
uint64_t size;
|
|
char name[64];
|
|
} range;
|
|
|
|
/* ---- guest memory access (hot path) -------------------------------------- */
|
|
|
|
/* Read `nmemb` bytes from guest VA `va` (translated under `cr3`) into `dst`.
|
|
* Crosses page boundaries internally. Returns 0 on success, -1 if any page in
|
|
* the range is not present/translatable (in which case `dst` is partially
|
|
* written and must be treated as invalid). */
|
|
int gva_read(vmie_mem* m, uintptr_t cr3, uintptr_t va, void* dst, size_t nmemb);
|
|
|
|
/* Write `nmemb` bytes from `src` to guest VA `va` (translated under `cr3`).
|
|
* The mapping is RW and coherent, so the guest observes the change. Returns 0
|
|
* on success, -1 if any page in the range is not present/translatable. */
|
|
int gva_write(vmie_mem* m, uintptr_t cr3, uintptr_t va, const void* src, size_t nmemb);
|
|
|
|
/* Zero-copy borrowed read: host pointer to the guest byte at `va` (under `cr3`),
|
|
* valid for *avail contiguous bytes (to the end of the containing leaf). NULL if
|
|
* `va` is not mapped or the leaf is not fully covered by the image (caller falls
|
|
* back to gva_read). Borrowed: valid until the mapping is closed, do NOT retain. */
|
|
const void* gva_ptr(vmie_mem* m, uintptr_t cr3, uintptr_t va, size_t* avail);
|
|
|
|
/* Enumerate mapped memory under `cr3`, clamped to the VA window [lo,hi]
|
|
* (inclusive), as runs of equal effective protection.
|
|
* lo, hi - inclusive VA window; MUST lie within a single canonical half
|
|
* (entirely user or entirely kernel). Use (0, ~0ull) loosely; the
|
|
* walk prunes whole subtrees outside the window.
|
|
* prot_any - protection filter: 0 keeps every run; otherwise a run is kept
|
|
* only if (run.prot & prot_any) != 0 (e.g. VR_W for writable-only)
|
|
* out - caller array receiving up to `nmax` `vregion` records
|
|
* nmax - capacity of `out`
|
|
* Returns the TOTAL number of matching runs found. If the return value exceeds
|
|
* `nmax` the output was truncated; enlarge the buffer and retry. */
|
|
int gva_regions(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
|
uint32_t prot_any, vregion* out, int nmax);
|
|
|
|
/* ---- shared windowed sweep engine ---------------------------------------- *
|
|
* gva_sweep() streams every mapped byte under `cr3` within [lo,hi] that passes
|
|
* the protection filter to `cb`, one contiguous window at a time. Physical
|
|
* fragmentation is hidden: each window is a flat buffer (gva_read-filled), and
|
|
* adjacent windows of one run share `overlap` leading bytes so an object or
|
|
* pattern straddling a window boundary is still seen whole. */
|
|
typedef int (*gva_sweep_cb)(void* user, const uint8_t* data, size_t len,
|
|
uint64_t base_va, size_t overlap, int last);
|
|
/* user - passed through verbatim
|
|
* data - host buffer with `len` valid bytes (do not retain past the call)
|
|
* len - valid bytes at data
|
|
* base_va - guest VA of data[0]
|
|
* overlap - bytes at the front of `data` shared with the previous window of
|
|
* this run (0 on a run's first window or right after a gap)
|
|
* last - nonzero if this window ends a contiguous segment (run end / gap):
|
|
* accept hits up to `len`; otherwise drop hits starting in the
|
|
* trailing `overlap` zone, the next window re-presents them
|
|
* cb returns nonzero to abort the sweep early (e.g. result buffer full).
|
|
*
|
|
* gva_sweep() returns 0 normally, 1 if a callback aborted it, -1 on allocation
|
|
* failure. `overlap` must be < the internal window (1 MiB); patterns longer
|
|
* than that are not supported by the windowed path. */
|
|
int gva_sweep(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
|
uint32_t prot_any, size_t overlap, gva_sweep_cb cb, void* user);
|
|
|
|
#endif /* VMIE_MEMMODEL_H */
|