首页
社区
课程
招聘
[翻译]Windows系统下的反调试机制第二部分(翻译自Symantec官网)
发表于: 3天前 891

[翻译]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需要额外的操作来处理这些特殊指令序列,这为开发者提供了一种有效的反调试手段。



调试寄存器(DR0DR7)用于设置硬件断点。保护机制可以操作这些寄存器,以检测是否设置了硬件断点(从而判断程序是否被调试)、清除寄存器内容,或将其设置为特定值以供后续代码检查。例如,像 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 是否被设置。如果被设置,表明程序正在被跟踪,进入调试状态处理。
  • 通过异常处理机制获取线程上下文(包含调试寄存器的状态),然后修改寄存器值并恢复执行。
  • 使用系统调用 NtGetContextThreadNtSetContextThread(在用户态通过 GetThreadContextSetThreadContext 提供)直接访问和修改线程上下文。
  • 使用 div 0 指令触发除零异常,使程序进入异常处理程序。

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//