ELF反调试初探
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反调试初探的更多相关文章
- Android反调试笔记
1)代码执行时间检测 通过取系统时间,检测关键代码执行耗时,检测单步调试,类似函数有:time,gettimeofday,clock_gettime. 也可以直接使用汇编指令RDTSC读取,但测试AR ...
- APP加固反调试(Anti-debugging)技术点汇总
0x00 时间相关反调试 通过计算某部分代码的执行时间差来判断是否被调试,在Linux内核下可以通过time.gettimeofday,或者直接通过sys call来获取当前时间.另外,还可以通过自定 ...
- 华为手机内核代码的编译及刷入教程【通过魔改华为P9 Android Kernel 对抗反调试机制】
0x00 写在前面 攻防对立.程序调试与反调试之间的对抗是一个永恒的主题.在安卓逆向工程实践中,通过修改和编译安卓内核源码来对抗反调试是一种常见的方法.但网上关于此类的资料比较少,且都是基于AOSP ...
- 使用KRPano资源分析工具强力加密KRPano项目(XML防破解,切片图保护,JS反调试)
软件交流群:571171251(软件免费版本在群内提供) krpano技术交流群:551278936(软件免费版本在群内提供) 最新博客地址:blog.turenlong.com 限时下载地址:htt ...
- 反调试技术常用API,用来对付检测od和自动退出程序
在调试一些病毒程序的时候,可能会碰到一些反调试技术,也就是说,被调试的程序可以检测到自己是否被调试器附加了,如果探知自己正在被调试,肯定是有人试图反汇编啦之类的方法破解自己.为了了解如何破解反调试技术 ...
- 强大反调试cm的奇葩破解
系统 : Windows xp 程序 : Crackme-xp 程序下载地址 :http://pan.baidu.com/s/1slUwmVr 要求 : 编写注册机 使用工具 : OD & I ...
- WinDbg调试流程的学习及对TP反调试的探索
基础知识推荐阅读<软件调试>的第十八章 内核调试引擎 我在里直接总结一下内核调试引擎的几个关键标志位,也是TP进行反调试检测的关键位. KdPitchDebugger : Boolean ...
- 基于TLS的反调试技术
TLS(Thread Local Storage 线程局部存储) 一个进程中的每个线程在访问同一个线程局部存储时,访问到的都是独立的绑定于该线程的数据块.在PEB(进程环境块)中TLS存储槽共64个( ...
- 去除ios反调试
在逆向过程中经常会遇到反调试,如下段代码: 0008bd8e movs r1, #0xa ; argument #2 for method imp___symbolstub1__dlopen 0008 ...
随机推荐
- linux怎么进home目录下
可以使用cd命令,cd命令的功能是切换到指定的目录: 命令格式:cd [目录名] 有几个符号作为目录名有特殊的含义: “/”代表根目录.“..”代表上一级目录.“~”代表HOME目录.“-”代表前一目 ...
- 3219: 求最高同学位置—C语言版
3219: 求最高同学位置—C语言版 时间限制: 1 Sec 内存限制: 128 MB提交: 207 解决: 115[提交][状态][讨论版][命题人:smallgyy] 题目描述 设一维数组存放 ...
- 反转链表[剑指offer]之python实现
输入一个链表,输出反转后的链表. 非递归实现: # -*- coding:utf-8 -*- # class ListNode: # def __init__(self, x): # self.val ...
- angular4 学习日志(一 依赖注入)
1.创建一个服务,为了好管理建一个名叫services的文件夹管理所有服务: ng g service services\person 2.在服务中定义一个person 类 : 3.在app.mdul ...
- python查看安装包
D:\Python27\Scripts>pip listbackports.ssl-match-hostname (3.4.0.2)basicauth (0.2)certifi (14.5.14 ...
- 【原创】数据处理中判断空值的方法(np.isnan、is np.nan和pd.isna)比较
转载请注明出处:https://www.cnblogs.com/oceanicstar/p/10869725.html 1.np.isnan(只有数组数值运算时可使用) 注意:numpy模块的i ...
- .net core 在IIS上发布502问题
本来迁移一个项目到.net core就是一件体力活,要找各种替代包,还有一些函数/属性的不支持 总之很头疼... 不要问我为什么用了.net core还要Host在IIS上,国内用.net的公司普遍都 ...
- STM32启动文件:startup_stm32f10x_hd.s等启动文件的简单描述
在官方的库文件中,分别有如下文件: startup │ │ │ ├─arm │ │ │ │ startup_stm32f10x_cl.s │ │ │ │ startup_stm32f10x_hd.s ...
- Docker从零到实践过程中的坑
欢迎指正: Centos7 下的ulimit在Docker中的坑 http://www.dockone.io/article/522 僵尸容器:Docker 中的孤儿进程 https://yq.ali ...
- HDU5952 Counting Cliques 暴搜优化
一.前言 这题看上去相当唬人(NPC问题),但是 因为限制了一些条件,所以实际上并没有太唬人. 二.题目 给你一个图,要求你找出数量为S的团的数量. 三.题解 暴搜,再加上一些玄学优化. 优化1:使用 ...