前言
網上看到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/