Shadow pages and quiet hooks

A compact model for thinking about EPT shadow pages, execute-only mappings, and where a monitor can observe without patching guest bytes.

hypervisorswindowsreverse-engineering

Problem statement

Inline hooks are easy to explain and hard to hide. They change the bytes that a guest reads, which makes them noisy under integrity checks, memory scanners, and routine forensic tooling.

An EPT-backed design gives the monitor a different primitive: split what the guest can read from what the CPU can execute.

Mental model

Think of the target page as two views of the same address:

ViewGuest actionBacking page
Read/writeInspect bytes or copy codeOriginal page
ExecuteFetch instructionsShadow page

The key property is that the guest virtual address does not change. Only the second-level translation changes.

Flow

Locate the target

Resolve the guest physical page that backs the function or basic block you want to observe. Keep this part boring: validate the module range, verify page permissions, and avoid assumptions about build-specific offsets.

Split the mapping

Large pages are convenient until you need per-page permissions. Split a 2 MiB mapping into 4 KiB entries before changing execute permissions on a single page.

guest VA -> guest PA -> EPT entry
                         |
                         +-- R/W: original page
                         +-- X:   shadow page

Install the shadow

Clone the original page, place the trampoline or trap sequence in the copy, and revoke execute access from the original mapping. Instruction fetches now leave a signal without changing what the guest sees during normal reads.

Cost model

The cheapest useful estimate is the exit rate multiplied by the average handling cost:

Ctrace=Nexit×(tdispatch+thandler+tresume)C_{trace} = N_{exit} \times (t_{dispatch} + t_{handler} + t_{resume})

For inline reasoning, I usually treat NexitN_{exit} as the first variable to reduce.

Notes worth keeping

  • TLB invalidation is part of the feature, not cleanup.
  • Per-core state makes debugging harder but can reduce global side effects.
  • Treat every VM exit path as hostile input.
  • Log the exact OS build, CPU vendor path, and relevant control bits.

Closing thought

The interesting part is not the hook. The interesting part is the boundary between observation and mutation.