NTDLL-Reconstruction

go back / p4p1


Created on Fri. 09 Oct 2026



Okay so I am back! I am a bit annoyed at myself for not posting more often but recently I have been stinking up my room not working that much after a big push on maldev academy trying to finish that massive grind I got roughly to halfway and decided to clock out for a solid month to just watch naruto and play video games! But during this break time I had an idea in my little brain.

From a evasion perspective ntdll.dll unhooking seems a little useless right EDR does integrity checks so just doing the whole typical workflow seen on open source crap is a bit broken and then why even bother when you could do direct / indirect syscalls? Well the whole point of ntdll.dll unhooking is to be used when you are running code you personally didn't write like in the context of a BOF loader for example since this code would run more or less in your own process you would technically be able to clean out your ntdll.dll instance and have a unhooked Beacon Object Files and get burned less often. But this also adds the issue of integrity checks an EDR theoretically would check okay is NTDLL.dll tampered with? Then in most techniques I saw on the maldev course was either spawning a new process (process telemetry), reading from disk (disk telemetry) or from the web (web telemetry). This got me thinking we know what a clean stub should look like since in hells gate varients we check yeah is the stub clean? No then you would look up / down to calculate the syscall number. Well what if we just inline wrote inside of our NTDLL.dll clean stubs.

Coding it up!

With that core idea explained let me tell you how I reasoned through this. First I figured I should just rip from hells gate how they check for a clean / dirty stub. Second I thought well if I want to keep ntdll.dll integrity I should store somewhere what I edited and how. I know a "clean" stub would only be 11 bytes so at least it's a fixed size! With those constraints in mind I wrote the header file: (sidenote: this is exactly how I usually code 1. header file, 2. the actual function 3. debug 4. repeat)

Once I had that done I went through what I did on maldev academy when I was doing the hells gate variants and ripped what I thought was useful:

As you can see nothing crazy on both accounts this technique is not that deep! Now compared to most hells gate implementations I saw online I decided to make mine recursive, why? Well I am just annoying to myself I thought it would fit well and it makes vibe coding more annoying if someone wanted to just take this code and plop it into a 100% automated workflow LOL. I personally don't care about AI crap btw but to think of it now it would be funny if that tripped someone up. From there I mainly had to do some internal pipping between the techniques and what I actually wanted nothing crazy:

Here it's the function that takes the ntdll.dll pointer to a hooked function creates a clean stub inside of the structure so that we can put it in ntdll.dll later. Then here is the actual function of the init of the technique:

1. We do the typical PEB stuff to just retrieve our ntdll.dll instance. 2. We walk the functions and check if it's hooked. 3. We capture the syscall number using the hells gate varient and save to the cache. 4. We flip the protection and write the clean stubs.

Kabooom

Okay so obviously that was a quick code to write-up but a few things tripped me up and blew up in my face! First is originally I wrote the protection flip where you would set ntdll.dll to PAGE_READWRITE because I didn't want the RWX telemetry to bother me like it's just better to do RW -> RX than just blindingly giving ntdll.dll to many permissions right but that means you cannot run anything inside of ntdll.dll with the RW permissions so I was just segfaulting over and over again the fix was just to wrap the VirtualProtect call in a direct syscall stub nothing crazy.

Then my other main issue was that well I am not loaded sadly I don't have a EDR and a personal company to just throw this POC against so I setup a little elastic 2 VM network one with win11 and a other one with ubuntu to see what would happen!

So yeah instant blow up in my face there it just got caught instantly honestly makes sense after bullying GLM-5.3 for answers it told me that specifically elastic went the kernel telemetry route it's actually obvious EDR that is not kernel level would never catch suspicious activity if you are using indirect sycall paired with ntdll.dll unhooking. From there I decided to give myself some leeway so I disabled the killing of suspicious process and decided to have a look at what would happen:

Yay it worked but here you can see something odd in our "hooked" list there is only 2 functions, why is that? What are they?

Well that doesn't seem right? So the main issue is the way I filtered for syscall "functions" was just dumb and it turns out it was flagging stuff that didn't exist and well like mentioned elastic is a EDR that is kernel telemetry focused so there wouldn't really be any user land hooks. So I decided to "borrow" something that did do user land hooks!

A Flipping Showcase

Maldev Academy has a mini "MalDevEdr" that will terminate a process if it detects suspicious RWX call to NtProtectVirtualMemory so I just borrowed that one and tested it out. Here is the example of the hooked function itself:

And here is the cache flipping to a clean stub.

If you want to have a in depth look at the source code you can find it here. Star it on github as well if you want why not.


Thank you for reading get some of my stickers from red bubble and follow me on github and/or linked-in to see what torture I put myself through next time and if you have any questions reach out on linked-in!

Links
p3ng0s
arch linux iso

A live linux distribution designed by me for physical engagments and red team automation.

wiki | repo
Questions / Feedback
For any questions or feedback you can contact me on LinkedIn
Stickers & Donations
stickers pacget

If you like the content of my website you can get the amazing stickers from this websize. You can also support me through my github sponsors page.