ELF反调试初探

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

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

1.背景知识

1.1 ELF文件布局

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

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

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

  1. (1) 每个segments可以包含多个sections
  2. (2) 每个sections可以属于多个segments
  3. (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. (1) .init_array: 动态库加载或可执行文件开始执行前调用的函数列表
  2. (2) .text: 代码
  3. (3) .got: Global offset table(GOT),包含加载时需要重定位的变量的地址
  4. (4) .got.plt: 包含动态库中函数地址的GOT

一些比较重要的segments如下:

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

1.2常用工具

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

(1) ptrace

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

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

(2) strace

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

(3) ltrace

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

(4) readelf/objdump

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

2.反调试技术

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

2.1 ptrace自身进程

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

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

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

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

2.2检查父进程名称

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

  1. #include <stdio.h>
  2. #include <string.h>
  3.  
  4. int main(int argc, char *argv[]) {
  5.    char buf0[32], buf1[128];
  6.    FILE* fin;
  7.  
  8.    snprintf(buf0, 24, "/proc/%d/cmdline", getppid());
  9.    fin = fopen(buf0, "r");
  10.    fgets(buf1, 128, fin);
  11.    fclose(fin);
  12.  
  13.    if(!strcmp(buf1, "gdb")) {
  14.        printf("Debugger detected");
  15.        return 1;
  16.    }  
  17.    printf("All good");
  18.    return 0;
  19. }

这里我们通过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的值来判断是否有正在被调试。示例代码如下:

  1. #include <stdio.h>
  2. #include <string.h>
  3. int main(int argc, char *argv[]) {
  4.    int i;
  5.    scanf("%d", &i);
  6.    char buf1[512];
  7.    FILE* fin;
  8.    fin = fopen("/proc/self/status", "r");
  9.    int tpid;
  10.    const char *needle = "TracerPid:";
  11.    size_t nl = strlen(needle);
  12.    while(fgets(buf1, 512, fin)) {
  13.        if(!strncmp(buf1, needle, nl)) {
  14.            sscanf(buf1, "TracerPid: %d", &tpid);
  15.            if(tpid != 0) {
  16.                 printf("Debuggerdetected");
  17.                 return 1;
  18.            }
  19.        }
  20.     }
  21.    fclose(fin);
  22.    printf("All good");
  23.    return 0;
  24. }

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

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

2.4设置程序运行最大时间

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

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

  1. #include <stdio.h>
  2. #include <signal.h>
  3. #include <stdlib.h>
  4. void alarmHandler(int sig) {
  5.    printf("Debugger detected");
  6.    exit(1);
  7. }
  8. void__attribute__((constructor))setupSig(void) {
  9.    signal(SIGALRM, alarmHandler);
  10.    alarm(2);
  11. }
  12. int main(int argc, char *argv[]) {
  13.    printf("All good");
  14.    return 0;
  15. }

在此例中,我们通过__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/下列出。于是我们的示例代码如下:

  1. #include <stdio.h>
  2. #include <dirent.h>
  3. int main(int argc, char *argv[]) {
  4.    struct dirent *dir;
  5.    DIR *= opendir("/proc/self/fd");
  6.    while(dir=readdir(d)) {
  7.        if(!strcmp(dir->d_name, "5")) {
  8.            printf("Debugger detected");
  9.            return 1;
  10.        }
  11.     }
  12.    closedir(d);
  13.    printf("All good");
  14.    return 0;
  15. }

这里,我们检查/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. [solr 管理界面] - 索引数据删除

    删除solr索引数据,使用XML有两种写法: 1) <delete><id>1</id></delete> <commit/> 2) < ...

  2. Web/Java Web项目如何模块化?没有正文,别点

    事情是这样的,两三年前做了几个Java Web项目,由于薪资原因,原主程都离开了. 由于公司不规范,也没有留下正规的开发文档,只有一个源程序在手里.后面的很多系统维护都很被动. 领导就觉得说,这样不好 ...

  3. java从键盘输入学生成绩,找出最高分,并输出学生成绩等级。

    /*从键盘输入学生成绩,找出最高分,并输出学生成绩等级:成绩 >=最高分-10 等级为A成绩 >=最高分-20 等级为B成绩 >=最高分-30 等级为C其余为 等级为D 提示:先输入 ...

  4. 51nod——1086、1257背包问题V2(多重背包二进制拆分转01) V3(分数规划+二分贪心)

    V3其实和dp关系不大,思想挂标题上了,丑陋的代码不想放了.

  5. German Collegiate Programming Contest 2018​ C. Coolest Ski Route

    John loves winter. Every skiing season he goes heli-skiing with his friends. To do so, they rent a h ...

  6. Linux 内核源码外编译 linux模块--编译驱动模块的基本方法

    1.先编写一个简单的hello模块,hello.c 源码如下: #ifndef __KERNEL__ # define __KERNEL__ #endif #ifndef MODULE # defin ...

  7. Android拨打电话不弹出系统拨号界面总结

    我在网上搜了一下,解决这个问题,有两种方式: 1.反射调用系统底层方法,并获取系统权限 反射调用的代码如下: Class phoneFactoryClass = Class.forName(" ...

  8. 16,Python网络爬虫之Scrapy框架(CrawlSpider)

    今日概要 CrawlSpider简介 CrawlSpider使用 基于CrawlSpider爬虫文件的创建 链接提取器 规则解析器 引入 提问:如果想要通过爬虫程序去爬取”糗百“全站数据新闻数据的话, ...

  9. 3 View - 错误视图函数

    1.定义视图 本质就是一个函数 视图的参数 一个HttpRequest实例 通过正则表达式组获取的位置参数 通过正则表达式组获得的关键字参数 在应用目录下默认有views.py文件,一般视图都定义在这 ...

  10. UVa 1455 Kingdom 线段树 并查集

    题意: 平面上有\(n\)个点,有一种操作和一种查询: \(road \, A \, B\):在\(a\),\(b\)两点之间加一条边 \(line C\):询问直线\(y=C\)经过的连通分量的个数 ...