linux-debugger-detecttive

1. procfs

procfs(Process File System)是 Linux 内核的一部分,通常挂载在 /proc。它是一个伪文件系统(pseudo filesystem),读取/proc下的文件时,内核会即时生成文件内容。该目录下存在许多文件,具体内容参考内核文档,与调试器检测相关的文件/目录有:

  • /proc/self/stat:当前进程的运行状态
  • /proc/self/maps:当前进程的虚拟内存映射关系
  • /proc/self/wchan: 当前进程的等待状态
  • /proc/self/status:以人类可读的方式展示当前进程的运行状态

其中 proc/self 是符号链,指向 proc/<pid>

2. /proc/self/stat

该文件描述了当前进程的运行状态,记录许多当前进程的状态信息,参考ubuntu mampage。这里只列出与调试器检测相关的字段:

  • state:/proc/self/stat文件中的第3个字段,可能得取值:
value state
R Running
S Sleeping in an interruptible wait
D Waiting in uninterruptible disk sleep
Z Zombie
T Stopped (on a signal) or (before Linux 2.6.33) trace stopped
t Tracing stop (Linux 2.6.33 onward)
W Paging (only before Linux 2.6.0)
X Dead (from Linux 2.6.0 onward)
x Dead (Linux 2.6.33 to 3.13 only)
K Wakekill (Linux 2.6.33 to 3.13 only)
W Waking (Linux 2.6.33 to 3.13 only)
P Parked (Linux 3.9 to 3.13 only)
I Idle (Linux 4.14 onward)

2.1 检测方法

检查文件/proc/self/stat,如果第三个字段取值为t,则表示当前进程被调试。

一个未被调试得程序,其stat文件大致内容:

1
2
3
cat /proc/self/stat

3638740 (cat) R 3553006 3638740 3553006 34817 3638740 0 122 0 0 0 0 0 0 0 20 0 1 0 51248826 8634368 431 18446744073709551615 93824992239616 93824992257201 140737488346048 0 0 0 0 0 0 0 0 0 17 8 0 0 0 0 0 93824992270992 93824992272488 93824992276480 140737488346980 140737488347000 140737488347000 140737488351211 0

其第三个字段为R代表 Running

一个被调试得程序(pid: 3635408),其stat文件大致内容:

1
2
3
cat /proc/3635408/stat

3635408 (proc_self_statu) t 3635364 3635408 3633135 34879 3635364 0 203 0 0 0 0 0 0 0 20 0 1 0 50687781 2609152 290 18446744073709551615 93824992237744 93824992238592 140737488344800 0 0 0 0 0 0 1 0 0 17 7 0 0 0 0 0 93824992247368 93824992247384 93824992247808 140737488345716 140737488345770 140737488345770 140737488351170 0

其第三个字段为t代表 Tracing stop。说明正在被调试。

3. /proc/self/maps

该文件描述了当前进程的虚拟内存映射关系,参考ubuntu mampage

一个未被调试得程序,其maps文件中显示的内容只有程序所必需的库文件,比如/bin/cat:

1
2
3
4
ldd /bin/cat
linux-vdso.so.1 (0x00007ffff7fc3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7c00000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fc5000)

其maps文件内容(隐藏了heap、stack、vsyscall、vdso和重复模块):

1
2
3
4
5
cat /proc/self/maps

555555554000-555555556000 r--p 00000000 08:02 1065861 /usr/bin/cat
7ffff7c00000-7ffff7c28000 r--p 00000000 08:02 1064418 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7fc5000-7ffff7fc6000 r--p 00000000 08:02 1064415 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2

当/bin/cat被frida注入调试时,其maps文件内容(隐藏了heap、stack、vsyscall、vdso和重复模块)如下:

1
2
3
4
5
6
cat /proc/3641369/maps

555555554000-555555556000 r--p 00000000 08:02 1065861 /usr/bin/cat
7ffff5c03000-7ffff5d63000 r--p 00000000 00:01 15783 /memfd:frida-agent-64.so (deleted)
7ffff7c00000-7ffff7c28000 r--p 00000000 08:02 1064418 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7fc5000-7ffff7fc6000 r--p 00000000 08:02 1064415 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2

利用该文件,检查是否存在特定模块以确定是否被特定调试器注入/调试。

3.1 检测方法

frida会向进程注入模块,并修改进程的虚拟内存映射关系,将frida模块的起始地址映射到进程的虚拟内存空间中。通过maps文件,如果看到类似:frida-agent-64.solibfrida-gadget.so等模块的注入。说明该进程被frida调试。

4. /proc/self/wchan

