转载: 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.”

这里,我们发现了导致调试会话失败的端倪。
上述的代码片段中,调试器调用了 EngineSetDebugPrivilege 函数。那么,来看看这个函数。
 
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 -".if (poi(@$extret+0x28) != 0) { u poi(@$extret+0x28); }" (poi(nt!PsProcessType)+0xc8)
 

在我的电脑上,有 4 个驱动程序通过 ObRegisterCallbacks 注册了 PreOperation 回调函数。

接着,我们通过 WinDBG 输出驱动程序的名字:

1
!list -".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 -".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 -".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 的一个例子(点击这里)、CylanceSophos等等。(尽管我没有把这些作为安全漏洞,但是 DKOM 不在讨论范围)(译者注:DKOM,全称是 Direct kernel object manipulation)。

参考资料

Windows 反调试技术——OpenProcess 权限过滤 - ObRegisterCallback的更多相关文章

  1. Windows反调试技术(上)

    写在前面 在逆向工程中为了防止破解者调试软件,通常都会在软件中采用一些反调试技术来防破解.下面就是一些在逆向工程中常见的反调试技巧与示例. BeingDebuged 利用调试器加载程序时调试器会通过C ...

  2. Windows反调试技术(下)

    OD的DBGHELP模块 检测DBGHELP模块,此模块是用来加载调试符号的,所以一般加载此模块的进程的进程就是调试器.绕过方法也很简单,将DBGHELP.DLL改名. #include <Wi ...

  3. 反调试技术常用API,用来对付检测od和自动退出程序

    在调试一些病毒程序的时候,可能会碰到一些反调试技术,也就是说,被调试的程序可以检测到自己是否被调试器附加了,如果探知自己正在被调试,肯定是有人试图反汇编啦之类的方法破解自己.为了了解如何破解反调试技术 ...

  4. 基于TLS的反调试技术

    TLS(Thread Local Storage 线程局部存储) 一个进程中的每个线程在访问同一个线程局部存储时,访问到的都是独立的绑定于该线程的数据块.在PEB(进程环境块)中TLS存储槽共64个( ...

  5. Windows Kernel Way 1:Windows内核调试技术

    掌握Windows内核调试技术是学习与研究Windows内核的基础,调试Windows内核的方式大致分为两种: (1)通过Windbg工具在Windows系统运行之初连接到Windows内核,连接成功 ...

  6. Linux下的反调试技术

    Linux下的反调试技术 2014年01月30日 ⁄ 综合 ⁄ 共 2669字 ⁄ 字号 小 中 大 ⁄ 评论关闭 转自  http://wangcong.org/blog/archives/310 ...

  7. Delphi_OD_代码_调试_Delphi反调试技术(以OD为例附核心原代码) (转)

    1.程序窗口[chuang kou]句柄[ju bing]检测原理:用FindWindow函数[han shu]查找[cha zhao]具有相同窗口[chuang kou]类名和标题的窗口[chuan ...

  8. Delphi_OD_代码_调试_Delphi反调试技术(以OD为例附核心原代码)

    1.程序窗口[chuang kou]句柄[ju bing]检测原理:用FindWindow函数[han shu]查找[cha zhao]具有相同窗口[chuang kou]类名和标题的窗口[chuan ...

  9. 反调试技术(Delphi版)

    1.程序窗口句柄检测原理:用FindWindow函数查找具有相同窗口类名和标题的窗口,如果找到就说明有OD在运行//****************************************** ...

随机推荐

  1. 提高python处理数据的效率方法

    处理大数据的方法有很多,目前我知道就这么多,后面会持续更新: 一.将数据分批次读取 csv格式是常见的数据存储方式,对于我们普通人而言易于读写.此外,在pandas中有pd.read_csv()函数可 ...

  2. 详解php概念以及主配置文件

    浏览器仅能够解码HTML格式的文档,对于非HTML格式的文档,浏览器调用插件或者通过CGI接口调用其他程序来解码. 动态网站: 我们在服务器端或客户端执行了一段脚本或者一段程序,这段程序执行的结果根据 ...

  3. Httpd服务入门知识-http协议版本,工作机制及http服务器应用扫盲篇

    Httpd服务入门知识-http协议版本,工作机制及http服务器应用扫盲篇 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.Internet与中国 Internet最早来源于美 ...

  4. Kali linux network is unreachable

    使用树莓派4 安装完kali linux后,配置好ip,能ping通上级路由. 但ping公网地址显示网络不可达. Ping: connect: network is unreachable 解决方法 ...

  5. 上传自己的构件(Jar)到Maven中央仓库

    背景: 用了Maven之后,你有没有这样的想法,自己一直在使用别人贡献的代码,自己能不能把自己觉得好的代码也贡献出来让大家方便. 还有如果你也是一名程序员,你会不会觉得要是把自己积累起来日常常用的代码 ...

  6. go mod 使用

    go modules 是 golang 1.11 新加的特性.现在1.12 已经发布了,是时候用起来了.Modules官方定义为: 模块是相关Go包的集合.modules是源代码交换和版本控制的单元. ...

  7. Windows下PHP7/5.6以上版本 如何连接Oracle 12c,并使用PDO

    https://blog.csdn.net/houpanqi/article/details/78841928 首先,本篇文章重点分享的是:在Win平台下,如何使用PHP7连接Oracle 12C,所 ...

  8. Colorful events

  9. LeetCode 1249. Minimum Remove to Make Valid Parentheses

    原题链接在这里:https://leetcode.com/problems/minimum-remove-to-make-valid-parentheses/ 题目: Given a string s ...

  10. 华三NAT总结

    1.保证网络之间的连通性,可以用动态路由协议使网络连通. 2.配置一条默认路由到外网. ip route-static 0.0.0.0 0 210.33.44.2 3.进入连接外网的端口,把这个端口设 ...