<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://reversewarrior.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://reversewarrior.github.io/" rel="alternate" type="text/html" /><updated>2026-05-06T10:38:33+00:00</updated><id>https://reversewarrior.github.io/feed.xml</id><title type="html">Liel’s Blog</title><subtitle>A Cybersecurity blog.</subtitle><author><name>Liel</name></author><entry><title type="html">Virtualization Based Security: A peek into the Secure Kernel</title><link href="https://reversewarrior.github.io/Virtualization-Based-Security/" rel="alternate" type="text/html" title="Virtualization Based Security: A peek into the Secure Kernel" /><published>2026-05-06T10:00:00+00:00</published><updated>2026-05-06T10:00:00+00:00</updated><id>https://reversewarrior.github.io/Virtualization-Based-Security</id><content type="html" xml:base="https://reversewarrior.github.io/Virtualization-Based-Security/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>As I got into Hyper-V research I saw there is little to no documentation or research available on the Hyper-V hypervisor and the internals of the Secure Kernel, and what little there is is
very opaque making it a black hole that a lot of people would be afraid to get into. But for me it was the exact opposite, I saw a void I can fill with the knowledge I’ll gain and contribute to the Cybersecurity community whose research has helped me in many different areas. With the introduction of Virtualization Based Security every Windows kernel is essentially running in an environment managed by Hyper-V, Almost like a virtual machine where the kernel doesn’t talk directly to the hardware. My goal today is to guide you through the findings I had about virtualization based security and show a tool I built to debug <code class="language-plaintext highlighter-rouge">Isolated User Mode</code> processes.</p>
<h2 id="environment-setup">Environment Setup</h2>

<p>I’ll use <code class="language-plaintext highlighter-rouge">Microsoft Windows 11 Version 24H2 (OS Build 26100.7623)</code> as my guest running on top of Hyper-V. Make sure to enable <a href="https://learn.microsoft.com/en-us/windows/security/hardware-security/enable-virtualization-based-protection-of-code-integrity?tabs=security">Memory Integrity</a> which will also enable Virtualization Based Security. If you are using Hyper-V to manage your virtual machines make sure to enable nested virtualization with 
<code class="language-plaintext highlighter-rouge">Set-VMProcessor -VMName &lt;VMName&gt; -ExposeVirtualizationExtensions $true</code> 
and ensure Guest Integration Services are enabled to have seamless communication between the host and the guest such as copy-paste and file sharing.</p>

<p>To setup kernel debugging on the guest open Command Prompt as administrator and type:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bcdedit /debug on
bcdedit /dbgsettings net hostip:172.23.128.1 port:50020 key:1.2.3.4
</code></pre></div></div>
<p>And to debug the hypervisor:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bcdedit /set hypervisordebug on
bcdedit /hypervisorsettings net hostip:172.23.128.1 port:50030 key:5.6.7.8
</code></pre></div></div>

<blockquote>
  <p>Remember to change the IP in accordance with environment. Note that in Hyper-V, whenever you close or change the network adapter, your guest IP will change along with the host for that switch, that includes turning off the host computer.</p>
</blockquote>

<p>We’ll make some adjustments later to debug the Secure Kernel but remember this is the vanilla setup for debugging the hypervisor and the kernel, it is obvious that for the host we need a Windows machine, so I’ll be using WinDbg Preview on my host, but for some of the Secure Kernel debugging I’ll have to use the original WinDbg. Have the LiveCloudKd Live Debugging module handy, I downloaded it from <a href="https://github.com/gerhart01/LiveCloudKd/releases/download/v1.0.20251103/LiveCloudKd.EXDI.debugger.v1.0.20251103.zip">here</a>.</p>

<p>Note: We won’t delve into topics such as “What is the VMCS?” or virtualization in general, as there are plenty of resources online about it. A great extension for learning virtualization on Windows is <a href="https://github.com/tandasat/hvext">hvext</a>, which helped me a lot in my journey.</p>
<h2 id="transitioning-from-vtl-0-to-vtl-1">Transitioning from VTL 0 to VTL 1</h2>

