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