天天看点

从ELF文件格式到软件断点检测反调试技术

前言

网上看到16年unlimit安全小组的反调试文章,写得非常详细还公布了源代码,遂研读了一下。为了看懂断点扫描反调试的那一节,我也把之前一直没弄明白的ELF文件格式弄明白了些。

软件断点检测原理

先说断点检测反调试的原理——“软件断点通过改写目标地址的头几字节为breakpoint指令,只需要遍历so中可执行segment,查找是否出现breakpoint指令“,如果出现breakpoint指令则认为存在调试器,就可以终止进程。为了实现这个技术,需要一定的ELF文件格式的知识。

从ELF文件格式到软件断点检测反调试技术

ELF文件格式概述

ELF文件有四种,大致分为可链接和可执行两大类,反调试中我们主要关注So文件。对于可链接文件,采用链接视图,Program header table是可选的。对于可执行文件,采用执行视图,section header table是可选的。链接视图是以节(section)为单位,执行视图是以段(segment)为单位。

从ELF文件格式到软件断点检测反调试技术

ELF文件的主要结构有四个ELF头部表、程序头部表、节区/段、节区头部表。用010editor+ELFtemplate可以清晰看到ELF结构。

从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 节区存放程序代码。

从ELF文件格式到软件断点检测反调试技术

总结

通过/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其实都是数据结构里通过相对偏移访问成员变量。

从ELF文件格式到软件断点检测反调试技术

参考感谢:

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/

继续阅读