4 Commits

Author SHA1 Message Date
lirent 8ca84a5861 Bump version to 0.1.6 2026-06-26 07:57:36 +03:00
lirent 91e5e05de4 Scope gva_code_xref by an explicit prot_any argument
It hard-coded VR_X, so a target whose page tables expose no executable
region silently yielded zero xrefs. Callers now pass the scope (VR_X to
restrict to code, VR_R when the X bit is unavailable); the decoder and
exact target match remain the correctness gate. Mirrors the prot_any
parameter gva_imm_xref already carries.
2026-06-26 07:57:35 +03:00
lirent 32440c7104 Drop the user-half top-level NX bit when deriving VR_X
Under KVA shadow (KPTI) the kernel CR3 marks user-half PML4Es NX as
hardening, so OR-ing that top-level NX down the walk made every user
region non-executable - even live code (ntdll .text read back r--u).
Ring 3 executes those pages via the user/shadow CR3, which reaches the
same shared lower tables through an NX-clear PML4E, so the kernel
table's user-half PML4E NX is not what enforces user execution. Ignore
it on the user half; sub-PML4E NX (shared between both CR3s) stays
authoritative.
2026-06-26 07:57:26 +03:00
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
6 changed files with 31 additions and 15 deletions
+1 -1
View File
@@ -86,7 +86,7 @@ jobs:
run: | run: |
apt-get update apt-get update
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
cmake make gcc libc6-dev dpkg-dev \ cmake make gcc libc6-dev dpkg-dev file \
gcc-mingw-w64-x86-64 ca-certificates curl gcc-mingw-w64-x86-64 ca-certificates curl
- name: Configure - name: Configure
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.18) # find_program(... REQUIRED) cmake_minimum_required(VERSION 3.18) # find_program(... REQUIRED)
set(VMIE_VERSION "0.1.4" CACHE STRING "Library version (MAJOR.MINOR.PATCH); CI passes the tag") set(VMIE_VERSION "0.1.6" CACHE STRING "Library version (MAJOR.MINOR.PATCH); CI passes the tag")
project(vmi-engine VERSION ${VMIE_VERSION} LANGUAGES C) project(vmi-engine VERSION ${VMIE_VERSION} LANGUAGES C)
set(CMAKE_C_STANDARD 17) # generation B uses no C23 feature set(CMAKE_C_STANDARD 17) # generation B uses no C23 feature
+5 -1
View File
@@ -125,7 +125,11 @@ typedef struct {
* x86-64 has no read bit: a present page is readable, so VR_R is always set on a * 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 * 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 * 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. */ * leaf entry, so they reflect what the guest CPU actually enforces. One
* exception keeps VR_X honest under KVA shadow (KPTI): the kernel CR3 marks
* user-half PML4Es NX as hardening, but ring 3 executes those pages via the
* user/shadow CR3 (same shared lower tables, NX-clear PML4E), so the user-half
* top-level NX bit is ignored when deriving VR_X. */
#ifndef VMIE_VREGION_DEFINED #ifndef VMIE_VREGION_DEFINED
#define VMIE_VREGION_DEFINED #define VMIE_VREGION_DEFINED
#define VR_R 0x1u /* readable (present => always set) */ #define VR_R 0x1u /* readable (present => always set) */
+12 -9
View File
@@ -70,18 +70,21 @@ int gva_sig_scan_multi(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
uint32_t prot_any, const sigset* s, uint32_t prot_any, const sigset* s,
sig_multi_hit* out, int max); sig_multi_hit* out, int max);
/* code-xref: every instruction in the X-regions of [lo,hi] whose near rel /* code-xref: every instruction in the regions of [lo,hi] kept by `prot_any`
* branch or RIP-relative memory operand resolves to `target_va`. Brute-scans * whose near rel branch or RIP-relative memory operand resolves to `target_va`.
* each byte offset with the light x86-64 decoder (x86dec.h, NOT a full * Brute-scans each byte offset with the light x86-64 decoder (x86dec.h, NOT a
* disassembler): an E8/E9/EB/Jcc rel branch matches when next_rip + rel == * full disassembler): an E8/E9/EB/Jcc rel branch matches when next_rip + rel ==
* target_va, and any RIP-relative operand (ModRM mod=00, rm=101) matches when * target_va, and any RIP-relative operand (ModRM mod=00, rm=101) matches when
* next_rip + disp32 == target_va (this covers lea/mov and any other rip-rel * next_rip + disp32 == target_va (this covers lea/mov and any other rip-rel
* form). Records each matching instruction-start VA. The sweep forces VR_X and * form). Records each matching instruction-start VA. The sweep is scoped by
* carries a >=15-byte overlap (max x86 instruction length) so no instruction is * `prot_any` (pass VR_X to restrict to code; pass VR_R when the target's
* cut at a window seam. Writes up to `max` VAs to `out` (NULL to count only) and * page-table X bit is unavailable - the decoder and exact target match keep the
* returns the TOTAL number of matches, or -1 on bad input. */ * result correct, only more bytes are decoded) and carries a >=15-byte overlap
* (max x86 instruction length) so no instruction is cut at a window seam. Writes
* up to `max` VAs to `out` (NULL to count only) and returns the TOTAL number of
* matches, or -1 on bad input. */
int gva_code_xref(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi, int gva_code_xref(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
uint64_t target_va, uint64_t* out, int max); uint32_t prot_any, uint64_t target_va, uint64_t* out, int max);
/* immediate / constant xref: every instruction in [lo,hi] (kept by the /* immediate / constant xref: every instruction in [lo,hi] (kept by the
* protection filter `prot_any`; pass VR_X to restrict to code) whose IMMEDIATE * protection filter `prot_any`; pass VR_X to restrict to code) whose IMMEDIATE
+10 -1
View File
@@ -158,7 +158,16 @@ int gva_regions(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
if (!(e4 & PG_P)) continue; if (!(e4 & PG_P)) continue;
const uint64_t b4 = VA_CANON((uint64_t)i4 << 39); const uint64_t b4 = VA_CANON((uint64_t)i4 << 39);
if (!rgn_hit(b4, 1ull << 39, lo, hi)) continue; if (!rgn_hit(b4, 1ull << 39, lo, hi)) continue;
const int rw4 = (e4 >> 1) & 1, us4 = (e4 >> 2) & 1, nx4 = (int)(e4 >> 63) & 1; const int rw4 = (e4 >> 1) & 1, us4 = (e4 >> 2) & 1;
/* On a KVA-shadow (KPTI) Windows the kernel CR3 marks user-half PML4Es
* NX as hardening: the kernel must not execute user pages through its own
* mapping. Ring 3 runs under the user/shadow CR3, which reaches the SAME
* lower tables (PDPT/PD/PT are shared) through a PML4E with NX clear, so
* that top-level NX is not what executes user code. Drop it on the user
* half; sub-PML4E NX stays authoritative (it is shared between the two
* CR3s). On a non-KPTI guest a code-covering user PML4E already has NX
* clear, so this only ever discards a zero. i4 < 256 == canonical user. */
const int nx4 = (i4 < 256) ? 0 : ((int)(e4 >> 63) & 1);
const uint64_t* t3 = gpa_ptr(m, e4 & PFN_MASK, 4096); const uint64_t* t3 = gpa_ptr(m, e4 & PFN_MASK, 4096);
if (!t3) continue; if (!t3) continue;
+2 -2
View File
@@ -115,10 +115,10 @@ static int xref_sweep_cb(void* u, const uint8_t* data, size_t len,
} }
int gva_code_xref(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi, int gva_code_xref(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
uint64_t target_va, uint64_t* out, int max) { uint32_t prot_any, uint64_t target_va, uint64_t* out, int max) {
struct xref_cb c; memset(&c, 0, sizeof c); struct xref_cb c; memset(&c, 0, sizeof c);
c.target = target_va; c.out = out; c.max = max; c.target = target_va; c.out = out; c.max = max;
if (gva_sweep(m, cr3, lo, hi, VR_X, X86_MAX_INSN, xref_sweep_cb, &c) < 0) { if (gva_sweep(m, cr3, lo, hi, prot_any, X86_MAX_INSN, xref_sweep_cb, &c) < 0) {
return -1; return -1;
} }
return c.n; return c.n;