本文介紹了ELF的基本結(jié)構(gòu)和內(nèi)存加載的原理,并用具體案例來分析如何通過ELF特性實現(xiàn)HIDSbypass、加固/脫殼以及輔助進行binaryfuzzing。
前言作為一個安全研究人員,ELF可以說是一個必須了解的格式,因為這關(guān)系到程序的編譯、鏈接、封裝、加載、動態(tài)執(zhí)行等方方面面。有人就說了,這不就是一種文件格式而已嘛,最多按照SPEC實現(xiàn)一遍也就會了,難道還能復(fù)雜過FLV/MP4?曾經(jīng)我也是這么認為的,直到我在日常工作時遇到了下面的錯誤:
$r2a.outSegmentationfault作為一個開源愛好者,我的radare2經(jīng)常是用master分支編譯的,經(jīng)過在github中搜索,發(fā)現(xiàn)radare對于ELF的處理還有不少同類的問題,比如issue#17300以及issue#17379,這還只是近一個月內(nèi)的兩個openissue,歷史問題更是數(shù)不勝數(shù)。
總不能說radare的開發(fā)者不了解ELF吧?事實上他們都是軟件開發(fā)和逆向工程界的專家。不止radare,其實IDA和其他反編譯工具也曾出現(xiàn)過各類ELF相關(guān)的bug。
說了那么多,只是為了引出一個觀點:ELF既簡單也復(fù)雜,值得我們?nèi)ド钊肓私狻>W(wǎng)上已經(jīng)有了很多介紹ELF的文章,因此本文不會花太多篇幅在SPEC的復(fù)制粘貼上,而是結(jié)合實際案例和應(yīng)用場景去進行說明。
ELF101ELF的全稱是ExecutableandLinkingFormat,這個名字相當(dāng)關(guān)鍵,包含了ELF所需要支持的兩個功能——執(zhí)行和鏈接。不管是ELF,還是Windows的PE,抑或是MacOS的Mach-O,其根本目的都是為了能讓處理器正確執(zhí)行我們所編寫的代碼。
大局觀在上古時期,給CPU運行代碼也不用那么復(fù)雜,什么代碼段數(shù)據(jù)段,直接把編譯好的機器碼一把梭燒到中斷內(nèi)存空間,PC直接跳過來就執(zhí)行了。但隨著時代變化,大家總不能一直寫匯編了,即便編譯器很給力,也會涉及到多人協(xié)作、資源復(fù)用等問題。這時候就需要一種可拓展(Portable)的文件標準,一方面讓開發(fā)者(編譯器/鏈接器)能夠高效協(xié)作,另一方面也需要系統(tǒng)能夠正確、安全地將文件加載到對應(yīng)內(nèi)存中去執(zhí)行,這就是ELF的使命。
從大局上看,ELF文件主要分為3個部分:
ELFHeaderSectionHeaderTableProgramHeaderTable其中,ELFHeader是文件頭,包含了固定長度的文件信息;SectionHeaderTable則包含了鏈接時所需要用到的信息;ProgramHeaderTable中包含了運行時加載程序所需要的信息,后面會進行分別介紹。
ELFHeaderELF頭部的定義在elf/elf.h中(以glibc-2.27為例),使用POD結(jié)構(gòu)體表示,內(nèi)存可使用結(jié)構(gòu)體的字段一一映射,頭部表示如下:
#defineEI_NIDENT(16)typedefstruct{unsignedchare_ident[EI_NIDENT];/*Magicnumberandotherinfo*/Elf32_Halfe_type;/*Objectfiletype*/Elf32_Halfe_machine;/*Architecture*/Elf32_Worde_version;/*Objectfileversion*/Elf32_Addre_entry;/*Entrypointvirtualaddress*/Elf32_Offe_phoff;/*Programheadertablefileoffset*/Elf32_Offe_shoff;/*Sectionheadertablefileoffset*/Elf32_Worde_flags;/*Processor-specificflags*/Elf32_Halfe_ehsize;/*ELFheadersizeinbytes*/Elf32_Halfe_phentsize;/*Programheadertableentrysize*/Elf32_Halfe_phnum;/*Programheadertableentrycount*/Elf32_Halfe_shentsize;/*Sectionheadertableentrysize*/Elf32_Halfe_shnum;/*Sectionheadertableentrycount*/Elf32_Halfe_shstrndx;/*Sectionheaderstringtableindex*/}Elf32_Ehdr;注釋都很清楚了,挑一些比較重要的來說。其中e_type表示ELF文件的類型,有以下幾種:
*.o*.soe_entry是程序的入口虛擬地址,注意不是main函數(shù)的地址,而是.text段的首地址_start。當(dāng)然這也要求程序本身非PIE(-no-pie)編譯的且ASLR關(guān)閉的情況下,對于非ET_EXEC類型通常并不是實際的虛擬地址值。
其他的字段大多數(shù)是指定SectionHeader(e_sh)和ProgramHeader(e_ph)的信息。Section/ProgramHeaderTable本身可以看做是數(shù)組結(jié)構(gòu),ELF頭中的信息指定對應(yīng)Table數(shù)組的位置、長度、元素大小信息。最后一個e_shstrndx表示的是sectiontable中的第e_shstrndx項元素,保存了所有sectiontable名稱的字符串信息。
SectionHeader上節(jié)說了sectionheadertable是一個數(shù)組結(jié)構(gòu),這個數(shù)組的位置在e_shoff處,共有e_shnum個元素(即section),每個元素的大小為e_shentsize字節(jié)。每個元素的結(jié)構(gòu)如下:
typedefstruct{Elf32_Wordsh_name;/*Sectionname(stringtblindex)*/Elf32_Wordsh_type;/*Sectiontype*/Elf32_Wordsh_flags;/*Sectionflags*/Elf32_Addrsh_addr;/*Sectionvirtualaddratexecution*/Elf32_Offsh_offset;/*Sectionfileoffset*/Elf32_Wordsh_size;/*Sectionsizeinbytes*/Elf32_Wordsh_link;/*Linktoanothersection*/Elf32_Wordsh_info;/*Additionalsectioninformation*/Elf32_Wordsh_addralign;/*Sectionalignment*/Elf32_Wordsh_entsize;/*Entrysizeifsectionholdstable*/}Elf32_Shdr;其中sh_name是該section的名稱,用一個word表示其在字符表中的偏移,字符串表(.shstrtab)就是上面說到的第e_shstrndx個元素。ELF文件中經(jīng)常使用這種偏移表示方式,可以方便組織不同區(qū)段之間的引用。
sh_type表示本section的類型,SPEC中定義了幾十個類型,列舉其中一些如下:
SHT_NULL:表示該section無效,通常第0個section為該類型SHT_PROGBITS:表示該section包含由程序決定的內(nèi)容,如.text、.data、.plt、.gotSHT_SYMTAB/SHT_DYNSYM:表示該section中包含符號表,如.symtab、.dynsymSHT_DYNAMIC:表示該section中包含動態(tài)鏈接階段所需要的信息SHT_STRTAB:表示該section中包含字符串信息,如.strtab、.shstrtabSHT_REL/SHT_RELA:包含重定向項信息雖然每個sectionheader的大小一樣(e_shentsize字節(jié)),但不同類型的section有不同的內(nèi)容,內(nèi)容部分由這幾個字段表示:
sh_offset:內(nèi)容起始地址相對于文件開頭的偏移sh_size:內(nèi)容的大小sh_entsize:有的內(nèi)容是也是一個數(shù)組,這個字段就表示數(shù)組的元素大小與運行時信息相關(guān)的字段為:
sh_addr:如果該section需要在運行時加載到虛擬內(nèi)存中,該字段就是對應(yīng)section內(nèi)容(第一個字節(jié))的虛擬地址sh_addralign:內(nèi)容地址的對齊,如果有的話需要滿足sh_addr%sh_addralign=0sh_flags:表示所映射內(nèi)容的權(quán)限,可根據(jù)SHF_WRITE/ALLOC/EXECINSTR進行組合另外兩個字段sh_link和sh_info的含義根據(jù)section類型的不同而不同,如下表所示:
至于不同類型的section,有的是保存符號表,有的是保存字符串,這也是ELF表現(xiàn)出拓展性和復(fù)雜性的地方,因此需要在遇到具體問題的時候查看文檔去進行具體分析。
ProgramHeaderprogramheadertable用來保存程序加載到內(nèi)存中所需要的信息,使用段(segment)來表示。與sectionheadertable類似,同樣是數(shù)組結(jié)構(gòu)。數(shù)組的位置在偏移e_phoff處,每個元素(segmentheader)的大小為e_phentsize,共有e_phnum個元素。單個segmentheader的結(jié)構(gòu)如下:
typedefstruct{Elf32_Wordp_type;/*Segmenttype*/Elf32_Offp_offset;/*Segmentfileoffset*/Elf32_Addrp_vaddr;/*Segmentvirtualaddress*/Elf32_Addrp_paddr;/*Segmentphysicaladdress*/Elf32_Wordp_filesz;/*Segmentsizeinfile*/Elf32_Wordp_memsz;/*Segmentsizeinmemory*/Elf32_Wordp_flags;/*Segmentflags*/Elf32_Wordp_align;/*Segmentalignment*/}Elf32_Phdr;既然programheader的作用是提供用于初始化程序進程的段信息,那么下面這些字段就是很直觀的:
p_offset:該segment的數(shù)據(jù)在文件中的偏移地址(相對文件頭)p_vaddr:segment數(shù)據(jù)應(yīng)該加載到進程的虛擬地址p_paddr:segment數(shù)據(jù)應(yīng)該加載到進程的物理地址(如果對應(yīng)系統(tǒng)使用的是物理地址)p_filesz:該segment數(shù)據(jù)在文件中的大小p_memsz:該segment數(shù)據(jù)在進程內(nèi)存中的大小。注意需要滿足p_memsz>=p_filesz,多出的部分初始化為0,通常作為.bss段內(nèi)容p_flags:進程中該segment的權(quán)限(R/W/X)p_align:該segment數(shù)據(jù)的對齊,2的整數(shù)次冪。即要求p_offset%p_align=p_vaddr。剩下的p_type字段,表示該programsegment的類型,主要有以下幾種:
PT_NULL:表示該段未使用PT_LOAD:LoadableSegment,將文件中的segment內(nèi)容映射到進程內(nèi)存中對應(yīng)的地址上。值得一提的是SPEC中說在programheader中的多個PT_LOAD地址是按照虛擬地址遞增排序的。PT_DYNAMIC:動態(tài)鏈接中用到的段,通常是RW映射,因為需要由interpreter(ld.so)修復(fù)對應(yīng)的的入口PT_INTERP:包含interpreter的路徑,見下文PT_HDR:表示programheadertable本身。如果有這個segment的話,必須要在所有可加載的segment之前,并且在文件中不能出現(xiàn)超過一次。在不同的操作系統(tǒng)中還可能有一些拓展的類型,比如PT_GNU_STACK、PT_GNU_RELRO等,不一而足。
小結(jié)至此,ELF文件中相關(guān)的字段已經(jīng)介紹完畢,主要組成也就是SectionHeaderTable和ProgramHeaderTable兩部分,整體框架相當(dāng)簡潔。而ELF中體現(xiàn)拓展性的地方則是在Section和Segment的類型上(s_type和p_type),這兩個字段的類型都是ElfN_Word,在32位系統(tǒng)下大小為4字節(jié),也就是說最多可以支持高達2^32-1種不同的類型!除了上面介紹的常見類型,不同操作系統(tǒng)或者廠商還能定義自己的類型去實現(xiàn)更多復(fù)雜的功能。
程序加載在新版的ELF標準文檔中,將ELF的介紹分成了三部分,第一部分介紹ELF文件本身的結(jié)構(gòu),第二部分是處理器相關(guān)的內(nèi)容,第三部分是操作系統(tǒng)相關(guān)的內(nèi)容。ELF的加載實際上是與操作系統(tǒng)相關(guān)的,不過大部分情況下我們都是在GNU/Linux環(huán)境中運行,因此就以此為例介紹程序的加載流程。
Linux中分為用戶態(tài)和內(nèi)核態(tài),執(zhí)行ELF文件在用戶態(tài)的表現(xiàn)就是執(zhí)行execve系統(tǒng)調(diào)用,隨后陷入內(nèi)核進行處理。
內(nèi)核空間內(nèi)核空間對execve的處理其實可以單獨用一篇文章去介紹,其中涉及到進程的創(chuàng)建、文件資源的處理以及進程權(quán)限的設(shè)置等等。我們這里主要關(guān)注其中ELF處理相關(guān)的部分即可,實際上內(nèi)核可以識別多種類型的可執(zhí)行文件,ELF的處理代碼主要在fs/binfmt_elf.c中的load_elf_binary函數(shù)中。
對于ELF而言,Linux內(nèi)核所關(guān)心的只有ProgramHeader部分,甚至大部分情況下只關(guān)心三種類型的Header,即PT_LOAD、PT_INTERP和PT_GNU_STACK。以3.18內(nèi)核為例,load_elf_binary主要有下面操作:
對ELF文件做一些基本檢查,保證e_phentsize=sizeof(structelf_phdr)并且e_phnum的個數(shù)在一定范圍內(nèi);循環(huán)查看每一項programheader,如果有PT_INTERP則使用open_exec加載進來,并替換原程序的bprm->buf;根據(jù)PT_GNU_STACK段中的flag設(shè)置棧是否可執(zhí)行;使用flush_old_exec來更新當(dāng)前可執(zhí)行文件的所有引用;使用setup_new_exec設(shè)置新的可執(zhí)行文件在內(nèi)核中的狀態(tài);setup_arg_pages在棧上設(shè)置程序調(diào)用參數(shù)的內(nèi)存頁;循環(huán)每一項PT_LOAD類型的段,elf_map映射到對應(yīng)內(nèi)存頁中,初始化BSS;如果存在interpreter,將入口(elf_entry)設(shè)置為interpreter的函數(shù)入口,否則設(shè)置為原ELF的入口地址;install_exec_creds(bprm)設(shè)置進程權(quán)限等信息;create_elf_tables添加需要的信息到程序的棧中,比如ELFauxiliaryvector;設(shè)置current->mm對應(yīng)的字段;從內(nèi)核的處理流程上來看,如果是靜態(tài)鏈接的程序,實際上內(nèi)核返回用戶空間執(zhí)行的就是該程序的入口地址代碼;如果是動態(tài)鏈接的程序,內(nèi)核返回用戶空間執(zhí)行的則是interpreter的代碼,并由其加載實際的ELF程序去執(zhí)行。
為什么要這么做呢?如果把動態(tài)鏈接相關(guān)的代碼也放到內(nèi)核中,就會導(dǎo)致內(nèi)核執(zhí)行功能過多,內(nèi)核的理念一直是能不在內(nèi)核中執(zhí)行的就不在內(nèi)核中處理,以避免出現(xiàn)問題時難以更新而且影響系統(tǒng)整體的穩(wěn)定性。事實上內(nèi)核中對ELF文件結(jié)構(gòu)的支持是相當(dāng)有限的,只能讀取并理解部分的字段。
用戶空間內(nèi)核返回用戶空間后,對于靜態(tài)鏈接的程序是直接執(zhí)行,沒什么好說的。而對于動態(tài)鏈接的程序,實際是執(zhí)行interpreter的代碼。ELF的interpreter作為一個段,自然是編譯鏈接的時候加進去的,因此和編譯使用的工具鏈有關(guān)。對于Linux系統(tǒng)而言,使用的一般是GCC工具鏈,而interpreter的實現(xiàn),代碼就在glibc的elf/rtld.c中。
interpreter又稱為dynamiclinker,以glibc2.27為例,它的大致功能如下:
將實際要執(zhí)行的ELF程序中的內(nèi)存段加載到當(dāng)前進程空間中;將動態(tài)庫的內(nèi)存段加載到當(dāng)前進程空間中;對ELF程序和動態(tài)庫進行重定向操作(relocation);調(diào)用動態(tài)庫的初始化函數(shù)(如.preinit_array,.init,.init_array);將控制流傳遞給目標ELF程序,讓其看起來自己是直接啟動的;其中參與動態(tài)加載和重定向所需要的重要部分就是ProgramHeaderTable中PT_DYNAMIC類型的Segment。前面我們提到在SectionHeader中也有一部分參與動態(tài)鏈接的section,即.dynamic。我在自己解析動態(tài)鏈接文件的時候發(fā)現(xiàn),實際上.dynamicsection中的數(shù)據(jù),和PT_DYNAMIC中的數(shù)據(jù)指向的是文件中的同一個地方,即這兩個entry的s_offset和p_offset是相同。每個元素的類型如下:
typedefstruct{Elf32_Swordd_tag;/*Dynamicentrytype*/union{Elf32_Wordd_val;/*Integervalue*/Elf32_Addrd_ptr;/*Addressvalue*/}d_un;}Elf32_Dyn;d_tag表示實際類型,并且d_un和d_tag相關(guān),可能說是很有拓展性了:)同樣的,標準中定義了幾十個d_tag類型,比較常用的幾個如下:
DT_NULL:表示_DYNAMIC的結(jié)尾DT_NEEDED:d_val保存了一個到字符串表頭的偏移,指定的字符串表示該ELF所依賴的動態(tài)庫名稱DT_STRTAB:d_ptr指定了地址保存了符號、動態(tài)庫名稱以及其他用到的字符串DT_STRSZ:字符串表的大小DT_SYMTAB:指定地址保存了符號表DT_INIT/DT_FINI:指定初始化函數(shù)和結(jié)束函數(shù)的地址DT_RPATH:指定動態(tài)庫搜索目錄DT_SONAME:SharedObjectName,指定當(dāng)前動態(tài)庫的名字(logicalname)其中有部分的類型可以和Section中的SHT_xxx類型進行類比,完整的列表可以參考ELF標準中的BookIII:OperatingSystemSpecific一節(jié)。
在interpreter根據(jù)DT_NEEDED加載完所有需要的動態(tài)庫后,就實現(xiàn)了完整進程虛擬內(nèi)存映像的布局。在尋找某個動態(tài)符號時,interpreter會使用廣度優(yōu)先的方式去進行搜索,即先在當(dāng)前ELF符號表中找,然后再從當(dāng)前ELF的DT_NEEDED動態(tài)庫中找,再然后從動態(tài)庫中的DT_NEEDED里查找。
因為動態(tài)庫本身是位置無關(guān)的(PIE),支持被加載到內(nèi)存中的隨機位置,因此為了程序中用到的符號可以被正確引用,需要對其進行重定向操作,指向?qū)?yīng)符號的真實地址。這部分我在之前寫的關(guān)于GOT,PLT和動態(tài)鏈接的文章中已經(jīng)詳細介紹過了,因此不再贅述,感興趣的朋友可以參考該文章。
實際案例有人也許會問,我看你bibi了這么多,有什么實際意義嗎?呵呵,本節(jié)就來分享幾個我認為比較有用的應(yīng)用場景。
InterpreterHack在滲透測試中,紅隊小伙伴們經(jīng)常能拿到目標的后臺shell權(quán)限,但是遇到一些部署了HIDS的大企業(yè),很可能在執(zhí)行惡意程序的時候被攔截,或者甚至觸發(fā)監(jiān)測異常直接被藍隊拔網(wǎng)線。這里不考慮具體的HIDS產(chǎn)品,假設(shè)現(xiàn)在面對兩種場景:
目標環(huán)境的可寫磁盤直接mount為noexec,無法執(zhí)行代碼目標環(huán)境內(nèi)核監(jiān)控任何非系統(tǒng)路徑的程序的執(zhí)行都會直接告警不管什么樣的環(huán)境,我相信老紅隊都有辦法去繞過,這里我們運用上面學(xué)到的ELF知識,其實有一種更為簡單的解法,即利用interpreter。示例如下:
$cathello.c#include<stdio.h>intmain(){returnputs("hello!");}$gcchello.c-ohello$./hellohello!$chmod-xhello$./hellobash:./hello:Permissiondenied$/lib64/ld-linux-x86-64.so.2./hellohello!$strace/lib64/ld-linux-x86-64.so.2./hello2>&1|grepexecexecve("/lib64/ld-linux-x86-64.so.2",["/lib64/ld-linux-x86-64.so.2","./hello"],0x7fff1206f208/*9vars*/)=0/lib64/ld-linux-x86-64.so.2本身應(yīng)該是內(nèi)核調(diào)用執(zhí)行的,但我們這里可以直接進行調(diào)用。這樣一方面可以在沒有執(zhí)行權(quán)限的情況下執(zhí)行任意代碼,另一方面也可以在一定程度上避免內(nèi)核對execve的異常監(jiān)控。
利用(濫用)interpreter我們還可以做其他有趣的事情,比如通過修改指定ELF文件的interpreter為我們自己的可執(zhí)行文件,可讓內(nèi)核在處理目標ELF時將控制器交給我們的interpreter,這可以通過直接修改字符串表或者使用一些工具如patchelf來輕松實現(xiàn)。
對于惡意軟件分析的場景,很多安全研究人員看到ELF就喜歡用ldd去看看有什么依賴庫,一般ldd腳本實際上是調(diào)用系統(tǒng)默認的ld.so并通過環(huán)境變量來打印信息,不過對于某些glibc實現(xiàn)(如glibc2.27之前的ld.so),會調(diào)用ELF指定的interpreter運行,從而存在非預(yù)期命令執(zhí)行的風(fēng)險。
當(dāng)然還有更多其他的思路可以進行拓展,這就需要大家發(fā)揮腦洞了。
加固/脫殼與逆向分析比較相關(guān)的就是符號表,一個有符號的程序在逆向時基本上和讀源碼差不多。因此對于想保護應(yīng)用程序的開發(fā)者而言,最簡單的防護***就是去除符號表,一個簡單的strip命令就可實現(xiàn)。strip刪除的主要是Section中的信息,因為這不影響程序的執(zhí)行。去除前后進行diff對比可看到刪除的section主要有下面這些:
$diff011c1<Thereare35sectionheaders,startingatoffset0x1fdc:--->Thereare28sectionheaders,startingatoffset0x1144:32,39c32<[27].debug_arangesPROGBITS0000000000104d00002000001<[28].debug_infoPROGBITS0000000000106d00035000001<[29].debug_abbrevPROGBITS000000000013bd00010000001<[30].debug_linePROGBITS000000000014bd0000cd00001<[31].debug_strPROGBITS0000000000158a00029301MS001<[32].symtabSYMTAB000000000018200004801033494<[33].strtabSTRTAB00000000001ca00001f400001<[34].shstrtabSTRTAB00000000001e9400014500001--->[27].shstrtabSTRTAB0000000000104d0000f500001其中.symtab是符號表,.strtab是符號表中用到的字符串。
僅僅去掉符號感覺還不夠,熟悉匯編的人放到反編譯工具中還是可以慢慢還原程序邏輯。通過前面的分析我們知道,ELF執(zhí)行需要的只是ProgramHeader中的幾個段,SectionHeader實際上是不需要的,只不過在運行時動態(tài)鏈接過程會引用到部分關(guān)聯(lián)的區(qū)域。大部分反編譯工具,如IDA、Ghidra等,處理ELF是需要某些section信息來構(gòu)建程序視圖的,所以我們可以通過構(gòu)造一個損壞SectionTable或者ELFHeader令這些反編譯工具出錯,從而干擾逆向人員。
當(dāng)然,這個***并不總是奏效,逆向人員可以通過動態(tài)調(diào)試把程序dump出來并對運行視圖進行還原。一個典型的例子是Android中的JNI動態(tài)庫,有的安全人員對這些so文件進行了加密處理,并且在.init/.initarray這些動態(tài)庫初始化函數(shù)中進行動態(tài)解密。破解這種加固***的策略就是將其從內(nèi)存中復(fù)制出來并進行重建,重建的過程可根據(jù)segment對section進行還原,因為segment和section之間共享了許多內(nèi)存空間,例如:
$readelf-lmain1...SectiontoSegmentmapping:SegmentSections...0001.interp02.interp.note.ABI-tag.note.gnu.build-id.gnu.hash.dynsym.dynstr.gnu.version.gnu.version_r.rel.dyn.rel.plt.init.plt.plt.got.text.fini.rodata.eh_frame_hdr.eh_frame03.init_array.fini_array.dynamic.got.got.plt.data.bss04.dynamic05.note.ABI-tag.note.gnu.build-id06.eh_frame_hdr0708.init_array.fini_array.dynamic.got在SectiontoSegmentmapping中可以看到這些段的內(nèi)容是跟對應(yīng)section的內(nèi)容重疊的,雖然一個segment可能對應(yīng)多個section,但是可以根據(jù)內(nèi)存的讀寫屬性、內(nèi)存特征以及對應(yīng)段的一般順序進行區(qū)分。
如果程序中有比較詳細的日志函數(shù),我們還可以通過反編譯工具的腳本拓展去修改.symtab/.strtab段來批量還原ELF文件的符號,從而高效地輔助動態(tài)調(diào)試。
BinaryFuzzing考慮這么一種場景,我們在分析某個IoT設(shè)備時發(fā)現(xiàn)了一個定制的ELF網(wǎng)絡(luò)程序,類似于httpd,其中有個靜態(tài)函數(shù)負責(zé)處理輸入數(shù)據(jù)。現(xiàn)在想要單獨對這個函數(shù)進行fuzz應(yīng)該怎么做?直接從網(wǎng)絡(luò)請求中進行變異是一種***,但是網(wǎng)絡(luò)請求的效率太低,而且觸達該函數(shù)的程序邏輯也可能太長。
既然我們已經(jīng)了解了ELF,那就可以有更好的辦法將該函數(shù)抽取出來進行獨立調(diào)用。在介紹ELF類型的時候其實有提到,可執(zhí)行文件可以有兩種類型,即可執(zhí)行類型(ET_EXEC)和共享對象(ET_DYN),一個動態(tài)鏈接的可執(zhí)行程序默認是共享對象類型的:
$gcchello.c-ohello$readelf-hhello|grepTypeType:DYN(Sharedobjectfile)而動態(tài)庫(.so)本身也是共享對象類型,他們之間的本質(zhì)區(qū)別在于前者鏈接了libc并且定義了main函數(shù)。對于動態(tài)庫,我們可以通過dlopen/dlsym獲取對應(yīng)的符號進行調(diào)用,因此對于上面的場景,一個解決方式就是修改目標ELF文件,并且將對應(yīng)的靜態(tài)函數(shù)導(dǎo)出添加到dynamicsection中,并修復(fù)對應(yīng)的ELF頭。
這個思想其實很早就已經(jīng)有人實現(xiàn)了,比如lief的bin2lib。通過該***,我們就能將目標程序任意的函數(shù)抽取出來執(zhí)行,比如hugsy就用這個方式復(fù)現(xiàn)了Exim中的溢出漏洞(CVE-2018-6789),詳見FuzzingarbitraryfunctionsinELFbinaries(中文翻譯)。
總結(jié)本文主要介紹了32位環(huán)境下ELF文件的格式和布局,然后從內(nèi)核空間和用戶空間兩個方向分析了ELF程序的加載過程,最后列舉了幾個依賴于ELF文件特性的案例進行具體分析,包括dynamiclinker的濫用、程序加固和反加固以及在二進制fuzzing中的應(yīng)用。
ELF文件本身并不復(fù)雜,只有三個關(guān)鍵部分,只不過在section和segment的類型上保留了極大的拓展性。操作系統(tǒng)可以根據(jù)自己的需求在不同字段上實現(xiàn)和拓展自己的功能,比如Linux中通過dymamic類型實現(xiàn)動態(tài)加載。但這不是必須的,例如在Android中就通過ELF格式封裝了特有的.odex、.oat文件來保存優(yōu)化后的dex。另外對于64位環(huán)境,大部分字段含義都是類似的,只是字段大小稍有變化(Elf32->Elf64),并不影響文中的結(jié)論。
作者:PansLabyrinth