首页
社区
课程
招聘
[原创] 记录一次鹅厂反作弊绕过之利用回调完成异常派遣的提前接收
2024-5-14 11:11 12702

[原创] 记录一次鹅厂反作弊绕过之利用回调完成异常派遣的提前接收

2024-5-14 11:11
12702

研究背景:

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


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

收藏
免费 10
打赏
分享
最新回复 (18)
雪    币: 563
活跃值: (762)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Dr_Knox 2024-5-15 14:18
2
0
强的哥们,想问下如果我用InstrumentationCallback来设置RIP跳转shellcode进行内存注入理论上可行吗?
雪    币: 619
活跃值: (255)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
GN-顾念 2024-5-15 14:47
3
1
我试过可以注入的,记得做好内存释放时机的判断
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_argpsphl 2024-5-15 19:21
4
0
硬断检测研究了好几天一点头绪都没有 根据前辈的思路在用户层异常派遣时清空dr和hook 都还是秒噶
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_argpsphl 2024-5-15 19:23
5
0
hook GetThreadContext
雪    币: 619
活跃值: (255)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
GN-顾念 2024-5-15 19:59
6
0
mb_argpsphl [em_4]硬断检测研究了好几天一点头绪都没有 根据前辈的思路在用户层异常派遣时清空dr和hook [em_35]都还是秒噶
哪家的反作弊
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_argpsphl 2024-5-15 20:19
7
0
GN-顾念 哪家的反作弊
企鹅厂 我试过Suspend drv模块的线程可以暂时干掉 但不是个长久之计 加了混淆也搞不明白啥原理
雪    币: 619
活跃值: (255)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
GN-顾念 2024-5-15 20:29
8
0
mb_argpsphl 企鹅厂 我试过Suspend drv模块的线程可以暂时干掉 但不是个长久之计 加了混淆也搞不明白啥原理
你是不是用AddVectoredExceptionHandler注册了异常处理函数,我记得它半年前在KiUserExceptionDispatcher这个函数里某个地方下了个hook大概率是通过派遣异常时进入他的检测函数遍历到了你注册的异常处理函数,并不会去扫dr寄存器。我测过只要不向VEH链表里插函数就没事
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_argpsphl 2024-5-15 20:48
9
0
GN-顾念 你是不是用AddVectoredExceptionHandler注册了异常处理函数,我记得它半年前在KiUserExceptionDispatcher这个函数里某个地方下了个hook大概率是通过派遣异 ...
没有 我目前的方法是hook KiUserExceptionDispatcher处理异常+内存断点方案 但是bug多又慢
雪    币: 619
活跃值: (255)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
GN-顾念 2024-5-15 20:58
10
0
mb_argpsphl 没有 我目前的方法是hook KiUserExceptionDispatcher处理异常+内存断点方案 但是bug多又慢
crc也是个问题
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_argpsphl 2024-5-15 21:31
11
0
也不能说crc 测试下来只要动dr就嘎
雪    币: 619
活跃值: (255)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
GN-顾念 2024-5-15 21:39
12
0
mb_argpsphl 也不能说crc 测试下来只要动dr就嘎
可以用回调注册自己的异常派遣配合软断,不需要去hook函数
雪    币: 12848
活跃值: (9103)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
hzqst 3 2024-5-15 22:09
13
0

不是,这波我失误了,我以为没那么多检测的,我感觉下次我就不会被嘎号了。
我是看这个藤子他这个检测关了我就上了,主要是我代码的执行速度是藤子代码的七倍我才上的,我原本想的是在它检测到我之前就执行完了的,但是没办法,检测来的太快了我没躲开。没办法,检测来的太快了,我觉得我这个代码是可以跟ACE换血的,主要是因为我当时的代码有ACE七倍的执行速度,我要是没有ACE七倍的执行速度我都不会去碰这个检测,而且当时检测的hook已经没了,我才去碰这个检测的。不是,你不把反作弊开在那儿我能去绕吗?反作弊放那儿不就是让人绕的吗?不是,我知道有调试器,调试器能让检测失效吗?

最后于 2024-5-15 22:23 被hzqst编辑 ,原因:
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_argpsphl 2024-5-15 22:30
14
0
GN-顾念 可以用回调注册自己的异常派遣配合软断,不需要去hook函数
软断就会处罚hook点的crc了
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_argpsphl 2024-5-15 22:32
15
0
可能是你那个游戏没开dr调试寄存器的检测 我这个测试veh和dr都有 还有自己的异常处理  走不到seh
雪    币: 12
活跃值: (239)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
clestor 2024-5-15 22:48
16
0
懂了,下次DR寄存器占坑,看你怎么操作。
雪    币: 152
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_wdgnqlfd 2024-5-27 20:13
17
0
win7 有人尝试过吗
雪    币: 619
活跃值: (255)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
GN-顾念 2024-5-28 08:41
18
0
mb_wdgnqlfd win7 有人尝试过吗
win7需要更新获取RtlRestoreContext偏移的硬编码
雪    币: 2948
活跃值: (3291)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
院士 2024-6-23 09:38
19
0
感谢分享,学习。
游客
登录 | 注册 方可回帖
返回