天天看點

從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/

繼續閱讀