Day12:反调试(一)
反调试基本原理、WINAPI检测、数据检测、进程检测
反调试基本原理
- 反调试技术通常借助操作系统相关的内容来实现,常见办法有调用WindowsAPI获得相关数据、有意触发异常监测异常处理(调试器会捕获异常)、时间差检测(实际运行过程十分迅速)、硬件断点检测、进程检测等
WINAPI检测
- 由于是直接使用WINAPI,所以一般在IDA或者动态调试器里会有明显的函数名字样,识别会比较容易
- 此类一般是利用函数的返回值或者函数传递回来的参数作为判断标准,应对方法主要有使用反反调试插件、修改相关值、hook等
IsDebuggerPresent
原理:
IsDebuggerPresent() 实际上是通过访问PEB(ProcessEnvironmentBlock,进程环境块,一个描述进程相关数据的结构体)中的BeingDebugged标志来判断是否被调试的,被调试的话,函数返回1。有关BeingDebugged的内容,见下文。
IDA和XDbg的效果图:


可以看到明显的IsDebuggerPresent字样。
应对方法:
在XDbg中开启scyllahide插件或者在TEST EAX,EAX后改变一下ZF就可以了
CheckRemoteDebuggerPresent
原理:
函数原型:
1
2
3
4BOOL WINAPI CheckRemoteDebuggerPresent(
In HANDLE hProcess,//参数1,一个进程句柄,传入当前进程句柄就是checklocal了
Inout PBOOL pbDebuggerPresent//参数2,一个用来传递的布尔值,用来表示是否被调试
);这个函数实际上是借用了NtQueryInfomationProcess函数来实现调试检测的,简单讲就是通过NtQueryInformationProcess查询一个数据然后和非调试状态下这个数据预期的值进行比较,比较结果再传给自己的参数2。更详细的原理见下一个API
效果图:


