6 Commits

Author SHA1 Message Date
lirent d26f6c0bf0 Install file(1) in the Debian CI job so shlibdeps can run
cpack's CPACK_DEBIAN_PACKAGE_SHLIBDEPS runs dpkg-shlibdeps, which needs
the file utility to detect ELF binaries and derive the runtime libc
dependency. The slim CI image does not ship it, so packaging failed at
the cpack step. Add file to the job toolchain.
2026-06-22 16:32:47 +03:00
lirent 0c3aa5ef25 Build a shared library and ship it as Debian packages
Factor the source list into an OBJECT library so the same objects back
both the existing static lib and a new shared libvmie.so (SONAME from the
project version; the version comes from the tag, falling back to a cached
default). Add install rules and a CPack DEB config that produce two
packages: libvmie0 (the runtime .so + SONAME symlink) and libvmie-dev
(public headers under include/vmie + the linker symlink), with the dev
package depending on the exact runtime version.

Add a CI job that, on a v* tag, builds the shared library, runs cpack,
and uploads both .deb files to the Gitea Debian package registry.
2026-06-22 15:52:39 +03:00
lirent 3028a4eb11 Take the kernel cr3 in the read-only win32 constructor
vmie_win32_open_ro_fd now takes a kcr3 and builds the struct-offset
profile read-only from the image (find ntoskrnl, resolve
PsInitialSystemProcess, build the profile, derive the System _EPROCESS),
so a read-only context has the full read surface - including
proc_list/proc_modules, not just the cr3-parameterised PE walks.

