1.SEH异常处理
windows中存在许多的异常处理类型,如下:
EXCEPTION_DATATYPE_MISALIGNMENT (0x80000002)
EXCEPTION_BREAKPOINT (0x80000003) 断点异常
EXCEPTION_SINGLE_STEP (0x80000004) 单步执行
EXCEPTION_ACCESS_VIOLATION (0x80000005) 非法访问内存
EXCEPTION_IN_PAGE_ERROR (0x80000006)
EXCEPTION_ILLEGAL_INSTRUCTION (0x8000001D) 无法识别的指令
EXCEPTION_NONCONTINUABLE_EXCEPTION (0x80000025)
EXCEPTION_INVALID_DISPOSITION (0x80000026)
EXCEPTION_ARRAY_BOUNDS_EXCEEDED (0x8000008C)
EXCEPTION_FLT_DENORMAL_OPERAND (0x8000008D)
EXCEPTION_INT_DIVIDE_BY_ZERO (0x80000094) 除法运算分母为0
...
而反调试技术则可以利用异常处理机制来进行反调试处理。
1)在异常处理过程函数中进行静态反调试判断。例如判断PEB的调试标识等。
2)利用异常处理机制进行动态反调试。
如果程序处于调试状态时,在程序抛出异常后,会优先派发给调试器进行处理。此时就可以利用这一点进行反调试处理。如下代码:
00401000 PUSH EBP
00401001 MOV EBP, ESP
00401003 PUSH EBX
00401004 PUSH DynAD_SE.004099A0
00401009 CALL DynAD_SE.00401077
0040100E ADD ESP, 4
00401011 PUSH DynAD_SE.0040102C
00401016 PUSH DWORD PTR FS:[0]
0040101D MOV DWORD PTR FS:[0], ESP
00401024 INT3
00401025 MOV EAX, -1
0040102A JMP EAX
0040102C MOV EAX, DWORD PTR SS:[ESP+C] //这里是异常处理函数
00401031 MOV EBX, DynAD_SE.00401040
00401036 MOV DWORD PTR DS:[EAX+B8], EBX
0040103D XOR EAX, EAX
0040103F RETN
00401040 POP DWORD PTR FS:[0]
00401047 ADD ESP, 4
0040104A PUSH DynAD_SE.004099B4
0040104F CALL DynAD_SE.00401077
00401054 ADD ESP, 4
00401057 POP EBX
00401058 POP EBP
00401059 RETN
如果异常处理函数没有执行的话,JMP EAX就会导致进程崩溃。因为在异常处理函数中,将EIP设置为了00401040。这就是典型的利用异常机制进行的反调试(调试器优先处理异常),如果跳过程序自己的SEH处理,可能就会导致崩溃的问题。同样,反调试可以不选择让程序崩溃,而是让其进入一大段混乱的代码中,让调试者迷失在其中。
2.SetUnhandledExceptionFilter()
当进程发生异常处理时,如果进程中没有处理异常的SEH函数,则会调用系统默认的异常函数。而SetUnhandledExceptionFilter()函数则允许设定这个默认函数。定义如下:
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(
__in LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);
lpTopLevelExceptionFilter函数定义如下:
typedef struct _EXCEPTION_POINTERS{
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
}EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
LONG TopLevelExceptionFilter(PEXCEPTION_POINTERS pExcept);
这里的处理函数需要返回上一个异常处理函数地址。
通常反调试就会在新的异常处理函数中进行调试状态判断,从而干扰调试工作。
3.时间测量法
时间测量主要是判断时间差,如果时间差过大,则有可能进程处于调试状态。因为调试执行耗时要比正常运行大的多。
其中获取时间的方法有如下几种方法:
1)Counter based method
RDTSC(RDTSC是一个汇编指令,执行后可获取当前时间,大小为64位。EDX存放高32位,EAX存放低32位)
kernel32!QueryPerformanceCounter() / ntdll!NtQueryPerformanceCounter()
kernetl32!GetTickCount()
而以上三种获取时间的精准度,靠上面的相对会更高。
2)Time based method
timeGetTime()
_ftime()
4.陷阱标识
陷阱标识指的是EFLAGS寄存器的第九个比特位(index:8)。
其中的单步标识就是陷阱标识(Trap Flag)。
当TF值为1时,CPU将进入单步调试模式。在单步执行模式中,CPU执行1条指令后就会触发一个异常EXCEPTION_SINGLE_SETP。然后陷阱标识就会自动清零。由于单步调试会抛出EXCEPTION_SINGLE_SETP异常,所以可以利用这一点进行SEH反调试处理。
例子如下,因为陷阱标识位无法直接修改,所以通过压栈的方式进行修改。
PUSHFD ;这个就是将EFALGS压栈
OR DWORD PTR SS:[ESP], 100 ;对栈中的位数据进行修改,TF至1,触发单步异常。
POPFD
5.INT 2D
INT 2D原为内核模式中用来触发断点异常的指令,也可以在用户模式下触发异常。但程序调试运行时不会触发异常,只是忽略。所以这种差异就会被利用在反调试之中。INT 2D有下面两个特征:
1)自动忽略下一条指令的第一个字节
在调试模式中执行完INT 2D后,下一条指令的第一个字节就会被自动忽略掉。
0040101E CD 2D INT 2D
00401020 B8 EB5D0000 MOV EAX, 5DE8
00401025 8D18 LEA EAX, DWORD PTR DS:[EAX]
00401027 83C3 10 ADD EBX, 10
执行完INT 2D后会自动忽略B8,下一条指令就变成了JMP SHORT 00401080(CB 5D)
00401021 EB 5D JMP SHORT 00401080
00401023 0000 ADD BYTE PTR DS:[EAX], AL
00401025 8D18 LEA EBX, DWORD PTR DS:[EAX]
00401027 83C3 10 ADD EBX, 10
像这样基于INT 2D的反调试技术能够形成较强的代码混淆(Obfuscated Code)效果。
(改变代码字节顺序扰乱程序代码的方法称为代码混淆技术,常被用于动态反调试中)
2)一直运行到断点处
INT 2D指令的另一个特征是,使用SetInto(F7)或SetOver(F8)命令跟踪INT 2D指令时,程序不会停在其下条指令开始的地方,而是一直运行,直到遇到断点,就像使用RUN(F9)命令运行程序一样。
6.0xCC探测
程序调试过程中,我们一般会设置许多的软件断点。断点对应的x86指令为“0xCC”。若能检测到该指令,即可判断程序是否处于调式状态。基于这一想法的反调试技术称为“0xCC探测”技术。
但该技术准确度不够,因为数据中可能也同样包含有CC。
7.API断点
当设置API断点时,API的开始部分的第一个字节会被设置为0xCC,所以可以通过探测这些API上的断点来判断当前程序是否处于调试状态。
8.比较校验和
该方法主要是通过对比一个区域的校验和值(CRC)来判断是否被人下过断点。因为当你下断点时,会将断点处的值改为0xCC,调试器之所以看不到,是为了方便调试故意隐藏了。但内存中的断点位置却是0xCC。所以可以对比同一区域的校验和值,来判断是不是被人下了断点。