反调试技术综述*

2019-09-17 00:39肖植灿
网络安全与数据管理 2019年9期
关键词:断点进程调试

吴 极,王 逍,肖植灿

(云南大学 软件学院,云南 昆明 650504)

0 引言

随着计算机技术的广泛应用,软件产业在人类日常生活、工业生产、科学研究等领域正在发挥着越发重要的作用。但是,在经济利益或纯粹爱好的驱使下,非法用户在未经许可的情况下对软件的非法复制和使用无疑破坏了软件开发人员的利益。这种行为同时也会对全球软件产业的健康发展造成不利影响。单纯依靠法律和道德约束来保护软件是不够的,因此,软件保护技术的开发是非常必要的,它可以保护软件在技术层面上的安全。

可以应用于软件程序以保护它们免受恶意逆向工程的工作机制被称为软件保护[1-2]。目前关于反逆向工程的研究通常侧重于混淆、虚拟机检测、反拆卸和防篡改[3]。但毫无疑问,随着软件产业的快速发展,以反调试技术为中心的软件保护已经成为一个需要受到重视的问题[4]。

本文将对现有的反调试技术进行分类阐述,并在现有反调试技术的基础上,对国内外反调试技术和相关软件保护技术的研究进行深入分析,总结、归纳出应用于软件保护的反调试技术的研究现状和发展趋势。

1 反调试技术概述

反调试是“在计算机代码中实现一种或多种技术,它阻碍了对目标进程进行逆向工程或调试的尝试”[5]。反调试技术是反逆向工程中一种常见的反检测技术。

软件在运行时如果通过反调试技术检测到自身处于被调试状态,就会触发预先设定好的反调试策略,来响应并处理调试行为,若自身未被调试,则软件程序继续正常运行。

文献[6]从逆向分析人员的立场出发,根据调试技术的破解方法将反调试技术分为静态反调试和动态反调试。静态反调试是指在程序启动时,系统会根据正常运行和调试运行分配不同的进程环境,通过检测进程环境来检测进程是否处于调试状态。许多静态反调试技术对OS系统有较强的依赖性,这意味着有些静态反调试技术在不同的平台上可能会失效。动态反调试是指在程序运行时,分析正在运行的进程的执行流程和执行状态是否正常来检测进程是否处于调试状态。例如,遇到异常时,调试器会先接收异常事件而不是直接传递给进程本身,所以可以利用异常处理机制来实现反调试[7]。反调试检测流程如图1所示。

图1 反调试检测流程图

基于运行环境检测的静态反调试技术是对整个程序的保护,反调试手段相对“死板”,破解方式也比较简单。其实程序开发者只是想对程序的关键算法和核心数据进行保护、避免逆向分析。动态反调试则是基于执行状态来判断调试器行为,使其无法正常跟踪程序的执行流程或加大其逆向分析难度。相比静态反调试,动态反调试隐蔽性较强,技术难度更高,破解难度也更大。

2 反调试技术分类

2.1 基于直接检测的反调试

此类别下的反调试方式通过直接观察调试器及其相关信息来检测调试器的存在。该类别存在的原因是调试器最初设计用于调试合法软件。因此,并没有被提供能使调试器隐身的对策。此外,通过调试工具分析程序时,工具会在系统的不同级别留下大量痕迹[8-12],这些系统痕迹可以被用于检查系统是否存在调试器工作。此类别可以基于读取PEB过程环境块、断点、NtQuerySystemInformation()函数、查找系统痕迹、父进程检查等方式实现。

2.1.1 PEB环境块字段

进程环境块(PEB)是系统中每个进程存在的数据结构,包含有关该进程的数据[12]。PEB的不同部分包含可由被直接探测以检测调试器是否存在的信息。总的来说,依靠PEB的反调试策略构成了大部分被观察到的反调试技术[13]。

对于读取PEB过程环境块,可以通过读取此字段的特定API,即IsDebuggerPresent(),CheckRemoteDebuggerPresent()或通过检测PEB其他稍微复杂的内容NtGlobalFLags和ProcessHeap等方式来实现反调试。基于NtGlobalFlags实现反调试如图2所示。LIMC等人在文献[14]中提出了一种基于动态二进制分析的恶意软件检测方法,该方法采取对应策略来破解恶意软件的软件保护。该方法分析了恶意软件基于PEB环境块实现反调试的过程以及对应的破解方法,也说明了单一反调试技术在软件保护方面存在的局限性。

图2 基于NtGlobalFLags实现反调试

2.1.2 断点检测