The read-only bring-up tail is factored out of host_bootstrap into a
shared helper; host_bootstrap keeps the beacon find / cr3 recovery / ACK
around it, so its behaviour and return codes are unchanged.
2026-06-20 17:02:29 +03:00
lirent 02e63f2ff0 Add a read-only win32 context over a file descriptor
vmie_win32_open_ro_fd maps the RAM backing file PROT_READ (via
gpa_from_ro_fd) and does NOT bootstrap; the caller passes a cr3 to each
read call. The read surfaces work this way - sections / functions /
imports / exports / callgraph / hooks, gva_read_text, and the generic
gva_* and scanner surfaces via vmie_win32_mem() - while any write returns
-1 because the mapping is read-only. The RW twin vmie_win32_open_fd and
host_bootstrap are unchanged.
2026-06-20 16:39:08 +03:00
lirent 5ea9a3785f Construct the memory source from a file descriptor
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.
2026-06-20 11:20:33 +03:00
lirent f401738702 Fix CI: install libc6-dev for the native compiler check
CMake's project(... C) check compiles AND links a test binary, which needs glibc's startup objects (Scrt1.o/crti.o). gcc only Recommends libc6-dev, so --no-install-recommends dropped it and configure failed before the cross-build. Install it explicitly.
2026-06-17 18:54:49 +03:00
7 changed files with 418 additions and 49 deletions
+56 -1
View File
@@ -22,7 +22,7 @@ jobs:
apt-get update
apt-get install -y --no-install-recommends \
cmake make zip jq curl ca-certificates \
gcc gcc-mingw-w64-x86-64
gcc libc6-dev gcc-mingw-w64-x86-64
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@@ -70,3 +70,58 @@ jobs:
curl -fsSL -X POST -H "$auth" \
-F "attachment=@dist/${asset};type=application/zip" \
"${api}/releases/${rid}/assets?name=${asset}"
deb:
runs-on: ubuntu-latest
container:
image: node:20-bookworm-slim
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install toolchain
run: |
apt-get update
apt-get install -y --no-install-recommends \
cmake make gcc libc6-dev dpkg-dev file \
gcc-mingw-w64-x86-64 ca-certificates curl
- name: Configure
env:
TAG: ${{ github.ref_name }}
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DVMIE_VERSION=${TAG#v}
- name: Build shared library
run: cmake --build build -j --target vmie_shared
- name: Package
run: |
set -euo pipefail
(cd build && cpack -G DEB)
ls -l build/*.deb
- name: Publish to Debian registry
env:
TOKEN: ${{ secrets.PUBLISH_TOKEN }} # requires scope: package:write
SERVER: ${{ github.server_url }}
OWNER: ${{ github.repository_owner }}
DISTRIBUTION: stable
COMPONENT: main
run: |
set -euo pipefail
url="${SERVER}/api/packages/${OWNER}/debian/pool/${DISTRIBUTION}/${COMPONENT}/upload"
auth="Authorization: token ${TOKEN}"
for deb in build/*.deb; do
# 201 Created = uploaded; 409 Conflict = this version already present (re-run).
# Both are success; anything else fails. Do not use curl -f (it would fail 409).
code=$(curl -s -o /dev/null -w '%{http_code}' \
-X PUT -H "$auth" -T "$deb" "$url")
echo "$deb -> HTTP $code"
if [ "$code" != 201 ] && [ "$code" != 409 ]; then
echo "upload failed: $deb (HTTP $code)" >&2
exit 1
fi
done
+69 -7
View File
@@ -1,15 +1,19 @@
cmake_minimum_required(VERSION 3.18) # find_program(... REQUIRED)
project(vmi-engine C)
set(VMIE_VERSION "0.1.5" CACHE STRING "Library version (MAJOR.MINOR.PATCH); CI passes the tag")
project(vmi-engine VERSION ${VMIE_VERSION} LANGUAGES C)
set(CMAKE_C_STANDARD 17) # generation B uses no C23 feature
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS ON) # deliberate: strnlen (POSIX) + void* arithmetic (GNU)
set(CMAKE_POSITION_INDEPENDENT_CODE ON) # -fPIC on all objects: static lib stays linkable into a shared object
include(GNUInstallDirs)
option(VMIE_LTO "Enable LTO" OFF) # build-only; shipped default is -O2, no LTO
# ---- host: VMI core as a static library ---------------------------------
add_library(vmie STATIC
# ---- host: VMI core objects (single owner of the source list) -----------
add_library(vmie_obj OBJECT
src/core/gpa.c
src/engine/gva.c
src/engine/sigphys.c
@@ -27,15 +31,32 @@ add_library(vmie STATIC
src/handlers/x86dec.c
src/handlers/pmap.c
src/handlers/snapdiff.c)
target_include_directories(vmie
target_include_directories(vmie_obj
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-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)
target_compile_options(vmie_obj PRIVATE -O2 -Wall -Wextra)
if(VMIE_LTO)
target_compile_options(vmie PRIVATE -flto)
target_link_options(vmie PRIVATE -flto)
target_compile_options(vmie_obj PRIVATE -flto)
target_link_options(vmie_obj PRIVATE -flto)
endif()
# ---- host: static library (in-tree consumers: CLIs + test) --------------
# $<TARGET_OBJECTS:> carries object files only, not usage requirements:
# re-declare the PUBLIC include scope so vmie_cli/vmie_scan still see include/.
add_library(vmie STATIC $<TARGET_OBJECTS:vmie_obj>)
target_include_directories(vmie PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
# ---- host: shared library (external delivery: libvmie.so.*) -------------
add_library(vmie_shared SHARED $<TARGET_OBJECTS:vmie_obj>)
target_include_directories(vmie_shared PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
set_target_properties(vmie_shared PROPERTIES
OUTPUT_NAME vmie
SOVERSION ${PROJECT_VERSION_MAJOR}
VERSION ${PROJECT_VERSION})
if(VMIE_LTO)
target_link_options(vmie_shared PRIVATE -flto)
endif()
# ---- host: CLI demonstrator over the library ----------------------------
@@ -61,3 +82,44 @@ add_custom_command(
COMMENT "Cross-compiling vmie-startup.exe (mingw-w64, x86-64)"
VERBATIM)
add_custom_target(vmie-startup ALL DEPENDS ${VMIE_STARTUP})
# ---- install: shared library + public headers ---------------------------
# Versioned file + SONAME symlink -> runtime; unversioned linker symlink -> dev.
install(TARGETS vmie_shared
LIBRARY
DESTINATION ${CMAKE_INSTALL_LIBDIR}
COMPONENT runtime
NAMELINK_COMPONENT dev)
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/vmie
COMPONENT dev
FILES_MATCHING PATTERN "*.h")
# ---- package: two Debian packages (runtime + dev) -----------------------
set(CPACK_GENERATOR "DEB")
set(CPACK_DEB_COMPONENT_INSTALL ON)
set(CPACK_COMPONENTS_ALL runtime dev)
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
# component -> Debian package name (Debian shared-library convention)
set(CPACK_DEBIAN_RUNTIME_PACKAGE_NAME "libvmie${PROJECT_VERSION_MAJOR}")
set(CPACK_DEBIAN_DEV_PACKAGE_NAME "libvmie-dev")
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64")
set(VMIE_DEB_MAINTAINER "Gregory Lirent <opensource@lirent.ru>"
CACHE STRING "Debian package Maintainer field")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${VMIE_DEB_MAINTAINER}")
set(CPACK_DEBIAN_PACKAGE_SECTION "libs")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_FILE_NAME "DEB-DEFAULT")
set(CPACK_COMPONENT_RUNTIME_DESCRIPTION
"VM introspection engine (Windows-guest memory introspection) - shared library")
set(CPACK_COMPONENT_DEV_DESCRIPTION
"VM introspection engine (Windows-guest memory introspection) - development files")
# dev depends on the exact runtime version; runtime libc dependency via shlibdeps
set(CPACK_DEBIAN_DEV_PACKAGE_DEPENDS "libvmie${PROJECT_VERSION_MAJOR} (= ${PROJECT_VERSION})")
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
include(CPack)
+41 -1
View File
@@ -62,7 +62,47 @@ vmie_mem* vmie_mem_open(const char* path, uint64_t low);
* 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. */
/* 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) ------------------------------------- *
+23
View File
@@ -83,6 +83,29 @@ typedef struct {
* 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);
/* Open a READ-ONLY context over the RAM backing file `fd`, for a consumer that
* only reads. It takes a caller-supplied `kcr3` (kernel cr3 / System DirectoryTableBase)
* and builds the struct-offset profile read-only from the image (no bootstrap
* handshake - beacon and ACK are not used). The result has the FULL read
* capability: proc_list/proc_modules AND the section / function / import / export
* / callgraph / hook surfaces, gva_read_text, and the generic gva_* and scanner
* surfaces via vmie_win32_mem(). Any write (gva_write) returns -1 because the
* mapping is PROT_READ. `fd` is BORROWED (dup()'d internally; the caller's fd
* stays valid after this call AND after vmie_win32_close()); an O_RDONLY fd is
* accepted. `low` is the below-4G split, as in vmie_win32_open_fd. Returns a new
* context, or NULL if `kcr3` does not resolve a kernel (ntoskrnl not found under
* it) or on dup/fstat/mmap failure. Free with vmie_win32_close(). */
vmie_win32* vmie_win32_open_ro_fd(int fd, uint64_t low, uint64_t kcr3);
/* 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);
+154 -18
View File
@@ -52,6 +52,9 @@ int gpa_read(vmie_mem* m, uintptr_t offs, void* buf, const size_t nmemb) {
}
int gpa_write(vmie_mem* m, uintptr_t offs, const void* src, const size_t nmemb) {
if (m->ro) {
return -1;
}
if (out_of_bounds(m, &offs, nmemb)) {
return -1;
}
@@ -88,37 +91,64 @@ static int segs_valid(const gpa_seg* segs, int nseg, uint64_t fsize) {
return 1;
}
/* Finish a vmie_mem over an ALREADY-OWNED fd (from open() or dup()): fstat it,
* validate the seg map against its real size, mmap it shared (PROT_READ when
* `ro`, else PROT_RW), and fill `m`. On ANY failure close `fd` and clean `m`
* (no partial state); on success store the fd in `m->fd`, record `ro`, and
* return 0. The fd's lifetime is now owned by `m` (closed in gpa_close). The
* single mmap/validate site shared by every constructor. */
__attribute__((cold))
int gpa_open_segs(vmie_mem* m, const char* path, const gpa_seg* segs, int nseg) {
static int gpa_map_owned_fd(vmie_mem* m, int fd, const gpa_seg* segs, int nseg, int ro) {
struct stat st;
if ((m->fd = open(path, O_RDWR)) < 0) {
goto ret_;
}
if (fstat(m->fd, &st) || !segs_valid(segs, nseg, (uint64_t)st.st_size)) {
if (fstat(fd, &st) || !segs_valid(segs, nseg, (uint64_t)st.st_size)) {
goto close_;
}
if ((m->pa = mmap(NULL, st.st_size, PROT_RW, MAP_SHARED, m->fd, 0)) == MAP_FAILED) {
const int prot = ro ? PROT_READ : PROT_RW;
if ((m->pa = mmap(NULL, st.st_size, prot, MAP_SHARED, fd, 0)) == MAP_FAILED) {
close_:
close(m->fd);
ret_:
close(fd);
clean_ctx(m);
return -1;
}
m->fd = fd;
m->fsize = st.st_size;
m->nseg = nseg;
m->ro = ro;
memcpy(m->seg, segs, (size_t)nseg * sizeof *segs);
return 0;
}
/* Convenience: the classic single-`low` QEMU map. Below the 4 GiB PCI hole the
* file maps 1:1 ([0,low)->file[0,low)); at and above 4 GiB it resumes at file
* offset low. When low >= fsize the hole is never reached, so one inert identity
* seg covering the whole image suffices. */
/* Build the classic single-`low` QEMU map into out[0..nseg) and return nseg.
* Below the 4 GiB PCI hole the file maps 1:1 ([0,low)->file[0,low)); at and
* above 4 GiB it resumes at file offset low. When low >= fsize the hole is never
* reached, so one inert identity seg covering the whole image suffices (nseg=1).
* The single low-map site shared by gpa_open (fsize from stat) and gpa_from_fd
* (fsize from fstat). */
__attribute__((cold))
static int low_segs(uint64_t fsize, uint64_t low, gpa_seg out[2]) {
if (low >= fsize) {
out[0] = (gpa_seg){ 0, fsize, 0 };
return 1;
}
out[0] = (gpa_seg){ 0, low, 0 };
out[1] = (gpa_seg){ RAM_H, fsize - low, low };
return 2;
}
__attribute__((cold))
int gpa_open_segs(vmie_mem* m, const char* path, const gpa_seg* segs, int nseg) {
const int fd = open(path, O_RDWR);
if (fd < 0) {
clean_ctx(m);
return -1;
}
return gpa_map_owned_fd(m, fd, segs, nseg, 0);
}
__attribute__((cold))
int gpa_open(vmie_mem* m, const char* path, uintptr_t low) {
struct stat st;
@@ -126,16 +156,70 @@ int gpa_open(vmie_mem* m, const char* path, uintptr_t low) {
clean_ctx(m);
return -1;
}
const uint64_t fsize = (uint64_t)st.st_size;
const gpa_seg one[1] = { { 0, fsize, 0 } };
const gpa_seg two[2] = { { 0, low, 0 }, { RAM_H, fsize > low ? fsize - low : 0, low } };
const gpa_seg* segs = low >= fsize ? one : two;
const int nseg = low >= fsize ? 1 : 2;
gpa_seg segs[2];
const int nseg = low_segs((uint64_t)st.st_size, low, segs);
return gpa_open_segs(m, path, segs, nseg);
}
/* fd-backed constructors: build a vmie_mem from an already-open file descriptor
* instead of a path. The fd is BORROWED via dup() - core owns the copy (closed
* in gpa_close); the caller's fd stays valid. The fd must reference an mmap-able
* RW object (regular file / memfd / shm); its file position is not used. */
__attribute__((cold))
int gpa_from_fd_segs(vmie_mem* m, int fd, const gpa_seg* segs, int nseg) {
const int dupfd = dup(fd);
if (dupfd < 0) {
clean_ctx(m);
return -1;
}
return gpa_map_owned_fd(m, dupfd, segs, nseg, 0);
}
__attribute__((cold))
int gpa_from_fd(vmie_mem* m, int fd, uintptr_t low) {
struct stat st;
if (fstat(fd, &st)) {
clean_ctx(m);
return -1;
}
gpa_seg segs[2];
const int nseg = low_segs((uint64_t)st.st_size, low, segs);
return gpa_from_fd_segs(m, fd, segs, nseg);
}
/* read-only twins of gpa_from_fd*: map the borrowed fd PROT_READ and mark the
* source read-only (m->ro), so gpa_write/gva_write return -1. Same dup-borrow
* semantics (core owns the copy, caller's fd stays valid). Accept O_RDONLY as
* well as O_RDWR fds - only read access is required for a PROT_READ mapping.
* Reads/gpa_ptr/scan behave exactly as on an RW source. */
__attribute__((cold))
int gpa_from_ro_fd_segs(vmie_mem* m, int fd, const gpa_seg* segs, int nseg) {
const int dupfd = dup(fd);
if (dupfd < 0) {
clean_ctx(m);
return -1;
}
return gpa_map_owned_fd(m, dupfd, segs, nseg, 1);
}
__attribute__((cold))
int gpa_from_ro_fd(vmie_mem* m, int fd, uintptr_t low) {
struct stat st;
if (fstat(fd, &st)) {
clean_ctx(m);
return -1;
}
gpa_seg segs[2];
const int nseg = low_segs((uint64_t)st.st_size, low, segs);
return gpa_from_ro_fd_segs(m, fd, segs, nseg);
}
__attribute__((cold))
void gpa_close(vmie_mem* m) {
if (m->pa) {
@@ -180,6 +264,58 @@ vmie_mem* vmie_mem_open_segs(const char* path, const gpa_seg* segs, int nseg) {
return m;
}
__attribute__((cold))
vmie_mem* vmie_mem_from_fd(int fd, uint64_t low) {
vmie_mem* m = calloc(1, sizeof *m);
if (!m) {
return NULL;
}
if (gpa_from_fd(m, fd, (uintptr_t)low)) {
free(m);
return NULL;
}
return m;
}
__attribute__((cold))
vmie_mem* vmie_mem_from_fd_segs(int fd, const gpa_seg* segs, int nseg) {
vmie_mem* m = calloc(1, sizeof *m);
if (!m) {
return NULL;
}
if (gpa_from_fd_segs(m, fd, segs, nseg)) {
free(m);
return NULL;
}
return m;
}
__attribute__((cold))
vmie_mem* vmie_mem_from_ro_fd(int fd, uint64_t low) {
vmie_mem* m = calloc(1, sizeof *m);
if (!m) {
return NULL;
}
if (gpa_from_ro_fd(m, fd, (uintptr_t)low)) {
free(m);
return NULL;
}
return m;
}
__attribute__((cold))
vmie_mem* vmie_mem_from_ro_fd_segs(int fd, const gpa_seg* segs, int nseg) {
vmie_mem* m = calloc(1, sizeof *m);
if (!m) {
return NULL;
}
if (gpa_from_ro_fd_segs(m, fd, segs, nseg)) {
free(m);
return NULL;
}
return m;
}
__attribute__((cold))
void vmie_mem_close(vmie_mem* m) {
if (!m) {
+10
View File
@@ -20,6 +20,7 @@ typedef struct vmie_mem {
int fd;
int nseg;
gpa_seg seg[VMIE_MAX_SEGS];
int ro; /* source is read-only (gpa_write/gva_write -> -1); 0 = RW */
} vmie_mem;
/* GPA <-> file-offset converters over the segment map.
@@ -40,6 +41,15 @@ static inline uintptr_t offset_gpa(const vmie_mem* m, uintptr_t off) {
/* guest-physical lifecycle + primitives (gpa.c) */
int gpa_open_segs(vmie_mem* m, const char* path, const gpa_seg* segs, int nseg);
int gpa_open (vmie_mem* m, const char* path, uintptr_t low);
/* fd-backed twins of gpa_open*: build from a borrowed fd (dup'd internally,
* caller's fd stays valid) instead of a path. fd must be an mmap-able RW object. */
int gpa_from_fd_segs(vmie_mem* m, int fd, const gpa_seg* segs, int nseg);
int gpa_from_fd (vmie_mem* m, int fd, uintptr_t low);
/* read-only twins of gpa_from_fd*: map the borrowed fd PROT_READ and mark the
* source read-only (gpa_write/gva_write -> -1). Accept O_RDONLY as well as
* O_RDWR fds (only read access is required). Reads/gpa_ptr/scan behave as RW. */
int gpa_from_ro_fd_segs(vmie_mem* m, int fd, const gpa_seg* segs, int nseg);
int gpa_from_ro_fd (vmie_mem* m, int fd, uintptr_t low);
void gpa_close(vmie_mem* m);
int gpa_read (vmie_mem* m, uintptr_t offs, void* dst, size_t nmemb);
int gpa_write(vmie_mem* m, uintptr_t offs, const void* src, size_t nmemb);
+65 -22
View File
@@ -6,6 +6,9 @@
#define MZ 0x5A4Du
__attribute__((cold))
static int host_profile_from_cr3(vmie_win32* v, uintptr_t cr3);
/* ---- lifecycle (cold) ---------------------------------------------------- */
__attribute__((cold))
@@ -21,6 +24,40 @@ vmie_win32* vmie_win32_open(const char* ram_path, uint64_t low) {
return v;
}
__attribute__((cold))
vmie_win32* vmie_win32_open_fd(int fd, uint64_t low) {
vmie_win32* v = calloc(1, sizeof *v);
if (!v) {
return NULL;
}
if (gpa_from_fd(&v->mem, fd, low)) {
free(v);
return NULL;
}
return v;
}
/* Read-only consumer context: maps the backing file PROT_READ and builds the
* struct-offset profile from the image under the caller-supplied kcr3 (no
* beacon, no cr3 recovery, no ACK write). All read APIs work, incl.
* proc_list/proc_modules; writes return -1. */
__attribute__((cold))
vmie_win32* vmie_win32_open_ro_fd(int fd, uint64_t low, uint64_t kcr3) {
vmie_win32* v = calloc(1, sizeof *v);
if (!v) {
return NULL;
}
if (gpa_from_ro_fd(&v->mem, fd, low)) {
free(v);
return NULL;
}
if (host_profile_from_cr3(v, (uintptr_t)kcr3)) {
vmie_win32_close(v);
return NULL;
}
return v;
}
__attribute__((cold))
void vmie_win32_close(vmie_win32* v) {
if (!v) {
@@ -153,6 +190,31 @@ static uint32_t ko_export_rva(vmie_mem* m, uintptr_t cr3, uint64_t kbase, const
return 0;
}
__attribute__((cold))
static int host_profile_from_cr3(vmie_win32* v, uintptr_t cr3) {
vmie_mem* m = &v->mem;
uint8_t guid[16];
uint32_t age, rva;
uint64_t sys_ep, dtb;
if (find_ntoskrnl(m, cr3, &v->kbase, guid, &age)) {
return -3;
}
rva = ko_export_rva(m, cr3, v->kbase, "PsInitialSystemProcess");
if (!rva || gva_read(m, cr3, v->kbase + rva, &sys_ep, 8)) {
return -4;
}
if (profile_build(v, cr3, sys_ep, guid, age)) {
return -5;
}
if (gva_read(m, cr3, sys_ep + v->prof.ep_dtb, &dtb, 8)) {
return -6;
}
v->kcr3 = dtb & PFN_MASK;
v->sysproc = sys_ep;
return 0;
}
static void beacon_ack(vmie_mem* m, uint64_t anchor_pa) {
uint64_t ack = CONTRACT_MAGIC_R;
gpa_write(m, anchor_pa + offsetof(contract, ack), &ack, 8);
@@ -167,10 +229,6 @@ int host_bootstrap(vmie_win32* v) {
vmie_mem* m = &v->mem;
uint64_t anchor_pa, va_self;
uintptr_t cr3boot;
uint32_t rva;
uint8_t guid[16];
uint32_t age;
uint64_t sys_ep;
if (beacon_find(m, &anchor_pa, &va_self)) {
return -1;
@@ -180,26 +238,11 @@ int host_bootstrap(vmie_win32* v) {
return -2;
}
if (find_ntoskrnl(m, cr3boot, &v->kbase, guid, &age)) {
return -3;
const int rc = host_profile_from_cr3(v, cr3boot);
if (rc) {
return rc;
}
rva = ko_export_rva(m, cr3boot, v->kbase, "PsInitialSystemProcess");
if (!rva || gva_read(m, cr3boot, v->kbase + rva, &sys_ep, 8)) {
return -4;
}
if (profile_build(v, cr3boot, sys_ep, guid, age)) {
return -5;
}
uint64_t dtb;
if (gva_read(m, cr3boot, sys_ep + v->prof.ep_dtb, &dtb, 8)) {
return -6;
}
v->kcr3 = dtb & PFN_MASK;
v->sysproc = sys_ep;
beacon_ack(m, anchor_pa);
return 0;
}