3 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
5 changed files with 30 additions and 14 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.18) # find_program(... REQUIRED)
set(VMIE_VERSION "0.1.5" 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)
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
* 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. */
* 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
#define VMIE_VREGION_DEFINED
#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,
sig_multi_hit* out, int max);
/* code-xref: every instruction in the X-regions of [lo,hi] whose near rel
* branch or RIP-relative memory operand resolves to `target_va`. Brute-scans
* each byte offset with the light x86-64 decoder (x86dec.h, NOT a full
* disassembler): an E8/E9/EB/Jcc rel branch matches when next_rip + rel ==
/* code-xref: every instruction in the regions of [lo,hi] kept by `prot_any`
* whose near rel branch or RIP-relative memory operand resolves to `target_va`.
* Brute-scans each byte offset with the light x86-64 decoder (x86dec.h, NOT a
* 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
* 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
* 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. */
* form). Records each matching instruction-start VA. The sweep is scoped by
* `prot_any` (pass VR_X to restrict to code; pass VR_R when the target's
* page-table X bit is unavailable - the decoder and exact target match keep the
* 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,
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
* 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;
const uint64_t b4 = VA_CANON((uint64_t)i4 << 39);
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);
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,
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);
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 c.n;