断点是任何调试过程的重要部分,因此检测断点可以实现检测并中断调试器。基于断点检测的反调试策略是最强大且难以绕过的策略之一。断点检测存在软件断点和硬件断点两种类型[4,9]。软件断点由调试器通过在代码中写入特殊操作码0xCC(int 3h指令)来设置断点。执行此指令时,将引发断点异常并通过Windows传递给调试器。对于硬件断点,该类型使用特定的调试寄存器设置硬件断点:DR0-DR7。使用它们,开发人员可以中断程序的执行并将控制权转移到调试器。

对于断点检测,可以通过自扫描、完整性检验、查找0xCC和使用函数GetThreadContext()检查CPU寄存器等方式实现反调试。TORRUBIA A等人在文献[15]中提出了一种通过编写主动使用所有可用于中断的代码(特别是硬件断点和软件断点中断)来实现反调试的方法。如图3所示。

图3 基于硬件断点实现反调试

2.1.3 父进程检查

通常,应用程序通过双击图标或通过命令行执行时,其父进程名称和对应ID可相应地检索。如果检查的父进程名称属于调试器或父进程名称不为explorer.exe,那么它将是调试器存在的明显标志[16]。同时,父进程ID与explorer.exe的进程ID如果不匹配,它也可能说明调试器存在。但可能存在多个explorer.exe和对应的PID导致匹配过程复杂化,所以基于父进程检查的反调试方式很少被使用[17]。

对于父进程检查,可以通过GetCurrentProcessId()、 CreateToolhelp32Snapshot(). (Process32First())、 Process32Next()等函数进行父进程名称和ID的匹配以实现反调试。基于GetCurrentProcessId实现反调试如图4所示。高兵等人在文献[18]中提出了一种基于代码自修改的软件反跟踪方法来实现软件保护,该方法使用了基于父进程检测的反调试方法来实现反加载模块。

图4 基于GetCurrentProcessId实现反调试

2.1.4 系统痕迹查找

从安装到配置和执行,调试器会在OS的不同级别留下痕迹,例如在文件系统、注册表、进程名称等中留下系统痕迹。因此,可以通过跟踪查找这些系统痕迹实现反调试[8-9]。

可以通过FindWindow()、FindProcess()、FindFirstFile()等API来检测调试器痕迹从而判断是否存在调试器。SHIELDS T在文献[19]中对FindWindow()这一最常见的应用于反调试的查找系统痕迹的API进行了详细介绍,并给出了实现过程,如图5所示。

2.1.5 其他检测方法

除了上述的主动检测方式,还有许多反调试方法基于使用系统信息直接检测是否存在调试器,这也是最直接的检测调试器的存在或操作的方法。

这些基于系统信息的检测机制不直接访问内存区域或内存的索引部分,而是依赖于已记录和未记录的Microsoft API函数调用的功能,这些函数可以在各种.DLL和.SYS文件中导出[17]。大多数情况下,所提出的基于API的检测机制将依赖于底层操作系统调用来直接访问存储器。

对于可用于调试器检测的系统信息,可以通过.NtQueryInformationProcess()、NtQuerySystemInformation()、NtQueryObject()、ZwSetInformationThread()、DebugActiveProcessStop()等API函数来获取并用来实现反调试。基于NtQuryInformationProcess 实现反调试如图6所示。CHEN P等人在文献[16]对这些可以访问系统信息以检测调试器存在的函数进行了列举,对函数功能和实现反调试的方式进行了说明。

图6 基于NtQueryInformationProcess实现反调试

2.2 基于间接推断的反调试

此类别下的反调试方式不是直接观察调试器及其相关信息,而是通过利用系统逻辑的相关信息来计算并评估推断调试器存在的概率。此类别可以基于异常机制、时间差异检测等方式实现。

2.2.1 异常机制

基于异常机制的反调试的原理是调试器捕获某些异常后,可以不将异常正确地传递给内部运行的进程,从而可以在进程内部的进程异常处理机制中检测到该异常,并对其进行操作。

可以利用Windows在存在调试器的情况下使用异常处理机制(Structured Exception Handling,SEH)处理异常的特点来实现反调试。作为异常处理的内部机制,SEH主要在系统级工作。因此,掌握SEH工作原理是基于SEH的软件反调试技术研究的关键问题[20]。

对于异常处理,可以通过int 2d、int 3引发中断异常,通过陷阱标志位或者ICEBP引发单步异常,通过RaiseException函数产生若干不同类型的异常等方式实现反调试。基于int 2d实现反调试如图7所示。BING C等人在文献[21]中提出了一种基于SEH处理机制的将单步异常和代码提取相结合的反调试方法。该方法在存在调试器的情况下,会使得代码中的异常将被调试器接管,则异常处理程序无法触发相应的跳转执行被提取的正常代码的过程,从而使程序无法正常运行以实现反调试。

