Windows反调试技术:过滤OpenProcess

一、前言

前一阵子我一直在研究与SYSTEM权限有关的技术,本周我想暂时休息一下,把目光转到一些反调试技术上。现在许多漏洞奖励计划中都会包含客户端应用程序方面内容,而许多安全产品(以及游戏中的反作弊引擎)会使用各种技术阻止用户调试核心组件,本文中我们介绍了某一种反调试技术以及相应的规避方法。

显而易见的是,本文中介绍的技术并不属于漏洞范畴,如果攻击者已经掌握系统的访问权限,那么就不用费那么多事,他们很有可能已经在系统中装上了rootkit,完成攻击过程。

在本文中我测试的是AVG安全产品,但其他许多AV解决方案及安全产品也使用了完全相同的技术,因此本文介绍的方法也可以在这些产品上使用。

 

二、具体问题

如果你之前曾经尝试使用x64dbg调试工具附加(attach)到某个AV组件上,你经常会看到如下画面,这一点已经见惯不怪:

实际结果是调试器无法成功attach到该进程上,我们只能茫然地盯着屏幕,不知下一步该干啥。另外,如果我们不attach,而是选择在调试器中启动该进程,会看到如下画面:

还是得到一样的结果,当程序即将启动时我们就已被踢出局。最后,像其他逆向分析师一样,我们尝试使用WinDBG来分析,结果会得到如下错误信息:

为了理解调试器的工作原理,也为了让我们更好地了解到底哪里出了问题,我们可以分析一下x64dbg的代码(实际上我们需要分析的是TitanEngine的代码,TitanEngine是x64dbg使用的调试引擎),看看我们无法attach进程背后的具体原因。

__declspec(dllexport) bool TITCALL AttachDebugger(DWORD ProcessId, bool KillOnExit, LPVOID DebugInfo, LPVOID CallBack)
{
...
if(ProcessId != NULL && dbgProcessInformation.hProcess == NULL)
{
    if(engineEnableDebugPrivilege)
    {
        EngineSetDebugPrivilege(GetCurrentProcess(), true);
        DebugRemoveDebugPrivilege = true;
    }
    if(DebugActiveProcess(ProcessId))
    {
    ...
    }
}
}

这里我们可以看到,x64dbg使用的是DebugActiveProcess.aspx)这个API函数,该API为KernelBase.dll提供的Win32 API。

 

三、DebugActiveProcess的工作原理

DebugActiveProcess 负责启动针对目标进程的调试会话,以进程的PID值作为参数。如果我们在MSDN.aspx)上查找这个函数,我们可以得到如下信息:

“调试器必须具备目标进程的合适权限,必须能够以PROCESS_ALL_ACCESS方式打开目标进程。

如果目标进程创建时使用了安全描述符(security descriptor),使调试器无法获得完全访问权限,那么调用DebugActiveProcess时可能会失败。如果调试进程具备SE_DEBUG_NAME权限,那么它就可以调试任何进程。”

这段话给了我们第一个提示信息,提示我们究竟是什么导致我们的调试会话无法正常建立。从上述代码片段中,我们可以看到调试器正在调用EngineSetDebugPrivilege,那么我们来看看这个函数的具体内容:

DWORD EngineSetDebugPrivilege(HANDLE hProcess, bool bEnablePrivilege)
{
    DWORD dwLastError;
    HANDLE hToken = 0;
    if(!OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
    {
        ...
    }
        ...
    if(!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
    {
        ...
    }
    tokenPrivileges.PrivilegeCount = 1;
    tokenPrivileges.Privileges[0].Luid = luid;
    if(bEnablePrivilege)
        tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    else
        tokenPrivileges.Privileges[0].Attributes = 0;
    AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, sizeof(TOKEN_PRIVILEGES), NULL, NULL);
    ...
}

在上述代码中,我们的进程具备SE_DEBUG_NAME权限,也就是说我们满足了上述要求,可以从调试器中调用DebugActiveProcess函数。

接下来我们需要看一下我们是否拥有目标进程的PROCESS_ALL_ACCESS访问权限。

 

四、DebugActiveProcess内部工作流程

DebugActiveProcess这个API仅接受一个参数,那就是目标进程的进程ID,函数内部会调用ProcessIdToHandle,利用这个值获得目标进程的句柄。

如果我们跳转到ProcessIdToHandle函数,我们会发现该函数只是NtOpenProcess API的封装函数:

对于NtOpenProcess函数,其DesiredAccess参数的传入值为C3Ah。查阅相关文档.aspx)后,我们发现这个值为一些标志的组合值,这些标志为:

  • PROCESS_CREATE_THREAD
  • PROCESS_VM_OPERATION
  • PROCESS_VM_WRITE
  • PROCESS_VM_READ
  • PROCESS_SUSPEND_RESUME
  • PROCESS_QUERY_INFORMATION

