好记性不如铅笔头

安全

【转】Linux反调试小记

本文转自【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();

}

发表评论

17 − 3 =

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据