图7 基于int 2d实现反调试

2.2.2 时间差异

基于时间差异的反调试是通过计算程序运行的时间差异来判断进程是否处于被调试状态。在调试状态下逐行跟踪代码比程序正常运行耗费的时间要长很多,因此,如果程序运行时间超过预先设定程序正常运行的阈值,就可以推断存在调试器。基于时间差异的反调试测试可以分为基于本地API和外部网络资源两种类型。但无论是利用本地时间还是外部资源,对抗基于时间差异的反调试仍然是一个悬而未决的问题[22]。

对于时间差异,可以在本地通过利用本地API(GetTickCount(),QueryPerformanceCounter()等)或者利用CPU rdtsc(读取时间戳计数器) 或者可以通过网络查询区别于本地的外部资源来进行定时来实现反调试。基于rdtsc 实现反调试如图8所示。KANZAKI Y等人在文献[23]提出了一种通过将时间差异检测的反调试方法和自修改代码相结合来实现软件保护的方法。该方法进一步增加了软件破解的难度和成本。自修改代码是一种在正在运行的程序中修改或生成代码的机制[24],其广泛用于运行时的代码生成、优化技术和即时编译器等。

图8 基于rdtsc实现反调试

2.3 基于干扰调试器的反调试

此类别下的反调试方式是因为有一些技术可以被用于干扰调试器的正常运行,这些技术当且仅当程序处于调试器控制时才会试图扰乱程序的运行。此类别可以通过流控制机制、锁定策略、调试器漏洞等方式实现。

2.3.1 流控制

基于Windows系统的隐式流控制机制可用于实现反调试。此方式包括回调、直接隐藏、单线程操作、多线程操作和自调试等子类别。

回调操作常被隐式控制流用来实现反调试。当调试器执行一个回调操作时,执行流将被转移到指定为其参数的函数[25]。它可以使用回调、枚举函数、线程本地存储(Thread Local Storage,TLS)等方式来实现“逃避调试器”的目标。

单线程操作可以通过隐藏线程或暂停线程使调试器无法继续调试,其中暂停线程只对用户模式的调试器生效。单线程操作可以通过使用ETHREAD内核结构的NtSetInformationThread()函数设置字段ThreadHideFromDebugger()来实现隐藏线程,通过使用来自ntdll的SuspendThread()或NtSuspendThread()来实现暂停线程[8]。基于ThreadHideFromDebugger实现反调试如图9所示。

图9 基于ThreadHideFromDebugger实现反调试

多线程操作通过启动在调试器外部运行的不同线程使得核心线程绕过调试器并继续运行来实现反调试。多线程操作可以通过CreateThread()来实现反调试。

自我调试可以防止调试器成功附加到软件中[26]。默认情况下,每个进程只能连接到一个调试器。软件利用这通过运行自身的副本并作为调试器附加到软件本身来阻止另一个调试器附加到软件中。它可以通过DbgUiDebugActiveProcess()或NtDebugActiveProcess()来实现。ABRATH B等人在文献[27]中提出了一种基于自调试的反调试方法,该方法通过将完整的代码片段从应用程序迁移到调试器,让代码处于自调试状态从而实现反调试保护。而完整的代码片迁移使调试器和主应用程序之间的耦合过程更加紧密,使该反调试过程更加难以破解。

2.3.2 锁定策略

在锁定策略中,软件调用系统API来锁定调试者的鼠标、键盘或屏幕。一旦锁定成功后,锁定效果将一直持续到软件进程退出。

基于锁定策略的反调试可以选择通过BlockInput()来阻止鼠标和键盘输入直到调试退出、通过SwitchDesktop()选择让调试工具运行在不同的活动桌面来实现反调试[13],如图10所示。CAIVANO D等人在文献[28]中对勒索软件进行了具体分析,部分勒索软件使用SwitchDesktop()来锁定桌面,在实现勒索目的同时防止其自身被调试。

图10 基于BlockInput实现反调试

2.3.3 调试器漏洞

和所有的软件一样,调试器也存在软件漏洞,这些漏洞是特定调试器独有的,难发现而易攻击。可以通过攻击调试器的软件漏洞实现反调试,但找到调试器漏洞是实现这一反调试方式的难点。

OllyDbg1.1存在格式化字符串漏洞,可以使用OutputDebugString()提供一个%S字符串的参数,让OllyDbg崩溃[29]。同时,也可以通过OutputDebugString()的返回值来检测调试器是否存在。SoftICE中可以通过其发生int 1中断时会产生一个特定异常信息EXCEPTION_SINGLE_STEP (0x80000004)的特点实现反调试[30],如图11所示BRANCO RR等人在文献[31]中对恶意软件使用的防止逆向分析的技术进行了总结,其中对基于调试器漏洞的反调试技术做出了具体的分析。

