ELF反调试初探

http://www.freebuf.com/sectool/83509.html

ELF(Executable and Linkable Format)是Unix及类Unix系统下可执行文件、共享库等二进制文件标准格式。为了提高动态分析的难度,我们往往需要对ELF文件增加反调试措施。本文便对相关技术进行了总结归纳。

1.背景知识

1.1 ELF文件布局

ELF文件主要由以下几部分组成:

(1) ELF header
(2) Program header table,对应于segments
(3) Section header table,对应于sections
(4) 被Program header table或Sectionheader table指向的内容

注意这里segments与sections是两个不同的概念。之间的关系如下:

(1) 每个segments可以包含多个sections
(2) 每个sections可以属于多个segments
(3) segments之间可以有重合的部分

可以说,一个segment是若干个sections的组合。

下图是readelf工具输出的某ELF文件segments与sections信息:

可以看到,segments部分包含各segments的地址、偏移、属性等;而sections部分则依次列出每个segment所包含的sections。注意到,sections通常为全小写字母,而segments通常为全大写字母。

此外,这两者之间另一个关键不同点是,sections包含的是链接时需要的信息,而segments包含运行时需要的信息。即,在链接时,链接器通过section header table去寻找sections;在运行时,加载器通过program header table去寻找segments。可见下图:

一些比较重要的sections如下:

(1) .init_array: 动态库加载或可执行文件开始执行前调用的函数列表
(2) .text: 代码
(3) .got: Global offset table(GOT),包含加载时需要重定位的变量的地址
(4) .got.plt: 包含动态库中函数地址的GOT

一些比较重要的segments如下:

(1) LOAD: 运行时需要被加载进内存的segment
(2) GNU_STACK: 决定运行时栈是否可执行
(3) DYNAMIC: 动态链接信息,对应于.dynamicsection

1.2常用工具

在静态、动态分析ELF文件时,经常用到以下工具:

(1) ptrace

#include <sys/ptrace.h>
 long ptrace(enum __ptrace_requestrequest, pid_t pid,
                 void *addr,void *data);

系统调用。用于监控其他进程,被gdb, strace, ltrace等使用

(2) strace

命令行工具。用于追踪进程与内核的交互,如系统调用、信号传递

(3) ltrace

命令行工具。类似于strace,但主要用于追踪库函数调用

(4) readelf/objdump

静态分析工具。用于读取ELF文件信息。

2.反调试技术

一般地,反调试是通过比较程序在未被调试和被调试两种运行状况下的不同点,来进行检测或中止程序运行。具体地,这里介绍几种常见的反调试方法。

2.1 ptrace自身进程

在同一时间,进程最多只能被一个调试器进行调试。于是,我们可以通过调试进程自身,来判断是否已经有其他进程(调试器)的存在。

具体地,我们使用ptrace来调试自身。示例代码如下:

#include <stdio.h>
#include <sys/ptrace.h>
int main(int argc, char *argv[]) {
    if(ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) {
       printf("Debugger detected");
       return 1;
   }  
   printf("All good");
   return 0;
}

这里我们使用'PTRACE_TRACEME'来指明进程将被调试,在此种情况下其他参数会被忽略。如果已经存在调试器,那么这次ptrace调用会失败,返回-1。由此我们可以实现对调试器的检测。实际运行结果如下图所示:

2.2检查父进程名称

通常,我们在使用gdb调试时,是通过gdb <TARGET>这种方式进行的。而这种方式是启动gdb,fork出子进程后执行目标二进制文件。因此,二进制文件的父进程即为调试器。我们可通过检查父进程名称来判断是否是由调试器fork。示例代码如下

#include <stdio.h>
#include <string.h>
 
int main(int argc, char *argv[]) {
   char buf0[32], buf1[128];
   FILE* fin;
 
   snprintf(buf0, 24, "/proc/%d/cmdline", getppid());
   fin = fopen(buf0, "r");
   fgets(buf1, 128, fin);
   fclose(fin);
 
   if(!strcmp(buf1, "gdb")) {
       printf("Debugger detected");
       return 1;
   }  
   printf("All good");
   return 0;
}

这里我们通过getppid获得父进程的PID,之后由/proc文件系统获取父进程的命令内容,并通过比较字符串检查父进程是否为gdb。实际运行结果如下图所示:

2.3检查进程运行状态

2.2节所提到的反调试方法,前提是被调试程序由调试器启动。但调试器也可以通过attach到某个已有进程的方法进行调试。这种情况下,被调试进程的父进程便不是调试器了。

