Win 10 1909( 18363.1198) ,x32 dbg, NoSEH.exe
借助书上的代码,自己修改了一下与现在系统相关实现的差异,下面是对函数伪代码的实现
//最好放到 VS 中查看
#include<winnt.h> #include<windef.h> #include<ntdef.h> #include<string.h> //MmExecutionFlags on Win7 #define MEM_EXECUTE_OPTION_DISABLE 0x1 #define MEM_EXECUTE_OPTION_ENABLE 0x2 #define MEM_EXECUTE_OPTION_DISABLE_THUNK_EMULATION 0x4 #define MEM_EXECUTE_OPTION_PERMANENT 0x8 #define MEM_EXECUTE_OPTION_EXECUTE_DISPATCH_ENABLE 0x10 #define MEM_EXECUTE_OPTION_IMAGE_DISPATCH_ENABLE 0x20 #define MEM_EXECUTE_OPTION_DISABLE_EXCEPTIONCHAIN_VALIDATION 0x40 #define MEM_EXECUTE_OPTION_VALID_FLAGS 0x7f // 该函数的功能是根据ControlPc查找它所在的模块的SafeSEHTable PVOID RtlLookupFunctionTable( IN PVOID ControlPc, OUT PVOID* ImageBase, OUT PULONG SizeOfTable ); VOID RtlInvalidHandlerDetected( IN PVOID Handler, IN PULONG FunctionTable, IN ULONG FunctionTableLength ); struct Excpt { DWORD ExcptCode; DWORD UNKNOWN1; DWORD UNKNOWN2; PVOID* Handler; DWORD UNKNOWN3; DWORD UNKNOWN4; }; struct __FuncTable { DWORD FuncTable; DWORD DllBase; DWORD DllSize; DWORD Index; }; LONG BinarySearch(LONG High, ULONG HandlerRVA, ULONG FunctionTable) { LONG Middle; LONG Low = 0; ULONG FunctionEntry; ULONG FunctionTableLength = High; if ((FunctionTableLength & 0x80000000) == 0) // 检测符号位 { do { Middle = (Low + High) / 2; FunctionEntry = *((DWORD*)FunctionTable + Middle * 4); if (HandlerRVA > FunctionEntry) { if (!Middle) break; High = Middle - 1; Low = 0; } else { if (HandlerRVA == FunctionEntry) return 1; Low = Middle + 1; } if (High < Low) break; } while (High >= Low); } return 0; } BOOLEAN __stdcall RtlIsValidHandler(PVOID Handler, int ProcessInformation) { PVOID FunctionTable; LONG High; ULONG HandlerRVA, DllSize; NTSTATUS status; void* v10; MEMORY_BASIC_INFORMATION MemoryInformation; ULONG ReturnLength; PVOID Base; PVOID HandlerTable; ULONG FunctionTableLength; struct __FuncTable _FuncTable; if (Handler < Base || Handler < Base + DllSize) _FuncTable = RtlLookupFunctionTable(Handler, &Base, &FunctionTableLength); else memcpy(_FuncTable, __safe_se_handler_table, (ULONG)0x10); // __safe_se_handler_table 是当前模块的 SafeSEHTable if (!FunctionTable) FunctionTableLength = 0; FunctionTableLength = _FuncTable.Index; FunctionTable = _FuncTable.FuncTable; High = FunctionTableLength; if (FunctionTableLength && FunctionTable) { if (FunctionTable != (PVOID)-1 || FunctionTableLength != -1) { HandlerRVA = (ULONG)Handler - (ULONG)Base; if (BinarySearch(High, HandlerRVA, FunctionTable)) return 1; } else return 0; } else { if (!ProcessInformation && ZwQueryInformationProcess( NtCurrentProcess(), ProcessExecuteFlags(0x22), &ProcessInformation, 4u, 0) < 0) { ProcessInformation = 0; } if ((ProcessInformation & 0x30) == 0x30) //允许在不可执行页、模块外执行 return 1; status = NtQueryVirtualMemory(NtCurrentProcess(), Handler, 0, &MemoryInformation, sizeof(MEMORY_BASIC_INFORMATION), &ReturnLength); if (!NT_SUCCESS(status)) return 1; if (!(MemoryInformation.Protect & 0xF0)) { if ((ProcessInformation & MEM_EXECUTE_OPTION_EXECUTE_DISPATCH_ENABLE)) return 1; } else if (MemoryInformation.Type == SEC_IMAGE) //当前页面属性是否是映像 { RtlCaptureImageExceptionValues(v10, (PVOID*)MemoryInformation.AllocationBase, (PULONG)&HandlerTable); if (HandlerTable && FunctionTableLength) return 1; } else if (!(ProcessInformation & MEM_EXECUTE_OPTION_IMAGE_DISPATCH_ENABLE)) return 1; } memset(ss: [ebp - 5C] , 0, 50); //将栈上的一块内存区域置0 *ss: [ebp - 5c] = Excpt; //将结构体放在栈上 _RtlReportException(Excpt, Contex, 0); //若有调试器存在,则会调用 RaiseExceotion 函数 return 0; //否则程序返回 }
参数有两个分别是 Handler 、ProcessInformation 参数是函数QueryProcessInformation( 0x22 (ProcessExecuteFlags) )的返回值 32
这个函数实现了 SafeSEH 机制
程序进入后对栈进行安全操作和局部变量存储后,会首先对地址进行判断,此处 jae 指令会执行跳转,忽略 _RtlpxLookupFunctionTable 函数
需要注意的是,ntdll 似乎总是占据用户态的最高地址。
__safe_se_handler_table:
函数地址表中全是 RVA,前面几个 RVA + ntdll 基址后得到的函数对应分别是
75220 ——> KiUserApcExceptionHandler 752B0 ——> KiUserCallBackExceptionHandler
77440 ——> UnWind_Handler4 7B130 ——> __Except_handler4
7BC40 ——> UnWind_handler 89F80 ——> Exceptionhandler
89FB0 ——> UnWindhandler 8A0B0 ~ 8A0EF ——> FinalExceptionHandlerPad0 ~ 63
经历 jmp 后,执行对取得地址表的检验
接下来计算得到传入参数的 RVA 地址(前面经过检验该函数地址在 ntdll 内)
接下来进入一个循环,因为存储的函数 RVA 都按照地址大小顺序排列的,因此该循环将会通过二分法对 ecx(0x47) 值的计算,取得存储着的 RVA。将取得的 RVA 与早已存储在局部函数中的 _excpet_handler 的 RVA 作比较,若相符则表明该函数位于异常处理函数地址表中,说明是合法有效的。
若在 ecx 为 1 前找到函数,则会跳出循环、退出函数并返回 1
否则程序将调用 memset 函数情况栈中的数据和记录异常的函数,然后退出并返回值 0