HVCI, and how far down a SYSTEM shell actually gets you
For there is nothing covered, that shall not be revealed; neither hid, that shall not be known.
Luke 12:2 (KJV)
i had a SYSTEM shell on the win-exp box from the SteelSeries updater bug and i wanted to know how far down it went. SYSTEM is the top of the usermode pile but it isn't the kernel, and the first thing i reached for, reading kernel memory to lift a token, didn't get me anything. that's HVCI. i hadn't actually sat down and worked out what it does and doesn't stop since it became the default, so i did, and this is the writeup. what holds, what people still get through with, and what intel and microsoft are adding to close the two gaps that are left.
the model
HVCI uses the hypervisor to split the machine into two trust levels. the normal windows kernel runs in VTL0. a small separate kernel, the Secure Kernel, runs in VTL1, and it owns the EPT, which is the hypervisor's set of page tables sitting underneath the ones windows manages. VTL0 can read and write its own page tables all it likes and it still can't touch the EPT. so every page ends up carrying two sets of permissions, the PTE windows controls and the EPT entry the Secure Kernel controls, and when the two disagree the hardware goes with the EPT entry. McGarr's phrasing for it is that physical memory trumps virtual memory. you can mark a page read-write-execute in the PTE and it changes nothing, because the EPT entry behind it says read-only and that's the one the CPU obeys.
virtual address
|
v
[ PTE ] R W X <- windows controls this, you can edit it
|
v
guest-physical page
|
v
[ EPTE ] R - - <- Secure Kernel controls this, you can't
|
v
effective R - - <- the two get AND'd. the EPTE wins.
this is what kills the standard kernel exploit. the usual ending is to get a page that's both writable and executable, drop shellcode into it, and jump. you can't, because nothing is allowed to be writable and executable at the same time at the EPT level. McGarr walked through it directly: he corrupted a PTE to mark a page executable, wrote a block of NOPs into it, and jumped in, and the box bugchecked. windows believed the page was executable because the PTE said so, but the EPT entry behind it still had execute cleared, and there is nothing in VTL0 that can clear it. the variant where you flip a page's user/supervisor bit so a user-mode RWX page runs as kernel code is gone too, because MBEC marks user pages non-executable whenever the processor is in the kernel.
what still gets through
so HVCI stops you running your own code in the kernel. that turns out to be all it stops, and on a box that has HVCI and nothing newer there are two ways through, both of them well documented.
the first is reusing the kernel's own code with ROP. kernel CFG checks the target of every indirect call and jump against a bitmap, but it never looks at return addresses. so you don't bring code, you reuse what's already loaded. with a read/write primitive you find a thread you control, overwrite a saved return address on its kernel stack, and when that function returns it runs a chain of gadgets out of the signed kernel image that's sitting in memory anyway. nothing unsigned ever runs, so HVCI has no reason to care. McGarr's HVCI post builds this end to end, and the way he puts it is that you can't run your shellcode so you call the same kernel functions your shellcode would have called.
the second one isn't about code at all, which is why it's the one i find more interesting. HVCI protects code from being changed or faked. it has nothing to say about whether your data means what you think it means. Kernel Data Protection is the feature meant to fill that in: it marks chosen kernel data read-only in the EPT, so even a kernel write can't change it, and that part genuinely works. write the protected page directly and the EPT says read-only and you take a fault. but KDP protects the contents of a physical page, not the path your virtual address takes to reach that page. microsoft said this themselves when they shipped it in 2020, that the Secure Kernel only checks on a periodic basis that a protected region still translates to the physical page it's supposed to. so you leave the protected page alone. you copy it somewhere ordinary, edit the copy, and change the page tables so the protected virtual address now resolves to your copy instead of the original. Tanda wrote up the three steps. your copy lives in a normal writable page so there's no EPT violation, and from there on everything that reads the protected address reads your version of it. the hypervisor never sees the switch, because seeing it would mean trapping and inspecting every single write to a page table, and a VM exit on every page-table write costs far more than anyone is willing to pay.
before after the PFN swap
prot. VA --PTE--> GPA_a prot. VA --PTE--> GPA_b
| |
EPTE R-- locked EPTE RW- normal page
| |
the real data your modified copy
the EPTE on GPA_a never changed. you just stopped pointing at it.
no write to protected memory, so no EPT violation ever fires.
the weak point is narrow but it's real. the EPT enforces the permissions of whatever page an address resolves to right now. it never checks that the address still resolves to the page it was meant to.
what's closing it
both of those have answers, and they're already arriving.
the ROP one is closed by kernel CET. the processor keeps its own protected copy of every return address and checks the real stack against it on each return, and if they don't match it faults. that takes away the writable return address the whole technique stands on. it's been in windows since the 22H2 build and it only runs with HVCI on, so the two travel together.
the remapping one is closed in hardware, by intel's VT-rp, and mostly by the HLAT piece of it. when HLAT is on, for the ranges it's protecting, the processor stops using the guest page tables and walks a separate set the hypervisor owns, found through a field in the VMCS. changing the PFN in the guest PTE does nothing now, because the guest PTE isn't part of the translation any more. two supporting pieces, paging-write and guest-paging verification, exist to stop you sidestepping that by aliasing the hypervisor's own page tables. microsoft ships the whole thing as HVPT, and they're clear that its job is to protect the page tables behind KDP, shadow stacks, and CFG. it's on by default in windows 11 24H2, on tiger lake and intel 11th gen and up.
so where does that leave it
which of these you're actually up against comes down to the hardware, and that's the part that gets skipped when someone says a box has HVCI. on an older CPU, or any machine where HVPT isn't running, the remapping attack works the way Tanda laid it out, KDP comes apart, and a kernel read/write still takes you wherever you want to go. on a 24H2 machine with current silicon, both of these are shut at once, CET on the ROP side and HLAT on the remap side. the same three words, HVCI is enabled, cover both of those boxes, and as targets they are nothing alike.
path plain HVCI + kCET / HLAT
-----------------------------------------------------------------
unsigned code (RWX / KRWX) blocked blocked
user/supervisor flip blocked (MBEC) blocked
ROP through signed code works closed by kCET
data-only remap (KDP) works closed by HLAT/HVPT
it isn't finished closing either. HLAT only locks the ranges the hypervisor pointed it at, not all of memory, and it has a fallback that drops back to ordinary paging under some conditions. the structures it doesn't cover, and whatever can trigger that fallback, are where i'd expect the next round of this to come from. Allievi has been publishing on the Secure Kernel internals, Tanda documented VT-rp end to end, and McGarr took the control-flow half of this to Black Hat this year. the data-only route isn't theoretical, either, Lazarus' FudModule rootkit goes admin to kernel and then does its work entirely through data with no unsigned code anywhere in it.
i don't have a clean way past HLAT on current hardware and i'm not going to pretend i do. what i'll say is that the cheap, universal version of kernel-read/write-to-SYSTEM is gone, the two routes that are left are both being closed as the install base turns over, and whether either one still works on the box in front of you is a question about the exact CPU and the exact windows build, not about whether the HVCI checkbox is ticked.
reading
- Connor McGarr, No Code Execution? No Problem! Living The Age of VBS, HVCI, and Kernel CFG. the KRWX crash and the signed-code ROP technique.
- Connor McGarr, Out Of Control: How KCFG and KCET Redefine Control Flow, Black Hat USA 2025.
- Satoshi Tanda, Intel VT-rp part 1 (HLAT) and part 2 (paging-write and guest-paging verification). the remap attack, the three steps, and the hardware fix.
- Microsoft, Introducing Kernel Data Protection. the periodic-verification line is theirs.
- Microsoft, Hardware security: silicon-assisted security. HVPT and kCET, on by default in 24H2.
- Andrea Allievi, Secure Kernel internals, NoHat 2024.
. SiCk · afflicted.sh