
知识点
反调试技术,恶意代码用它来识别自身是否被调试,或者让调试器失效,或者通过反调试技术盐城分析人员对代码的分析时间,为了组织调试器的分析,当恶意代码意识到自己被调试时,他们可能改变正常的执行路径或者修改自身程序让自己崩溃,从而增加调试时间和复杂度。
探测Windows调试器:恶意代码会使用多种技术探测调试器调试它的痕迹,其中包括使用Windows API、手动检测调试器人工痕迹的内存结构,查询调试器遗留在系统中的痕迹等。调试器探测时恶意代码常用的反调试技术。
使用Windows API函数:使用Windows API函数探测调试器是否存在时最简单的反调试技术,Windows提供了一些函数可以让应用程序调用这些API来探测自己是否被调试。这些API中有些时专门来探测调试器存在的,而另一些API时处于其他目的而设计的。通常方式恶意代码使用API函数进行反调试的最简单方法是在恶意代码运行期间修改恶意代码,使其不能调用探测调试器的API函数,或者修改这些函数的返回值,也可以通过挂钩这些函数,如使用rootkit技术,下列时恶意代码常用来探测调试器的API函数:
IsDebuggerPresent:这是探测调试器最简单的API函数。它查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试环境中,则函数返回0,否侧返回一个非0值。CheckRemoteDebuggerPresent:这个函数跟IsDebuggerPresent很类似,它也查询进程环境块(PEB)中的IsDebugged标志。它可以探测自身进程或者系统其它进程是被调试,这个函数将一个句柄作为参数,检查这个句柄对应的进程是否被其他调试器附加。NtQueryInformationProcess:这个函数时Ntdll.dll中的一个原生态API,它用来提取一个进程的信息,他的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型,例如将该参数设置为哦ProcessDebugPort,将会告诉你这个句柄标识的进程是否被调试。如果进程正在被调试,则返回被调试的端口,否则返回0.OutputDebugString:这个函数的作用是在调试器中显示一个字符串,先通过SetLastError函数为当前的错误码设置一个随机的值,然后调用OutputDebugString,如果当前进程没有被调试则OutputDebugString的调用不会影响错误码的值。
手动检测数据结构:手动检测数据是恶意代码编写者最常用的方法。因为很多时候通过Windows API实现的反调试技术无效,例如这些函数被rootkit挂钩,并返回错误信息,因此,恶意代码编写者经常手动执行这些与API功能相同的操作:
- 检测
BeingDebugged属性:
Windows操作系统维护者每个正在运行进程的PEB结构,它包含与这个进程相关的所有用户态参数,这些参数包括进程环境数据,环境数据包括环境变量、加载的模块列表、内存地址、调试器状态,例如如下的两种检测方法:
1 |
//mov 方法 |
1 |
//push /pop 方法 |
以上两种方法都是通过将PEB的基地址载入EAX寄存器中然后对比响应标志的值来判断,这种检查由多种形式,对于这种检测方法我们可以在执行跳转指令之前修改0标志,强制执行跳转或者不跳转,或者手动设置BeingDebugged属性值为0,另外OD中一些插件可以帮助我们修改BeingDebugged标志,其中流行的有Hide Debugger、Hidedebuging和PhantOm。
- 检测
ProcessHeap属性:
Reserved4数组中有一个未公开的位置叫做ProcessHeap,他被设置为加载器为进程分配的第一个堆的位置。ProcessHeap位于PEB结构的0x18处。第一个堆头部有一个属性字段,它告诉内核这个堆是否在调试器中创建,这些属性叫ForceFlags和Flags,如下代码:
1 |
mov eax, large fs:30h |
对于这种反调试技术最贱的方法是使用手动修改ProcessHeap,或者使用调试器的隐藏调试插件,如果使用WinDbg来调试可以禁用调试堆栈来启动进程。
- 检测
NTGlobalFlag:
对于调试器中启动进程与正常模式下启动进程的不同,所有它们创建内存堆的方式也不同,系统使用PEB结构偏移量0x68处的一个未公开位置来决定如何创建堆结构。如果这个位置的值为0x70,我们就知道进程正运行在调试器中。如果进程从调试器启动则进程的如下标志被设置:
1 |
(FLG_HEAP_ENABLE_TAIL_CHECK|FLG_HEAP_ENABLE_FREE_CHECK|FLG_HEAP_VALIDATE_PARAMETERS) |
NTGlobalFlag检测代码:
1 |
mov eax,large fs:30h |
对抗这种反调试措施的方法与上面一样。
系统痕迹检测:调试器工具回在系统中留下一些痕迹,如以下是调试器在注册表中的一个常用位置:
1 |
HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindows NTCurrentVersionAeDebug |
改注册表指定当应用程序发生错误时触发哪一个调试器,默认情况下,他被设置为Dr.Watson。如果注册表的键值被修改为OllyDbg则代表他可能正在被调试,恶意代码也可能会遍历文件系统来查找系统中是否存在调试器的可执行文件,或者查看当前进程列表,更普遍的做法是使用FindWindow来查找调试器:
1 |
if(FindWindow("OllyDbg",0) == NULL) |
识别调试器行为:在逆向工程中,为了帮助分析人员进行分析,可以使用调试器设置一个断点或者单步执行一个进程,但是在调试器执行这些操作是,会修改进程中的代码,这也为反调试的技术实现提供了一个思路。
INT扫描:调试器设置断点的基本机制是使用软件中断指令INT 3,临时替换运行程序中的一条指令,然后当程序运行到这条指令时,调用调试器异常处理例程。除了特定的断点指令INT 3以外 INT immediate指令可以设置任意中断。此指令使用两个操作码0xCD value但是调试器不常使用这种双字节的机器码,以下是扫描代码断点的汇编代码:
1 |
call $+5 |
逐段代码首先执行一个函数调用,随后用pop指令将EIP的寄存器的值存入EDI,然后将EDI设为代码的开始位置。接下来扫描这段代码的0xCC字节。如果发现了0xCC字节则证明存在调试器。可使用硬件断点若不是软件断点来对抗这种措施。
执行代码校验和检查:恶意代码可以计算代码段的校验并实现与扫描INT中断相同的目的。与扫描0xCC不同,这种检查仅执行恶意代码中的机器码的CRC(循环冗余校验)或者MD5校验和检查,虽然这种技术不如INT 3扫描那么常见没但是他同样有效,可以通过硬件断点,或者在代码运行过程中用调试器手动修改执行路径来对抗这种反调试技术。
时钟检测:被调试时进程的运行速度大大降低,如下是使用时钟检测来探测调试器存在的方法:
- 记录一段操作前后的时间戳,然后比较这两个时间戳,如果存在滞后则代表调试器存在。
- 记录触发一个异常前后的时间戳。如果不调试进程,可以很快处理该异常,调试器处理异常会造成很大的时间延迟,尽管我们可以用调试器忽略异常,但是这样的操作依然存在很大的延迟。
使用rdstc指令:较常用的时钟检测方法是使用rdstc指令(操作码0x0F31)它返回至操作系统重新启动以来的时钟数,并且将其作为一个64位的值存储在EDX:EAX中,恶意代码运行两次rdstc指令并比较这两次读数之间的差值。
使用QueryPerFormanceCounter和GetTickCount:同rdstc一样,这两个Windows API函数也被用来执行一个反调试时钟检测,使用这种方法的前提是处理器有高分辨能力的计数器–寄存器,他能存储处理器活跃的时钟数,GetTickCount函数返回最近系统重启时间与当前时间的相差毫秒数。
对抗以上时钟检测方法的是在时钟检测函数之后设置断点,或者强制修改时间的对比结果。
干扰调试器的功能:代码可鞥会使用一些技术来干扰调试器的正常运行,例如:线程本地存储(TLS)回调、异常、插入中断等。这些技术仅当程序处于调试器控制之下才视图扰乱程序的运行。
使用TLS回调:实际上并不是程序在加载到调试器后,会让第一条指令执行之前而暂停程序的运行,而是调试器从程序PE头部指定的入口点开始。TLS回调被用来在程序入口点执行之前运行代码,因为这些代码可以在调试器中秘密地执行。
TLS是Windows的一个存储类,其中数据对象不是一个自动的堆栈变量,而是代码中运行的每个线程的一个本地变量,TLS允许每个线程维护一个用TLS声明的专有变量,在应用程序实现TLS的情况下,可执行程序的PE头部会包含一个.tls段。TLS提供了初始化和终止TLS对象的回调函数。使用PEView可以看到.tls段,正常程序不会使用这个段。可以使用调试器在TLS回调函数运行之前下断点来分析这些函数。
使用异常:调试器利用中断产生异常,来执行类似于断点的操作。修改SEH链可以应用在对抗反汇编和反调试的技术中。恶意代码可以使用异常来破坏或者探测调试器,调试器捕获异常后,并不会将处理权返回被调试的进程处理,大多数利用异常的反调试技术往往利用这一点来探测调试器,如果调试器不能将异常结果正确的返回到被调试的进程,呢么这种异常失效可以被进程内部的异常处理机制所探测。对抗这种反调试措施的方法是设置调试器将异常的处理权返还给应用程序。
插入中断:
-
插入INT 3:调试器使用
INT 3来设置软件断点,所以一种反调试技术是在合法代码中插入0xCC欺骗调试器,使其一位这些0xCC是自己设置的断点,一些调试器使用跟踪自身设置的断点的方法来避免这种反调试技术。双字节操作码0xCD03也可以产生INT 3中断,这是代码干扰WinDbg调试器的有效方法,在调试器外,0xCD03指令产生一个STATUS_BREAKPOINT异常。 -
插入INT 2D断点:
INT 2D反调试技术的功能与INT 3类似,但是INT 0x2D指令能够用来探测内核态的调试器。 -
插入ICE断点:片内仿真器(ICE)断点指令
icebp(操作码0xF1)是Intel未公开的指令之一。由于ICE难以在任意位置设置断点,所以icebp指令被设计用来境地使用ICE设置断点的难度。运行icebp指令将会产生一个单步异常,如果通过单步调试跟踪程序,调试器会认为这是单步调试产生的异常,从而不中兴先前设置的异常处理例程,利用这一点,恶意代码使用异常处理例程作为它的正常执行流程,而在这种情况下,会被反调试技术干扰。为了防止这种反调试技术,指令icebp指令时不要使用单步调试。
调试器漏洞:与所有软件一样,调试器也存在漏洞,有时恶意代码编写者为了防止被调试会攻击这些漏洞。
PE头漏洞:第一种技术使修改二进制可执行文件的PE头,当OllyDbg调试器加载修改后的二进制文件时会导致OllyDbg崩溃,结果是错误:“Band or Unknown 32-bit Executable File”,但是程序在调试器外运行时一切正常。
OutputDebugString漏洞:恶意代码尝试利用OllyDbg 1.1的格式化字符串漏洞,为OutputDebugString函数提供一个%S参数,让OD崩溃,如程序中出现如下调用:
1 |
OutputDebugString("%s%s%s%s%s%s%s%s%s%s%s") |
课后练习
Lab16-1
使用一款调试器分析Lab16-01.exe的恶意代码。Lab16-01.exe是与Lab09-01.exe相同的恶意代码,不同点在于Lab16-01.exe加入了反调试技术。
问题
1.这个恶意代码使用了哪些反调试技术?
程序中有很多如下结构的汇编代码:
1 |
.text:00403554 mov eax, large fs:30h |
恶意代码通过检测BeingDebugged、ProcessHeap、NTGlobalFlag的值来检测自身是否被调试。
2.当每种反调试技术成功执行时,有什么现象?
程序会执行自我删除。
3.如何应对这些反调试技术?
这些反调试措施是检测API中的数据结构的值,我们只要在对比之后修改标志寄存器的值使程序以为我们并没有调试它。
4.如何在调试过程中手动修改检测的数据结构?
单步步进修改这些数据结构的值。
5.哪一种OllyDbg插件可以帮你逃避恶意代码的反调试技术?
PhantOM和Immunity Pycommand hidedebug。
Lab16-2
使用调试器分析恶意代码Lab16-02.exe.本实验的目的是找出正确的密码。另外,恶意代码不会放弃使用攻击负载(payload)。
问题
1.在命令行中运行Lab16-02.exe时会发生什么?
1 |
C:>Lab16-02.exe |
2.当使用猜测的命令行参数运行Lab16-02.exe时会发生什么?
1 |
C:>Lab16-02.exe 1234 |
3.命令行密码是什么?
byrr
4.使用IDA Pro加载Lab16-02.exe,在main函数的何处可以找到strncmp函数?
1 |
.tls:0040122C push 4 ; size_t |
5.在默认设置下,将这个恶意代码加载到OllyDbg中会发生什么?