使用这个值后,函数就可以拥有所需的所有权限,开始调试目标进程。

现在,我们已经知道调试器具备SE_DEBUG_NAME权限,并且DebugActiveProcess调用也拿到了目标进程的正确的访问权限。那么,到底是什么在阻止我们调试目标进程?

 

五、ObRegisterCallbacks登场

当我加入游戏以及游戏修改(modding)社区时,我才第一次见到ObRegisterCallbacks的身影,社区爱好者在绕过反作弊及DRM驱动时,经常使用该函数在游戏中修改或注入其他功能。

根据微软的说法,ObRegisterCallbacks.aspx)是“一个例程,负责为线程、进程以及桌面句柄操作注册一系列回调例程”。这一过程在内核模式下完成,借助这一功能,当调用OpenProcess函数以及当OpenProcess函数返回时,驱动程序可以获得相关通知信息。

但为什么这个函数可以阻止调试器访问AV进程呢?想要阻止程序成功调用DebugActiveProcess的一种方法就是过滤掉在NtOpenProcess调用期间所请求的访问权限。阻止调试器请求目标进程的PROCESS_ALL_ACCESS权限后,我们自然就失去调试该进程的能力。这一方法也顺便解释了我们之前在WinDBG中看到的错误现象。

但是我们还需要确定这是否就是我们当前问题的症结所在。现在让我们使用内核调试器,看一下Ring-0状态下系统如何处理已注册的回调函数。

(我不会在本文中介绍如何设置内核调试器,大家可以查看我之前发表的文章了解具体细节)。

 

六、ObRegisterCallback内部流程

建立内核调试器连接后,首先我们可以使用nt!ProcessType来获得一些信息:

kd> dt nt!_OBJECT_TYPE poi(nt!PsProcessType)
+0x000 TypeList         : _LIST_ENTRY [ 0xffffcb82`dee6cf20 - 0xffffcb82`dee6cf20 ]
+0x010 Name             : _UNICODE_STRING "Process"
+0x020 DefaultObject    : (null) 
+0x028 Index            : 0x7 ''
+0x02c TotalNumberOfObjects : 0x26
+0x030 TotalNumberOfHandles : 0xe8
+0x034 HighWaterNumberOfObjects : 0x26
+0x038 HighWaterNumberOfHandles : 0xea
+0x040 TypeInfo         : _OBJECT_TYPE_INITIALIZER
+0x0b8 TypeLock         : _EX_PUSH_LOCK
+0x0c0 Key              : 0x636f7250
+0x0c8 CallbackList     : _LIST_ENTRY [ 0xffffa002`d31bacd0 - 0xffffa002`d35d2450 ]

该符号提供了指向_OBJECT_TYPE对象的一个指针,该对象定义了“Process”类型,其中我们最感兴趣的是CallbackList属性。这个属性定义了由ObRegisterCallbacks.aspx)注册的一系列回调函数,随后,当有程序尝试获取进程句柄时内核就会调用这些回调函数。知道这些信息后,我们可以遍历回调函数列表,查找有哪些已注册的回调会干扰我们调用OpenProcess

CallbackList是指向CALLBACK_ENTRY_ITEM结构的一个LIST_ENTRY。微软文档中并没有公开这个结构,感谢“DOUGGEM’S GAME HACKING AND REVERSING NOTES”这个网站,我们最终可以得知该结构的具体定义,如下所示:

typedef struct _CALLBACK_ENTRY_ITEM {
LIST_ENTRY EntryItemList;
OB_OPERATION Operations;
CALLBACK_ENTRY* CallbackEntry;
POBJECT_TYPE ObjectType;
POB_PRE_OPERATION_CALLBACK PreOperation;
POB_POST_OPERATION_CALLBACK PostOperation;
__int64 unk;
}CALLBACK_ENTRY_ITEM, *PCALLBACK_ENTRY_ITEM;

这里我们关心的是该结构中的PreOperation属性。

我们可以使用如下WinDBG命令来遍历CALLBACK_ENTRY_ITEM列表:

