前言
反调试技术可以被恶意代码用来识别是否被调试,或者让调试器失效,而倘若想要分析相应的包含反调试机制的恶意代码,则需要进行一些操作。我们先来看看主流的一些反调试技术。
0x1探测调试器
使用windows api
使用windows api函数探测调试器是否存在是最简单的反调试技术。下面是一些api
IsDebuggerPresent
它查询PEB中的IsDebugged标志。如果进程没有运行在调试器环境中,返回0,否则返回一个非0值。
CheckRemoteDebuggerPresent
与IsDebuggerPresent几乎一样,而且也是检查本机的某一进程中的PEB中的IsDebugged标志,要传一个句柄作为参数,返回值与上一个一样。
RtQueryInformationProcess
Ntdll.dll中的原生态API,用来提取一个给定进程的信息。将参数置为ProcessDebugPort(0x7),就可以返回句柄标识的进程是否在被调试,如果在被调试,返回调试端口,否则返回0。
OutputDebugString
这个函数的作用是在调试器中显示一个字符串。同时也可以具有探测功能。使用SetLastError函数,将当前的错误码设置为任意值,如果进程没有被调试,调用OutputDebugString函数会失败,并且返回错误码,那么错误码就不是我们之前设置的那个任意值了,反之,错误码就不会变。
通常,防止恶意代码使用API进行反调试的最简单方法就是在恶意代码运行期间修改恶意代码,使其不能调用探测函数,或者修改这些探测函数的返回值,相对复杂的方法就是使用Hook函数去影响这些函数。
手动检测数据结构
虽然API探测调试器是简单的一种方法,不过手动检查数据结构是恶意代码最常用的手段。因为很多时候通过API的方法无效。
手动检测中,PEB结构中的一些标志暴露了调试器存在的信息。
PEB结构:
typedef struct _PEB{
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
BYTE Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
BYTE Reserved4[104];
PVOID Reserved5[52];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved6[128];
PVOID Reserved7[1];
ULONG SessionId;
}PEB,*PPEB;
BeingDebugged属性
进程运行时,fs:[30h]指向PEB的基地址,而距基地址偏移量2的BeingDebugged标志正是标识进程是否被调试
检测方法
mov eax,dword ptr fs:[30h]
mov ebx,byte ptr [eax+2]
test ebx,ebx
jz noDebuggerDetected
因为BeingDebugged标识被保存在ebx中,若值为0,则表示没有被调试。
处理这种机制的方法便是修改跳转指令或者修改BeingDebugged的值。
ProcessHeap属性
Reserved4数组中有一个未公开的位置叫做ProcessHeap,它被设置为加载器为进程分配的第一个堆的位置。ProcessHeap在PEB结构的0x18处。第一个堆头部有一个属性字段,告诉内核这个堆是否在调试器中创建。属性叫做ForceFlags和Flags(这个属性的位置偏移量在不同的系统中是不同的,xp中是0x10)。
检测方法(xp)
mov eax,large fs:30h
mov eax,dword ptr [eax+18h]
cmp dword ptr ds:[eax+10h],0
jne DebuggerDetected
处理这种机制的方法与上一种一样。
NTGlobalFlag
系统使用的PEB结构偏移量0x68处的一个未公开的位置,决定如何创建堆结构。如果值为0x70则进程正在调试
检测方法
mov eax,large fs:30h
cmp dword ptr ds:[eax+68h],70h
jz DebuggerDetected
对付这种机制就是修改ProcessHeap属性或者使用隐藏调试插件。
系统痕迹检测
当我们在调试的时候,注册表项HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug会从原始的Dr.Watson修改为OllyDbg。所以就可以被检测出来了
if(FindWindow("OLLYDBG",0)==NULL){
Debugger not found
}else{
Debugger detected
}
0x2识别调试器的行为
INT扫描
调试器设置断点的基本机制是软件中断指令INT3,临时替换运行程序中的一条指令,然后当程序运行到这条指令的时候,调用调试异常处理例程。INT3的机器码是0xCC,因此无论何时,使用调试器设置一个断点,都会插入一个0xCC修改代码。
所以恶意代码会在代码中查找机器码0xCC。
对抗这种机制的方法就是设置硬件断点
时钟检测
被调试时,进程的运行速度大大降低,所以利用时钟检测也是恶意代码探测调试器的一种常用方法。
记录执行一段操作前后的时间戳,然后比较两个时间戳,如果存在滞后,则可以认为存在调试器
记录触发一个异常前后的时间戳,如果不调试进程,可以很快处理完异常,因为调试器处理异常的速度非常慢。默认情况下,调试器处理异常时需要人为干预,导致大量延迟。
使用rdstc指令
rdtsc
xor eax,eax
add ecx,eax
rdtsc
sub eax,ecx
cmp eax,0xFFF
jb NoDebuggerDetected
rdtsc
push eax
ret
使用QueryPerformanceCount和GetTickCount
这两个函数都可以获取时间,所以计算两次获取的时间差即可,若时间差比较大,则可能进程正在被调试
应对方法是在时钟检测之后设置断点,或者修改时间差。
0x3干扰调试器的功能
使用TLS回调
程序加载到调试器后大部分调试器会从程序PE头部指定的入口点处开始。TLS回调被用来在程序入口点执行之前运行代码,所以这些代码可能被忽略,秘密的运行了。
使用IDA可以用ctrl+E明显的看到程序的入口点以及是否存在TLS回调。也可以设置调试器在TLS回调前暂停。
下面有三个实例程序,先看第一个
程序开始的时候流程图长这样,已经可以很明显的看到之前所说的手动探测数据结构的手段了
所以当打开调试器之后,我们就可以开始修改PEB了,不论是OD还是ImmunityDebug都可以用命令行改
dump fs:[30]+2,dump ds:[fs:[30]+0x18]+0x10,dump fs:[30]+0x68
数据窗口会分别显示相应的数据,分别修改便可以绕过检测了,不过修改指令应该也可以吧。
下面来看第二个程序
main函数长这样,还有个TLS回调,这是一个密码验证程序,我们下意识载入调试器,不过直接退出了
因为TLS回调长这样,所以我们要nop掉exit调用。
紧接着是回调函数中的sub_401020函数,这个函数就是之前讨论过的API探测,所以就不用说直接nop掉byte_40A968加1的这条指令。
然后我们调试器直接跟进到main函数中的strncmp函数处观察两个参数,果然一个是输入,一个是密码,不过测试发现不对。
所以我们来到解密函数处观察发现这里也有一个反调试的机制
这一段中,bl被赋值成了PEB中的BeingDebugged标识,而后又进入了解密过程的计算,所以还需要改一下PEB中的标识。
最后才是正确答案。
下面是最后一个程序
程序一开始就逐个字符的定义了两个字符串,第一个看不懂,第二个是一个可执行文件名。
接着获取自身文件名与定义的文件名比对只有对了才能执行。
不过初始化的文件名的字符串还进行了更改,并且有反调试机制,利用的便是时间戳的差值,所以我们只需要改一下变量num就可以了。不过这段汇编代码却是包含了SEH的处理。
这一段里面eax已经被赋值为自身的地址0x401228了加上2Ch也就是0x401254,下面把这个地址入栈,到SEH调用链,到div ecx时,ecx是0,无法做除法,触发SEH,执行下面的0x401254的异常处理代码,在调试状态下会花费大量时间,所以可以辨别处是否处于调试状态。
同样,后面也调用了GetTickCount()函数进行反调试处理
最后的网址解密函数同样也是利用了rdstc的时间处理机制。
反调试技术不单单有这些,还有插入中断,利用调试器漏洞等方法,这几种只是最具有代表性的,方便学习在这里做个笔记。
内容与程序选自《恶意代码分析实战》一书16章。