本周我有了休息时间,来回顾一下反调试技术。目前,Bug Bounty平台上有大量程序依赖于客户端应用,而且许多安全产品和游戏反作弊引擎都采用了这些反调试技术来阻止你调试核心模块。我想有必要来分享其中一项反调试技术,以及如何绕过它。
本文所述的技术并不是一个安全漏洞,很明显,如果攻击者拥有了这个级别的系统访问权限,游戏就已经结束了。他们只需要安装一个 rookit 就够了。
文中我将以 AVG 产品为例。尽管我尽量避免过多地讨论这一款产品,然而其他的反病毒解决方案和安全产品使用了完全相同的技术,所以相同的原则也同样适用这些产品。
如果你以前尝试过打开 x64dbg,并把它附加到一个 AV(译者注:AntiVirus) 组件中,通常会看到如下界面:(下图是GIF动图1)
调试器基本没有附加成功,并停在了启动页。如果我们在调试器内不采用附加的方式,而是直接启动刚才的进程:(下图是GIF动图2)
还是不行,出现了相同的结果。当进程刚要启动时,调试程序被踢出了。最后,我们试试 WinDBG,得到了下面的错误信息:
为了理解调试器刚才做了什么,同时发现哪里出了问题,我们看一下 x64dbg 的源码(实际上,是 x64dbg 使用的调试引擎 TitanEngine 的源码)。
从代码中发现,x64dbg 使用了一个 KernelBase.dll 提供的 Win32 函数 “DebugActiveProcess”。
DebugActiveProcess 函数用于在目标进程上开启一个调试会话。该函数的唯一参数是目标进程的PID。如果在 MSDN 上查阅该函数,可以看到如下的描述:
“The debugger must have appropriate access to the target process, and it must be able to open the process for PROCESS_ALL_ACCESS.
DebugActiveProcess can fail if the target process is created with a security descriptor that grants the debugger anything less than full access.
If the debugging process has the SE_DEBUG_NAME privilege granted and enabled, it can debug any process.”
从上述代码中可以看到,SE_DEBUG_NAME 权限已经设置到进程令牌(process token)上。
这意味着,调用 DebugActiveProcess 函数的要求已经满足(译者注:要求指的是MSDN中描述的 SE_DEBUG_NAME权限要求)。
接着检查,是否拥有对于目标进程的 PROCESS_ALL_ACCESS 权限。
DebugActiveProcess 接受唯一的参数是“进程ID”。在该函数内部,使用进程ID 调用 ProcessIdToHandle,打开目标进程的句柄:
进入 ProcessIdToHandle 函数内部,可以发现该函数仅仅是对 NtOpenProcess 的封装:
NftOpenProcess函数中有一个形参叫做“Desired Access”,即所需的访问权。该参数的实参是 C3Ah。通过微软官方文档发现,这个值是以下值的组合:
于是,这次调用具备了调试进程所需要的全部授权。
到这里,调试器具备了 SE_DEBUG_NAME 授权,DebugActiveProcess 调用也给自身赋予了正确的访问目标进程的权限。
那么是什么阻止了附加过程呢?
我是在一个游戏模组社区(译者注:即游戏mod社区)中第一次知道 ObRegisterCallbacks 函数的。在绕过反作弊和 DRM 驱动时,该函数被用于阻止修改或注入游戏功能。
按照微软官方说法,ObRegisterCallbacks 是“这样一个函数,它为线程、进程、桌面句柄操作注册了一系列回调函数。”这是在操作系统内核态完成的。主要是给驱动程序开发者提供一种能力,用于在 OpenProcess 函数被调用时和返回时收到通知。
为什么这个函数能够用于阻止调试器访问 AV 进程呢?阻止 DebugActiveProcess 调用成功的其中一个方法就是,过滤掉 “调用NtOpenProcess“所需要的访问权限(译者注:NtOpenProcess 函数有一个形参 DesiredAccess,这里指的是,该参数对应的实参被过滤后,就不是所需要的值了)。通过移除调试器“请求目标进程的 PROCESS_ALL_ACCESS 访问权”的能力,我们就无法调试一个进程。这也解释了刚刚在 WinDBG看到的错误。
怎么确认这就是问题所在呢?我们接着进入内核调试器,观察注册的回调函数是如何在 Ring-0 被处理的。(这里不会详细介绍如何使用内核调试器,如果你需要一些资料,可以阅读我之前的博客)
当启动内核调试后,从 nt!ProcessType 开始分析:
这个符号包含了一个指向 _OBJECT_TYPE 类型对象的指针,该对象定义了 “Process” 类型,并包含了一个CallbackList属性。
这个属性值得我们注意。该属性定义了一个回调函数列表,其中存储了由 ObRegisterCallbacks 注册的函数。
之后,其中的每个函数都会在获取进程句柄时由内核调用。基于这个理解,我们将遍历这个列表,找到阻止成功调用 OpenProcess 函数的回调函数句柄。
CallbackList 是一个 _LIST_ENTRY,指向 CALLBACK_ENTRY_ITEM 结构体。该结构体在微软的文档中没有说明,然而有一篇文章 “DOUGGEM’S GAME HACKING AND REVERSING NOTES” 给出了结构体的定义:
结构体中的 PreOperation 引起了我们的注意。
通过如下 WinDBG 命令,遍历 CALLBACK_ENTRY_ITEM 列表:
在我的电脑上,有 4 个驱动程序通过 ObRegisterCallbacks 注册了 PreOperation 回调函数。
接着,我们通过 WinDBG 输出驱动程序的名字:
这 4 个驱动程序中,其中一个立刻引起了我们关注,很可能它就是问题的关键:avgSP.sys。
可以判断出:就是 “AVG self protection module” 模块在阻止我们将调试器附加到进程中(更有可能的是,当反病毒引擎阻止恶意软件时,产生了这样的副作用)。接着,我们深入分析下这个驱动程序,找出其影响 OpenProcess 调用的痕迹。
首先,找到 ObRegisterCallbacks 函数,它注册了一个函数句柄:
我们如果检查这个刚注册的函数句柄,可以发现:
在反汇编代码中,出现了一个幻数(Magic Number)A0121410。实际上,它表示以下权限:
其实,如果只设置这些权限的话,则没有进一步的权限检查操作,OpenProcess 函数继续执行。然而,如果请求上述权限白名单以外的权限,还要执行一系列的检查操作,最终在函数返回前,所需要的权限被过滤掉。
由于本文主要讲解“识别和移除”这种钩子的通用方法,所以我不打算深入驱动程序的细节了。
从上面的分析可知,我们发现有一个驱动程序在拦截和修改 OpenProcess 调用。
现在,已经找到问题根源,接下来就是从内核中拆下这个钩子。
为了移除 OpenProcess 的权限过滤函数,首先需要找到过滤函数所在的 PreOperation 属性的地址。输入 WinDBG 命令:
一旦发现了正确的属性地址,我们使用下面的命令将其置为 NULL,以此来禁止回调句柄:
此时,再次将调试器附加到被调试程序,可以得到如下界面:
[注意]APP应用上架合规检测服务,协助应用顺利上架!