!list -x ".if (poi(@$extret+0x28) != 0) { u poi(@$extret+0x28); }" (poi(nt!PsProcessType)+0xc8)

在我的测试环境中,我总共发现了4个驱动使用ObRegisterCallbacks注册了PreOperation回调函数。我们可以使用WinDBG获得驱动名称,以便后续分析:

!list -x ".if (poi(@$extret+0x28) != 0) { lmv a (poi(@$extret+0x28)) }" (poi(nt!PsProcessType)+0xc8)

上面列出的4个驱动中,有一个非常值得怀疑,那就是avgSP.sys

“AVG自我保护模块(AVG self protection module)”这个驱动会限制我们使用调试器来附加到相关进程(其实该驱动最大的作用很有可能是防止恶意软件篡改反病毒引擎)。让我们深入分析这个驱动,看是否有任何蛛丝马迹表明该驱动的确会修改我们的OpenProcess调用。

首先,搜索ObRegisterCallbacks后,我们可以找到某个处理程序的注册过程,如下所示:

如果我们检查已注册的处理程序,我们很快就能看到如下类似信息:

上图显示的汇编代码中,A0121410这个魔术值实际上对应的是如下权限:

  • PROCESS_VM_READ
  • PROCESS_QUERY_INFORMATION
  • PROCESS_QUERY_LIMITED_INFORMATION
  • READ_CONTROL
  • SYNCHRONIZE

如果只设置了这些权限,那么驱动就不会执行后续检查过程,因而就不会过滤OpenProcess调用。然而,除了这几个白名单权限以外,如果程序请求其他权限,驱动会进一步执行一些检查过程,最终在调用返回前过滤掉相应的权限:

这里我不会详细介绍驱动的工作细节,因为本文的目的是介绍如何识别并移除各种产品所使用的这种hook的通用方法,根据前面已知的这些信息,我们已经找到了干扰并修改OpenProcess调用的那个驱动。

找到幕后黑手后,现在我们可以从内核中去掉已绑定的这个处理程序。

 

七、解开OpenProcess过滤器

如果想解开OpenProcess过滤器,我们首先需要找到与过滤器函数对应的PreOperation属性的地址。我们可以使用如下WinDBG命令完成这一任务:

!list -x ".if (poi(@$extret+0x28) != 0) { .echo handler at; ?? @$extret+0x28; u poi(@$extret+0x28); }" (poi(nt!PsProcessType)+0xc8)

识别出正确的地址后,我们可以清空这个指针,禁用掉处理函数,具体命令如下:

eq 0xffffa002`d31bacf8 0

此时,如果我们重新attach调试器,我们可以看到如下画面:

非常好,看上去我们已经打败了这个反调试技术!

好吧,其实结果并不完美。经过一些交互操作后,我们注意到调试器中会出现一些错误信息,事情并没有那么顺利。即使是在上一张图中,我们也可以发现寄存器的值全部为0,并且出现了访问冲突(access violation)问题,肯定还有一些细节我们没有注意到。

 

八、解开线程过滤器

根据前文描述,我们已经知道ObRegisterCallbacks可以用来hook OpenProcess,但这个函数还有其他用途吗?如果我们回头查一下官方文档.aspx),就会知道该函数也可以用来hook OpenThread调用:

幸运的是,最难的那一部分工作我们已经完成了,现在我们所需要做的就是找到线程回调函数所存储的具体位置,该位置对应的是nt!PsThreadType

我们可以修改前面用过的那条WinDBG命令,查看这个驱动是否hook了OpenThread

!list -x ".if (poi(@$extret+0x28) != 0) { .echo handler at; ?? @$extret+0x28; u poi(@$extret+0x28); }" (poi(nt!PsThreadType)+0xc8)

果不其然,我们找到了这个hook。与进程hook类似,我们可以使用类似的eq命令解开这个过滤器:

eq 0xffffc581`89df32e8 0

再次启动调试器:

现在,我们终于得到了完美的调试器会话。

 

九、总结

许多安全产品会使用这种反调试方法,希望阅读本文后,大家对这种方法有所了解。如果大家对此感兴趣,可以探索一下相关的漏洞奖励项目,比如bugcrowd上有一些安全产品的漏洞奖励计划,这些产品包括AVGCylanceSophos等(但我并没有将本文介绍的这种方法以漏洞内容提交,因为DKOM(Direct Kernel Object Manipulation,直接操作内核对象)技术很有可能不属于漏洞奖励范畴)。

 

十、参考资料

(完)