在本文中,我们将介绍如何使用Frida绕过一些应用程序实现的反调试技术的实际示例。
Frida
Frida是一个动态代码检测工具包。换句话说,它是一组允许代码插装的工具,提供给我们一些API,使我们能够在执行过程中拦截、分析和修改Windows、macOS、GNU/Linux、iOS、Android和QNX应用程序的部分代码。本质上,Frida允许在运行时对即将执行的操作进行操作。比如,当我们从一个简单的c++程序开始,该程序使用一个函数将两个值相加并返回结果。我们想要操作的函数声明为Add(int,int)。首先,我们将更改其中一个int参数,然后,更改返回的结果。编译完代码后,我们需要通过分析可执行代码来定位Add函数的偏移量。在本例中,我们使用IDA Pro来分析代码,但也可以使用允许我们分析可执行代码的任何其他方法或应用程序。我们在偏移量0x00401000处确定函数,并且我们知道可执行文件的基址是0x00400000,因此,函数的偏移量是0x00001000。
然后,我们可以拦截函数调用并使用Frida修改其行为。我们将开发一个小Javascript脚本来完成这项工作。首先,我们需要确定程序在执行时被加载的位置、它的基址,然后,我们可以通过将这个基址和之前获得的偏移量(base + 0x00001000)相加来定位Add函数的偏移量。现在,我们可以使用带有函数地址的Interceptor来在函数执行之前或之后添加一些代码。我们使用 Frida Javascript API 来完成这一切。
因此,当我们以' 1 '和' 2 '作为参数执行应用程序时,我们期望结果是' 1 + 2 = 3 '。但是,如果我们取消注释第一行(args[0] = ptr(' 100 ');)在函数执行之前,我们将变量op1的值替换为100,得到的结果为' 1 + 2 = 102 '。另一方面,如果取消注释第二行(retval.replace(' 3210 ')) ,我们会在函数 Add 执行之后但在返回结果之前替换返回值,得到 '1 + 2 = 3210'。
基于系统调用的技术
在这个检测结果中,我们考虑使用Windows API的函数来获取与调试器存在相关的信息的技术。有很多函数可以用于这个目标:从像IsDebuggerPresent这样的函数,它返回一个布尔值,这个值取决于应用程序是否被调试(True或False),到像FindWindow这样的函数,它告诉我们是否存在一个带有知名调试器(IDA、Ollydbg、Inmunity 调试器等)名称的窗口。
基于内存检查的技术
应用程序对内存中的某些标志进行显式验证的方法,这些标志显示进程是否正在调试。可以用于此目的的一些标志是 IsDebugged 标志、Heap标志或NTGlobalFlag。这些标志是 Windows 为每个进程维护的结构的成员,其中包含有关它们的信息。
基于时间的技术
这个检测结果包括使用与时间相关的计算来确定是否正在调试进程的方法,当一个进程正在被调试时,执行同一组指令所花费的时间要比未被调试时多。这种时差通常是很明显的。出于这个原因,应用程序可以检查一组指令执行开始和结束的时间,如果它花费的时间超过一个既定的阈值,它可以很有可能确定该过程是正在调试。
基于异常的技术
最后,我们根据触发异常对一组方法进行分组,以确定进程是否正在被调试。在调试进程和未调试进程时,系统处理异常的方式是不同的。程序可以利用这一事实来确定是否附加了调试器。
设置测试环境
为了实现我们的设置,我们将使用Windows 10虚拟机,我们最初将在其中安装 Python 3.8.6rc1。
然后我们安装Frida,它可以直接从GitHub下载,或者使用Python pip工具安装。我们使用pip是因为它比其他方法更简单。
我们还安装了Visual Studio Community 2019来开发示例程序,在示例程序中我们实现了一些反调试技术来展示它是如何工作的。这些程序将被用来测试绕过这些反调试技术的不同方法。Frida 的使用方式有很多种:我们可以直接使用包中包含的可执行文件(每个可执行文件都有特定的功能),也可以使用包中也包含的 Python 模块来开发我们自己的接口。
我们选择了第二种方法,开发了一个小接口,允许我们生成新的进程或附加到现有进程中,注入一个或多个提供某些功能的脚本。我们选择这种方式是因为我们希望能够根据特定的需求定制接口,这些需求将在以后的文章中详细介绍。
接下来,我们将讨论基于以下系统调用的技术:IsDebuggerPresent、NtQueryInformationProcess 和 CreateToolhelp32Snapshot。所有这些都使我们能够了解如何以不同的方式使用 Frida,以绕过这些控制。
IsDebuggerPresent
我们将尝试绕过的第一个检查是基于系统调用 IsDebuggerPresent 的方法。正如 Microsoft 文档中所指出的,此函数不接收任何参数,并根据进程是否正在调试返回一个布尔值:返回值“True”表示正在调试进程,“False”表示相反。为了说明这个方法,我们开发了一个使用这个系统调用执行调试检测的简单程序:
在第一个控制台中,我们看到当我们从Visual Studio执行应用程序时,应用程序如何指示它正在被调试。但是,如果应用程序是直接从终端执行的,则表明它没有被调试。检查这个事实的另一种方法是从像 x64dbg 这样的调试器中执行它。唯一用来做决定的是函数 IsDebuggerPresent 返回的值,因此,我们应该用 Frida 开发一个脚本,拦截这个调用并修改返回值,总是返回 False (0x0)。下面的脚本是为了完成所有这些而开发的:
首先,它使用 Module.findExportByName(第 3 行)定位函数“IsDebuggerPresent”的地址。
一旦获得这个地址,它就会使用 Interceptor.attach(...) 拦截这个调用(第 8 行)。
最后,它在函数结束之前(第 13 行)将返回值替换为 0x0 (False)。
NtQueryInformationProcess
我们将展示的第二个系统调用是NtQueryInformationProcess。这个函数让我们获得与进程相关的不同信息。它比上一个更复杂,因为它允许我们使用ProcessInformationClass参数选择要查询的信息,并且它将在ProcessInformation参数上为我们提供这些信息。
在本例中,我们将展示如何绕过 4 项检查,每项检查使用不同的 ProcessInformationClass 值。这些值是:ProcessDebugPort、ProcessDebugFlags、ProcessDebugObjectHandle 和 ProcessBasicInformation。
ProcessDebugPort (0 x7)
如果附加了任何调试器,此类用于获取调试器的端口号。如果附加了调试器,此值将不同于 0。
ProcessDebugFlags (0x1F)
使用此类,我们可以检索一个标志,该标志将为我们提供有关活动调试器存在的信息。在本例中,如果processinformation参数中返回的值为0,则表明正在调试应用程序。
ProcessDebugObjectHandle (0x1E)
将此值表示为 ProcessInformationClass,仅当正在调试进程时,此系统调用才会返回有效的句柄。
ProcessBasicInformation (0 x0)
使用这个类,我们在 ProcessInformation 参数中获得了一个名为 PROCESS_BASIC_INFORMATION 的结构,其中包括其他数据:指向 PEB 结构的指针(偏移量 0x4)、进程的 PID(偏移量 0x16)和父进程的 PID(偏移量 0x20)。软件可以使用此信息应用的一种反调试技术是使用父进程的 PID 获取父进程的名称,并将其与众所周知的调试器名称列表进行检查。
如上所述,我们开发了一个小型 C++ 应用程序,展示了这种技术的一个示例。为了逃避这些检查,我们必须使用 Frida 开发一个必须执行以下操作的脚本:
首先,它必须使用 Module.findExportByName 定位函数“NtQueryInformationProcess”的地址。
然后,它必须使用 Interceptor.attach(...) 拦截函数调用。
每次调用函数“NtQueryInformationProcess”(OnEnter)时,脚本必须执行以下操作:
保存参数 ProcessInformationClass,允许我们选择必须修改哪些返回信息(第 40 行)。
保存指向返回参数 ProcessInformation 的指针(第 44、48、52 和 57 行)。
还要保存每种情况下所需的参数。
最后,就在函数结束之前(OnLeave),脚本可以使用之前保存的信息来确定需要使用参数 ProcessInformation 返回什么值。根据 this.ProcessInformationClass 我们应该返回以下值:
0x7 (ProcessDebugPort),ProcessInformation 将被替换为值 0x0(第 63 行)。
0x1F (ProcessDebugFlags),ProcessInformation 将被替换为值 0x1(第 68 行)。
0x1E(ProcessDebugObjectHandle),在本例中,我们将检查返回值,如果成功,则将参数ProcessInformation替换为0x0。参数 ReturnLength 也将被替换(第 72 - 81 行)。
0x0 (ProcessBasicInformation),最后,如果选择了这个选项,我们应该知道 PROCESS_BASIC_INFORMATION 结构体将父进程的 PID (InheritedFromUniqueProcessId, offset 0x20) 替换为一个非可疑的 PID,例如进程 ‘explorer.exe’ 的 PID。
要使用 Frida 获取进程“explorer.exe”的 PID,我们可以使用 Windows API 调用,例如函数 GetShellWindow 和 GetWintowThreadProcessId。我们可以使用 NativeFunction del API de Frida 通过 Javascript 声明这些函数,一旦它们被声明,我们就可以使用它们来获取进程 PID,如下所示:
使用Visual Studio调试器执行这个示例程序,我们得到如下结果:
如果我们使用相同的调试器执行相同的应用程序,但注入之前描述的 Frida 脚本,我们会得到另一个结果:
CreateToolhelp32Snapshot
接下来我们将讨论CreateToolhelp32Snapshot函数。使用这个函数的最常见的反调试技术是验证父进程的名称和PID,确定父进程是否是一个知名的调试器,但是这个函数也可以用于其他目的。首先,这个函数创建一个包含一些关于进程、线程和模块的系统信息的快照。可以使用第一个参数选择该快照的信息。允许使用以下值:TH32CS_INHERIT、TH32CS_SNAPALL、TH32CS_SNAPHEAPLIST、TH32CS_SNAPMODULE、TH32CS_SNAPMODULE32、TH32CS_SNAPPROCESS、TH32CS_SNAPTHREAD。
使用该函数最基本的反调试技术是只使用TH32CS_SNAPPROCESS来获取正在运行的进程列表。然后,查找我们的进程,识别我们的进程父进程的 PID,然后在列表中查找父进程的信息。最后,验证父进程的信息。但是,通过使用不同标志或使用 TH32CS_SNAPALL的函数给出的信息,我们可以执行其他验证。例如:
处理相关验证(使用 TH32CS_SNAPPROCESS):
在整个列表中搜索被禁止的进程(如 VsDebugConsole.exe、devenv.exe、x32dbg.exe...),无论该进程是否相关;
模块相关验证(使用TH32CS_SNAPMODULE和TH32CS_SNAPMODULE32):
在与我的进程相关的模块列表中搜索被禁止的模块(如 frida-agent.dll ...)。
线程相关验证(使用 TH32CS_SNAPTHREAD):
验证列出的所有线程都有一个相关联的进程,试图检测隐藏的进程;
验证应用程序线程数;
验证我的应用程序中没有引用禁止模块的任何线程。
可见,使用此函数的应用程序可以验证必须在它们之间保持一致的不同内容。因此,要绕过这些检查,我们的任务必须是找到一种变通方法,允许我们绕过所有这些检查,并保持列表的一致性。带着这个目标,我们研究了这个函数究竟返回了哪些信息,以及我们如何操作它。
函数 CreateToolhelp32Snapshot 返回一个 SECTION 类型的 HANDLE。如果我们分析 HANDLE 指向的内存部分,我们可以找到一个未记录的结构,其中包含以下部分:
我们可以使用NtQuerySection和NtMapViewOfSection函数来修改与Handle相关的内存,从而修改CreateToolhelp32Snapshot函数返回的快照的内容。因此,我们可以开发一个Frida脚本来修改CreateToolhelp32Snapshot返回的列表,隐藏可以检查以检测调试器的进程、模块和线程,但保持了其一致性。
首先,我们应该定义一个应该隐藏的进程和模块列表。我们需要知道他们的名字。在这个例子中,我们将隐藏Visual Studio调试器以及与Frida相关的可执行文件和模块。所以我们定义了2个列表,内容如下:
禁止进程列表:VsDebugConsole.exe、devenv.exe、frida-winjector-helper-32.exe
禁止模块列表:frida-agent.dll
一旦我们确定要隐藏哪些进程和模块,我们将像以前一样挂钩函数 CreateToolhelp32Snapshot。在本例中,我们不修改任何参数或返回值。我们将在函数返回句柄之前截取函数返回句柄,并在返回发生之前执行一些代码。正如我们之前看到的,使用函数NtQuerySection和NtMapViewOfSection,我们将定位与句柄相关的内存,我们将执行以下步骤:
进程列表修改:
我们应该从进程列表中删除那些在禁止进程列表中找到的进程。
我们应该删除对禁止进程的引用,以保持一致性。
例如,我们将要调试的程序可能有一个被禁止的进程作为父进程。因此,我们应该将 Parent 的 PID 值更改为非可疑进程的 PID(如 explorer.exe PID)。
模块列表修改:
我们应该从模块列表中删除那些在禁止模块列表中找到的模块。
线程列表修改:
我们应该从线程列表中删除那些由进程列表中删除的进程拥有的线程;
我们应该从线程列表中删除那些指向从模块列表中删除的模块的线程;
我们可以通过使用具有 THREAD_QUERY_INFORMATION 权限的函数 OpenThread 和请求 ThreadQuerySetWin32StartAddress 的 NtQueryInformationThread 来获取此信息。将此查询获得的信息与与禁止模块关联的内存进行比较,我们可以确定是否应该从列表中删除线程。
本文翻译自:https://www.layakk.com/blog/practical-examples-with-fridafrida-vs-anti-debug-techniques-on-windows-ii/如若转载,请注明原文地址