应对方法:
scyllahide依旧管用,也可以在call CheckRemoteDebuggerPresent下面的cmp后改变zf来实现反反调试
NtQueryInformationProcess
原理:
函数原型:
1
2
3
4
5
6
7__kernel_entry NTSTATUS NtQueryInformationProcess(
IN HANDLE ProcessHandle,//要查询的进程句柄
IN PROCESSINFOCLASS ProcessInformationClass,//要查询的信息类型
OUT PVOID ProcessInformation,//接收查询结果的缓冲区(变量)
IN ULONG ProcessInformationLength,//缓冲区大小
OUT PULONG ReturnLength OPTIONAL//实际返回的数据大小,可选
);通过设置第二个参数为0x7(表示查询一个叫ProcessDebugPort的变量),函数会通过第三个参数传递调试信息,被调试则传递非0
或者设置第二个参数为0x1E(表示查询一个叫ProcessDebugObjectHandle的变量),被调试则传递非NULL
或者设置第二个参数为0x1F(表示查询一个叫ProcessDebugFlags的变量),被调试则传递0
原型是这样,但是这个函数是Windows内部API,即系统不想用户使用这个函数(可能是出于安全考虑),所以直接调用是不行的,要动态加载。下面是DS给的实现源码参考:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50#include <windows.h>
#include <stdio.h>
// 1. 定义函数指针类型(必须手动声明)
typedef LONG NTSTATUS;
typedef NTSTATUS (NTAPI* PNtQueryInformationProcess)(
HANDLE ProcessHandle,
int ProcessInformationClass, // 直接使用 int 代替 PROCESSINFOCLASS
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
int main() {
// 2. 动态加载 ntdll.dll
HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
if (!hNtdll) {
printf("hello\n");
system("pause");
return 0;
}
// 3. 获取 NtQueryInformationProcess 函数地址
PNtQueryInformationProcess NtQueryInformationProcess =
(PNtQueryInformationProcess)GetProcAddress(hNtdll, "NtQueryInformationProcess");
if (!NtQueryInformationProcess) {
printf("hello\n");
system("pause");
return 0;
}
// 4. 调用函数查询调试端口(0x7 = ProcessDebugPort)
DWORD debugPort = 0;
NTSTATUS status = NtQueryInformationProcess(
GetCurrentProcess(),
0x7, // ProcessDebugPort
&debugPort,
sizeof(debugPort),
NULL
);
// 5. 严格按照你的逻辑输出
if (debugPort)
printf("bye\n");
else
printf("hello\n");
system("pause");
return 0;
}下面是IDA的反汇编结果
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105.text:000000014007E130 sub_14007E130 proc near ; CODE XREF: sub_14007920B↑j
.text:000000014007E130 ; DATA XREF: .pdata:00000001401AB818↓o
.text:000000014007E130
.text:000000014007E130 var_180 = byte ptr -180h
.text:000000014007E130 var_160 = qword ptr -160h
.text:000000014007E130 var_150 = byte ptr -150h
.text:000000014007E130 hModule = qword ptr -148h
.text:000000014007E130 var_128 = qword ptr -128h
.text:000000014007E130 var_10C = dword ptr -10Ch
.text:000000014007E130 var_EC = dword ptr -0ECh
.text:000000014007E130 var_18 = qword ptr -18h
.text:000000014007E130
.text:000000014007E130 ; __unwind { // j___GSHandlerCheck
.text:000000014007E130 push rbp
.text:000000014007E132 push rdi
.text:000000014007E133 sub rsp, 178h
.text:000000014007E13A lea rbp, [rsp+30h]
.text:000000014007E13F lea rdi, [rsp+180h+var_150]
.text:000000014007E144 mov ecx, 22h ; '"'
.text:000000014007E149 mov eax, 0CCCCCCCCh
.text:000000014007E14E rep stosd
.text:000000014007E150 mov rax, cs:__security_cookie
.text:000000014007E157 xor rax, rbp
.text:000000014007E15A mov [rbp+150h+var_18], rax
.text:000000014007E161 lea rcx, unk_1401B80A2
.text:000000014007E168 call sub_14007A313
.text:000000014007E16D nop
.text:000000014007E16E lea rcx, ModuleName ; "ntdll.dll"
.text:000000014007E175 call cs:GetModuleHandleW ;此处加载ntdll库
.text:000000014007E17B mov [rbp+150h+hModule], rax
.text:000000014007E17F cmp [rbp+150h+hModule], 0
.text:000000014007E184 jnz short loc_14007E1A7
.text:000000014007E186 lea rcx, aHello ; "hello\n"
.text:000000014007E18D call sub_14007886F
.text:000000014007E192 nop
.text:000000014007E193 lea rcx, aPause ; "pause"
.text:000000014007E19A call sub_140078810
.text:000000014007E19F nop
.text:000000014007E1A0 xor eax, eax
.text:000000014007E1A2 jmp loc_14007E240
.text:000000014007E1A7 ; ---------------------------------------------------------------------------
.text:000000014007E1A7
.text:000000014007E1A7 loc_14007E1A7: ; CODE XREF: sub_14007E130+54↑j
.text:000000014007E1A7 lea rdx, ProcName ; "NtQueryInformationProcess"
.text:000000014007E1AE mov rcx, [rbp+150h+hModule] ; hModule
.text:000000014007E1B2 call cs:GetProcAddress ;此处获取NtQueryInformationProcess函数地址
.text:000000014007E1B8 mov [rbp+150h+var_128], rax ;此处把函数地址传给局部变量
.text:000000014007E1BC cmp [rbp+150h+var_128], 0
.text:000000014007E1C1 jnz short loc_14007E1E1
.text:000000014007E1C3 lea rcx, aHello ; "hello\n"
.text:000000014007E1CA call sub_14007886F
.text:000000014007E1CF nop
.text:000000014007E1D0 lea rcx, aPause ; "pause"
.text:000000014007E1D7 call sub_140078810
.text:000000014007E1DC nop
.text:000000014007E1DD xor eax, eax
.text:000000014007E1DF jmp short loc_14007E240
.text:000000014007E1E1 ; ---------------------------------------------------------------------------
.text:000000014007E1E1
.text:000000014007E1E1 loc_14007E1E1: ; CODE XREF: sub_14007E130+91↑j
.text:000000014007E1E1 mov [rbp+150h+var_10C], 0
.text:000000014007E1E8 call cs:GetCurrentProcess
.text:000000014007E1EE mov [rsp+180h+var_160], 0
.text:000000014007E1F7 mov r9d, 4 ;缓冲区大小
.text:000000014007E1FD lea r8, [rbp+150h+var_10C] ;接收结果的缓冲区
.text:000000014007E201 mov edx, 7 ;指定查询的信息类型
.text:000000014007E206 mov rcx, rax ;查询的进程句柄
.text:000000014007E209 call [rbp+150h+var_128] ;调用NtQueryInformationProcess
.text:000000014007E20C mov [rbp+150h+var_EC], eax
.text:000000014007E20F cmp [rbp+150h+var_10C], 0
.text:000000014007E213 jz short loc_14007E224
.text:000000014007E215 lea rcx, aBye ; "bye\n"
.text:000000014007E21C call sub_14007886F
.text:000000014007E221 nop
.text:000000014007E222 jmp short loc_14007E231
.text:000000014007E224 ; ---------------------------------------------------------------------------
.text:000000014007E224
.text:000000014007E224 loc_14007E224: ; CODE XREF: sub_14007E130+E3↑j
.text:000000014007E224 lea rcx, aHello ; "hello\n"
.text:000000014007E22B call sub_14007886F
.text:000000014007E230 nop
.text:000000014007E231
.text:000000014007E231 loc_14007E231: ; CODE XREF: sub_14007E130+F2↑j
.text:000000014007E231 lea rcx, aPause ; "pause"
.text:000000014007E238 call sub_140078810
.text:000000014007E23D nop
.text:000000014007E23E xor eax, eax
.text:000000014007E240
.text:000000014007E240 loc_14007E240: ; CODE XREF: sub_14007E130+72↑j
.text:000000014007E240 ; sub_14007E130+AF↑j
.text:000000014007E240 mov rdi, rax
.text:000000014007E243 lea rcx, [rbp+150h+var_180]
.text:000000014007E247 lea rdx, unk_140171FF0
.text:000000014007E24E call sub_140079A35
.text:000000014007E253 mov rax, rdi
.text:000000014007E256 mov rcx, [rbp+150h+var_18]
.text:000000014007E25D xor rcx, rbp ; StackCookie
.text:000000014007E260 call j___security_check_cookie
.text:000000014007E265 lea rsp, [rbp+148h]
.text:000000014007E26C pop rdi
.text:000000014007E26D pop rbp
.text:000000014007E26E retn
.text:000000014007E26E ; } // starts at 14007E130
.text:000000014007E26E sub_14007E130 endp
.text:000000014007E26E特征应该是 call cs:GetModuleHandleW、call cs:GetProcAddress、 call cs:GetCurrentProcess
应对方法:
scyllahide也是支持反NtQueryInformationProcess反调试的,手动绕过一般是更改缓冲区的值与标准值的比较结果,当然在DS给的源码中因为加入了一些错误处理(如ntdll载入失败就跳过反调试),可以通过人为改动错误判断的结果来实现直接跳过反调试
GetLastError
这个函数用于获取最近一次异常的返回值。使用的方法一般是故意构造异常,如果处于被调试状态,调试器会捕获异常导致返回的错误码和预期的不符。
效果:


应对方法:
可以看到,GetLastError之后,会把返回值和预设的值进行比较,所以修改cmp后的zf就可以了。由于GetLastError的自由度比较高(触发异常的方法多样),所以scyllahide应该是不支持的(笔者没有认出什么相关的设置)
数据检测
BeingDebugged
BeingDebugged是PEB的成员变量之一,用于描述进程是否处于被调试状态,为1时表示处于被调试状态。
PEB结构体具体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;//偏移为2
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;反调试手段通常是访问进程的该参数(如WINAPI的IsDebuggerPresent())来判断是否处于被调试状态。32位下,PEB用FS:[0x30]表示;64位下,PEB用FS:[0x60]表示。加上偏移2就是对BeingDebugged的访问了(这就是这种方法的特征了)
效果图:


应对方法:
scyllahide可绕过,手动绕过只需要在访问指令下方的cmp后修改ZF即可
NTGlobalFlag
NTGlobalFlag也是PEB结构体的成员,但是是非公开的(因此上文的PEB结构体定义中看不到)。32位下,它位于PEB偏移0x68处,64位下,它位于PEB偏移0xBC处(特征)。它本是描述进程堆管理的,但是由于调试器中的进程创建堆的方式和正常情况有所不同,被调试时该值会被设为0x70(又一个特征),因此也被用来判断是否被调试
效果:


应对方法:
syllahide可以绕过,也可以在下面的比较逻辑(因为是内联汇编,所以比较逻辑的实现会有不同)后修改ZF绕过
进程检测
FindWindow
实际上是窗口检测,这个函数获取的是进程窗口的类名或者标签,通过与预期的名字进行比较来判断是否处于被调试状态。由于使用的是系统函数,所以实际上也属于WINAPI检测,但是不重要。特征和WINAPI检测一样,会有明显的标记
效果:


本例的程序创建了一个判断函数,在主函数中是将返回值test后来判断是否被调试的,所以只需要修改主函数中的test eax,eax后的ZF就可以实现绕过了。scyllahide应该是不支持的
父进程检测
原理是正常程序双击运行父进程为explorer.exe,而拖进调试器打开父进程为调试器,通过比较获得的父进程名和预设的父进程名可以判断是否处于被调试状态。一般会通过遍历进程或者通过NtQueryInfomationProcess查询来实现。特征是出现大量和进程相关的api,如遍历进程时会调用CreateToolhelp32Snapshot、th32ParentProcessID、Process32Next等,NtQueryInfomationProcess查询会出现QueryFullProcessImageNameA或者其他相关函数与调试器名称字符串同时出现的情况
效果:




实测scyllahide绕过不了第一种方法但可以绕过第二种方法。手动绕过也可以,主要基于后续对进程名的判断,修改判断结果即可绕过
特征码检测
有的调试器在内存中会有特征码,通过遍历进程和搜索特征码可以判断是否被调试。这种情况会比较复杂,难点应该在识别上。一方面可以通过分析是否出现特征码(可能会是很明显的一连串数字初始化),另一方面遍历进程会出现父进程检测中提及的api,但是这些一般不会出现在主函数中,需要跟进函数查看。
特征码示例:


scyllahide一般绕过不了,可以在主函数涉及判断结果的逻辑中进行改动绕过。
Day12:反调试(一)