本文转自【Linux反调试_人而已的博客-CSDN博客】
CONTENTS
忽略int3指令
我们知道,X86从它的第一代产品8086开始就提供了int 3作为它的调试指令,int 3又叫做断点指令,专门用来给调试器使用。那么如果这样的话,很容易就能够联想到,要反调试,只要插入int 3来迷惑调试器即可。但是这会影响正常的程序吗?当然了,因为int 3会在用户空间产生SIGTRAP。其实这个很简单,我们只要忽略这个信号就可以啦。
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> #include <dirent.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/stat.h> void handler(int signo) { } void PassByInt3() { signal(SIGTRAP, handler); __asm__("nop\n\t" "int3\n\t"); printf("Hello from main!\n"); } int main(int argc, char *argv[]) { PassByInt3(); }
利用getppid
利用getppid进行探测。要知道,在Linux中要跟踪一个程序,那么一定是它的父进程才能够做到,因此,如果目标程序的父进程不是意料之中的bash、sh等,而是gdb、strace之类的程序,那么就能够说明它被跟踪了。就是利用这个原理,来实现反调试功能。
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> #include <dirent.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/stat.h> void Usegetppid() { char name[1024]; pid_t ppid = getppid(); memset(name, 0, 1024); printf("getppid:%d\n", ppid); if(GetNameByPid(ppid, name)) printf("error!\n"); if(strcmp(name, "bash") == 0 || strcmp(name, "init") == 0) printf("OK!\n"); else if(strcmp(name, "gdb") == 0|| strcmp(name, "strace") == 0 || strcmp(name, "ltrace") == 0) { printf("Traced!\n"); exit(0); } else printf("Unknown!Maybe traced!\n"); } int main(int argc, char *argv[]) { Usegetppid(); }
利用session id
在程序运行过程中,不论是否被跟踪,session id是不变的,而ppid会变!getsid函数返回参数所指向的进程所在的会话的会话首进程的进程组ID(因为会话首进程总是一个进程组的组长,所以返回的进程组ID与首进程ID是相同的)。
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> #include <dirent.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/stat.h> void UseSessionId() { printf("getsid:%d\n", getsid(getpid())); printf("getppid:%d\n", getppid()); if(getsid(getpid()) != getppid()) { printf("traced!\n"); exit(0); } printf("OK!\n"); } int main(int argc, char *argv[]) { UseSessionId(); }
利用环境变量
在Linux中,bash有一个环境变量叫$_,它保存的是上一个执行的命令的最后一个参数。如果在被跟踪的状态下,这个变量的值会发生变化。下面是可能产生变化的几种情况:
程序名 | 参数argv[0] | 环境变量getenv(“_”) |
shell | ./test | ./test |
strace | ./test | /usr/bin/strace |
ltrace | ./test | /usr/bin/ltrace |
gdb | /home/user/test | (NULL) |
从表中可以看到只要参数argv[0]与环境变量getenv(“_”)不同,就表示程序被调试了。有了这样的认识,那么程序就好写了,代码如下:
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> #include <dirent.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/stat.h> void UseEnv(char *argvarray) { printf("getenv(_):%s\n", getenv("_")); printf("argv[0]:%s\n", argvarray); if(strcmp(argvarray, (char*) getenv("_"))) { printf("traced!\n"); exit(0); } printf("OK\n"); } int main(int argc, char *argv[]) { UseEnv(argv[0]); }
利用ptrace
调试过程中要用到ptrace这个函数,那么如果被跟踪了再调用ptrace(PTRACE_TRACEME…)自然不会成功。
代码如下:
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> #include <dirent.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/stat.h> void UsePtrace() { if(ptrace(PTRACE_TRACEME, 0, 1, 0) < 0) { printf("traced!\n"); exit(0); } printf("OK!\n"); } int main(int argc, char *argv[]) { UsePtrace(); }
检查父进程名称
通常,我们在使用gdb调试时,是通过gdb <TARGET>这种方式进行的。而这种方式是启动gdb,fork出子进程后执行目标二进制文件。因此,二进制文件的父进程即为调试器。我们可通过检查父进程名称来判断是否是由调试器fork。
代码如下:
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> #include <dirent.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/stat.h> void UseFatherName() { char buf0[32], buf1[128]; FILE* fin; memset(buf0, 0, 32); memset(buf1, 0, 128); snprintf(buf0, 24, "/proc/%d/cmdline", getppid()); fin = fopen(buf0, "r"); fgets(buf1, 128, fin); fclose(fin); if(!strcmp(buf1, "gdb")) { printf("Debugger detected"); exit(0); } printf("All good!\n"); } int main(int argc, char *argv[]) { UseFatherName(); }
检查进程运行状态
调试器可以通过attach到某个已有进程的方法进行调试。这种情况下,被调试进程的父进程便不是调试器了。
在这种情况下,我们可以通过直接检查进程的运行状态来判断是否被调试。而这里使用到的依然是/proc文件系统。具体地,我们检查/proc/self/status文件。
当由非调试状态转变为调试状态时,进程状态由sleeping变为tracing stop,TracerPid也由0变为非0的数,即调试器的PID。由此,我们便可通过检查status文件中TracerPid的值来判断是否有正在被调试。值得注意的是,/proc目录下包含了进程的大量信息。我们在这里是读取status文件,此外,也可通过/proc/self/stat文件来获得进程相关信息,包括运行状态。
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> #include <dirent.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/stat.h> void UseProcStatus() { const char *needle = "TracerPid:"; size_t nl = strlen(needle); char buf1[512]; FILE* fin; int tpid; memset(buf1, 0, 512); fin = fopen("/proc/self/status", "r"); while(fgets(buf1, 512, fin)) { if(!strncmp(buf1, needle, nl)) { sscanf(buf1, "TracerPid: %d", &tpid); if(tpid != 0) { printf("Debugger detected!\n"); fclose(fin); exit(0); } } } fclose(fin); printf("All good\n"); } int main(int argc, char *argv[]) { UseProcStatus(); }
设置程序运行最大时间
这种方法经常在CTF比赛中看到。由于程序在调试时的断点、检查修改内存等操作,运行时间往往要远大于正常运行时间。所以,一旦程序运行时间过长,便可能是由于正在被调试。
具体地,在程序启动时,通过alarm设置定时,到达时则中止程序。
但是需要注意的是,这种方式可以被轻易绕过。我们可以设置gdb对signal的处理方式,如果我们选择将SIGALRM忽略而非传递给程序,则alarmHandler便不会被执行。
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> #include <dirent.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/stat.h> void alarmHandler(int sig) { printf("Debugger detected"); exit(0); } void UseAlarm() { signal(SIGALRM, alarmHandler); alarm(2); } int main(int argc, char *argv[]) { UseAlarm(); }
检查进程打开的filedescriptor
如果被调试的进程是通过gdb <TARGET>的方式启动,那么它便是由gdb进程fork得到的。而fork在调用时,父进程所拥有的fd(file descriptor)会被子进程继承。由于gdb在往往会打开多个fd,因此如果进程拥有的fd较多,则可能是继承自gdb的,即进程在被调试。
具体地,进程拥有的fd会在/proc/self/fd/下列出。
要注意的是,这种方法有概率检查不到的。
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> #include <dirent.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/stat.h> void UseFdNum() { struct dirent *dir; DIR *d = opendir("/proc/self/fd"); while(dir = readdir(d)) { if(!strcmp(dir->d_name, "5")) { printf("Debugger detected!"); closedir(d); exit(0); } } closedir(d); printf("All good!\n"); } int main(int argc, char *argv[]) { UseFdNum(); }
发表评论