Manual DLL-Wrapping technique (AMSI DLL-Implant)
TLDR: Repository with final AMSI Dll-Implant
Standard DLL-Proxy using for DLL Hijacking
Many tutorials show the easiest methods to implement DLL-Wrapper (or maybe I should call it DLL-Proxy) using the following syntax:
This actually exports a legitimate function (exportedFunc
) from our custom DLL. The problem is that this way we have no actual control over the function. Usually in the case of standard DLL hijacking it looks like this:
As we can see above, our action is performed only when the DLL is loaded. We have no direct control over the functions we export. But I want to control the execution of these functions - the incoming parameters and the returned values. I want to add my own DLL-implant. This can come in handy for more stealthy DLL-Hijacking tactics, or for conveniently manipulating module parameters to get a better idea of what is going on.
It can also be a way to bypass certain security features as in the case of AMSI. By controlling the AmsiScanBuffer()
function from amsi.dll
we are able to return 0
every time and effectively disable AMSI checks. See my previous post for a better understanding of this concept.
More manual way of DLL-Wrapping
So how can we do it? Well, we can create functions in our custom fake-amsi.dll
DLL module, whose names and parameters will coincide with those of the legit amsi.dll
. Let's see what is exported from amsi.dll
using PE-bear program:
Here we see the names of the functions we need to export from our DLL module. We also need to know what parameters these functions expect. This is what we can find out from the Microsoft documentation: amsi.h header.
I thought that when writing my own wrapper-functions, in addition to the name and parameters of functions, I also need to keep the calling convention in sync with the legit DLL. It turned out that with x86-64 architecture Microsoft uses only one calling convention. Basically all calling conventions (e.g. stdcall, thiscall, cdecl, and fastcall) resolve to using this one ultimate convention. Special keywords like
__fastcall
are simply ignored by compiler. Read more here.
Now we have everything to start implementing our DLL wrapper. First we load legit-amsi.dll using the absolute path, then we get the addresses of the functions we want to wrap.
Then we create wrappers for each function according to the scheme below. It's best to make a wrapper for each function to make sure that everything will work flawlessly and no one will realize that it's just a wrapper for a legitimate DLL.
I write in C++, so
extern "C"
is used to avoid default name mangling of exported functions.__declspec(dllexport)
is used to export a function from our DLL file.
After compiling as a DLL file, we can see the exported functions again using PE-Bear:
I didn't implement literally all the functions from the original amsi.dll, but the rest proved useless, at least for my case. powershell.exe
, like most executable binaries, looks for DLL modules in the same directory where the .exe file is located. This allows us to perform DLL hijacking and give him our fake amsi.dll
implant.
The implant is ready. Of course, this is also one of the AMSI bypass techniques, because we fully control the execution of the AmsiScanBuffer()
function, so it can always return 0
and execute any PowerShell script we want. My goal, however, was to see what exactly is sent, and what AMSI (actually Windows Defender) returns, in the case of a malicious script:
We can see that our implant is working. It is loaded right at the start of powershell.exe
which sends the entire PowerShell startup scripts to AMSI without any chunking at all. Literally every command you type, every line of PowerShell is scanned by AMSI. Now let's check what happens when AMSI detects a malicious PowerShell script.
Highlighted in yellow is the result that the ScanStringBuffer()
function returned on the malicious script. Microsoft's documentation says: Any return result equal to or larger than 32768 is considered malware (source). Our value is exactly 32768
which means everything works as intended. From my observations, this number (at least with the default Windows Defender) is never higher than 32768
, although it could be.
The experiment was successful. Now you know how to manually create your own wrappers for DLL modules and have full control over the execution of exported functions.
~ Print3M