反调试技术总结

发布于 2020-07-26  3902 次阅读


Introduction

程序的反调试是保护程序不被它人轻易逆向的必备技术,本文将探讨几个常见的 Linux 平台下的反调试技术,供大家参考。

int3

int3 其实就是专门为调试器准备的一个指令,调试器把想下断点的地方的机器码修改为 0xCC,CPU运行到此处时便会触发 SIGTRAP 中断,从而被调试器接管。

同时,用户空间也会产生相应的 SIGTRAP 信号,我们只要注册回调函数就可以忽略该信号。

#include<stdio.h>
#include<signal.h>
void handler(int signo){}

int main(void)
{
    signal(SIGTRAP, handler);
    __asm__("nop\n\t"
        "int3\n\t");
    printf("Hello from main!\n");
    return 0;
}

当然,触发其他形式的信号也是可以的。只需要注册相应的回调函数即可。(甚至可以利用信号量编写一个VM,能够极大提高逆向的难度)

获取进程信息

Linux 下的 ID

Linux中,进程都拥有以下的ID

  1. Process ID(PID)
    Linux中标识进程的一个数字,它的值是不确定的,是由系统分配的(但是有一个例外,启动阶段,kernel运行的第一个进程是init,它的PID是1,是所有进程的最原始的父进程),每个进程都有唯一PID,当进程退出运行之后,PID就会回收,可能之后创建的进程会分配这个PID
  2. Parent Process ID(PPID)
    字面意思,父进程的PID
  3. Process Group ID(PGID)
    PGID就是进程所属的Group的Leader的PID,如果PGID=PID,那么该进程是Group Leader
  4. Session ID(SID)
    和PGID非常相似,SID就是进程所属的Session Leader的PID,如果SID==PID,那么该进程是session leader

获取父进程信息

调试器要附加进程,必须成为该进程的父进程,也就是说主程序一定是调试器的子进程。

通过getpid,我们可以获取到 /proc/pid/cmdline 下程序的启动参数。

若启动参数不为 -bash-init,那么就可以判断其使用了调试器。

#include<stdio.h> 
#include<unistd.h>
#include<fcntl.h>

int get_name_by_pid(pid_t pid, char* name)
{
    int fd;
    char buf[1024] = {0};
    snprintf(buf, 1024, "/proc/%d/cmdline", pid);
    if ((fd = open(buf, O_RDONLY)) == -1)
        return -1;
    read(fd, buf, 1024);
    strncpy(name, buf, 1023);
    return 0;
}

int main(void)
{
    char name[1024];
    pid_t ppid = getppid();
    printf("getppid: %d\n", ppid);

    if (get_name_by_pid(ppid, name))
        return -1;

    printf("%s\n\n", name);

    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");
    else
        printf("Unknown! Maybe traced!\n");

    return 0;
}

利用 session id

在同一个终端内,程序的 sid 是不会变的,等于 group leader 的 pid。

而如果使用了调试器,程序的父进程便不再是 -bash,而是 -bash 的子进程。

所以 getsid(getpid()) ≠ getppid(),故检测出程序使用了调试器

#include<stdio.h>
#include<unistd.h>

int main(void)
{
    printf("getsid: %d\n", getsid(getpid()));
    printf("getppid: %d\n", getppid());

        if (getsid(getpid()) != getppid()) {
        printf("traced!\n");
        exit(0);
    }
        printf("OK\n");

    return 0;
}

getenv

bash有一个环境变量叫$_,它保存的是上一个执行的命令的最后一个参数。

getenv 用于获取环境变量,若上一个执行的命令不是 argv[0],则说明在调用程序之前,可能启动了调试器。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main(int argc, char *argv[]){
    printf("getenv(_): %s\n", getenv("_"));
    printf("argv[0]: %s\n", argv[0]);

    if(strcmp(argv[0], (char *)getenv("_")))
        printf("traced!\n");
    else
        printf("OK\n");
    return 0;
}

ptrace

调试器在调试目标程序时会进行 fork() 产生子进程,并且 exec() 来执行相应的程序。

如果对自己所在的进程进行 ptrace 时,该进程已经被 gdb 附加了,那么 ptrace 就会报错返回 -1,这也从另外一种角度揭示了调试器的存在。

ptrace 的更多功能: https://www.cnblogs.com/heixiang/p/10988992.html

#include<stdio.h>
#include<unistd.h>
#include<sys/ptrace.h>

int main(void)
{
     if ( ptrace(PTRACE_TRACEME, 0, 0, 0) < 0 ) {
        printf("traced!\n");
        return 1;
    }
    printf("OK\n");
    return 0;
}

alarm

设置定时器,让程序在指定时间内自动退出。


CTFer|NOIPer|CSGO|摸鱼|菜鸡