动态反调试技术

使用 SEH 异常进行动态反调试

正常运行的进程发生异常时,在 SEH 机制作用下,OS 会接收异常,然后调用进程中注册的 SEH 处理。若进程在调试运行中发生异常,调试器就会接收处理。利用该特性可以判断程序是否处于调试状态,然后根据不同结果执行不同操作。

常见 SEH 异常:

1
2
3
4
5
EXCEPTION_ACCESS_VIOLATION    非法访问异常
EXCEPTION_BREAKPOINT int 3 断点异常
EXCEPTION_ILLEGAL_INSTRUCTION 无法解析指令异常
EXCEPTION_INT_DIVIDE_BY_ZERO 整数除 0 异常
EXCEPTION_SINGLE_STEP 单步工作模式异常

示例1:利用 SEH 在调试运行时跳转到非法地址处使调试终止:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;注册SEH
push handler
push DWORD ptr fs : [0]
mov DWORD ptr fs : [0] , esp

;触发 int 3 断点
int 3

;若无视异常则跳转到非法地址
mov eax, 0xFFFFFFFF
jmp eax
;若非调试状态则正常运行
handler:
;ss : [esp + 0xc]是 CONTEXT *pContext结构体指针,ds:[eax+0xb8]指向EIP成员,使异常处理返回后跳转到正常代码处
mov eax, dword ptr ss : [esp + 0xc]
mov ebx, normal_code
mov dword ptr ds : [eax + 0xb8] , ebx
xor eax, eax
retn

normal_code :
pop dword ptr fs : [0]
add esp, 4

示例2:利用 SEH 在调试运行和非调试运行时对 flag 作不同的加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
char ch[] = { 87,93,80,86,74,69,89,88,66,110,88,66,110,72,94,68,67,110,87,93,80,86,76,0 };
char flag[40];
scanf("%24s", flag);

__try {
int* a = NULL;
*a = 1;
for (int i = 0; i < strlen(flag); i++)
{
flag[i] ^= '2';
}
}
__except (EXCEPTION_EXECUTE_HANDLER){
for (int i = 0; i < strlen(flag); i++)
{
flag[i] ^= '1';
}
}
if(!strcmp(flag,ch))
printf("right");
return 0;

示例3:单步执行异常

CPU 中的 EFLAGS 寄存器的 TF 标志位(Index 8)为1时,CPU 将进入单步执行模式,每执行一条指令就会触发一次EXCEPTION_SINGLE_STEP异常,同时 TF 变为0。利用该特性可以探测调试器。

1
2
3
4
5
;由于 EFLAGS 寄存器无法直接修改,只能通过栈修改
pushfd
or DWORD PTR SS:[ESP],100
popfd
nop ;用来触发异常

或者也有C语言版本:

1
2
3
4
5
6
7
8
9
10
#include<intrin.h>
__try {
int a = __readeflags();
a |= 0x100;
__writeeflags(a);
a++; ;随便一个指令触发异常
}
__except (GetExceptionCode()==EXCEPTION_SINGLE_STEP?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH) {
printf("EXCEPTION_SINGLE_STEP");
}

示例4:int 2D 异常

int 2D 原为内核模式中用来触发断点异常的指令,也可以在用户模式下触发异常,但程序调试运行时不会触发异常,只是忽略。在调试模式下执行 int 2d 指令后,下条指令的第一个字节将被忽略,后一个字节会被识别为新的指令继续执行。并且在调试器(OllyDbg、x32dbg)中,跟踪 int 2d 指令时,不会停在下条指令开始的地方,而是一直运行,直到遇到断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
BOOL bDebugging = FALSE;

__asm {
// install SEH
push handler
push DWORD ptr fs:[0]
mov DWORD ptr fs:[0], esp

int 0x2d

nop
mov bDebugging, 1
jmp normal_code

handler:
mov eax, dword ptr ss:[esp+0xc]
mov dword ptr ds:[eax+0xb8], offset normal_code
mov bDebugging, 0
xor eax, eax
retn

normal_code:
// remove SEH
pop dword ptr fs:[0]
add esp, 4
}

printf("Trap Flag (INT 2D)\n");
if( bDebugging ) printf(" => Debugging!!!\n\n");
else printf(" => Not debugging...\n\n");

Timing Check 运行时间反调试

在调试器或虚拟机中逐行跟踪代码比程序正常运行耗费的时间多。时间计数器的准确程度从高到低:

1
RDTSC>NtQueryPerformanceCouner()>GetTickCount()

RDTSC

x86或x64 CPU 中存在一个 TSC(TimeStampCounter)的64位寄存器,CPU对每个时钟周期计数,然后保存到 TSC 。 RDTSC是一条汇编指令,将TSC的值高32位读入EDX,低32位读入EAX。在 x64 中,仍然先存入两个寄存器,然后对 rax 作移位并和 rdx 做 or 运算,使最终值存储在 rax 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
	RDTSC
push EDX ;记录原值
push EAX
;需要保护的代码
xor eax,eax
mov ecx,0x3e8
loc:inc EAX
loop loc
RDTSC

pop esi
pop edi
;进行比较,如果两次值之差>0xFFFFFF就return
cmp edx,edi
ja re

sub eax,esi
mov ss:[ebp-4],eax
cmp eax,0xFFFFFF
ja re
normalcode:
...
re:
retn

在 VS2019 和 gcc 里,msvc 将RDTSC指令包装为了函数,使使用变得简单:

1
2
3
4
5
6
7
8
9
10
long long time=__rdtsc();
for (int i = 0; i < 1000; i++)
{
}
long long time2 = __rdtsc();
if (time2 - time > 0xFFFFFF)
{
return 0;
}
printf("success");

0xCC 断点探测法

程序在进行调试时会设置软件断点,软件断点对应的x86指令为0xCC。因此,检测该指令即可判断程序是否处于调试状态。但由于代码和数据中也会有0xCC,因此只扫描是否存在0xCC并不可靠。

探测 API 断点

一般想调试程序的局部功能时,会在 API 最开始处设置断点,然后查看栈中的数据,于是就可以探测 API 头部的0XCC。或者探测自己的重要函数头部等关键点的0xCC

比较校验和

由于0xCC的存在,在存在0xCC的区域的校验和值会和原值不同。校验和的计算有多种方式,实际应用可以使用CRC32算法。