研究背景:
emm...
大概半年之前鹅厂反作弊某次更新后偶然发现在调用AddVectoredExceptionHandler注册了一个异常处理函数不到几分钟就封号了,这让我产生了bypass的想法,于是在网上找了找资料还真有了思路。
下面给大家介绍具体的原理吧。
实现原理:
那段时间在网上看到一个非常有意思的尾部挂钩方法: InstrumentationCallback
在KPROCESS结构的偏移地址0x2c8处,包含一个名为InstrumentationCallback的域,Windows系统Vista以及之后的版本中,可以使用InstrumentationCallback域来指定回调函数的地址,每次函数从内核态返回用户态之后系统都会调用指定的回调函数。
至于原理大致就是以上阐述的,那么了解Windows的异常派遣机制后可以知道每次系统产生异常时会从内核返回到用户层,既然这样那么我们是不是可以通过回调在回用户层的时候先拦截派遣,调用我们的异常处理函数后再放过它回去执行原来的派遣呢?
实践出真理,那就行用来验证想法是否正确吧。
创建一个动态库项目:
定义一个Exception类后来写一个注册回调和异常函数安装的功能吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | bool Exception::InstallException(pfnExceptionHandlerApi p_exception_api){ DWORD old; //获取syscall函数地址 NtSetContextThread = (pfnNtSetContextThread)NtSetContextThreadProc; ::VirtualProtect((PVOID)((DWORD64)&NtSetContextThreadProc + 0x04), 4, PAGE_EXECUTE_READWRITE, &old); *(DWORD*)((DWORD64)&NtSetContextThreadProc + 0x04) = (DWORD)GetSSDTIndexByName("NtSetContextThread"); ::VirtualProtect((PVOID)((DWORD64)&NtSetContextThreadProc + 0x04), 4, old, NULL); NtSuspendThread = (pfnNtSuspendThread)NtSuspendThreadProc; ::VirtualProtect((PVOID)((DWORD64)&NtSuspendThreadProc + 0x04), 4, PAGE_EXECUTE_READWRITE, &old); *(DWORD*)((DWORD64)&NtSuspendThreadProc + 0x04) = (DWORD)GetSSDTIndexByName("NtSuspendThread"); ::VirtualProtect((PVOID)((DWORD64)&NtSuspendThreadProc + 0x04), 4, old, NULL); NtResumeThread = (pfnNtResumeThread)NtResumeThreadProc; ::VirtualProtect((PVOID)((DWORD64)&NtResumeThreadProc + 0x04), 4, PAGE_EXECUTE_READWRITE, &old); *(DWORD*)((DWORD64)&NtResumeThreadProc + 0x04) = (DWORD)GetSSDTIndexByName("NtResumeThread"); ::VirtualProtect((PVOID)((DWORD64)&NtResumeThreadProc + 0x04), 4, old, NULL); NtContinue = (pfnNtContinue)NtContinueProc; ::VirtualProtect((PVOID)((DWORD64)&NtContinueProc + 0x04), 4, PAGE_EXECUTE_READWRITE, &old); *(DWORD*)((DWORD64)&NtContinueProc + 0x04) = (DWORD)GetSSDTIndexByName("NtContinue"); ::VirtualProtect((PVOID)((DWORD64)&NtContinueProc + 0x04), 4, old, NULL); //保存函数指针 this->_self_exception_api = p_exception_api; HMODULE ntdll = ::GetModuleHandleA("ntdll.dll"); if (ntdll == NULL) ntdll = ::LoadLibraryA("ntdll.dll"); //获取hook的返回地址 sysret_address = (DWORD64)::GetProcAddress(ntdll, "KiUserExceptionDispatcher"); if (sysret_address == NULL) sysret_address = (DWORD64)::GetProcAddress(ntdll, "KiUserExceptionDispatcher"); rtl_restore_context_offset = this->GetOffset(sysret_address, 0x70, 0x10); if (rtl_restore_context_offset <= 0) ::MessageBoxA(::GetActiveWindow(), "未找到函数偏移", "Error", MB_OK); PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION info; info.Version = 0; info.Reserved = 0; info.Callback = MyCallbackEntry; ULONG status = NtSetInformationProcess(GetCurrentProcess(), 0x28, &info, sizeof(info)); if (status) return false; return true;} |
至于为什么要自己实现syscall和拿RtlRestoreContext偏移放到后面用的时候再说,这里主要是拿到KiUserExceptionDispatcher函数地址,用于回调中判断是否为我们想拦截的函数。
写一段回调的汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | MyCallbackEntry PROC mov gs:[2E0H], rsp ;Win10 TEB InstrumentationCallbackPreviousSp (保存的线程参数地址) mov gs:[2D8H], r10 ;Win10 TEB InstrumentationCallbackPreviousPc (syscall 的返回地址) mov r10, rcx ;保存rcx sub rsp, 4D0H ;Context结构大小 and rsp, -10H ;align rsp mov rcx, rsp ;parameters are fun call __imp_RtlCaptureContext ;保存线程Context上下文 sub rsp, 20H ;开辟栈空间 call MyCallbackRoutine ;调用我们的函数 int 3 ;不应该执行到这里MyCallbackEntry ENDP |
当我们注册了回调后,内核返回应用层会首先执行我们的CallbackEntry
下面实现MyCallbackRoutine:
1 2 3 4 5 6 7 8 9 10 11 12 | void MyCallbackRoutine(CONTEXT* context){ context->Rip = __readgsqword(0x02D8);//syscall 的返回地址 context->Rsp = __readgsqword(0x02E0);//context = rsp, ExceptionRecord = rsp + 0x4F0 context->Rcx = context->R10; if (context->Rip == sysret_address) if (exception->_self_exception_api((PEXCEPTION_RECORD)(context->Rsp + 0x4F0), (PCONTEXT)context->Rsp) == EXCEPTION_CONTINUE_EXECUTION) context->Rip = rtl_restore_context_offset; NtContinue(context, 0);} |
如果rip是KiUserExceptionDispatcher地址那么我们就先调用我们注册的回调函数处理异常后判断返回值影响执行原始异常派遣流程。
上面获取RtlRestoreContext主要是因为在这里直接调用此函数达不到用户模式上下文的设置效果导致进程崩溃,所以我这里就先通过特征码定位到它在KiUserExceptionDispatcher函数中的偏移,修改rip让他返回执行到这里就正常啦!

这里的_self_exception_api类型为:typedef LONG(__stdcall* pfnExceptionHandlerApi)(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT context),后面介绍这个。
到这里回调基本就跑起来了,接下来实现修改设置硬件断点(dr0-3)0-3
实现设置硬断函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | int Exception::SetHardWareBreakPoint(const wchar_t* main_modulename, DWORD64 dr7_statu, DWORD64 dr0, DWORD64 dr1, DWORD64 dr2, DWORD64 dr3){ this->_dr0 = dr0; this->_dr1 = dr1; this->_dr2 = dr2; this->_dr3 = dr3; //遍历线程 通过openthread获取到线程环境后设置硬件断点 HANDLE hTool32 = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (hTool32 != INVALID_HANDLE_VALUE) { THREADENTRY32 thread_entry32; //线程环境结构体 thread_entry32.dwSize = sizeof(THREADENTRY32); HANDLE h_hook_thread = NULL; MODULEINFO module_info = { 0 }; //模块信息 HANDLE target_modulehandle = GetModuleHandleW(main_modulename); //从 ntdll.dll 中取出 ZwQueryInformationThread (FARPROC&)ZwQueryInformationThread = ::GetProcAddress(GetModuleHandleA("ntdll"), "ZwQueryInformationThread"); if (target_modulehandle != 0) { //获取模块结束地址 GetModuleInformation(GetCurrentProcess(), (HMODULE)target_modulehandle, &module_info, sizeof(MODULEINFO)); __int64 target_modulehandle_endaddress = ((__int64)module_info.lpBaseOfDll + module_info.SizeOfImage); //遍历线程 if (Thread32First(hTool32, &thread_entry32)) { do { //如果线程父进程ID为当前进程ID if (thread_entry32.th32OwnerProcessID == GetCurrentProcessId()) { h_hook_thread = OpenThread(THREAD_ALL_ACCESS, FALSE, thread_entry32.th32ThreadID); // 获取线程入口地址 PVOID startaddr;//用来接收线程入口地址 ZwQueryInformationThread(h_hook_thread, (THREADINFOCLASS)ThreadQuerySetWin32StartAddress, &startaddr, sizeof(startaddr), NULL); if (((__int64)startaddr >= (__int64)target_modulehandle) && ((__int64)startaddr <= target_modulehandle_endaddress)) { //暂停线程 ULONG previous_count = NULL; NtSuspendThread(h_hook_thread, &previous_count); //设置硬件断点 CONTEXT thread_context = { CONTEXT_DEBUG_REGISTERS }; thread_context.ContextFlags = CONTEXT_ALL; //得到指定线程的环境(上下文) if (!GetThreadContext(h_hook_thread, &thread_context)) return 3; thread_context.Dr0 = dr0; thread_context.Dr1 = dr1; thread_context.Dr2 = dr2; thread_context.Dr3 = dr3; thread_context.Dr7 = dr7_statu; if (NtSetContextThread(h_hook_thread, &thread_context) != NULL) return 4; if (!GetThreadContext(h_hook_thread, &thread_context)) return 3; //恢复线程 NtResumeThread(h_hook_thread, &previous_count); } CloseHandle(h_hook_thread); } } while (Thread32Next(hTool32, &thread_entry32)); } CloseHandle(hTool32); return true; } else return 2;//模块句柄获取失败 } return 0;} |
这里逻辑比较混乱,毕竟半年前写的了...朋友们这里可以自行修改。
上面说的自己实现syscall是因为怕反作弊勾住NtSuspendThread、NtSetContextThread所以干脆自己写不过它的钩子。
另外对于操作的线程一定要排除掉自己的线程,别把自己线程给暂停了恢复不起来(#^.^#),至于设置硬断的坑朋友们自己去踏吧>.<。
到这里此Exception类基本完成了,下面在类外定义好自己的回调函数
定义自己的异常处理函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | LONG WINAPI ExceptionHandler(PEXCEPTION_RECORD exception_record, PCONTEXT context){ //hardware breakpoint if (exception_record->ExceptionCode == EXCEPTION_SINGLE_STEP) { if (exception_record->ExceptionAddress == (PVOID64)exception->_dr0) { //ACE-Base64.dll + 815844 - 48 89 47 08 - mov[rdi + 08], rax //Hook点 //ACE-Base64.dll + 815848 - FF 53 20 - call qword ptr[rbx + 20] //跳过执行 //ACE-Base64.dll + 81584B - 48 8B 1B - mov rbx, [rbx] std::cout << "caller address: " << std::hex << *(DWORD64*)context->Rsi << std::endl; std::cout << "callee address: " << std::hex << *(DWORD64*)(context->Rbx + 0x20) << std::endl; context->Rip = exception->_dr0 + 0x07; return EXCEPTION_CONTINUE_EXECUTION; } else if (exception_record->ExceptionAddress == (PVOID64)exception->_dr1) { return EXCEPTION_CONTINUE_EXECUTION; } else if (exception_record->ExceptionAddress == (PVOID64)exception->_dr2) { return EXCEPTION_CONTINUE_EXECUTION; } else if (exception_record->ExceptionAddress == (PVOID64)exception->_dr3) { return EXCEPTION_CONTINUE_EXECUTION; } else { context->Dr0 = exception->_dr0; context->Dr1 = exception->_dr1; context->Dr2 = exception->_dr2; context->Dr3 = exception->_dr3; return EXCEPTION_CONTINUE_SEARCH; } } //software breakpoint else if (exception_record->ExceptionCode == EXCEPTION_BREAKPOINT) { } return EXCEPTION_CONTINUE_SEARCH;} |
上图中注释的地方展示了我的Hook点,相信懂鹅厂家反作弊的朋友都知道是什么函数了(#^.^#)。
上面拿了Rsi寄存器,它在保存的是Caller地址
Rsp+0x20指向Callee地址。
最后附上函数调用:
1 2 3 4 5 6 7 8 9 10 11 12 | exception = std::make_shared<Exception>();exception->InstallException(ExceptionHandler);DWORD64 ace_base_module = 0;while (true){ ace_base_module = (DWORD64)::GetModuleHandleA("ACE-Base64.dll"); if (ace_base_module > 0x1000) break;}auto value = exception->SetHardWareBreakPoint(L"crossfire.exe", 0x455, ace_base_module + 0x815844, 0x0, 0x0, 0x0);printf("value:%d\n", value); |
效果展示:
经过上面一顿折腾后来启动游戏看看效果:

OK,大功告成!
分析到此结束。
总结:
最后想说的是鹅厂不知道在干嘛,之前检测异常现在就放任不管了,dr也不去遍历>.< 还是希望早点修复吧。
另外有不足的地方还请大家多多指教。
贴上项目地址:https://github.com/gn277/ExceptionByInstCallback.git
硬断检测研究了好几天一点头绪都没有 根据前辈的思路在用户层异常派遣时清空dr和hook
都还是秒噶