前言
网上看到16年unlimit安全小组的反调试文章,写得非常详细还公布了源代码,遂研读了一下。为了看懂断点扫描反调试的那一节,我也把之前一直没弄明白的ELF文件格式弄明白了些。
软件断点检测原理
先说断点检测反调试的原理——“软件断点通过改写目标地址的头几字节为breakpoint指令,只需要遍历so中可执行segment,查找是否出现breakpoint指令“,如果出现breakpoint指令则认为存在调试器,就可以终止进程。为了实现这个技术,需要一定的ELF文件格式的知识。

ELF文件格式概述
ELF文件有四种,大致分为可链接和可执行两大类,反调试中我们主要关注So文件。对于可链接文件,采用链接视图,Program header table是可选的。对于可执行文件,采用执行视图,section header table是可选的。链接视图是以节(section)为单位,执行视图是以段(segment)为单位。
ELF文件的主要结构有四个ELF头部表、程序头部表、节区/段、节区头部表。用010editor+ELFtemplate可以清晰看到ELF结构。
1.ELF头部表
ELF头部表是一个32字节Elf_Ehdr类型的数据结构,在这里我们需要关注的成员有:
e_phnum,它用2字节描述了程序头部表中一共有多少个段,相对数据结构首地址偏移44字节;
e_phoff成员,它用4字节描述了程序头部表相对于ELF首地址的偏移,相对数据结构首地址偏移28字节。
2.程序头部表
程序头部表是一个数组,数组的每个元素是一个32字节ELF_phdr类型的结构,每个ELF_phdr的结构描述了一个对应段(segment)的信息。在这里我们需要关注的成员有:
p_flags描述段访问权限,相对数据结构首地址偏移24字节;
p_vaddr描述本段内容的开始位置在进程空间中的虚拟地址,相对数据结构首地址偏移8字节;
p_memsz描述段长度,相对数据结构首地址偏移20字节。
3.节区/段
段(segments)与节区(sections)是包含的关系,一个segment包含若干个section,这种设计可以减少页面内部的碎片,节省了空间,显著提高内存利用率。
4.节区头部表
节区头部表描述了各个节区的信息,在反调试中可关注.text节区,如果仅针对普通断点,可以只检测.text节区,因为.text 节区存放程序代码。
总结
通过/proc/[pid]/maps的进程内存映射,可以获取进程在内存中的地址,再结合ELF头部表、程序头部表里提供的信息,我们可以通过以下计算公式,计算出每一个段的首地址:
某段首地址=程序在内存中的地址(base)+ELF头部表大小(32)+ELF头部表中的程序表数(p_phnum)*单个程序表的大小(32)+此段在进程空间中的虚拟地址(p_vaddr)
然后通过每一个段的大小p_memsz限制扫描范围,即可实现段扫描检测软件断点。
unlimit安全小组的实现代码如下,添加了个人理解的注释:
bool checkBreakPoint ()
{
int i, j;
unsigned int base, offset, pheader;
Elf32_Ehdr *elfhdr;ELF程序头部表(ELF_Header)
Elf32_Phdr *ph_t;//程序头部表(Program_Header_Table)
base = getLibAddr ("libnative-lib.so");//通过/proc/[pid]/maps 获取so文件虚拟基址
if (base == 0) {
LOGI ("getLibAddr failed");
return false;
}
elfhdr = (Elf32_Ehdr *) base;//ELF头部表地址(虚拟)
pheader = base + elfhdr->e_phoff;//获取程序头部表地址=ELF头部表虚拟地址+程序头部表偏移(phoff的位置一般在 0x1C=28处,占四字节)
for (i = 0; i < elfhdr->e_phnum; i++) {//程序头部表的表数,逐个表扫描(phnum的位置一般在 0x2C=44处,占二字节)
ph_t = (Elf32_Phdr*)(pheader + i * sizeof(Elf32_Phdr)); // 每个程序表头地址=程序表头部地址+表编号*单个程序头部表的大小(sizeof(Elf32_phdr)一般等于0x20=32字节))
if ( !(ph_t->p_flags & 1) ) continue;//检查段访问权限(p_flags相对表头的偏移是0x1C=24字节)
offset = base + ph_t->p_vaddr;//(p_vaddr相对表头的偏移是8字节)
offset += sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) * elfhdr->e_phnum;//段首地址=程序在内存中的虚拟基址+ELF头部表大小+ELF头部表中的程序表数*程序表大小+此段偏移
char *p = (char*)offset;
for (j = 0; j < ph_t->p_memsz; j++) {//段长度
if(*p == 0x01 && *(p+1) == 0xde) {//thumb16
LOGI ("Find thumb bpt %p", p);
return true;
} else if (*p == 0xf0 && *(p+1) == 0xf7 && *(p+2) == 0x00 && *(p+3) == 0xa0) {//thumb32
LOGI ("Find thumb2 bpt %p", p);
return true;
} else if (*p == 0x01 && *(p+1) == 0x00 && *(p+2) == 0x9f && *(p+3) == 0xef) {//arm断点
LOGI ("Find arm bpt %p", p);
return true;
}
p++;
}
}
return false;
}
再来看IDA下的checkBreakPoint ()代码,也基本能看懂了,+8、+20、+24、+28、+44、+52其实都是数据结构里通过相对偏移访问成员变量。
参考感谢:
http://www.vuln.cn/6063
https://www.jianshu.com/p/dd5aec5826da
https://www.52pojie.cn/thread-591986-1-1.html
http://bdxnote.blog.163.com/blog/static/844423520154502715467/
https://www.freebuf.com/sectool/83509.html
https://gtoad.github.io/2017/06/25/Android-Anti-Debug/