文章出處

linux ELF反調試初探:ELF是Unix及類Unix系統下可執行文件、共享庫等二進制文件標準格式。為了提高動態分析的難度,我們往往需要對ELF文件增加反調試措施。本文便對相關技術進行了總結歸納。

1.背景知識

1.1 ELF文件布局

ELF文件主要由以下幾部分組成:

(1)ELFheader(2)Programheadertable,對應于segments(3)Sectionheadertable,對應于sections(4)被Programheadertable或Sectionheadertable指向的內容

注意這里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:Globaloffsettable(GOT),包含加載時需要重定位的變量的地址(4).got.plt:包含動態庫中函數地址的GOT

一些比較重要的segments如下:

(1)LOAD:運行時需要被加載進內存的segment(2)GNU_STACK:決定運行時棧是否可執行(3)DYNAMIC:動態鏈接信息,對應于.dynamicsection

1.2常用工具

在靜態、動態分析ELF文件時,經常用到以下工具:

(1) ptrace

#includelongptrace(enum__ptrace_requestrequest,pid_tpid,void*addr,void*data);

系統調用。用于監控其他進程,被gdb, strace, ltrace等使用

(2) strace

命令行工具。用于追蹤進程與內核的交互,如系統調用、信號傳遞

(3) ltrace

命令行工具。類似于strace,但主要用于追蹤庫函數調用

(4) readelf/objdump

靜態分析工具。用于讀取ELF文件信息。

2.反調試技術

一般地,反調試是通過比較程序在未被調試和被調試兩種運行狀況下的不同點,來進行檢測或中止程序運行。具體地,這里介紹幾種常見的反調試方法。

2.1 ptrace自身進程

在同一時間,進程最多只能被一個調試器進行調試。于是,我們可以通過調試進程自身,來判斷是否已經有其他進程(調試器)的存在。

具體地,我們使用ptrace來調試自身。示例代碼如下:

#include#includeintmain(intargc,char*argv[]){if(ptrace(PTRACE_TRACEME,0,0,0)==-1){printf("Debuggerdetected");return1;}printf("Allgood");return0;}

這里我們使用'PTRACE_TRACEME'來指明進程將被調試,在此種情況下其他參數會被忽略。如果已經存在調試器,那么這次ptrace調用會失敗,返回-1。由此我們可以實現對調試器的檢測。實際運行結果如下圖所示:

\

2.2檢查父進程名稱

通常,我們在使用gdb調試時,是通過gdb 這種方式進行的。而這種方式是啟動gdb,fork出子進程后執行目標二進制文件。因此,二進制文件的父進程即為調試器。我們可通過檢查父進程名稱來判斷是否是由調試器fork。示例代碼如下

#include#includeintmain(intargc,char*argv[]){charbuf0[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("Debuggerdetected");return1;}printf("Allgood");return0;}

這里我們通過getppid獲得父進程的PID,之后由/proc文件系統獲取父進程的命令內容,并通過比較字符串檢查父進程是否為gdb。實際運行結果如下圖所示:

\

2.3檢查進程運行狀態

2.2節所提到的反調試方法,前提是被調試程序由調試器啟動。但調試器也可以通過attach到某個已有進程的方法進行調試。這種情況下,被調試進程的父進程便不是調試器了。

在這種情況下,我們可以通過直接檢查進程的運行狀態來判斷是否被調試。而這里使用到的依然是/proc文件系統。具體地,我們檢查/proc/self/status文件。當進程正常運行而未被調試時,該文件的內容如下圖:

而當我們使用gdb 命令,attach到目標進程進行調試后,status文件內容變化如下圖:

\

可見,進程狀態由sleeping變為tracing stop,TracerPid也由0變為非0的數,即調試器的PID。由此,我們便可通過檢查status文件中TracerPid的值來判斷是否有正在被調試。示例代碼如下:

#include#includeintmain(intargc,char*argv[]){inti;scanf("%d",&i);charbuf1[512];FILE*fin;fin=fopen("/proc/self/status","r");inttpid;constchar*needle="TracerPid:";size_tnl=strlen(needle);while(fgets(buf1,512,fin)){if(!strncmp(buf1,needle,nl)){sscanf(buf1,"TracerPid:%d",&tpid);if(tpid!=0){printf("Debuggerdetected");return1;}}}fclose(fin);printf("Allgood");return0;}

實際運行結果如下圖所示:

\

值得注意的是,/proc目錄下包含了進程的大量信息。我們在這里是讀取status文件,此外,也可通過/proc/self/stat文件來獲得進程相關信息,包括運行狀態。

2.4設置程序運行最大時間

這種方法經常在CTF比賽中看到。由于程序在調試時的斷點、檢查修改內存等操作,運行時間往往要遠大于正常運行時間。所以,一旦程序運行時間過長,便可能是由于正在被調試。

具體地,在程序啟動時,通過alarm設置定時,到達時則中止程序。示例代碼如下:

#include#include#includevoidalarmHandler(intsig){printf("Debuggerdetected");exit(1);}void__attribute__((constructor))setupSig(void){signal(SIGALRM,alarmHandler);alarm(2);}intmain(intargc,char*argv[]){printf("Allgood");return0;}

在此例中,我們通過__attribute__((constructor)),在程序啟動時便設置好定時。實際運行中,當我們使用gdb在main函數下斷點,稍候片刻后繼續執行時,則觸發了SIGALRM,進而檢測到調試器。如下圖所示:

\

順便一提,這種方式可以輕易地被繞過。我們可以設置gdb對signal的處理方式,如果我們選擇將SIGALRM忽略而非傳遞給程序,則alarmHandler便不會被執行,如下圖所示:

\

2.5檢查進程打開的filedescriptor

如2.2中所說,如果被調試的進程是通過gdb 的方式啟動,那么它便是由gdb進程fork得到的。而fork在調用時,父進程所擁有的fd(file descriptor)會被子進程繼承。由于gdb在往往會打開多個fd,因此如果進程擁有的fd較多,則可能是繼承自gdb的,即進程在被調試。

具體地,進程擁有的fd會在/proc/self/fd/下列出。于是我們的示例代碼如下:

#include#includeintmain(intargc,char*argv[]){structdirent*dir;DIR*d=opendir("/proc/self/fd");while(dir=readdir(d)){if(!strcmp(dir->d_name,"5")){printf("Debuggerdetected");return1;}}closedir(d);printf("Allgood");return0;}

這里,我們檢查/proc/self/fd/中是否包含fd為5。由于fd從0開始編號,所以fd為5則說明已經打開了6個文件。如果程序正常運行則不會打開這么多,所以由此來判斷是否被調試。運行結果見下圖:

\

3.總結

以上列出的反調試技術中,往往只進行了一次檢測。為了提高強度,我們可以fork得到一個新的進程,在這個子進程中,每隔一段時間對父進程進行一次檢測。

然而,這些技術都面臨存在另外一個致命弱點:可以通過反匯編靜態分析,找到相應的檢測代碼并修改,從而繞過反調試檢測。因此,我們通常還需要對二進制文件進行混淆以對抗靜態分析,這樣與反調試技術相結合,才能得到理想的保護效果。

看文倉www.kanwencang.com網友整理上傳,為您提供最全的知識大全,期待您的分享,轉載請注明出處。
歡迎轉載:http://www.kanwencang.com/bangong/20170104/81434.html

文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()