在这种情况下,我们可以通过直接检查进程的运行状态来判断是否被调试。而这里使用到的依然是/proc文件系统。具体地,我们检查/proc/self/status文件。当进程正常运行而未被调试时,该文件的内容如下图:

而当我们使用gdb <TARGET FILE> <TARGET PID>命令,attach到目标进程进行调试后,status文件内容变化如下图:

可见,进程状态由sleeping变为tracing stop,TracerPid也由0变为非0的数,即调试器的PID。由此,我们便可通过检查status文件中TracerPid的值来判断是否有正在被调试。示例代码如下:

#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
   int i;
   scanf("%d", &i);
   char buf1[512];
   FILE* fin;
   fin = fopen("/proc/self/status", "r");
   int tpid;
   const char *needle = "TracerPid:";
   size_t nl = strlen(needle);
   while(fgets(buf1, 512, fin)) {
       if(!strncmp(buf1, needle, nl)) {
           sscanf(buf1, "TracerPid: %d", &tpid);
           if(tpid != 0) {
                printf("Debuggerdetected");
                return 1;
           }
       }
    }
   fclose(fin);
   printf("All good");
   return 0;
}

实际运行结果如下图所示:

值得注意的是,/proc目录下包含了进程的大量信息。我们在这里是读取status文件,此外,也可通过/proc/self/stat文件来获得进程相关信息,包括运行状态。

2.4设置程序运行最大时间

这种方法经常在CTF比赛中看到。由于程序在调试时的断点、检查修改内存等操作,运行时间往往要远大于正常运行时间。所以,一旦程序运行时间过长,便可能是由于正在被调试。

具体地,在程序启动时,通过alarm设置定时,到达时则中止程序。示例代码如下:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void alarmHandler(int sig) {
   printf("Debugger detected");
   exit(1);
}
void__attribute__((constructor))setupSig(void) {
   signal(SIGALRM, alarmHandler);
   alarm(2);
}
int main(int argc, char *argv[]) {
   printf("All good");
   return 0;
}

在此例中,我们通过__attribute__((constructor)),在程序启动时便设置好定时。实际运行中,当我们使用gdb在main函数下断点,稍候片刻后继续执行时,则触发了SIGALRM,进而检测到调试器。如下图所示:

顺便一提,这种方式可以轻易地被绕过。我们可以设置gdb对signal的处理方式,如果我们选择将SIGALRM忽略而非传递给程序,则alarmHandler便不会被执行,如下图所示:

2.5检查进程打开的filedescriptor

如2.2中所说,如果被调试的进程是通过gdb <TARGET>的方式启动,那么它便是由gdb进程fork得到的。而fork在调用时,父进程所拥有的fd(file descriptor)会被子进程继承。由于gdb在往往会打开多个fd,因此如果进程拥有的fd较多,则可能是继承自gdb的,即进程在被调试。

具体地,进程拥有的fd会在/proc/self/fd/下列出。于是我们的示例代码如下:

#include <stdio.h>
#include <dirent.h>
int main(int argc, char *argv[]) {
   struct dirent *dir;
   DIR *d = opendir("/proc/self/fd");
   while(dir=readdir(d)) {
       if(!strcmp(dir->d_name, "5")) {
           printf("Debugger detected");
           return 1;
       }
    }
   closedir(d);
   printf("All good");
   return 0;
}

这里,我们检查/proc/self/fd/中是否包含fd为5。由于fd从0开始编号,所以fd为5则说明已经打开了6个文件。如果程序正常运行则不会打开这么多,所以由此来判断是否被调试。运行结果见下图:

3.总结

以上列出的反调试技术中,往往只进行了一次检测。为了提高强度,我们可以fork得到一个新的进程,在这个子进程中,每隔一段时间对父进程进行一次检测。

然而,这些技术都面临存在另外一个致命弱点:可以通过反汇编静态分析,找到相应的检测代码并修改,从而绕过反调试检测。因此,我们通常还需要对二进制文件进行混淆以对抗静态分析,这样与反调试技术相结合,才能得到理想的保护效果。

* 作者:银河实验室(企业账号),转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)

