反调试技术(部分)

  • 动态调试是一种观察程序运行状态的一种手段。

  • 逆向工程中的动态调试的目的主要有:验证静态分析结果和观察程序运行时的数据。

  • 而为了防止我们对程序进行调试,程序开发者通常会设置反调试来检测自己开发的程序是否正在被调试。

  • 花指令通常干扰静态分析;反调试与之相反,主要为了干扰动态调试

API反调试

Windows内部提供了一些用于检测调试器的API

基于PEB(Process Environment Block,进程环境块)的静态反调试,存放进程信息的一个结构体

TEB (Thread Environment Block,线程环境块)结构体,进程中的每一个线程都对应着一个TEB结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kd>dt_TEB
nt! _TEB
...
+0x030 ProcessEnvironmentBlock :Ptr32_PEB
...
kd>dt_TEB
...
+0x002 BeingDebugged :UChar
...
+Ox018 ProcessHeap :Ptr32 Void
...
+0x068 NtGlobalF1ag :Uint4B
...

PEB结构体中中,BeingDebuggedProcessHeapNtGlobalFlag是与调试信息相关

  • BeingDebugged:当进程处于被调试状态时,值为1,否则为0。

    1
    2
    3
    4
    mov eax,dword ptr fs:[18];          //获取TEB地址,AddressOfTEB
    mov eax,dword ptr ds:[eax+30]; //通过TEB获取PEB的地址,AddressOfPEB
    mov eax byte ptr da:[eax+2]; //获取PEB偏移为2的结构体元素BeginDebugged,并返回这个值
    retn
  • ProcessHeap:指向Heap结构体,偏移0xC处为Flags成员,偏移0x10处为ForceFlags成员。

    通常情况下,Flags的值为2,ForceFlags的值为0,当进程被调试时会发生改变

    1
    2
    3
    mov eax,dword ptr fs:[0x18];     //TEB的起始地址
    mov eax,dword ptr ds:[eax+30]; //PEB的地址
    mov eax,dword ptr ds:[eax+18]; //PEB.Processheap的地址
  • NGlobalFlag:占四个字节,默认值为0。当进程处于被调试状态时,第一个字节会被置为0x70。


IsDebuggerPresent()

1
BOOL IsDebuggerPresent();

返回值为1表示当前进程被调试的状态,反之为0.

1
2
3
call IsDebuggerPresent
test al, al
jne being_debugged

当程序处于3环(低权限)时, FS:[0] 寄存器指向TEBTEB向后偏移0x30字节FS[0x30h]的位置保存的是PEB结构体的地址。

在 PEB 0x2 偏移处存储的是一字节长度的 BeingDebugged 标志位,

IsDebuggerPresent函数本质是读取该进程对应 PEB 的 BeingDebugged 标志位并返回。

1
2
3
4
mov eax,dword ptr fs:[18];          //获取TEB地址,AddressOfTEB
mov eax,dword ptr ds:[eax+30]; //通过TEB获取PEB的地址,AddressOfPEB
mov eax byte ptr da:[eax+2]; //获取PEB偏移为2的结构体元素BeginDebugged,并返回这个值
retn

6ded46482d53190075b609d8596b2218

  • 绕过方法
    • 只需要将PEB 0x2偏移处的结构体元素BeingDebugged标志设为0即可 (或改变一下返回值)
    • 把判断je改为jne

CheckRemoteDebuggerPresent()

另一个常用的APICheckRemoteDebuggerPresent,返回值为1表示当前进程被调试的状态,反之为0

1
2
3
4
BOOL WINAPI CheckRemoteDebuggerPresent(
_In_ HANDLE hProcess,
_Inout_ PBOOL pbDebuggerPresent
);

如果调试器存在 (通常是检测自己是否正在被调试), 该函数会将pbDebuggerPresent指向的值设为0xffffffff

函数在判断 hProcess 不是 0 以及检查输出参数指针是否为NULL 后就调用了 NtQueryInformationProcess函数

b057599fb76ceaed483d0eb9a3e16daf


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, optional] PULONG ReturnLength
);

参数解释:
ProcessHandle:要为其检索信息的进程句柄。
ProcessInformationClass:要检索的进程信息的类型。
ProcessInformation:指向调用应用程序提供的缓冲区的指针,函数将请求的信息写入其中。 写入的信息的大小因 ProcessInformationClass 参数的数据类型而异。
ProcessInformationLength:指向的缓冲区的大小(以字节为单位)。
ReturnLength:指向变量的指针,其中函数返回所请求信息的大小。 如果函数成功,则这是 由 ProcessInformation 参数指向的缓冲区中写入的信息的大小 (如果缓冲区太小,则为成功接收信息) 所需的最小缓冲区大小。


进程名反调试

CreateToolhelp32Snapshot()

遍历当前系统中的进程列表,检测是否存在与调试器相关的进程名

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
#include <windows.h>
#include <tlhelp32.h>
#include <iostream>
#include <string>