图11 基于OutputDebugString实现反调试

2.3.4 反调试技术分类总结

总结归纳本文提到的反调试技术分类,所用反调试技术的策略,反调试技术的具体实现方法,反调试技术的实施难度,对抗反调试技术的难度和反调试技术的普遍性,如表1所示(见下页)。

3 反调试技术的发展趋势

对于已有的反调试技术,针对不同的反调试技术有不同的对抗方法,且存在相关研究提出实现反调试破解技术的方法。文献[32]从调试器辅助技术和代码检查及修补两个方面陈述了对抗部分反调试技术的相关策略。文献[33]提出了一个基于规则的反反调试系统,该系统分析和跟踪通用寄存器的值,以确定是否存在反调试指令,并根据分析内容总结出反抗调试规则集,可以避免二进制文件中的反调试技术。文献[34]提出了一个针对环境进行分析的反调试程序自动检测方法,该方法使用调试器和模拟器,通过使用模拟器提取应用程序接口(API)上的跟踪信息以及执行指令,检查并比较提取的指令来自动检测可疑的反调试程序,且该方式不依赖于某种反调试方法,能对大多数常用反调试技术能进行有效检测。文献[35]提出了一个基于系统管理模式(System Management Mode,SMM)的调试框架,该调试框架相对现有的调试器更加“透明化”,可以实现不被反调试技术发现的调试功能。文献[36]提出了一种对混淆的反调试技术进行自动静态检测的检测引擎。

面对越来越多的检测和破解反调试技术的方法,单纯的反调试技术很难保障软件的安全性。近年来提出了将反调试和其他主流软件保护技术结合以实现综合型软件保护方案。

文献[37]将反调试技术与代码混淆、迷宫加密等技术融合,实现了一种防反转,防篡改和防破解效果好的迷宫型软件保护系统。文献[38]中描述的用于软件保护参考架构将反调试、数据混淆、白盒加密、代码隐藏、代码混淆和其他软件相关技术组合,共同实现对软件应用程序的保护。文献[39]提出了一种将反调试和反跟踪、基于安全令牌的加密技术、PE文件调用过程保护结合的打包程序,用于实现对PE文件机密性的保护,防止PE文件被逆向分析。文献[40]介绍了嵌入式系统软件的二进制保护框架,该框架通过反调试和应用加密、软件防篡改等不同机制的结合来确保嵌入式系统软件的完整性并阻碍逆向工程。

表1 反调试技术对比表

同时,随着软件安全技术的进一步发展,针对软件保护的逆向工程技术的研究也会越发深入,软件保护要想在对抗逆向工程的过程中抢占先机,如何比逆向工具运行在系统的更底层无疑是实现软件保护的关键点之一。

针对传统的被限制运行于Ring 0或以上层的反调试技术,文献[41]提出了一种基于硬件虚拟化技术的反调试框架:虚拟机监视器(Virtual Machine Monitor,VMM),它拥有比Ring 0更高的权限级别从而可以禁用断点异常以及阻止应用程序由调试器等其他应用程序外部访问而实现更加可靠的反调试目的。文献[42]参考文献[41]提出的反调试框架实现了一套基于硬件虚拟化技术进行反调试软件保护的框架,在实现VMM的基础上,它还利用硬件虚拟化技术为该框架设计了自隐藏模块来实现更加隐蔽、可靠且对系统性能影响较小的基于反调试的软件保护方法。文献[43]将进程级虚拟机与反调试技术相结合,在运行时将受保护程序和反调试代码转换为虚拟指令,当虚拟机中的反调试技术未检测到程序被调试时从虚拟机中调度程序,同时该方法结合防篡改技术保护虚拟指令来实现软件保护。

4 结论

本文详细介绍了反调试技术,叙述了反调试的定义,阐述了现有反调试技术的分类,并在软件保护技术的基础上展开了对反调试技术的研究。毫无疑问,单一的软件保护技术已经很难实现软件保护的目的,而反调试和代码混淆、软件加壳、软件防篡改、虚拟化等其他主流软件保护技术相结合能够提供更加可靠的软件保护。同时,反调试与其他主流软件保护技术的结合也会改变现有的反调试乃至软件保护的实现方法,促进软件产业健康发展。

猜你喜欢
断点进程调试
断点
高温气冷堆示范工程TSI系统安装及调试
电气仪表自动化安装与调试分析
调试新设备
调试机械臂
债券市场对外开放的进程与展望
用Eclipse调试Python
火力发电机组自启停(APS)系统架构设计方案
一类无限可能问题的解法
改革开放进程中的国际收支统计