ELF反调试初探的更多相关文章

  1. Android反调试笔记

    1)代码执行时间检测 通过取系统时间,检测关键代码执行耗时,检测单步调试,类似函数有:time,gettimeofday,clock_gettime. 也可以直接使用汇编指令RDTSC读取,但测试AR ...

  2. APP加固反调试(Anti-debugging)技术点汇总

    0x00 时间相关反调试 通过计算某部分代码的执行时间差来判断是否被调试,在Linux内核下可以通过time.gettimeofday,或者直接通过sys call来获取当前时间.另外,还可以通过自定 ...

  3. 华为手机内核代码的编译及刷入教程【通过魔改华为P9 Android Kernel 对抗反调试机制】

    0x00  写在前面 攻防对立.程序调试与反调试之间的对抗是一个永恒的主题.在安卓逆向工程实践中,通过修改和编译安卓内核源码来对抗反调试是一种常见的方法.但网上关于此类的资料比较少,且都是基于AOSP ...

  4. 使用KRPano资源分析工具强力加密KRPano项目(XML防破解,切片图保护,JS反调试)

    软件交流群:571171251(软件免费版本在群内提供) krpano技术交流群:551278936(软件免费版本在群内提供) 最新博客地址:blog.turenlong.com 限时下载地址:htt ...

  5. 反调试技术常用API,用来对付检测od和自动退出程序

    在调试一些病毒程序的时候,可能会碰到一些反调试技术,也就是说,被调试的程序可以检测到自己是否被调试器附加了,如果探知自己正在被调试,肯定是有人试图反汇编啦之类的方法破解自己.为了了解如何破解反调试技术 ...

  6. 强大反调试cm的奇葩破解

    系统 : Windows xp 程序 : Crackme-xp 程序下载地址 :http://pan.baidu.com/s/1slUwmVr 要求 : 编写注册机 使用工具 : OD & I ...

  7. WinDbg调试流程的学习及对TP反调试的探索

    基础知识推荐阅读<软件调试>的第十八章 内核调试引擎 我在里直接总结一下内核调试引擎的几个关键标志位,也是TP进行反调试检测的关键位. KdPitchDebugger : Boolean ...

  8. 基于TLS的反调试技术

    TLS(Thread Local Storage 线程局部存储) 一个进程中的每个线程在访问同一个线程局部存储时,访问到的都是独立的绑定于该线程的数据块.在PEB(进程环境块)中TLS存储槽共64个( ...

  9. 去除ios反调试

    在逆向过程中经常会遇到反调试,如下段代码: 0008bd8e movs r1, #0xa ; argument #2 for method imp___symbolstub1__dlopen 0008 ...

随机推荐

  1. 【BZOJ1972】[SDOI2010] 猪国杀(恶心的大模拟)

    点此看题面 大致题意: 让你模拟一个游戏猪国杀的过程. 几大坑点 对于这种模拟题,具体思路就不讲了,就说说有哪些坑点. 题面有锅,反猪是\(FP\). 数据有锅,牌堆中的牌可能不够用,牌堆为空之后需一 ...

  2. 第四章 用javascript和DOM去建立一个图片库

    把整个图片库的浏览链接集中安排在你的图片库里,只在用户点击了这个主页里的某个图片链接时才把相应的图片传送给它. 代码如下: <body> <ul> <li> < ...

  3. python_8_guess

    #python3和2都可以 #方法1 age_of_oldboy=56 count=0 while True: if count==3: break guess_age=int(input('gues ...

  4. Mybatis generator(复制粘贴完成)

    命令行模式 1.java -jar mybatis-generator-core-x.x.x.jar -configfile generatorConfig.xml 2.Maven plugin(my ...

  5. java调用摄像头了

    http://www.cnblogs.com/cnweiblog/p/4602207.html

  6. iOS 反射函数: performSelector, NSInvocation, objc_msgSend

    当我们有方法名和参数列表,想要动态地给对象发送消息,可用通过反射函数机制来实现,有两种常用的做法: 一.performSelector - (id)performSelector:(SEL)aSele ...

  7. C++ 无限定名称查找

    无限定名称查找 (关键字:懒惰,挑捡,using指令的特殊性) 无限定名称查找实际上就是指没有限定(名称空间和名称空间运算符)名存在的一个名字的出现,其中对于using指令,其内部包含的所有的声明是被 ...

  8. 1143: [CTSC2008]祭祀river

    Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 4018  Solved: 2048[Submit][Status][Discuss] Descript ...

  9. ELFhash - 优秀的字符串哈希算法

    ELFhash - 优秀的字符串哈希算法 2016年10月29日 22:12:37 阅读数:6440更多 个人分类: 算法杂论算法精讲数据结构 所属专栏: 算法与数据结构   版权声明:本文为博主原创 ...

  10. yum仓库及配置

    本文由秀依林枫提供友情赞助,首发于烂泥行天下. 最近由于服务器需求,需要在公司内网搭建内网yum源. 搭建内网yum源需要分以下几个步骤,如下: 1. yum是什么 2. repo文件是什么 3. r ...