// 检查是否存在指定的调试器进程
bool IsDebuggerProcessRunning() {
const char* debuggerNames[] = { "ollydbg.exe", "x64dbg.exe", "ida.exe", "windbg.exe" };

// 创建进程快照
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
return false;
}

PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);

// 遍历进程列表
if (Process32First(hSnapshot, &pe32)) {
do {
// 遍历已知的调试器名称
for (const auto& debuggerName : debuggerNames) {
// 将进程名转换为小写以进行匹配
std::string processName = pe32.szExeFile;
for (auto& c : processName) c = tolower(c);

if (processName == debuggerName) {
CloseHandle(hSnapshot);
return true; // 找到匹配的调试器进程
}
}
} while (Process32Next(hSnapshot, &pe32));
}

CloseHandle(hSnapshot);
return false; // 未找到调试器进程
}

int main() {
if (IsDebuggerProcessRunning()) {
std::cout << "Debugger process detected! Exiting...\n";
ExitProcess(1); // 检测到调试器,退出程序
} else {
std::cout << "No debugger detected.\n";
}

// 正常程序逻辑
std::cout << "Program is running.\n";
return 0;
}

  • 调试器进程名列表:在debuggerNames数组中列出了常见的调试器进程名(如ollydbg.exe, x64dbg.exe等)。可以根据需要添加更多的调试器进程名。
  • CreateToolhelp32Snapshot:这是一个Windows API,用于创建系统中所有进程的快照,以便遍历这些进程。
  • Process32First 和 Process32Next:这些函数用于遍历进程快照中的每一个进程。
  • tolower:为了匹配时忽略大小写,将进程名全部转换为小写进行比较。
  • ExitProcess:如果发现调试器进程,程序直接退出。
  • 这种方法可以用来检测外部调试器是否正在运行,但它不是百分之百可靠,因为高级调试器可能会通过修改进程名或隐藏自己来规避检测。

实例

Fuko’s starfish

一个很典的 windows exe 逆向,考察基本的 dll 程序调试,windows 常见反调试和常见花指令。

程序开始时,在挂载 dll 的同时启动一个线程函数(dll中)。

PixPin_2025-03-02_15-56-09

PixPin_2025-03-02_16-25-37

在 exe 初始化和加载 dll 的过程中都会加载一个全局对象,在全局对象的构造函数中设置了一个反调试。

PixPin_2025-03-02_15-47-05

PixPin_2025-03-02_15-47-50

这里只需要 patchpush raxpop rax 的代码就能看到真实逻辑。

PixPin_2025-03-02_15-48-34

PixPin_2025-03-02_15-49-37

反调试IsDebuggerpersent()

PixPin_2025-03-02_15-50-15

keypatchjz改为jez,或直接改成jmp

PixPin_2025-03-02_15-51-19

PixPin_2025-03-02_15-52-01

对密钥进行了初始化。这里开始的初始化是个障眼法,在花指令后继续更新了密钥,可以看到在后面还有一段更新密钥的代码。

PixPin_2025-03-02_20-17-11

然后玩过游戏后,进入加密逻辑。这里其实就是一个标准 aes-ecb 加密算法。唯一需要注意的是,在密钥扩展前,会对key 进行最后的操作。如果在调试状态,则会输出hmm...,如果不在调试状态,则会将密钥异或 0x17

PixPin_2025-03-02_16-12-43

  • CreateToolhelp32Snapshot()

PixPin_2025-03-02_16-25-37

  • CheckRemoteDebuggerPresent()

PixPin_2025-03-02_20-34-08

首先获取正确的 key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>

int main() {
unsigned char key[16]; // 定义一个长度为16的数组来存储密钥
srand(114514); // 初始化随机数种子

for (int i = 0; i < 16; i++) {
key[i] = 0x17 ^ (rand() % 0xff); // 生成密钥的每个字节
if (key[i] < 16) printf("0"); // 如果小于16,补0
printf("%x", key[i]); // 打印密钥字节
}

return 0; // 程序正常结束
}
// 09e5fdeb683175b6b13b840891eb78d2

然后拿到密文,进行 aes-ecb 解密

1
2
3
4
5
6
7
8
9
10
11
12
from Crypto.Cipher import AES
import binascii

hex_key = "09e5fdeb683175b6b13b840891eb78d2"
hex_ciphertext = "3d011c190ba090815f672731a89aa47497362167ab2eb4a09418d37d93e646e7"
key = binascii.unhexlify(hex_key)
ciphertext = binascii.unhexlify(hex_ciphertext)
cipher = AES.new(key, AES.MODE_ECB)
decrypted = cipher.decrypt(ciphertext)
print(decrypted)
# b"VNCTF{W0w_u_g0t_Fuk0's_st4rf1sh}"

参考连接

反调试技术例题 - CTF Wiki

逆向常见反调试合集 - 纸飞机低空飞行 - 博客园

【CTF-Reverse】IDA动态调试,反调试技术_ida 动态调试-CSDN博客

【第拾期 REVERSE 分享会】VNCTF2025-REVERSE 出题人集结!哔哩哔哩视频