Windows 反调试技术——OpenProcess 权限过滤 - ObRegisterCallback
转载: https://blog.xpnsec.com/anti-debug-openprocess/
看雪翻译:https://bbs.pediy.com/thread-223857.htm
本周我有了休息时间,来回顾一下反调试技术。目前,Bug Bounty平台上有大量程序依赖于客户端应用,而且许多安全产品和游戏反作弊引擎都采用了这些反调试技术来阻止你调试核心模块。我想有必要来分享其中一项反调试技术,以及如何绕过它。
本文所述的技术并不是一个安全漏洞,很明显,如果攻击者拥有了这个级别的系统访问权限,游戏就已经结束了。他们只需要安装一个 rookit 就够了。
文中我将以 AVG 产品为例。尽管我尽量避免过多地讨论这一款产品,然而其他的反病毒解决方案和安全产品使用了完全相同的技术,所以相同的原则也同样适用这些产品。
面临什么问题?
如果你以前尝试过打开 x64dbg,并把它附加到一个 AV(译者注:AntiVirus) 组件中,通常会看到如下界面:(下图是GIF动图1)

调试器基本没有附加成功,并停在了启动页。如果我们在调试器内不采用附加的方式,而是直接启动刚才的进程:(下图是GIF动图2)

还是不行,出现了相同的结果。当进程刚要启动时,调试程序被踢出了。最后,我们试试 WinDBG,得到了下面的错误信息:

为了理解调试器刚才做了什么,同时发现哪里出了问题,我们看一下 x64dbg 的源码(实际上,是 x64dbg 使用的调试引擎 TitanEngine 的源码)。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
__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 使用了一个 KernelBase.dll 提供的 Win32 函数 “DebugActiveProcess”。
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.”
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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 权限已经设置到进程令牌(process token)上。
这意味着,调用 DebugActiveProcess 函数的要求已经满足(译者注:要求指的是MSDN中描述的 SE_DEBUG_NAME权限要求)。
接着检查,是否拥有对于目标进程的 PROCESS_ALL_ACCESS 权限。
深入 DebugActiveProcess 内部
DebugActiveProcess 接受唯一的参数是“进程ID”。在该函数内部,使用进程ID 调用 ProcessIdToHandle,打开目标进程的句柄:

进入 ProcessIdToHandle 函数内部,可以发现该函数仅仅是对 NtOpenProcess 的封装:

NftOpenProcess函数中有一个形参叫做“Desired Access”,即所需的访问权。该参数的实参是 C3Ah。通过微软官方文档发现,这个值是以下值的组合:
- PROCESS_CREATE_THREAD (译者注:0x0002)
- PROCESS_VM_OPERATION (0x0008)
- PROCESS_VM_WRITE (0x0020)
- PROCESS_VM_READ (0x0010)
- PROCESS_SUSPEND_RESUME (0x0800)
- PROCESS_QUERY_INFORMATION (0x0400)
于是,这次调用具备了调试进程所需要的全部授权。
到这里,调试器具备了 SE_DEBUG_NAME 授权,DebugActiveProcess 调用也给自身赋予了正确的访问目标进程的权限。
那么是什么阻止了附加过程呢?
ObRegisterCallbacks 简介
我是在一个游戏模组社区(译者注:即游戏mod社区)中第一次知道 ObRegisterCallbacks 函数的。在绕过反作弊和 DRM 驱动时,该函数被用于阻止修改或注入游戏功能。
按照微软官方说法,ObRegisterCallbacks 是“这样一个函数,它为线程、进程、桌面句柄操作注册了一系列回调函数。”这是在操作系统内核态完成的。主要是给驱动程序开发者提供一种能力,用于在 OpenProcess 函数被调用时和返回时收到通知。
为什么这个函数能够用于阻止调试器访问 AV 进程呢?阻止 DebugActiveProcess 调用成功的其中一个方法就是,过滤掉 “调用NtOpenProcess“所需要的访问权限(译者注:NtOpenProcess 函数有一个形参 DesiredAccess,这里指的是,该参数对应的实参被过滤后,就不是所需要的值了)。通过移除调试器“请求目标进程的 PROCESS_ALL_ACCESS 访问权”的能力,我们就无法调试一个进程。这也解释了刚刚在 WinDBG看到的错误。
怎么确认这就是问题所在呢?我们接着进入内核调试器,观察注册的回调函数是如何在 Ring-0 被处理的。(这里不会详细介绍如何使用内核调试器,如果你需要一些资料,可以阅读我之前的博客)
深入 ObRegisterCallback 内部
当启动内核调试后,从 nt!ProcessType 开始分析:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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 注册的函数。
之后,其中的每个函数都会在获取进程句柄时由内核调用。基于这个理解,我们将遍历这个列表,找到阻止成功调用 OpenProcess 函数的回调函数句柄。
CallbackList 是一个 _LIST_ENTRY,指向 CALLBACK_ENTRY_ITEM 结构体。该结构体在微软的文档中没有说明,然而有一篇文章 “DOUGGEM’S GAME HACKING AND REVERSING NOTES” 给出了结构体的定义:
|
1
2
3
4
5
6
7
8
9
|
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 列表:
|
1
|
!list -x ".if (poi(@$extret+0x28) != 0) { u poi(@$extret+0x28); }" (poi(nt!PsProcessType)+0xc8) |