该文件描述了当前进程的等待状态。官方文档的介绍:若内核编译启用了 CONFIG_KALLSYMS=y,且任务正处于可解析的阻塞点,/proc//wchan 文件会给出函数名;否则为 0(或在部分系统为 -)。和调试器相关的部分阻塞点:

block point description
ptrace_stop 被调试进程在等待调试器继续执行
ptrace_do_notify 向调试器报告事件(临时停)
do_signal_stop 因信号被停止(包括调试器触发)
do_jobctl_trap 被 SIGSTOP 等控制停止

其中最重要的阻塞点是 ptrace_stop,因为在调试过程中,大部分状态都是调试器在断点分析。

一个未被调试的程序,其wchan文件内容大部分时间都为0:

1
2
3
cat /proc/self/wchan

0

一个被调试的程序,其wchan文件内容如下:

1
2
3
cat /proc/3641369/wchan

ptrace_stop

4.1 检测方法

检测方法同/proc/self/stat,如果wchan文件内容为 ptrace_stop,则表示当前进程被调试。

5. /proc/self/status

该文件以人类可读的形式描述了当前进程的状态,相比于 /proc/self/stat,该文件内容更详细。该文件中记录的字段中,有两个字段和调试器相关,分别是 TracerPidState。其他字段参考ubuntu status

State字段有如下取值:

  • R (running)S (sleeping)D (disk sleep)T (stopped)Z (zombie)t (tracing stop)X (dead)

TracerPid字段有如下取值:

  • 0:没有调试器正在调试该进程。
  • PID:正在调试该进程的调试器的进程ID。

一个未被调试的程序,其status文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat /proc/self/status

Name: cat
Umask: 0002
State: R (running)
Tgid: 3642757
Ngid: 0
Pid: 3642757
PPid: 3641385
TracerPid: 0
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
FDSize: 64

# 忽略后续大量与调试检测无关的字段

一个被调试的程序,其status文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cat /proc/3642187/status

Name: proc_self_statu
Umask: 0002
State: t (tracing stop) # 被调试
Tgid: 3642187
Ngid: 0
Pid: 3642187 # 调试器进程的pid
PPid: 3642155
TracerPid: 3642155
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000

# 忽略后续大量与调试检测无关的字段

5.1 检测方法

该文件中记录了两个调试器相关的字段,TracerPidState。因此存在两个检测点,打开/proc/self/status文件,查看TracerPid字段,如果为非0,则表示当前进程被调试。或者检测State字段,如果为 t (tracing stop),则表示当前进程被调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 检测TracerPid字段
void check_TracerPid() {
FILE *fp = fopen("/proc/self/status", "r");
char line[256];
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "TracerPid")) {
if (atoi(line + strlen("TracerPid:")) != 0) {
printf("Debugger detected!\n");
fclose(fp);
exit(1);
}
}
}
fclose(fp);
}

6. fork 双进程互检

基于上述的检测方法:有效的检测手段是 fork 创建子进程,父子进程相互检测。

6.1 为什么需要 fork 双进程互检?

上述检测文件都是伪文件,当访问这些伪文件时候,内核会即时生成文件内容。其中 /proc/self/status 文件的 State 字段、/proc/self/wchan 文件、/proc/self/stat 文件的 state 字段都和程序当前的运行状态相关。

当调试器调试程序暂停时,通过另一程序检查这些字段或文件,毫无疑问会检测到调试器。但是当程序自身读取并检查时,由于文件内容是实时生成的,读取文件时程序处于正常运行状态,因此会表现出未被调试器调试的状态。

所以需要 fork 创建子进程,互相充当彼此的另一程序

简单的实现如下:

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
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/wait.h>

void check_State(pid_t pid) {
char path[0x20];
sprintf(path, "/proc/%d/status", pid);

FILE *fp = fopen(path, "r");
char line[256];
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "State:")) {
if (strstr(line, "t (tracing stop)")) {
printf("Debugger detected:State:\tt (tracing stop)\n");
fclose(fp);
kill(pid, 9); // 检测到对方被调试,杀死对方
exit(1);
}
}
}
}

void check_stat(pid_t pid) {
char path[0x20];
sprintf(path, "/proc/%d/stat", pid);

FILE *fp = fopen(path, "r");
char buffer[1024];
fgets(buffer, sizeof(buffer), fp);
fclose(fp);

char *token = strtok(buffer, " ");
int count = 0;
while (token != NULL) {
if (token[0] == 't') {
printf("Debugger detected:t\n");
kill(pid, 9); // 检测到对方被调试,杀死对方
exit(1);
}
token = strtok(NULL, " ");
}
}

