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.so、libfrida-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:
一个被调试的程序,其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,该文件内容更详细。该文件中记录的字段中,有两个字段和调试器相关,分别是 TracerPid 和 State。其他字段参考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 PPid: 3642155 TracerPid: 3642155 Uid: 1000 1000 1000 1000 Gid: 1000 1000 1000 1000
|
5.1 检测方法
该文件中记录了两个调试器相关的字段,TracerPid 和 State。因此存在两个检测点,打开/proc/self/status文件,查看TracerPid字段,如果为非0,则表示当前进程被调试。或者检测State字段,如果为 t (tracing stop),则表示当前进程被调试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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){ 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);
signal(SIGALRM, alarm_handler); }
void do_work(){ putchar('.'); fflush(stdout); sleep(1); }
int main(){ 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; 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占坑。