在我的电脑上,有 4 个驱动程序通过 ObRegisterCallbacks 注册了 PreOperation 回调函数。
接着,我们通过 WinDBG 输出驱动程序的名字:
|
1
|
!list -x ".if (poi(@$extret+0x28) != 0) { lmv a (poi(@$extret+0x28)) }" (poi(nt!PsProcessType)+0xc8) |

这 4 个驱动程序中,其中一个立刻引起了我们关注,很可能它就是问题的关键:avgSP.sys。

可以判断出:就是 “AVG self protection module” 模块在阻止我们将调试器附加到进程中(更有可能的是,当反病毒引擎阻止恶意软件时,产生了这样的副作用)。接着,我们深入分析下这个驱动程序,找出其影响 OpenProcess 调用的痕迹。
首先,找到 ObRegisterCallbacks 函数,它注册了一个函数句柄:

我们如果检查这个刚注册的函数句柄,可以发现:

在反汇编代码中,出现了一个幻数(Magic Number)A0121410。实际上,它表示以下权限:
- PROCESS_VM_READ (译者注:0x0010)
- PROCESS_QUERY_INFORMATION (0x0400)
- PROCESS_QUERY_LIMITED_INFORMATION (0x1000)
- READ_CONTROL (0x00020000L)
- SYNCHRONIZE (0x00100000L)
其实,如果只设置这些权限的话,则没有进一步的权限检查操作,OpenProcess 函数继续执行。然而,如果请求上述权限白名单以外的权限,还要执行一系列的检查操作,最终在函数返回前,所需要的权限被过滤掉。

由于本文主要讲解“识别和移除”这种钩子的通用方法,所以我不打算深入驱动程序的细节了。
从上面的分析可知,我们发现有一个驱动程序在拦截和修改 OpenProcess 调用。
现在,已经找到问题根源,接下来就是从内核中拆下这个钩子。
移除 OpenProcess 权限过滤
为了移除 OpenProcess 的权限过滤函数,首先需要找到过滤函数所在的 PreOperation 属性的地址。输入 WinDBG 命令:
|
1
|
!list -x ".if (poi(@$extret+0x28) != 0) { .echo handler at; ?? @$extret+0x28; u poi(@$extret+0x28); }" (poi(nt!PsProcessType)+0xc8) |

一旦发现了正确的属性地址,我们使用下面的命令将其置为 NULL,以此来禁止回调句柄:
|
1
|
eq 0xffffa002`d31bacf8 0 |
此时,再次将调试器附加到被调试程序,可以得到如下界面:

太棒了!看上去我们已经成功了。
嗯,几乎是……我们稍加操作就可以发现大量错误,问题还没有处理干净。
即使在上述界面,我们也可以看到寄存器的值都是0,并且出现了访问冲突。这一定是漏掉了什么。
记住还有线程
我们已经知道 ObRegisterCallbacks 函数可以给 OpenProcess 加上钩子函数,还能做什么呢?再次查看官方文档发现,ObRegisterCallbacks 也可以给 OpenThread 加上钩子。

庆幸的是,很多工作已经完成了,我们只需要找到线程的钩子函数所在的位置即可。这个位置恰好定义在 nt!PsThreadType 中。
修改一下刚才输入的命令,观察驱动程序(译者注:指的是 avgSP.sys)是否为 OpenThread 函数添加了钩子:
|
1
|
!list -x ".if (poi(@$extret+0x28) != 0) { .echo handler at; ?? @$extret+0x28; u poi(@$extret+0x28); }" (poi(nt!PsThreadType)+0xc8) |

真的有钩子!和刚才的进程钩子类似,我们使用 eq 命令移除钩子:
|
1
|
eq 0xffffc581`89df32e8 0 |
再次附加调试器到进程:(下图是GIF动图3)