void check_wchan(pid_t pid) {
char path[0x20];
sprintf(path, "/proc/%d/wchan", pid);

FILE *fp = fopen(path, "r");
char buffer[256];
fgets(buffer, sizeof(buffer), fp);
fclose(fp);

if (strstr(buffer, "ptrace_stop")) {
printf("Debugger detected:ptrace_stop\n");
kill(pid, 9); // 检测到对方被调试,杀死对方
exit(1); // 自身也退出
}
}

void check() {
pid_t child_pid = fork();
if (child_pid < 0) {
perror("fork");
exit(1);
}
if(child_pid == 0){ // child process
int ccnt = 3;
while(ccnt){
check_wchan(getppid());
check_stat(getppid());
check_State(getppid());
sleep(1);
ccnt--;
}
}
int pcnt = 3;
while (pcnt) {
check_wchan(child_pid);
check_stat(child_pid);
check_State(child_pid);
sleep(1);
pcnt--;
}
int status;
waitpid(child_pid, &status, 0);
}

int main() {
check();
printf("Program running normally.\n");
return 0;
}

6.2 init_array 子线程检测

在程序初始化阶段(加载阶段)创建一个子线程,在循环中检查调试状态,同主线程中业务线程一起运行。

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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>

void* check_TracerPid(void* arg) {
FILE *fp = fopen("/proc/self/status", "r");
char line[256];
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "TracerPid")) {
if (atoi(line + strlen("TracerPid:")) != 0) {
printf("Debugger detected!\n");
fclose(fp);
kill(getpid(), 9);
exit(1);
}
}
}
fclose(fp);
}

void __attribute__((constructor)) detect_thread() {
pthread_t tid;
int ret = pthread_create(&tid, NULL, (void *(*)(void*))check_TracerPid, NULL);
if (ret != 0) {
perror("pthread_create");
exit(1);
}
pthread_detach(tid);
}

void work_thread() {
// 模拟正常业务处理
int cnt = 10;
while (cnt) {
putchar('.');
fflush(stdout);
sleep(1);
cnt--;
}
puts("\nWork thread completed.");
}

int main() {
work_thread();
printf("Program running normally.\n");
return 0;
}

7. alarm 信号检测

调试器分析时难免会让程序运行时间变长,笃定程序在短时间内能完成业务操作,可以使用alarm函数通知内核设置一个定时器,在定时器到期时,会向本进程发送 SIGALRM 信号。

在Linux中,程序收到SIGALRM信号时,如果没有自定义对该信号的处理,则使用默认的信号处理函数 SIG_DFL,对于大部分的信号,默认处理函数会终止进程。SIGALRM 信号的默认处理函数也是终止进程。

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
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>

// 收到信号说明存在调试器,直接退出
void alarm_handler(int sig){
puts("got alarm signal, debugger exist, exit!");
exit(0);
}

void __attribute__((constructor)) init(){
alarm(7); // 7秒后发送SIGALRM信号

// 注册 SIGALRM 信号处理函数
signal(SIGALRM, alarm_handler);
}

void do_work(){
// 模拟正常业务处理
putchar('.');
fflush(stdout);
sleep(1);
}

int main(){
// 5秒完成正常业务,超过5秒则认为存在调试器
int work = 5;
while(work){
do_work();
work--;
}
puts("\nProgram completed normally.");

return 0;
}

8. ptrace 检测

Linux下,默认只允许一个调试器调试一个进程,不允许多个调试器同时调试一个进程。如果多个调试器同时调试一个进程,会发生错误。

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
#include <stdio.h>
#include <sys/ptrace.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

void ptrace_detect() {
errno = 0;
// 将父进程(该程序的shell)设置为tracer
long r = ptrace(PTRACE_TRACEME, 0, NULL, NULL);
if (r == -1) { // 发生错误,说明已经被调试
if (errno == EPERM) {
puts("Debugger detected (ptrace TRACEME -> EPERM).");
kill(getpid(), 9);
}
}
}

void do_work(){
ptrace_detect();

// 模拟正常业务处理
int work = 5;
while(work){
putchar('.');
fflush(stdout);
sleep(1);
work--;
}
puts("\nwork done");
}

int main(void) {
do_work();
return 0;
}

但是当父进程退出时,调试关系被破坏,任然可以在适当的时机进行调试。稳当的还得是ptrace占坑。


linux-debugger-detecttive
https://zhaoyinshan.github.io/2025/10/31/linux-debugger-detective/
Author
Ys Zhao
Posted on
October 31, 2025
Licensed under