目录
笔记
反调试技术:识别是否被调试,或者让调试器失效。
探测windows调试器:
Windows API:IsDebuggerPresent-->查询进程块环境块PEB中的IsDebugged标志
CheckRemoteDebuggerPresent-->检测本地机器中的一个进程是否运行在调试器中,也可以通过传递自身进程句柄来探测自己是否被调试
NtQueryInformationProcess-->Ntdll.dll中的一个原生的API,NtQueryInformationProcess(句柄,进程信息类型),第二个参数设置为0x7,会告诉这个句柄的标识是否正在被调试。
OutputDebugString-->在调试器中显示一个字符串,事先设置一个错误码。
手动检测调试器:手动检测数据结构
1.检测BeingDebugged
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged; //被调试状态
BYTE Reserved2[1];
PVOID 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;
BYTE BeingDebugged //,这里就是记录程序的调试状态的,(1代表被调试,0代表没有被调试) 我们可以使用 BOOL WINAPI IsDebuggerPresent(void);这个函数来检测是否有调试器存在,返回非0值代表被调试,如果返回0代表没有被调试。
76A9A720 64 A1 30 00 00 00 mov eax,dword ptr fs:[00000030h] //获取PEB结构基地址
76A9A726 0F B6 40 02 movzx eax,byte ptr [eax+2] //根据PEB结构,我们知道是取
76A9A72A C3 ret
PEB结构在TEB结构的0x30偏移的地方,也就是fs:[0x30]处可以取到PEB的基地址。
解决方法:手动改0标志,设置BeingDebugged属性为0
2.检测ProcessHeap
Reserved4数组中一个未公开的位置叫做ProcessHeap,位于PEB结构的0x18处,第一个堆头部都有一个属性来指明这个堆是否在调试中创建。这些属性叫ForceFlags(堆头部偏移量为0x10/0x44处)和Flags(堆头部偏移量为0x0c/0x40处)。
解决办法:手动修改ProcessHeap值
3.检测NTGlobalFlag
调试器中启动进程与正常模式下启动进程有些不同,所以创建内存堆的方式也不同,使用PEB结构偏移量0x68的位置决定如何创建堆结构。如果值为0x70就是正在运行。
解决办法:手动修改ProcessHeap属性
系统痕迹检测:注册表,查找文件目录,进程列表,FindWindow
默认情况下是Dr.Watson,当被调试时,可能变为OllyDbg
识别调试器的行为:
INT扫描:在代码中查找0xCC
repne scasb扫描代码CC,解决的办法就是应用硬件断点
代码校验和检查:CRC循环冗余校验或者MD5
时钟检测:被调试的时候,代码的运行速度会降低。 rdstc(0x0F31)-->返回至系统启动以来的时间。QueryPerformanceCounter函数和GetTickCount函数有同样的功能
一种方法记录操作前后的时间戳,比较这两个时间戳:查看两次调用 rdstc的差值是否大于0xFF
一种方法记录一个异常前后的时间戳。
干扰调试器功能:
使用TLS回调:在程序入口点之前执行的代码是TLS回调。TLS是Windows的一个存储类。TLS运行每个线程维护一个用TLS声明的专有变量。实现TLS回调时,会在PE头部保包含一个.tls段。
解决:Ctrl+E查看所有二进制的入口点,每一个TLS回调函数都有一个前缀字符串TlsCallback。
OD Options --> Debugging Options --> Events,设置System break-point为第一个暂停的位置
使用异常:使用异常来破坏或者探测调试器,多数调试不把异常传递给应用程序,这时来探测调试器。
解决:把所有异常传递给应用程序。
插入中断:在合法指令序列插入中断,破坏程序正常运行
1.插入INT3
设置一个新的SEH,然后调用INT 3
2.插入INT 2D --->内核调试器设置断点的方法
3.插入ICE断点--->会产生一个单步异常,所以在遇到这个指令时,不要使用单步。
调试器漏洞:
PE头漏洞:
1.PE头中的NumberOfRvaAndSize属性是后面DataDirectory数组中的元素个数,当大于0x10时,Windows加载器会忽略,而OD将会崩溃(Bad or Unknown 32-bit Executable File)错误。
2.当SizeOfRawData不合法调试就会报错(File contains too much data)。因为Windows加载器加载时会比较VirtualSize和SizeOfRawData的大小,取小的加载到内存。而调试器直接会选SizeOfRawData的值
OutputDebugString漏洞:格式化字符串漏洞,%s会崩溃。
实验
Lab16-1
检测BeingDebugged为1正在调试状态。fs:30的位置线程环境块
检测ProcessHeap是否为0
检测NTGlobalFlag
这个插件可以很好的去抵抗这些反调试技术,或者手动dump fs:[30]+2
Lab16-2
查看导出函数。Ctrl+K 程序有两个入口点,第一个函数的功能是查看是否有OLLYDBG调试器,用于反调试。并且第一个函数位于TLS节中,先于start函数执行。禁用这个反调试技术是把exit的函数调用变成NOP,或者选上一个例子的PHantOm插件。
创建一个线程CreateThread,然后调用比较字符串函数strncmp。可以在OD里面载入发现要比较的字符串是bzrr,实际上这个字符串是不正确的。其中压入的一个参数是encode_password,应该是一个正确的字符串。现在跟入CreateThread创建的StartAddress函数。
StartAddress
关于encode_password的移位操作,说明这是一个解密函数
检测BeingDebugged是否为调试状态,要对抗这个反调试,就需要bl恒为0
byte_40A968参与了运算
byte_40A968在第二条语句被修改了
先将3039定义为一个错误码,然后利用一个OutputDebugStringA输出一个b,然后得到这个错误码与刚刚定义的错误码进行比较。这里的区别是在调试中的错误码就是3039,所以会有一个自增的操作,而在DOS窗口中,这个错误码不是3039,所以不会有自增操作。这里也实现反调试的功能。解决方法把自增NOP掉
然后OD调试,跟到密码处为byrr
Lab16-3
根据导出函数,可以看到这个两个函数都是获取时候
联续两次调用,相减来与4B0进行比较
联续两次调用,获取时间差,这两个函数用于回至系统启动以来的时间。
main函数开始看,在栈空间上创建了两个字符串:1qbz2wsx3edc和ocl.exe,然后调用了字符串比对函数。但在比对之前调用sub_4011E0
sub_4011E0
调用了两次QueryPerformanceCounter。第二个QueryPerformanceCounter函数下方的语句。函数的返回值保存在了edx中,然后用edx减去第一次调用QueryPerformanceCounter函数所得到的返回值。接下来用二者的差值与0x4B0,也就是十进制的1200毫秒进行比较。如果二者的差值大于1200毫秒,那么var_118的值就会被设置为2,否则它会保持它的初始值1。
第一处QueryPerformanceCounter后面的两条语句的作用就是获取当前EIP的值,并将这个值存入eax寄存器
两个QueryPerformanceCounter的中间有一个除0异常(div ecx),在此之前还定义了一个异常处理例程。(恶意程序获取了当前的EIP值,并将其保存到eax寄存器以后,程序会间接地将eax中保存的值与0x2C相加,也就是0x00401228+0x2C=0x00401254,并且将这个值入栈)
结论:运行在调试器中的程序会花费较多的时间来进行异常的处理。通过这个时间差,于是就能够判定当前程序是否运行在调试器中了
该循环会利用var_118与传入这个函数的参数字符串进行运算。因此,QueryPerformanceCounter对于时间的计算结果会直接对这个循环中的运算产生影响。解决办法:nop
将0x00401292处的语句填充掉
接着往下,这里调用了两次取时间的函数,然后检验差值是不是等于1毫秒,如果超出了1毫秒,执行eax的异或指令,之后将edx中的内容保存到地址为0的内存空间中,那么这显然是一个非法操作,因此程序就会崩溃。如果比1小就直接退出。中间有一个sub_401000
解决办法:为了修正这个位置,需要让jbe的跳转指令始终执行才行。或者将0x004015B2到0x004015B6位置的语句全都nop掉。
sub_401000
发现,这里再次出现了除零异常
跟入sub_401300函数
两次调用rdtsc,rdstc(0x0F31)-->返回至系统启动以来的时间。而在这两处指令中间,又是一个除零异常。
结论:QueryPerformanceCounter检查成功,恶意程序会修改它正常运行所需的字符串;如果GetTickCount检查成功,恶意程序会产生一个导致程序崩溃的异常;如果rdtsc检查成功,恶意程序会从硬盘上删除自身。
恶意程序修改SEH机制,在两个计时函数调用之间,加入了恶意程序自己的异常处理例程,这样它就可以捕获一个它处理的异常。异常在调试器内处理比在调试器外处理慢得多