前文请前往-> https://bbs.kanxue.com/thread-252156.htm
CPU反调试
(1) Rogue Int3
这是一个经典的反调试技巧,用于迷惑较弱的Debugger。其原理是在一段有效的指令序列中插入一个 INT3 操作码。当执行到 INT3 时,如果程序未被调试,控制权将交给保护机制的异常处理程序,并继续执行。
由于 INT3 指令通常被Debugger用来设置软件断点,通过插入 INT3 操作码,可以欺骗Debugger认为这是其自身设置的断点。因此,控制权不会交给异常处理程序,程序的正常执行流程会被改变。为了避免被这种技巧欺骗,Debugger需要跟踪其设置的软件断点位置。
此外,需注意,INT3 还可以编码为 0xCD, 0x03。
示例:
push offset @handler ; 压入异常处理程序的地址
push dword fs:[0] ; 保存原始的异常处理链
mov fs:[0], esp ; 设置新的异常处理程序
; ...
db 0CCh ; 插入 INT3(0xCC)
; 如果执行到这里,说明程序被调试
; ...
@handler: ; 异常处理程序
; 继续执行
; ...
通过这种方式,程序可以判断是否正在被调试,并采取相应的保护措施。
(2) "Ice" 断点
所谓的 "Ice 断点" 是 Intel 的未公开指令之一,其操作码为 0xF1。它通常用于检测程序是否正在被跟踪(tracing)。
执行此指令会触发一个 SINGLE_STEP 异常。因此,如果程序已经在被跟踪,Debugger会认为这是由于设置了标志寄存器中的 SingleStep 位而导致的正常异常。此时,相关的异常处理程序将不会被执行,程序的执行流程也不会按预期继续。
绕过此技巧的方法很简单:可以直接运行该指令,而不是逐步执行它。虽然会触发异常,但如果程序未被跟踪,Debugger应能够理解需要将控制权交给异常处理程序。
示例:
push offset @handler ; 压入异常处理程序的地址
push dword fs:[0] ; 保存原始的异常处理链
mov fs:[0], esp ; 设置新的异常处理程序
; ...
db 0F1h ; 插入 "Ice" 断点 (0xF1)
; 如果执行到这里,说明程序正在被跟踪
; ...
@handler: ; 异常处理程序
; 继续执行
; ...
这种方法可用于检测Debugger的存在,因为Debugger在处理 Ice 断点 产生的异常时可能会表现出异常行为,从而暴露其存在。
(3) 中断 2Dh
执行 INT 2Dh(中断 2Dh)指令时,如果程序未被调试,会触发一个断点异常。如果程序正在被调试,但未设置 trace flag(跟踪标志位),则不会产生异常,程序会正常继续执行。如果程序正在被调试并且指令被逐步跟踪,执行时会跳过下一字节并继续执行。
因此,INT 2Dh 可用作一种强大的反调试和反跟踪机制,通过检测Debugger的行为来判断是否正在被调试。
示例:
push offset @handler ; 压入异常处理程序的地址
push dword fs:[0] ; 保存原始的异常处理链
mov fs:[0], esp ; 设置新的异常处理程序
; ...
db 02Dh ; 插入中断 2Dh (INT 2Dh)
mov eax, 1 ; 如果程序被跟踪,此指令可能被跳过(反跟踪)
; ...
@handler: ; 异常处理程序
; 继续执行
; ...
分析:
- 未调试状态:触发异常,程序转到异常处理程序继续执行。
- 调试状态(非逐步执行):无异常,程序正常执行。
- 调试状态(逐步执行):跳过下一字节,可能导致程序逻辑被修改,从而暴露Debugger。
实用性
此方法通过引入未公开或非常规的异常行为,巧妙地利用Debugger可能的错误行为来检测调试状态。
(4) 时间戳计数器
高精度计数器存储自机器启动以来的 CPU 周期数,可以通过 RDTSC 指令查询。经典的反调试方法是在程序的关键点(通常是异常处理程序周围)测量时间差(delta)。如果时间差过大,可能表明程序运行在Debugger的控制下,因为Debugger处理异常并将控制权交还给被调试程序需要较长时间。
示例代码:
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 ; 返回继续执行
工作原理:
- 时间戳测量:程序通过 RDTSC 读取当前的 CPU 周期计数值,并在触发异常(如 div 0)前后记录两次计数值。
- 时间差计算:异常处理完成后,计算异常处理前后的时间差。
- 判断调试状态:
- 如果时间差较小(低于阈值),说明程序未被调试。
- 如果时间差较大,可能表示程序运行在Debugger中,因为Debugger处理异常和恢复控制需要更多时间。
- 异常跳过:在异常处理程序中,修改上下文使程序跳过触发异常的指令,从而避免影响正常执行。
应用场景:
这种方法利用Debugger处理异常的时间开销来检测调试行为,非常适合用于对抗调试和逆向。通过调整阈值,可以根据目标系统和Debugger的性能优化检测精度。
(5) POPF 和陷阱标志
陷阱标志(Trap Flag, TF)位于 Flags 寄存器中,用于控制程序的逐条指令跟踪。如果该标志被设置,每执行一条指令都会触发一个 SINGLE_STEP 异常。通过操作陷阱标志,可以对抗Debugger的跟踪。
例如,以下指令序列将设置陷阱标志:
pushf ; 将当前标志寄存器的值压入栈
mov dword [esp], 0x100 ; 设置栈顶为 0x100(启用陷阱标志)
popf ; 将栈顶值弹回标志寄存器,更新 TF
原理:
- 如果程序未被调试,执行上述指令会成功设置陷阱标志,从而触发逐条指令跟踪。
- 如果程序被调试,Debugger通常会干扰对标志寄存器的直接操作,导致 TF 无法真正被设置。随后触发的异常被Debugger处理,程序的异常处理程序不会被执行。
绕过方法:
Debugger可以直接运行该指令序列而不逐步跟踪,避免触发异常,从而绕过此反跟踪技巧。
应用场景:
这种方法利用Debugger对标志寄存器的干扰行为检测调试状态,通常用于对抗Debugger的跟踪功能。虽然Debugger可以通过快速运行指令序列绕过,但它仍是一种有效的防护措施,用于与其他反调试技术结合。
(6) 栈段寄存器反跟踪
这是一个非常独特的反跟踪技巧,首次发现于一个名为 MarCrypt 的加壳工具中。这种方法通过对Debugger处理特定指令序列的行为进行检测,显得相对少见且高效。
指令序列:
push ss ; 将栈段寄存器的值压入栈
pop ss ; 将栈顶值弹回栈段寄存器
pushf ; 将标志寄存器的值压入栈
nop ; 空操作
原理:
- 当Debugger逐步跟踪这段代码时,在执行
pop ss 后,虽然下一条指令会被执行,但Debugger不会在 pushf 上中断,而是直接停在后续指令(此处为 nop)。 - Debugger无法正确处理栈段寄存器和标志寄存器的交互操作。
典型实现(MarCrypt 示例):
push ss ; 保存栈段寄存器
; 一些无关指令(混淆Debugger)
pop ss ; 恢复栈段寄存器
pushf ; 保存标志寄存器
; 一些无关指令(混淆Debugger)
pop eax ; 恢复标志寄存器的值到 eax
and eax, 0x100 ; 检查 TF(陷阱标志)
or eax, eax ; 设置条件标志
jnz @debugged ; 如果 TF 被设置,跳转到调试状态处理
; 正常执行
...
@debugged:
; 检测到Debugger,终止程序
关键点:
- 如果Debugger正在逐步跟踪,当执行到
popf 时,陷阱标志(TF)可能会被隐式设置。 - 程序随后会检测 TF 是否被设置。如果被设置,表明程序正在被跟踪,进入调试状态处理。
绕过方法:
- 断点绕过:在执行到
popf 时设置断点,直接运行程序。这样可以避免Debugger通过设置 TF 导致检测触发。 - 快速运行:让Debugger快速运行整个指令序列,而不是逐步执行,从而避免 TF 被设置。
应用场景:
这种方法利用Debugger在处理栈段寄存器(SS)和标志寄存器(Flags)的交互行为中的潜在弱点,检测是否存在调试行为。由于Debugger需要额外的操作来处理这些特殊指令序列,这为开发者提供了一种有效的反调试手段。
(7) 调试寄存器操作
调试寄存器(DR0 到 DR7)用于设置硬件断点。保护机制可以操作这些寄存器,以检测是否设置了硬件断点(从而判断程序是否被调试)、清除寄存器内容,或将其设置为特定值以供后续代码检查。例如,像 tElock 这样的加壳工具利用调试寄存器来防止逆向。
调试寄存器操作方法
从用户模式来看,调试寄存器无法直接通过特权指令(如 mov drx, ...)来设置。但有以下两种间接方法:
- 生成异常并修改线程上下文:
- 通过异常处理机制获取线程上下文(包含调试寄存器的状态),然后修改寄存器值并恢复执行。
- 系统调用:
- 使用系统调用 NtGetContextThread 和 NtSetContextThread(在用户态通过 GetThreadContext 和 SetThreadContext 提供)直接访问和修改线程上下文。
大多数保护机制更倾向于使用第一种 "非官方" 的方法。
代码示例
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 ; 返回继续执行
原理
- 触发异常:
- 使用 div 0 指令触发除零异常,使程序进入异常处理程序。
- 修改调试寄存器:
- 在异常处理程序中,清空调试寄存器(DR0–DR3 设置硬件断点,DR6 和 DR7 控制调试状态),防止Debugger利用这些寄存器。
- 恢复执行:
绕过方法
- 断点绕过:
- 在异常触发点之前设置断点,并直接运行指令,避免进入异常处理程序。
- 修改上下文:
- 在Debugger中手动设置调试寄存器值,绕过清除操作。
应用场景
这种方法通过检测和操作调试寄存器,有效防止逆向和调试。它不仅能够清理寄存器状态,还能检测Debugger行为(如硬件断点的存在),是反调试保护的重要手段。
(8) 上下文修改
与调试寄存器操作类似,上下文也可以用来以非常规方式修改程序的执行流。Debugger很容易因此被迷惑!
需要注意的是,另一个系统调用 NtContinue 可以用来加载新的上下文到当前线程中(例如,该系统调用由异常处理程序管理器使用)。
(1) TLS 回调
这种反调试方法在几年前并不广为人知。它通过在 PE 可选头中第 10 个目录项(Thread Local Storage 条目)引用程序的第一个入口点来指示 PE 加载器。这样,程序的入口点不会首先被执行,而是由 TLS 回调执行反调试检查,从而以隐蔽的方式运行。
需要注意的是,这种技术在实际中并不广泛使用。尽管较旧的Debugger(包括 OllyDbg)不支持 TLS,但可以通过插件或自定义补丁工具轻松应对。
(2) CC 扫描
封包工具常用的一种保护功能是 CC 扫描循环,旨在检测由Debugger设置的软件断点。为避免此类问题,可以使用硬件断点或自定义类型的软件断点。例如 CLI(0xFA) 是替代传统 INT3(0xCC) 操作码的一个不错选择。该指令满足断点需求:如果由用户模式程序执行,它会引发一个特权指令异常,同时仅占用 1 字节空间。
(3) 入口点 RVA 设置为 0
某些封包文件的入口点 RVA 被设置为 0,这意味着它们会从 MZ... 开始执行,对应指令为 dec ebx / pop edx ...。
虽然这本质上不是一种反调试技巧,但如果使用软件断点在入口点处中断,可能会导致问题。
例如,如果创建一个挂起的进程,然后在 RVA 0 设置 INT3,将会覆盖部分 MZ 魔术值的内容(如 'M')。当进程创建时,MZ 已经被检查过,但当通过 ntdll 恢复进程以进入入口点时,魔术值会再次被检查。在这种情况下,会引发 INVALID_IMAGE_FORMAT 异常。
为避免此问题,如果你创建自己的跟踪或调试工具,建议使用硬件断点。
(4) 结论
了解恶意软件或保护程序中常见或非常见的反调试和反跟踪技术,对逆向工程师来说是有用的知识。程序总能找到方法检测它是否在Debugger中运行——这同样适用于虚拟或仿真环境。但由于用户模式Debugger是最常见的分析工具之一,了解常见技巧及其绕过方法始终是有用的。
(5) 参考链接
- MSDN
- 《Portable Executable Tutorial》 by Matt Pietrek
- Syscall Reference - The Metasploit Project
- 《Undocumented Functions for MS Windows NT/2K》
- Intel 手册
- Microsoft Windows SDK 和 ntdll.h 中的常见异常代码
- Microsoft Windows DDK 和 ntstatus.h 中的状态代码列表(包括常见异常代码)
- Microsoft Windows SDK 和 ntdll.h 中的上下文结构文档
补充一下:GPT-4o给出的8的内容(原文网页并没有详细描述)
上下文修改
上下文修改是一种非常规的方式,用于改变程序的执行流,类似于对调试寄存器的操作。这种技术可以有效迷惑Debugger,使其在分析程序时出现错误。
概述
上下文修改通过操控线程上下文(包含寄存器、标志寄存器、调试寄存器等信息)来干扰程序的正常执行流。上下文修改可以通过以下两种方式实现:
通过异常处理机制:
- 触发异常后,获取异常上下文并修改其中的值(如指令指针
EIP、通用寄存器等),恢复执行时程序将按修改后的上下文继续运行。
通过系统调用:
- 使用系统调用 NtContinue 来加载新的上下文至当前线程,从而改变程序的执行流。此方法通常用于异常处理程序来恢复线程状态。
代码示例
以下是一个使用异常处理机制修改上下文的示例:
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 ; 返回并加载新上下文
在此示例中:
- 触发异常:
div eax 用于生成除零异常。 - 修改指令指针:在异常处理程序中,通过修改上下文中的指令指针 EIP,使程序跳转到新的执行地址。
- 恢复执行:程序根据修改后的上下文继续运行。
系统调用 NtContinue
NtContinue 是另一种实现上下文修改的方法。此调用用于加载新的上下文至当前线程,并恢复执行。例如:
CONTEXT context;// 填充 context 结构(设置新的寄存器值、标志等)NtContinue(&context, FALSE);
调试器的困惑
调试器在以下场景中容易受到干扰:
- 异常处理中的上下文修改:调试器需要额外处理异常上下文,可能无法准确捕捉执行流的改变。
- 系统调用的干扰:直接调用 NtContinue 会跳过常规的调试流程,使调试器难以跟踪程序执行。
应用场景
- 反调试:通过上下文修改隐藏程序的实际执行流,绕过调试器的断点和分析。
- 代码混淆:结合动态跳转或异常处理,使程序执行路径难以预测。
- 逆向保护:通过动态修改执行流,让逆向分析工具无法获得准确的代码路径。
绕过方法
- 监控上下文修改:
- 使用调试器跟踪上下文(如寄存器和标志寄存器)的变化,分析异常处理程序的行为。
- 禁用异常处理程序:
- 对异常处理链进行修改,阻止程序进入异常处理程序,从而绕过上下文修改的逻辑。
- 记录系统调用:
- 在调试器中监控系统调用(如 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