6.Lab16-02.exe中PE结构的独特之处是什么?
该程序有.tls段:
1 |
Name Start End R W X D L Align Base Type Class AD es ss ds fs gs |
7.回调(callback)发生在哪些位置?(提示:在IDA Pro中使用Ctrl+E组合键)

8.恶意代码使用哪一种反调试技术使它在调试器中立即终止运行?如何避免这种检查?
1 |
void __stdcall (int a1, int a2, int a3) |
在调试器中修改函数返回值,或者修改跳转逻辑。
9.当你禁用反调试技术后,你在调试器中看到的命令行密码是什么?
在我Win10虚拟机中的OD默认设置了在TLS调用前的断点:


10.调试器中找到的密码在命令行中运行有效吗?
肯定是无效的,这里恶意代码采用了两个不同的执行流程。
1 |
DWORD __stdcall StartAddress(LPVOID lpThreadParameter) |
11.哪种反调试技术为调试器和命令行设置不同的密码?如何防御它们?
1 |
DWORD sub_401020() |
用NOP替换ADD指令。
Lab16-3
使用调试器分析Lab16-03.exe中的恶意代码.这个恶意代码与Lab09-02.exe中的相似,不同之处使做了一些修改,引入了反调试技术。如果你在分析的过程中被卡住,请查看Lab9-2.
问题
1.当使用静态分析法分析这个二进制文件时,你看到了哪些字符串?
1 |
Address Length Type String |
2.当运行这个二进制文件时会发生什么?
黑框一闪,没有其他迹象了。
3.如何重命名它才能使这个二进制文件正常运行?
先进入调试器调试在401518处下断点发现如下字符串作为_strncmp的参数:qgr.exe,但是发现用这个字符串作为文件名还是不行,因此觉得代码可能判断程序自身是否被调试来进入不同的执行流程
4.这个恶意代码使用了哪些反调试技术?
rdtsc技术:
1 |
int __cdecl sub_401300(const char *a1, int a2, int a3) |
QueryPerformanceCounter技术:
1 |
int __cdecl sub_4011E0(int a1) |
GetTickCount技术:
1 |
v14 = GetTickCount(); |
5.对每一种反调试技术而言,如果恶意代码确定它运行在调试器中,它将做什么?
rdtsc技术:当两次rdtsc()调用之间的时间差大于500000时程序会删除自身。
QueryPerformanceCounter技术:会检测运行时间,如果运行时间过长则会改变运行结果的值。
GetTickCount技术:如果检测成功程序会返回一个异常。
6.为什么反调试技术能在这个恶意代码中成功?
程序修改了SEH链,在程序运行期间会产生异常,再判断异常处理的时间差从而判断是否处于调试状态。
7.恶意代码使用了什么域名?
adg.malwareanalysisbook.com




近期评论