<blockquote>
  <p>Virtualization Based Security will be written as VBS, Virtual Trust Level as VTL.</p>
</blockquote>

<p>As I mentioned before, every Windows kernel runs in a sort of virtual machine, Why is that the case? With the introduction of VBS we split the operating system into the regular kernel and the Secure Kernel where the most sensitive parts of the system such as TPM and biometric data will be stored in the Secure Kernel. So how do we communicate between the NT Kernel (the regular one) and the Secure Kernel? As the following diagram shows there is no direct connection or pipe between the two so the only communication channel that can exist is via the Hyper-V hypervisor represented by the <code class="language-plaintext highlighter-rouge">hvix64.exe</code> file for Windows computers running Intel processors, <code class="language-plaintext highlighter-rouge">hvax64.exe</code> for AMD and <code class="language-plaintext highlighter-rouge">hvaa64.exe</code> for ARM (Windows on ARM is a topic I’m very passionate about!).</p>

<p><img src="/images/vbs-diagram.png" alt="vbs-diagram" /></p>

<p>On the NT side, the lowest function in the secure-call stack that still has a stable single entry point is <code class="language-plaintext highlighter-rouge">VslpEnterIumSecureMode</code> in <code class="language-plaintext highlighter-rouge">ntoskrnl.exe</code>. Following its cross-references upward and outward, three distinct flows fall out: synchronous NT-&gt;SK requests (the bulk of the traffic), asynchronous SK-&gt;NT callbacks for I/O and paging, and the IUM user-mode-&gt;SK system-call path. The middle flow shares the hypercall plumbing with the first (Returns the status/value for the request); only the IUM user-mode-&gt;SK system-call has an independent entry shape. 
For now let’s focus on Secure calls. Back to <code class="language-plaintext highlighter-rouge">VslpEnterIumSecureMode</code> we know that it accepts an <code class="language-plaintext highlighter-rouge">SKCALL</code> object pointer as the fourth argument and <code class="language-plaintext highlighter-rouge">Secure Service Call Number</code> or <code class="language-plaintext highlighter-rouge">SSCN</code> as the second argument. The <code class="language-plaintext highlighter-rouge">SKCALL</code> structure is 104 (0x68) bytes and is used to describe the kind of operation (invoke service, flush TB (Translation Lookaside Buffer), resume thread, or call an <a href="https://learn.microsoft.com/en-us/windows/win32/trusted-execution/vbs-enclaves">enclave</a>), the secure call number, and a maximum of twelve 8-byte parameters.
We can see there is a while loop that calls the function <code class="language-plaintext highlighter-rouge">HvlSwitchToVsmVtl1</code> where we pass the 0 as the first argument and the <code class="language-plaintext highlighter-rouge">SKCALL</code> structure and a <code class="language-plaintext highlighter-rouge">SECURE_THREAD</code> object as second and third arguments.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>__int64 __fastcall HvlSwitchToVsmVtl1(__int64 a1, __int64 *SKCALL, __int64 SECURE_THREAD)
{
  __int64 v3; // rbx
  __m128i v4; // xmm10
  __m128i v5; // xmm11
  __m128i v6; // xmm12
  __m128i v7; // xmm13
  __m128i v8; // xmm14
  __m128i v9; // xmm15
  __int64 result; // rax
  __int64 v11; // [rsp+8h] [rbp-130h]

  v3 = *SKCALL;
  v4 = _mm_loadu_si128((SKCALL + 1));
  v5 = _mm_loadu_si128((SKCALL + 3));
  v6 = _mm_loadu_si128((SKCALL + 5));
  v7 = _mm_loadu_si128((SKCALL + 7));
  v8 = _mm_loadu_si128((SKCALL + 9));
  v9 = _mm_loadu_si128((SKCALL + 11));
  result = (*&amp;HvlpVsmVtlCallVa)(a1, SKCALL, KeGetCurrentIrql(), SECURE_THREAD);
  *v11 = v3;
  *(v11 + 8) = v4;
  *(v11 + 24) = v5;
  *(v11 + 40) = v6;
  *(v11 + 56) = v7;
  *(v11 + 72) = v8;
  *(v11 + 88) = v9;
  return result;
}
</code></pre></div></div>
<p>As we don’t have any <code class="language-plaintext highlighter-rouge">vmcall</code> instruction we can see that statically, <code class="language-plaintext highlighter-rouge">HvlpVsmVtlCallVa</code> is an empty memory location populated at runtime. Setting a hardware breakpoint on its load into <code class="language-plaintext highlighter-rouge">rax</code> reveals a small stub injected by the hypervisor that issues a <code class="language-plaintext highlighter-rouge">vmcall</code> with codes 11 and 12.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1: kd&gt; dq nt!HvlpVsmVtlCallVa L1
fffff800`de401860  fffff800`6c60000f
1: kd&gt; u fffff800`6c60000f
fffff800`6c60000f 488bc1          mov     rax,rcx
fffff800`6c600012 48c7c111000000  mov     rcx,11h
fffff800`6c600019 0f01c1          vmcall
fffff800`6c60001c c3              ret
fffff800`6c60001d 8bc8            mov     ecx,eax
fffff800`6c60001f b812000000      mov     eax,12h
fffff800`6c600024 0f01c1          vmcall
fffff800`6c600027 c3              ret
</code></pre></div></div>
<p>Under <code class="language-plaintext highlighter-rouge">Appendix A: Hypercall Code Reference</code> we can see that Hypercall code 0x0011 is <code class="language-plaintext highlighter-rouge">HvCallVtlCall</code> and 0x0012 is <code class="language-plaintext highlighter-rouge">HvCallVtlReturn</code>. If you read carefully you noticed there’s a <code class="language-plaintext highlighter-rouge">ret</code> after the first <code class="language-plaintext highlighter-rouge">vmcall</code>, that’s because these are basically 2 separate code stubs injected into memory by the hypervisor. the second vmcall will be called by the Secure Kernel upon returning.
The first argument is an operation type that only becomes meaningful in light of who actually calls in with what. Walking the cross-references to <code class="language-plaintext highlighter-rouge">VslpEnterIumSecureMode</code>:</p>
<ul>
  <li>op 2 - Calls to services that will dispatch <code class="language-plaintext highlighter-rouge">IumInvokeSecureService</code> in the <code class="language-plaintext highlighter-rouge">securekernel.exe</code> binary in VTL 1.</li>
  <li>op 1 - the enclave invocation path, behind the user-mode enclave call dispatch.</li>
  <li>op 0 - the resume-thread family, used to re-enter a secure thread after a normal-call detour.</li>
  <li>op 3 - <code class="language-plaintext highlighter-rouge">VslFlushEntireTb</code>, single cross-reference.</li>
</ul>

<blockquote>
  <p>Normal hypercalls go through the <code class="language-plaintext highlighter-rouge">HvcallInitiateHypercall</code> function in the NT Kernel.</p>
</blockquote>

<h3 id="onto-the-hypervisor">Onto the hypervisor</h3>
<p>Now that we’ve issued a <code class="language-plaintext highlighter-rouge">vmcall</code>, where is our VM exit? A VM exit is the event of the processor transitioning from VMX non-root operation back to VMX root operation which, is basically saying we transfer execution to the hypervisor. 
We will open up the <code class="language-plaintext highlighter-rouge">hvix64.exe</code> in IDA and see a 2MB binary we don’t have symbols for, the best thing to do here is to see existing blogs and previous comments people had on the binary and luckily <a href="https://www.microsoft.com/en-us/msrc/blog/2018/12/first-steps-in-hyper-v-research">Saar Amar</a> has written that it can help to perform a binary difference with other related Windows system files like the bootloader and the Secure Kernel.
I merged all functions with confidence of 0.98+ and similarity of 0.99+ to <code class="language-plaintext highlighter-rouge">hvix64.exe</code> from these files:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntkrla57.exe 
vid.sys
winload.efi
ntoskrnl.exe
vmwp.exe
securekernel.exe 
winhVirtual Processorlatform.dll 
vmbkmcl.sys 
winhvr.sys 
vmbusr.sys 
</code></pre></div></div>
<p>There are more files to diff, but after these I got enough function names for my research. The second thing I did is updating the IDA 7.5 script to be compatible with IDA 9.2, Which is available here <a href="https://github.com/ReverseWarrior/Hypervisors-Scripts">ida92_CreatemVmcallHandlersTableWin11.py</a>.</p>
<h4 id="searching-for-the-vm-exit-handler">Searching for the VM exit handler</h4>
<p>There are a few documented ways to find the VM exit handler online. The easiest would be to search for the string “VM” and you will see the string “[%d] MinimalLoop VMX_EXIT_REASON_INIT_INTR. Rebooting the system”, cross-referencing it shows only one instance in a switch statement in what appears to be our VM exit handler due to the raw register access we see and the switch case matching specific exit reason codes. If we didn’t have this string we could’ve just searched for a <code class="language-plaintext highlighter-rouge">vmresume</code> instruction as after a VM exit we need to <em>resume</em> the VM. Reviewing the <a href="https://github.com/ionescu007/SimpleVisor/blob/master/vmx.h">Intel VMX header file</a> we see that the integers checked for the cases match the Intel VMX VM exit reason constants. For example 18 (0x12) is indeed in the switch case as the <code class="language-plaintext highlighter-rouge">VMX_EXIT_REASON_VMCALL</code> enum field. Showing us what function is responsible for executing hypercalls.</p>

<p><img src="/images/vm-exit-handler.png" alt="The VM exit handler" /></p>
<h4 id="reviewing-the-hvcallvtlcall-hypercall">Reviewing the HvCallVtlCall hypercall</h4>
<p>After entering the <code class="language-plaintext highlighter-rouge">HypercallHandler</code> function we see a similar switch case checking the hypercall code provided and dispatching the according function, Using the TLFS, we know exactly where each hypercall is. In our scope are the <code class="language-plaintext highlighter-rouge">HvCallVtlCall</code> and <code class="language-plaintext highlighter-rouge">HvCallVtlReturn</code> hypercalls.</p>

<p><img src="/images/hyper-handler-hypercalls.png" alt="hypercall handler" /></p>

<p>In order to understand the transition to the Secure Kernel, It will be good to know about the Virtual Processor object. The Virtual Processor object is the hypervisor’s abstraction of a CPU exposed to a partition (Like when we setup a virtual machine with it’s own processors). The hypervisor multiplexes Virtual Processors onto physical logical processors (A logical processor in VMX is a single hardware execution context as seen by the OS/hypervisor) via its scheduler. Referencing Saar’s blog post we see that:</p>

<blockquote>
  <p>You will probably notice accesses to different structures pointed by the primary gs structure. Those structures signify the current state (e.g. the current running partition, the current virtual processor, etc.). For instance, most hypercalls check if the caller has permissions to perform the hypercall, by testing permissions flags in the gs:CurrentPartition structure.</p>
</blockquote>

<p>What we learn from this is that the Virtual Processor object, as well as the current partition and privilege mask are stored in an offset from the <code class="language-plaintext highlighter-rouge">gs</code> register base. I would detect the Virtual Processor in a Hypercall function but that wasn’t the case. However I did see a reference to <code class="language-plaintext highlighter-rouge">gs:360h</code> with a bitmask at offset <code class="language-plaintext highlighter-rouge">0x1b0</code>. As I saw this prologue repeat in most hypercall functions We can safely assume that <code class="language-plaintext highlighter-rouge">gs:360h</code> is the “current partition” and that <code class="language-plaintext highlighter-rouge">0x2b</code> is the privilege mask, specifically the debugging bit as shown in the <code class="language-plaintext highlighter-rouge">HV_PARTITION_PRIVILEGE_MASK</code> <a href="https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/datatypes/hv_partition_privilege_mask">enum</a>.</p>

<p><img src="/images/HvCallPostDebugData.png" alt="HvCallPostDebugData" /></p>

<p>Now all we are left with is the Virtual Processor. So in order to find the Virtual Processor, I began by surveying references to the gs register, and with the help of the <a href="https://blog.quarkslab.com/a-virtual-journey-from-hardware-virtualization-to-hyper-vs-virtual-trust-levels.html">Quarkslab article</a> I saw that the load is just two calls above the VM exit handler.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; __int64 sub_343D90()
sub_343D90      proc near               ; CODE XREF: sub_23F040:loc_23F4B2↑p

arg_0           = qword ptr  8

                mov     [rsp+arg_0], rbx
                push    rdi
                sub     rsp, 20h
                mov     rbx, gs:0
                mov     rdi, [rbx+368h] -&gt; Load VirtualProcessor Object
                test    rdi, rdi
                jnz     short loc_343DB2

loc_343DAF:                             ; CODE XREF: sub_343D90+20↓j
                hlt
                jmp     short loc_343DAF

loc_343DB2:                             ; CODE XREF: sub_343D90+1D↑j
                xor     ecx, ecx
                call    sub_343E40
                lea     rcx, [rdi+0EC0h] -&gt; Pass VirtualProcessor + 0xEC0 to sub_3326D8 which later on passes it as the first argument to VMExitHandler
                xor     r8d, r8d
                mov     rdx, rbx
                call    sub_3326D8
                mov     rbx, [rsp+28h+arg_0]
                add     rsp, 20h
                pop     rdi
                retn
sub_343D90      endp
</code></pre></div></div>
<h3 id="the-secure-call-handler">The Secure Call Handler</h3>
<p>The secure call handler will, first, extract <code class="language-plaintext highlighter-rouge">VirtualProcessor + 0x3c0</code>, which seems to be a structure, and then will extract from what seems to be <em>another</em> structure at offset <code class="language-plaintext highlighter-rouge">0x14</code>. When we transition to a new VTL, executes <em>in context of a particular VTL</em> as well! Hyper-V manages the “current VTL” information via the Virtual Processor structure. In this version of Hyper-V, the “current VTL” is maintained through the current virtual processor at offset <code class="language-plaintext highlighter-rouge">0x3c0</code>. Additionally, offset <code class="language-plaintext highlighter-rouge">0x14</code> into this “VTL structure” contains the VTL associated with the VTL structure (which, in this case, means the VTL of the <em>current</em> processor). For this one IDA’s decompiler got messed up so I edited the pseudocode on my own.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>void __usercall SecureCallHandler(
    _VIRTUAL_PROCESSOR *VirtualProcessor,
    __int64 SecureCallReady
)
{
    int currentVtl;          // eax
    bool isVtlInitialized;   // zf
    int mask;                // esi

    currentVtl = 1 &lt;&lt; VirtualProcessor-&gt;CurrentVtl-&gt;VtlNumber;

    isVtlInitialized = !_BitScanForward(
        &amp;mask,
        VirtualProcessor-&gt;VtlMask &amp; ~(currentVtl | (currentVtl - 1))
    );

    if (!isVtlInitialized &amp;&amp; !SecureCallReady)
    {
        FixupVtl0RipToNextInstruction(
            VirtualProcessor-&gt;VmExitInstructionLen
        );

        SetupVtlTransition(VirtualProcessor, mask);
        FinishTransition(VirtualProcessor, mask, 1LL);
    }
}
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">VtlMask</code> tracks which VTLs have been initialized; a bitmask where each bit represents the initialization state of a corresponding VTL. As shown in the screenshot below, the Virtual Processor structure holds two related fields: the currently active VTL and an array containing every known VTL.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>VirtualProcessor-&gt;VtlMask |= 1 &lt;&lt; targetVtl;
VirtualProcessor-&gt;CurrentVtl = VirtualProcessor-&gt;VtlArray[targetVtl];
</code></pre></div></div>
<p>When the secure call handler determines that the call is eligible to proceed, its first action is to advance the instruction pointer for the current VTL. An important detail to recall here is that VSM introduces two distinct VMCS structures: one associated with VTL 0 and another with VTL 1. In this scenario, VTL 0 is the current VTL, as it is the one requesting services from VTL 1 by way of the secure call.  The standard convention for handling a VM exit is to increment the guest’s instruction pointer past the instruction that triggered the exit, so that when the hypervisor completes its work and a VM entry occurs, the guest resumes at the following instruction. Performing this fixup first ensures that VTL 0 returns to the next instruction rather than re-issuing the hypercall — in this case, the secure call itself. The update is performed either through the enlightened VMCS or by accessing the VMCS directly via the <code class="language-plaintext highlighter-rouge">vmread</code> and <code class="language-plaintext highlighter-rouge">vmwrite</code> instructions. With the VTL 0 instruction pointer corrected, execution moves into the transition logic targeting VTL 1. One of its preliminary checks is the requirement that the target VTL not match the currently active one.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>void __fastcall SetupVtlTransition(
    _VIRTUAL_PROCESSOR *VirtualProcessor,
    unsigned __int8 TargetVtl
)
{
    __int64 self;         // rsi
    __int64 currentVtl;   // r8

    self = __readgsqword(0);
    currentVtl = VirtualProcessor-&gt;CurrentVtl-&gt;VtlNumber;

    if (currentVtl != TargetVtl)
    {
        if (byte_FFFFF800000785E0)
        {
            if ((dword_FFFFF800000785C8 &amp; 0x2000) != 0)
                sub_FFFFF8000025E15C(
                    0x1D4D,
                    currentVtl | (TargetVtl &lt;&lt; 16)
                );
        }

        sub_FFFFF800002AF304(self, VirtualProcessor);
        PerformVtlTransition(self, VirtualProcessor, TargetVtl);
    }
}
</code></pre></div></div>
<p>Here we update the VTL data to contain the new state of VTL 1 and update the current Virtual Processor state to know it’s in VTL 1 territory.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>void __fastcall PerformVtlTransition(__int64 Self, _VIRTUAL_PROCESSOR *VirtualProcessor, unsigned __int8 TargetVtl)
{

    //
    // Get the new VTL 1 we target
    //
    newVtlData = VirtualProcessor-&gt;VtlArray[TargetVtl];

    //
    // Update the current Virtual Processor state to VTL 1
    //
    VirtualProcessor-&gt;CurrentVtlNumber = TargetVtl;

    //
    // Update the current VTL data for the current processor
    //
    VirtualProcessor-&gt;CurrentVtl = newVtlData;
}
</code></pre></div></div>
<p>With those fields settled, focus shifts to the VMCS swap: the Virtual Processor’s active VMCS must be replaced with the structure belonging to VTL 1. I’ll attribute this step to a function I’ve named <code class="language-plaintext highlighter-rouge">TransitionToNewVtl</code>. The incoming VTL is described by what I’ll call its private VTL data. VTL state data would work equally well as a label. And the relevance of this structure is that it holds a pointer to the target VMCS. Once that pointer is reachable, the swap proceeds: on platforms without enlightenments, <code class="language-plaintext highlighter-rouge">vmptrld</code> is executed against the physical VMCS address; where enlightenments are present, the VMCS is loaded by its virtual address instead.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>void __fastcall TransitionToNewVtl(__int64 Self, _VTL_PRIVATE_DATA *PrivateVtlData)
{
    _HV_VMX_ENLIGHTENED_VMCS *enlightenedVmcs; // rdx
    unsigned __int64 v6; // r8
    unsigned __int64 v7; // r8
    unsigned __int64 v8; // r8
    __int64 v9; // rax
    _VTL_VMCS_DATA *VtlVmcsData; // rax
    unsigned __int64 vtlVmcsPhysAddr; // rcx
    unsigned __int64 self; // rax
    __int64 v13; // [rsp+38h] [rbp+10h]

    PrivateVtlData-&gt;VtlVmcsData-&gt;Unknown = 0;
    _RCX = PrivateVtlData-&gt;VtlVmcsData;
    enlightenedVmcs = _RCX-&gt;VtlVmcsEnlightenedAddress;

    if (enlightenedVmcs)
    {
        //
        // Do we use enlightenments?
        //
        if ((dword_FFFFF800000AECB0 &amp; 1) != 0)
        {
            vtlVmcsPhysAddr = _RCX-&gt;VtlVmcsPhysicalAddress;
            enlightenedVmcs-&gt;SyntheticControls = 1;
            self = __readgsqword(0);

            //
            // Update the current VMCS to that of VTL 1
            //
            *(self + 0x2C680) = enlightenedVmcs;
            *(*(self + 0x2C4C8) + 0x30LL) = vtlVmcsPhysAddr;
        }
    }
    else
    {
        __asm { vmptrld qword ptr [rcx+188h] }
    }
}
</code></pre></div></div>

<h3 id="isolated-user-mode">Isolated User Mode</h3>
<p>Now that we’ve grasped how we transition from the NT Kernel to the Secure Kernel, we’ll focus on the user mode side of the Secure Kernel. In Isolated User Mode (IUM), processes interact with the NT Kernel through system calls. Common IUM processes include <code class="language-plaintext highlighter-rouge">LsaIso.exe</code> and <code class="language-plaintext highlighter-rouge">vmsp.exe</code>. Even with kernel-level privileges in the VTL0 domain, it is impossible to manipulate memory in VTL1. This design defends against kernel-level attacks and protects confidential information such as user password hashes and BitLocker encryption keys. The Secure Kernel schedules IUM processes system calls. We have existing documentation of how to debug the Secure Kernel thanks to <a href="https://windows-internals.com/secure-kernel-research-with-livecloudkd/">Yarden Shafir</a> and <a href="https://github.com/gerhart01/LiveCloudKd/tree/master">gerhart</a>. But what about debugging the processes running inside the Isolated User Mode?
After launching the WinDbg debugger as Administrator and trying to attach it to the <code class="language-plaintext highlighter-rouge">LsaIso.exe</code> process I get the following error:
<img src="/images/windbg-debug-fail.png" alt="windbg debugging fail" />
So I guess we got some work to do!</p>
<h2 id="patching-the-secure-kernel">Patching the Secure Kernel</h2>

<p>Using the latest version of <a href="https://github.com/gerhart01/Hyper-V-Tools/tree/main/HvlibPowershell">LiveCloudKd</a>, I found a method to debug isolated user-mode processes within a virtual machine in a nested virtualization mode. The principle lies in the fact that the Virtual Secure Mode in the virtual machine partition isolates cross-VTL memory access permissions. For the virtual machine’s parent partition, kernel-mode hypercalls or the <code class="language-plaintext highlighter-rouge">winhv.sys</code> driver API can still be used to directly manipulate the virtual machine’s linear physical memory. <code class="language-plaintext highlighter-rouge">LiveCloudKd</code> provides a signed driver, <code class="language-plaintext highlighter-rouge">hvmm.sys</code>, which enables these operations through application-layer APIs. Even if this protected physical memory is allocated to VTL1 within the virtual machine, it can still be accessed from the parent partition using physical addresses. To debug user IUM mode processes, the <code class="language-plaintext highlighter-rouge">SkpsIsProcessDebuggingEnabled</code> function in the <code class="language-plaintext highlighter-rouge">securekernel.exe</code> needs to be patched. In the new version, this function is inlined in the implementation of the <code class="language-plaintext highlighter-rouge">IumInvokeSecureService</code> system call. A brief description of its characteristics.
<code class="language-plaintext highlighter-rouge">SkpsIsProcessDebuggingEnabled</code> only gives out the permission to debug processes. The actual state change is done by its sibling, <code class="language-plaintext highlighter-rouge">SkpsEnableDebugging</code>:
<img src="/images/skpsEnableDebugging.png" alt="SkpsEnableDebugging" /></p>

<p>The default debugging strategy for the Secure Kernel is stored in its image strategy configuration. Directly modifying the configuration or patch file binary will cause <code class="language-plaintext highlighter-rouge">securekernel.exe</code> signature verification to fail, resulting in a blue screen upon virtual machine restart. Therefore, this method is not feasible. However, patching at runtime by locating the inlined check and overwriting the failure-path branch with <code class="language-plaintext highlighter-rouge">NOP</code>s is enough to let <code class="language-plaintext highlighter-rouge">SkpsEnableDebugging</code> proceed unconditionally.
The technique pivots on three pieces of information, recovered in this order:</p>

<ol>
  <li>The Guest Physical Address (GPA) of the page containing our patch site, found by scanning guest physical memory for a known byte signature.</li>
  <li>The corresponding Guest Virtual Address (GVA) and CR3, recovered by trapping a vCPU at that page and reading its registers via <code class="language-plaintext highlighter-rouge">HvGetVpRegisters</code>.</li>
  <li>The GVA to GPA mapping for the actual <code class="language-plaintext highlighter-rouge">SkpsIsProcessDebuggingEnabled</code> site, derived by walking the page tables from the recovered CR3.</li>
</ol>

<p>The most reliably-invoked securekernel.exe function I found was IumInvokeSecureService. One of the largest functions in the binary, and therefore one of the easiest to fingerprint. Using <a href="https://github.com/justinstenning/SharpDisasm">SharpDisasm</a> I located its ret instruction, then scanned guest physical memory for a byte pattern around that offset until a matching page surfaced. At the page-relative offset of the ret I wrote five bytes:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>f3 90                   pause  
eb fc                   jmp    0x0  
c3                      ret
</code></pre></div></div>

<p>Modifying memory does not cause a blue screen in the virtual machine. By querying the RIPs of all vCPUs in the virtual machine using the hypercall <code class="language-plaintext highlighter-rouge">HvGetVirtualProcessorRegisters</code>. If the lower bits of the RIP match the lower 12 bits of this instruction, we obtain a matching virtual address <code class="language-plaintext highlighter-rouge">gva</code> and a matching physical address <code class="language-plaintext highlighter-rouge">gpa</code>. We can then calculate the base address <code class="language-plaintext highlighter-rouge">cr3</code> of the page table and page directory (also via a hypercall). Similarly, by matching the string <code class="language-plaintext highlighter-rouge">SkpsIsProcessDebuggingEnabled</code> with the <code class="language-plaintext highlighter-rouge">SharpDisasm</code> assembly instruction,<br />
we obtain the address of the code to be patched. Based on the base address calculated by the Secure Kernel, we add an offset to convert it to a physical address and then patch the target assembly code. This successfully bypasses and enables debugging user IUM mode processes.</p>

<p>And this will be our view after attaching a debugger to the <code class="language-plaintext highlighter-rouge">LsaIso.exe</code> process :)
<img src="/images/windbg-debug-success-original.png" alt="Debugging an IUM process with WinDbg" /></p>

<p>The <a href="https://github.com/ReverseWarrior/IUM-Debugger">IUM-Debugger</a> is available for public use.</p>
<h3 id="conclusion">Conclusion</h3>
<p>It’s been a fascinating journey and I’ve packed a lot into this short post. The parts I’m glad made it in: tracing how Secure Calls cross from the NT Kernel into the Secure Kernel, and walking through how to debug processes running in Isolated User Mode.</p>

<p>Thank you so much for reading!</p>]]></content><author><name>Liel</name></author><category term="hyper-v" /><category term="vbs" /><category term="secure-kernel" /><category term="windows" /><category term="virtualization" /><summary type="html"><![CDATA[Tracing how Secure Calls cross from the NT Kernel into the Secure Kernel under VBS, and introducing a tool for debugging Isolated User Mode processes in VTL 1.]]></summary></entry></feed>