大功告成!可以开始正常调试了。
希望本文有助于你了解这项反调试技术。如果感兴趣,还有很多 Bug Bounty 程序可供学习,包括 BugCrowd 平台上 AVG 的一个例子(点击这里)、Cylance、Sophos等等。(尽管我没有把这些作为安全漏洞,但是 DKOM 不在讨论范围)(译者注:DKOM,全称是 Direct kernel object manipulation)。
参考资料
Windows 反调试技术——OpenProcess 权限过滤 - ObRegisterCallback的更多相关文章
- Windows反调试技术(上)
写在前面 在逆向工程中为了防止破解者调试软件,通常都会在软件中采用一些反调试技术来防破解.下面就是一些在逆向工程中常见的反调试技巧与示例. BeingDebuged 利用调试器加载程序时调试器会通过C ...
- Windows反调试技术(下)
OD的DBGHELP模块 检测DBGHELP模块,此模块是用来加载调试符号的,所以一般加载此模块的进程的进程就是调试器.绕过方法也很简单,将DBGHELP.DLL改名. #include <Wi ...
- 反调试技术常用API,用来对付检测od和自动退出程序
在调试一些病毒程序的时候,可能会碰到一些反调试技术,也就是说,被调试的程序可以检测到自己是否被调试器附加了,如果探知自己正在被调试,肯定是有人试图反汇编啦之类的方法破解自己.为了了解如何破解反调试技术 ...
- 基于TLS的反调试技术
TLS(Thread Local Storage 线程局部存储) 一个进程中的每个线程在访问同一个线程局部存储时,访问到的都是独立的绑定于该线程的数据块.在PEB(进程环境块)中TLS存储槽共64个( ...
- Windows Kernel Way 1:Windows内核调试技术
掌握Windows内核调试技术是学习与研究Windows内核的基础,调试Windows内核的方式大致分为两种: (1)通过Windbg工具在Windows系统运行之初连接到Windows内核,连接成功 ...
- Linux下的反调试技术
Linux下的反调试技术 2014年01月30日 ⁄ 综合 ⁄ 共 2669字 ⁄ 字号 小 中 大 ⁄ 评论关闭 转自 http://wangcong.org/blog/archives/310 ...
- Delphi_OD_代码_调试_Delphi反调试技术(以OD为例附核心原代码) (转)
1.程序窗口[chuang kou]句柄[ju bing]检测原理:用FindWindow函数[han shu]查找[cha zhao]具有相同窗口[chuang kou]类名和标题的窗口[chuan ...
- Delphi_OD_代码_调试_Delphi反调试技术(以OD为例附核心原代码)
1.程序窗口[chuang kou]句柄[ju bing]检测原理:用FindWindow函数[han shu]查找[cha zhao]具有相同窗口[chuang kou]类名和标题的窗口[chuan ...
- 反调试技术(Delphi版)
1.程序窗口句柄检测原理:用FindWindow函数查找具有相同窗口类名和标题的窗口,如果找到就说明有OD在运行//****************************************** ...
随机推荐
- Nginx下隐藏index.php
在用NGINX搭建web网站时遇到一个问题,那就是除了网站的首页能够正常打开,其他的子网站都打不开,显示找不到文件.但是如果你在网址后面加上index.php,子网站就又可以打开了.那么我们怎么才能不 ...
- httprunner学习20-跳过用例skip/skipIf/skipUnless
前言 在实际工作中,我们有时候会需要对测试用例加判断,比如某个接口功能暂时去掉了,我们希望对这个用例skip不去执行. 当其它的接口依赖于登陆接口返回的token时候,如果登陆都失败了,后面的接口,我 ...
- C#串口关闭SerialPort.Close()导致的卡死
https://blog.csdn.net/fengda2870/article/details/51554838上面的链接给出了提示: 将Invoke变为BeginInvoke. 亲测可行. pri ...
- 《团队名称》第八次团队作业:Alpha冲刺day4
项目 内容 这个作业属于哪个课程 2016计算机科学与工程学院软件工程(西北师范大学) 这个作业的要求在哪里 实验十二 团队作业8-软件测试与ALPHA冲刺 团队名称 快活帮 作业学习目标 (1)掌握 ...
- 查看服务器被访问最大的ip
网站有时会很卡,可以先看看哪些ip访问最多,一行命令就可以列出来,如下所示 netstat -ntu | awk '{print $5}' | cut -d: -f1 | sort | uniq -c ...
- IMP本质上是一个通用的函数指针
IMP:通用的函数指针 /// A pointer to the function of a method implementation. #if !OBJC_OLD_DISPATCH_PROTOTY ...
- 第1章 Spring的应用
一.Spring 的两种核心容器:BeanFactory 和 ApplicationContext(都通过xml加载Bean的) 二.通过ApplicationContext实例化: 1.通过Clas ...
- 【每天学一点Linux】快速清除文件内容
linux几种快速清空文件内容的方法 几种快速清空文件内容的方法: $ : > filename #其中的 : 是一个占位符, 不产生任何输出. $ > filename $ echo “ ...
- re.sub 实现多处替换
1 | 表示或的意思 将所有字母替换掉 result_content = re.sub('a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z', ...
- luogu_4317: 花神的数论题
花神的数论题 题意描述: 设\(sum(i)\)表示\(i\)的二进制数中\(1\)的个数. 给定一个整数\(N\),求\(\prod_{i=1}^Nsum(i)\). 输入描述: 输入包含一个正整数 ...