-
-
[翻译]Windows系统下的反调试机制第二部分(翻译自Symantec官网)
-
发表于: 3天前 891
-
这是一个经典的反调试技巧,用于迷惑较弱的Debugger。其原理是在一段有效的指令序列中插入一个 INT3 操作码。当执行到 INT3 时,如果程序未被调试,控制权将交给保护机制的异常处理程序,并继续执行。
由于 INT3 指令通常被Debugger用来设置软件断点,通过插入 INT3 操作码,可以欺骗Debugger认为这是其自身设置的断点。因此,控制权不会交给异常处理程序,程序的正常执行流程会被改变。为了避免被这种技巧欺骗,Debugger需要跟踪其设置的软件断点位置。
此外,需注意,INT3 还可以编码为 0xCD, 0x03。
通过这种方式,程序可以判断是否正在被调试,并采取相应的保护措施。
所谓的 "Ice 断点" 是 Intel 的未公开指令之一,其操作码为 0xF1。它通常用于检测程序是否正在被跟踪(tracing)。
执行此指令会触发一个 SINGLE_STEP 异常。因此,如果程序已经在被跟踪,Debugger会认为这是由于设置了标志寄存器中的 SingleStep 位而导致的正常异常。此时,相关的异常处理程序将不会被执行,程序的执行流程也不会按预期继续。
绕过此技巧的方法很简单:可以直接运行该指令,而不是逐步执行它。虽然会触发异常,但如果程序未被跟踪,Debugger应能够理解需要将控制权交给异常处理程序。
这种方法可用于检测Debugger的存在,因为Debugger在处理 Ice 断点 产生的异常时可能会表现出异常行为,从而暴露其存在。
执行 INT 2Dh(中断 2Dh)指令时,如果程序未被调试,会触发一个断点异常。如果程序正在被调试,但未设置 trace flag(跟踪标志位),则不会产生异常,程序会正常继续执行。如果程序正在被调试并且指令被逐步跟踪,执行时会跳过下一字节并继续执行。
因此,INT 2Dh 可用作一种强大的反调试和反跟踪机制,通过检测Debugger的行为来判断是否正在被调试。
此方法通过引入未公开或非常规的异常行为,巧妙地利用Debugger可能的错误行为来检测调试状态。
高精度计数器存储自机器启动以来的 CPU 周期数,可以通过 RDTSC 指令查询。经典的反调试方法是在程序的关键点(通常是异常处理程序周围)测量时间差(delta)。如果时间差过大,可能表明程序运行在Debugger的控制下,因为Debugger处理异常并将控制权交还给被调试程序需要较长时间。
这种方法利用Debugger处理异常的时间开销来检测调试行为,非常适合用于对抗调试和逆向。通过调整阈值,可以根据目标系统和Debugger的性能优化检测精度。
陷阱标志(Trap Flag, TF)位于 Flags 寄存器中,用于控制程序的逐条指令跟踪。如果该标志被设置,每执行一条指令都会触发一个 SINGLE_STEP 异常。通过操作陷阱标志,可以对抗Debugger的跟踪。
例如,以下指令序列将设置陷阱标志:
Debugger可以直接运行该指令序列而不逐步跟踪,避免触发异常,从而绕过此反跟踪技巧。
这种方法利用Debugger对标志寄存器的干扰行为检测调试状态,通常用于对抗Debugger的跟踪功能。虽然Debugger可以通过快速运行指令序列绕过,但它仍是一种有效的防护措施,用于与其他反调试技术结合。
这是一个非常独特的反跟踪技巧,首次发现于一个名为 MarCrypt 的加壳工具中。这种方法通过对Debugger处理特定指令序列的行为进行检测,显得相对少见且高效。
原理:
关键点:
这种方法利用Debugger在处理栈段寄存器(SS
)和标志寄存器(Flags
)的交互行为中的潜在弱点,检测是否存在调试行为。由于Debugger需要额外的操作来处理这些特殊指令序列,这为开发者提供了一种有效的反调试手段。
调试寄存器(DR0 到 DR7)用于设置硬件断点。保护机制可以操作这些寄存器,以检测是否设置了硬件断点(从而判断程序是否被调试)、清除寄存器内容,或将其设置为特定值以供后续代码检查。例如,像 tElock 这样的加壳工具利用调试寄存器来防止逆向。
从用户模式来看,调试寄存器无法直接通过特权指令(如 mov drx, ...
)来设置。但有以下两种间接方法:
大多数保护机制更倾向于使用第一种 "非官方" 的方法。
这种方法通过检测和操作调试寄存器,有效防止逆向和调试。它不仅能够清理寄存器状态,还能检测Debugger行为(如硬件断点的存在),是反调试保护的重要手段。
与调试寄存器操作类似,上下文也可以用来以非常规方式修改程序的执行流。Debugger很容易因此被迷惑!
需要注意的是,另一个系统调用 NtContinue 可以用来加载新的上下文到当前线程中(例如,该系统调用由异常处理程序管理器使用)。
这种反调试方法在几年前并不广为人知。它通过在 PE 可选头中第 10 个目录项(Thread Local Storage 条目)引用程序的第一个入口点来指示 PE 加载器。这样,程序的入口点不会首先被执行,而是由 TLS 回调执行反调试检查,从而以隐蔽的方式运行。
需要注意的是,这种技术在实际中并不广泛使用。尽管较旧的Debugger(包括 OllyDbg)不支持 TLS,但可以通过插件或自定义补丁工具轻松应对。
封包工具常用的一种保护功能是 CC 扫描循环,旨在检测由Debugger设置的软件断点。为避免此类问题,可以使用硬件断点或自定义类型的软件断点。例如 CLI(0xFA) 是替代传统 INT3(0xCC) 操作码的一个不错选择。该指令满足断点需求:如果由用户模式程序执行,它会引发一个特权指令异常,同时仅占用 1 字节空间。
某些封包文件的入口点 RVA 被设置为 0,这意味着它们会从 MZ...
开始执行,对应指令为 dec ebx / pop edx ...
。
虽然这本质上不是一种反调试技巧,但如果使用软件断点在入口点处中断,可能会导致问题。
例如,如果创建一个挂起的进程,然后在 RVA 0 设置 INT3,将会覆盖部分 MZ
魔术值的内容(如 'M'
)。当进程创建时,MZ
已经被检查过,但当通过 ntdll 恢复进程以进入入口点时,魔术值会再次被检查。在这种情况下,会引发 INVALID_IMAGE_FORMAT 异常。
为避免此问题,如果你创建自己的跟踪或调试工具,建议使用硬件断点。
了解恶意软件或保护程序中常见或非常见的反调试和反跟踪技术,对逆向工程师来说是有用的知识。程序总能找到方法检测它是否在Debugger中运行——这同样适用于虚拟或仿真环境。但由于用户模式Debugger是最常见的分析工具之一,了解常见技巧及其绕过方法始终是有用的。
补充一下:GPT-4o给出的8的内容(原文网页并没有详细描述)
上下文修改是一种非常规的方式,用于改变程序的执行流,类似于对调试寄存器的操作。这种技术可以有效迷惑Debugger,使其在分析程序时出现错误。
上下文修改通过操控线程上下文(包含寄存器、标志寄存器、调试寄存器等信息)来干扰程序的正常执行流。上下文修改可以通过以下两种方式实现:
通过异常处理机制:
通过系统调用:
以下是一个使用异常处理机制修改上下文的示例:
在此示例中:
NtContinue
是另一种实现上下文修改的方法。此调用用于加载新的上下文至当前线程,并恢复执行。例如:
调试器在以下场景中容易受到干扰:
此文第一部分翻译时间为 2019年6月下旬,当时说好吃个饭再接着翻译来着,忘了发生了个什么,耽搁了,一搁置就忘了,后来又有好多事发生。导致直到最近重新逛看雪的时候才发现当时居然还被评为优秀文章(挺不好意思的)
既然当初说了还要翻译后半部分,咱就说到做到了。
当初还有人问我要原文链接来着,这儿一并贴出。
原文:https://community.broadcom.com/symantecenterprise/communities/community-home/librarydocuments/viewdocument?DocumentKey=230d68b2-c80f-4436-9c09-ff84d049da33&CommunityKey=1ecf5f55-9545-44d6-b0f4-4e4a7f5f5e68
push offset @handler ; 压入异常处理程序的地址 push dword fs:[0] ; 保存原始的异常处理链 mov fs:[0], esp ; 设置新的异常处理程序 ; ... db 0CCh ; 插入 INT3(0xCC) ; 如果执行到这里,说明程序被调试 ; ... @handler: ; 异常处理程序 ; 继续执行 ; ...
push offset @handler ; 压入异常处理程序的地址 push dword fs:[0] ; 保存原始的异常处理链 mov fs:[0], esp ; 设置新的异常处理程序 ; ... db 0F1h ; 插入 "Ice" 断点 (0xF1) ; 如果执行到这里,说明程序正在被跟踪 ; ... @handler: ; 异常处理程序 ; 继续执行 ; ...
push offset @handler ; 压入异常处理程序的地址 push dword fs:[0] ; 保存原始的异常处理链 mov fs:[0], esp ; 设置新的异常处理程序 ; ... db 02Dh ; 插入中断 2Dh (INT 2Dh) mov eax, 1 ; 如果程序被跟踪,此指令可能被跳过(反跟踪) ; ... @handler: ; 异常处理程序 ; 继续执行 ; ...
push offset handler ; 压入异常处理程序的地址 push dword ptr fs:[0] ; 保存原始异常处理链 mov fs:[0], esp ; 设置新的异常处理程序 rdtsc ; 读取时间戳计数器 push eax ; 保存当前计数值 xor eax, eax ; 清除 eax div eax ; 触发除零异常 rdtsc ; 再次读取时间戳计数器 sub eax, [esp] ; 计算时间差(tick delta) add esp, 4 ; 清理堆栈 pop fs:[0] ; 恢复异常处理链 add esp, 4 cmp eax, 10000h ; 与阈值比较 jb @not_debugged ; 如果时间差小于阈值,则未被调试 @debugged: ; 调试状态处理 ... @not_debugged: ; 未调试状态处理 ... handler: mov ecx, [esp+0Ch] ; 读取异常上下文 add dword ptr [ecx+0B8h], 2 ; 跳过触发的 div 指令 xor eax, eax ; 清除 eax ret ; 返回继续执行
pushf ; 将当前标志寄存器的值压入栈 mov dword [esp], 0x100 ; 设置栈顶为 0x100(启用陷阱标志) popf ; 将栈顶值弹回标志寄存器,更新 TF
push ss ; 将栈段寄存器的值压入栈 pop ss ; 将栈顶值弹回栈段寄存器 pushf ; 将标志寄存器的值压入栈 nop ; 空操作
push ss ; 保存栈段寄存器 ; 一些无关指令(混淆Debugger) pop ss ; 恢复栈段寄存器 pushf ; 保存标志寄存器 ; 一些无关指令(混淆Debugger) pop eax ; 恢复标志寄存器的值到 eax and eax, 0x100 ; 检查 TF(陷阱标志) or eax, eax ; 设置条件标志 jnz @debugged ; 如果 TF 被设置,跳转到调试状态处理 ; 正常执行 ... @debugged: ; 检测到Debugger,终止程序
push offset handler ; 压入异常处理程序的地址 push dword ptr fs:[0] ; 保存原始异常处理链 mov fs:[0], esp ; 设置新的异常处理程序 xor eax, eax ; 清除 eax div eax ; 触发除零异常 pop fs:[0] ; 恢复异常处理链 add esp, 4 ; 继续执行 ; ... handler: mov ecx, [esp+0Ch] ; 获取异常上下文结构 add dword ptr [ecx+0B8h], 2 ; 跳过触发异常的指令 mov dword ptr [ecx+04h], 0 ; 清除 DR0 mov dword ptr [ecx+08h], 0 ; 清除 DR1 mov dword ptr [ecx+0Ch], 0 ; 清除 DR2 mov dword ptr [ecx+10h], 0 ; 清除 DR3 mov dword ptr [ecx+14h], 0 ; 清除 DR6 mov dword ptr [ecx+18h], 0 ; 清除 DR7 xor eax, eax ; 清除 eax ret ; 返回继续执行
push offset handler ; 压入异常处理程序的地址 push dword ptr fs:[0] ; 保存原始异常处理链 mov fs:[0], esp ; 设置新的异常处理程序 xor eax, eax ; 清除 eax div eax ; 触发除零异常 pop fs:[0] ; 恢复异常处理链 add esp, 4 ; 继续执行 ; ... handler: mov ecx, [esp+0Ch] ; 获取异常上下文结构 mov dword ptr [ecx+0B8h], new_execution_address ; 修改 EIP(指令指针) xor eax, eax ; 清除 eax ret ; 返回并加载新上下文
CONTEXT context;// 填充 context 结构(设置新的寄存器值、标志等)NtContinue(&context, FALSE);
- 未调试状态:触发异常,程序转到异常处理程序继续执行。
- 调试状态(非逐步执行):无异常,程序正常执行。
- 调试状态(逐步执行):跳过下一字节,可能导致程序逻辑被修改,从而暴露Debugger。
- 如果时间差较小(低于阈值),说明程序未被调试。
- 如果时间差较大,可能表示程序运行在Debugger中,因为Debugger处理异常和恢复控制需要更多时间。
- 如果程序未被调试,执行上述指令会成功设置陷阱标志,从而触发逐条指令跟踪。
- 如果程序被调试,Debugger通常会干扰对标志寄存器的直接操作,导致 TF 无法真正被设置。随后触发的异常被Debugger处理,程序的异常处理程序不会被执行。
- 如果Debugger正在逐步跟踪,当执行到
popf
时,陷阱标志(TF)可能会被隐式设置。 - 程序随后会检测 TF 是否被设置。如果被设置,表明程序正在被跟踪,进入调试状态处理。
- 通过异常处理机制获取线程上下文(包含调试寄存器的状态),然后修改寄存器值并恢复执行。
- 使用系统调用 NtGetContextThread 和 NtSetContextThread(在用户态通过 GetThreadContext 和 SetThreadContext 提供)直接访问和修改线程上下文。
- 使用 div 0 指令触发除零异常,使程序进入异常处理程序。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课