ELF&PE 文件結(jié)構(gòu)分析
說簡單點萧福,ELF 對應(yīng)于UNIX 下的文件,而PE 則是Windows 的可執(zhí)行文件窘疮,分析ELF 和 PE 的文件結(jié)構(gòu),是逆向工程冀墨,或者是做調(diào)試闸衫,甚至是開發(fā)所應(yīng)具備的基本能力。在進(jìn)行逆向工程的開端诽嘉,我們拿到ELF 文件蔚出,或者是PE 文件,首先要做的就是分析文件頭虫腋,了解信息骄酗,進(jìn)而逆向文件。不說廢話悦冀,開始分析:
ELF和PE 文件都是基于Unix 的 COFF(Common Object File Format) 改造而來趋翻,更加具體的來說,他是來源于當(dāng)時著名的 DEC(Digital Equipment Corporation) 的VAX/VMS 上的COFF文件格式盒蟆。我們從ELF 說起踏烙。
ELF
ELF 文件標(biāo)準(zhǔn)里把系統(tǒng)中采用ELF 格式的文件歸類為四種:
- 可重定位文件,Relocatable File ,這類文件包含代碼和數(shù)據(jù)茁影,可用來連接成可執(zhí)行文件或共享目標(biāo)文件宙帝,靜態(tài)鏈接庫歸為此類,對應(yīng)于Linux 中的.o 募闲,Windows 的 .obj.
- 可執(zhí)行文件步脓,Executable File ,這類文件包含了可以直接執(zhí)行的程序浩螺,它的代表就是ELF 可執(zhí)行文件靴患,他們一般沒有擴(kuò)展名。比如/bin/bash 要出,Windows 下的 .exe
- 共享目標(biāo)文件鸳君,Shared Object File ,這種文件包含代碼和數(shù)據(jù)患蹂,鏈接器可以使用這種文件跟其他可重定位文件的共享目標(biāo)文件鏈接或颊,產(chǎn)生新的目標(biāo)文件。另外是動態(tài)鏈接器可以將幾個這種共享目標(biāo)文件與可執(zhí)行文件結(jié)合传于,作為進(jìn)程映像來運行囱挑。對應(yīng)于Linux 中的 .so,Windows 中的 DLL
- 核心轉(zhuǎn)儲文件沼溜,Core Dump File平挑,當(dāng)進(jìn)程意外終止,系統(tǒng)可以將該進(jìn)程地址空間的內(nèi)容及終止時的一些信息轉(zhuǎn)存到核心轉(zhuǎn)儲文件。 對應(yīng) Linux 下的core dump通熄。
ELF 文件的總體結(jié)構(gòu)大概是這樣的:
ELF Header |
---|
.text |
.data |
.bss |
... other section |
Section header table |
String Tables, Symbol Tables,.. |
- ELF 文件頭位于最前端唆涝,它包含了整個文件的基本屬性,如文件版本唇辨,目標(biāo)機(jī)器型號廊酣,程序入口等等。
- .text 為代碼段助泽,也是反匯編處理的部分啰扛,他們是以機(jī)器碼的形式存儲嚎京,沒有反匯編的過程基本不會有人讀懂這些二進(jìn)制代碼的嗡贺。
- .data 數(shù)據(jù)段,保存的那些已經(jīng)初始化了的全局靜態(tài)變量和局部靜態(tài)變量鞍帝。
- .bss 段诫睬,存放的是未初始化的全局變量和局部靜態(tài)變量,這個很容易理解消略,因為在未初始化的情況下添怔,我們單獨用一個段來保存当悔,可以不在一開始就分配空間,而是在最終連接成可執(zhí)行文件的時候亲澡,再在.bss 段分配空間。
- 其他段纫版,還有一些可選的段床绪,比如.rodata 表示這里存儲只讀數(shù)據(jù), .debug 表示調(diào)試信息等等其弊,具體遇到可以查看相關(guān)文檔癞己。
- 自定義段,這一塊是為了實現(xiàn)用戶特殊功能而存在的段梭伐,方便擴(kuò)展痹雅,比如我們使用全局變量或者函數(shù)之前加上 attribute(section('name')) 就可以吧變量或者函數(shù)放到以name 作為段名的段中。
- 段表糊识,Section Header Table 绩社,是一個重要的部分,它描述了ELF 文件包含的所有段的信息赂苗,比如每個段的段名愉耙,段長度,在文件中的偏移哑梳,讀寫權(quán)限和一些段的其他屬性劲阎。
ELF Header
ELF 文件信息的查看利器在Linux 下是是objdump, readelf, 相關(guān)命令較多,可查鸠真。下面我們從ELF 文件頭說起悯仙。
文件頭包含的內(nèi)容很多龄毡,我們在Ubuntu 系統(tǒng)下使用 readelf 命令來查看ELF 文件頭:
我們以bash 這個可執(zhí)行文件為例,我們可以看到ELF 文件頭定義了ELF 魔數(shù)锡垄,文件機(jī)器字節(jié)長度沦零,數(shù)據(jù)存儲方式,版本货岭,運行平臺路操,ABI版本,ELF 重定位類型千贯,硬件平臺屯仗,硬件平臺版本,入口地址搔谴,程序頭入口和長度魁袜,段表的位置和長度,段的數(shù)量敦第。
ELF 文件頭的結(jié)構(gòu)和相關(guān)常數(shù)一般定義在了 /usr/include/elf.h 中峰弹,我們可以進(jìn)去查看一下:
除了第一個,其他都是一一對應(yīng)的芜果,第一個是一個對應(yīng)了Magic number, Class, Data, Version, OS/ABI, ABI version.
出現(xiàn)在最開始的ELF Magic number鞠呈, 16字節(jié)是用來標(biāo)識ELF 文件的平臺屬性,比如字長右钾,字節(jié)序蚁吝,ELF 文件版本。在加載的時候霹粥,首先會確認(rèn)魔數(shù)的正確性灭将,不正確的話就拒絕加載。
另一個重要的東西是段表(Section Header Table) ,保存了各種各樣段的基本屬性后控,比如段名庙曙,段長度,文件中的偏移浩淘,讀寫權(quán)限捌朴,段的其他屬性。而段表自己在ELF 文件中的位置是在ELF 頭文件 e_shoff 決定的张抄。
我們可以使用 objdump -h 的指令來查看ELF 文件中包含哪些段砂蔽,以bash 這個可執(zhí)行為例,其實除了我們之前說的哪些基本結(jié)構(gòu)署惯,他包含很多其他的結(jié)構(gòu):
同樣的左驾,我們使用readelf -S 的指令也可以進(jìn)行查看。
下面我們來看一下結(jié)構(gòu),還是到elf.h 中去查看诡右,他的結(jié)構(gòu)體名字叫 Elf32_Shdr安岂,64位對應(yīng)Elf64_Shdr,結(jié)構(gòu)如下:
以上結(jié)構(gòu)中,分別對應(yīng)于:
- 段名
- 段類型
- 段標(biāo)志位
- 段虛擬地址
- 段偏移
- 段長度
- 段鏈接
- 段對齊
- 項帆吻,一些大小固定的項域那,如符號表等。
這些項目猜煮,在使用readelf -S 指令時一一對應(yīng)次员。
另外還有一個重要的表,叫重定位表王带,一般段名叫.rel.text淑蔚, 在上邊沒有出現(xiàn),鏈接器在處理目標(biāo)文件時辫秧,需要對目標(biāo)文件中的某些部位進(jìn)行重定位束倍,就是代碼段和數(shù)據(jù)段中那些對絕對地址引用的位置被丧,這個時候就需要使用重定位表了盟戏。
字符串表
為什么會有字符串表呢?其實這個也是在不斷發(fā)展改進(jìn)中找到的解決辦法甥桂,在ELF 文件中柿究,會用到很多的字符串,段名黄选,變量名等等蝇摸,但是字符串其本身又長度不固定,如果使用固定結(jié)構(gòu)來表示办陷,就會帶來空間上的麻煩貌夕。所以,構(gòu)造一個字符串表民镜,將使用的字符串統(tǒng)一放在那里啡专,然后通過偏移量來引用字符串,豈不美哉制圈。
需要使用的時候们童,只需要給一個偏移量,然后就到字符串該位置找字符串鲸鹦,遇到\0 就停止慧库。
字符串在ELF 文件中,也是以段的形式保存的馋嗜,常見的段名 .strtab齐板, .shstrtab 兩個字符串分別為字符串表和段表字符串,前者用來保存普通的字符串,后者保存段名甘磨。
在我們使用readelf -h 的時候听皿,我們看到最后一個成員,section header string table index 宽档,實際上他指的就是字符串表的下標(biāo)尉姨,bash 對應(yīng)的字符串表下標(biāo)為27,在使用objdump 的時候吗冤,實際上忽略了字符串表又厉,我們使用readelf ,就可以看到第27位即字符串表:
下面我們回顧一下椎瘟,這個ELF 構(gòu)造的精妙之處覆致,當(dāng)一個ELF 文件到來的時候,系統(tǒng)自然的找到他的開頭肺蔚,拿到文件頭煌妈,首先看魔數(shù),識別基本信息宣羊,看是不是正確的璧诵,或者是可識別的文件,然后加載他的基本信息仇冯,包括CPU 平臺之宿,版本號,段表的位置在哪苛坚,還可以拿到字符串表在哪比被,以及整個程序的入口地址。這一系列初始化信息拿到之后泼舱,程序可以通過字符串表定位等缀,找到段名的字符串,通過段表的初始位置娇昙,確認(rèn)每個段的位置尺迂,段名,長度等等信息涯贞,進(jìn)而到達(dá)入口地址枪狂,準(zhǔn)備執(zhí)行。
當(dāng)然宋渔,這只是最初始的內(nèi)容州疾,其后還要考慮鏈接,Import,Export 等等內(nèi)容皇拣,留待以后完善严蓖。
PE 文件
下面我們?nèi)タ纯锤鼮槌R姷腜E 文件格式薄嫡,實際上PE 與 ELF 文件基本相同,也是采用了基于段的格式颗胡,同時PE 也允許程序員將變量或者函數(shù)放在自定義的段中毫深, GCC 中attribute(section('name')) 擴(kuò)展屬性。
PE 文件的前身是COFF毒姨,所以分析PE 文件哑蔫,先來看看COFF 的文件格式,他保存在WinNT.h 文件中弧呐。
COFF 的文件格式和ELF 幾乎一毛一樣:
Image Header |
---|
SectionTable Image_SECTION_HEADER |
.text |
data |
.drectve |
.debug$S |
... other sections |
Symbol Table |
文件頭定義在WinNT.h 中闸迷,我們打開來看一下:
我們可以看到,它這個文件頭和ELF 實際上是一樣的俘枫,也在文件頭中定義了段數(shù)腥沽,符號表的位置,Optional Header 的大小鸠蚪,這個Optional Header 后邊就看到了今阳,他就是PE 可執(zhí)行文件的文件頭的部分,以及段的屬性等茅信。
跟在文件頭后邊的是COFF 文件的段表盾舌,結(jié)構(gòu)體名叫 IMAGE_SECTION_HEADER :
屬性包括這些,和ELF 沒差:
- 段名
- 物理地址 PhysicalAddress
- 虛擬地址 VirtualAddress
- 原始數(shù)據(jù)大小 Sizeof raw data
- 段在文件中的位置 File pointer to raw data
- 該段的重定位表在文件中的位置 File pointer to relocation table
- 該段的行號表在文件中的位置 File pointer to line number
- 標(biāo)志位汹押,包括段的類型矿筝,對齊方式,讀取權(quán)限等標(biāo)志棚贾。
DOS 頭
在我們分析PE 的之前,還有另外一個頭要了解一下榆综,DOS 頭妙痹,不得不說,微軟事兒還是挺多的鼻疮。
微軟在創(chuàng)建PE 文件格式時怯伊,人們正在廣泛使用DOS 文件,所以微軟為了考慮兼容性的問題判沟,所以在PE 頭的最前邊還添加了一個 IMAGE_DOS_HEADER 結(jié)構(gòu)體耿芹,用來擴(kuò)展已有的DOS EXE。在WinNTFS.h 里可以看到他的身影挪哄。
DOS 頭結(jié)構(gòu)體的大小是40字節(jié)吧秕,這里邊有兩個重要的成員,需要知道迹炼,一個是e_magic 又見魔數(shù)砸彬,一個是e_lfanew颠毙,它只是了NT 頭的偏移。
對于PE 文件來說砂碉,這個e_magic蛀蜜,也就是DOS 簽名都是MZ,據(jù)說是一個叫 Mark Zbikowski 的開發(fā)人員在微軟設(shè)計了這種ODS 可執(zhí)行文件增蹭,所以...
我們以Windows 下的notepad++ 的可執(zhí)行文件為例滴某,在二進(jìn)制編輯軟件中打開,此類軟件比較多滋迈,Heditor 打開:
開始的兩個字節(jié)是4D5A壮池,e_lfanew 為00000108 注意存儲順序,小端杀怠。
你以為開頭加上了DOS 頭就完事了么椰憋,就可以跟著接PE 頭了么。為了兼容DOS 當(dāng)然不是這么簡單了赔退,緊接著DOS 頭橙依,跟的是DOS 存根,DOS stub硕旗。這一塊就是為DOS 而準(zhǔn)備的窗骑,對于PE 文件,即使沒有它也可以正常運行漆枚。
旁邊的ASCII 是讀不懂的创译,因為他是機(jī)器碼,是匯編墙基,為了在DOS 下執(zhí)行软族,對于notepad++ 來說,這里是執(zhí)行了一句残制,this program cannot be run in DOS mode 然后退出立砸。逗我= =,有新的人初茶,可以在DOS 中創(chuàng)造一個程序颗祝,做一些小動作。
NT頭
下面進(jìn)入正題恼布,在HEditor 上也看到了PE螺戳,這一塊就是正式的步入PE 的范疇。
這是32位的PE 文件頭定義折汞,64位對應(yīng)改倔幼。第一個成員就是簽名,如我們所說字支,就是我們看到的「PE」凤藏,對應(yīng)為50450000h奸忽。
這里邊有兩個東西,第一個就是我們之前看到的COFF 文件頭揖庄,這里直接放進(jìn)來了栗菜,我們不再分析。
看第二個蹄梢,IMAGE_OPTIONAL_HEADER 不是說這個頭可選疙筹,而是里邊有些變量是可選的,而且有一些變量是必須的禁炒,否則會導(dǎo)致文件無法運行:
有這么幾個需要重點關(guān)注的成員而咆,這些都是文件運行所必需的:
- Magic 魔數(shù),對于32結(jié)構(gòu)體來說是10B幕袱,對于64結(jié)構(gòu)體來說是20B.
- AddressOfEntryPoint 持有EP 的RVA 值暴备,之處程序最先執(zhí)行的代碼起始位置,也就是程序入口们豌。
- ImageBase 進(jìn)程虛擬內(nèi)存的范圍是0-FFFFFFFF (32位)涯捻。PE 文件被加載到這樣的內(nèi)存中,ImageBase 指出文件的優(yōu)先裝入位置望迎。
- SectionAlignment, FileAlignment PE 文件的Body 部分劃分為若干段障癌,F(xiàn)ileAlignment 之處段在磁盤文件中的最小單位,SectionAlignment指定了段在內(nèi)存中的最小單位辩尊。
- SizeOfImage 指定 PE Image 在虛擬內(nèi)存中所占的空間大小涛浙。
- SizeOfHeader PE 頭的大小
- Subsystem 用來區(qū)分系統(tǒng)驅(qū)動文件與普通可執(zhí)行文件。
- NumberOfRvaAndSizes 指定DataDirectory 數(shù)組的個數(shù),雖然最后一個值摄欲,指出個數(shù)是16轿亮,但實際上PE 裝載還是通過識別這個值來確定大小的。至于DataDirectory 是什么看下邊
- DataDirectory 它是一個由IMAGE_DATA_DIRECTORY 結(jié)構(gòu)體組成的數(shù)組蒿涎,數(shù)組每一項都有定義的值哀托,里邊有一些重要的值,EXPORT/IMPORT/RESOURCE, TLS direction 是重點關(guān)注的劳秋。
段頭
PE 的段頭直接沿用的COFF 的段頭結(jié)構(gòu),上邊也說過了胖齐,我們查看notepad++ 的段頭玻淑,可以獲得各個段名,以及其信息呀伙,這里补履,我們可以使用一些軟件查看,更加方便:
RVA to RAW
理解PE 最重要的一個部分就是理解文件從磁盤到內(nèi)存地址的映射過程剿另,做逆向的人員箫锤,只有熟練地掌握才能跟蹤到程序的調(diào)用過程和位置贬蛙,才能分析和尋找漏洞。
對于文件和內(nèi)存的映射關(guān)系谚攒,其實很簡單阳准,他們通過一個簡單的公式計算而來:
換算公式是這樣的:
RAW -PointToRawData = RVA - VirtualAddress
尋找過程就是先找到RVA 所在的段,然后根據(jù)公式計算出文件偏移馏臭。因為我們通過逆向工具野蝇,可以在內(nèi)存中查找到所在的RVA,進(jìn)而我們就可以計算出在文件中所在的位置括儒,這樣绕沈,就可以手動進(jìn)行修改。
看回我們剛才載入的nodepad++ 帮寻,其中的V Addr, 實際上就是VirtualAddress乍狐,R offset 就是PointerToRawData。
假如我們的RVA 地址是5000固逗,那么計算方法就是浅蚪,查看區(qū)段,發(fā)現(xiàn)在.text 中抒蚜,5000-1000+400 = 4400掘鄙,這就是RAW 00004400,而實際上嗡髓,因為我們的ImageBase 是00400000操漠,所以,我們在反編譯時候內(nèi)存中的地址是00405000.
接下來饿这,使我們的PE頭中的核心內(nèi)容浊伙,IAT 和 EAT,也就是 Import address table, export address table.
IAT
導(dǎo)入地址表的內(nèi)容與Windows 操作系統(tǒng)的核心進(jìn)程长捧,內(nèi)存嚣鄙,DLL 結(jié)構(gòu)有關(guān)。他是一種表格串结,記錄了程序使用哪些庫中的哪些函數(shù)哑子。
下面,讓我們把目光轉(zhuǎn)到DLL 上肌割,Dynamic Linked Library 支撐了整個 OS卧蜓。DLL 的好處在于,不需要把庫包含在程序中把敞,單獨組成DLL 文件弥奸,需要時調(diào)用即可,內(nèi)存映射技術(shù)使加載后的DLL 代碼奋早,資源在多個進(jìn)程中實現(xiàn)共享盛霎,更新庫時候只要替換相關(guān)DLL 文件即可赠橙。
加載DLL 的方式有兩種,一種是顯式鏈接愤炸,使用DLL 時候加載期揪,使用完釋放內(nèi)存。另一種是隱式鏈接摇幻,程序開始就一同加載DLL横侦,程序終止的時候才釋放掉內(nèi)存。而IAT 提供的機(jī)制與隱式鏈接相關(guān)绰姻,最典型的Kernel32.dll枉侧。
我們來看看notepad++ 調(diào)用kernel32.dll 中的CreateFileW, 使用PE 調(diào)試工具Ollydbg
我們看到填入?yún)?shù)之后,call 了35d7ffff 地址的內(nèi)容狂芋,然后我們?nèi)ump 窗口榨馁,找一下kernel.CreateFileW:
我們雙擊匯編窗口,啟動編輯帜矾,發(fā)現(xiàn)確實是call 的這個數(shù)值:
可是問題來了翼虫,上邊是E8 35D7FFFF,下邊地址卻是 00C62178屡萤。其實這是Win Visita, Win 7的ASLR 技術(shù)珍剑,主要就是針對緩沖溢出攻擊的一種保護(hù)技術(shù),通過隨機(jī)化布局死陆,讓逆向跟蹤者招拙,難以查找地址,就難以簡單的進(jìn)行溢出攻擊措译。不過還是可以通過跳板的方式别凤,找到溢出的辦法,這就是后話了领虹。
現(xiàn)在可以確定的是规哪,35D7FFFF 可以認(rèn)為保存的數(shù)值就是 CreateFileW 的地址。而為什么不直接使用CALL 7509168B 這種方式直接調(diào)用呢塌衰? Kernel32.dll 版本各不相同诉稍,對應(yīng)的CreateFileW 函數(shù)也各不相同,為了兼容各種環(huán)境最疆,編譯器準(zhǔn)備了CreateFileW 函數(shù)的實際地址均唉,然后記下DWORD PTR DS:[xxxxxx] 這樣的指令,執(zhí)行文件時候肚菠,PE 裝載器將CreateFileW 函數(shù)地址寫到這個位置。
同時罩缴,由于重定位的原因存在蚊逢,所以也不能直接使用CALL 7509168B 的方式层扶,比如兩個DLL 文件有相同的 ImageBase,裝載的時候烙荷,一個裝載到該位置之后镜会,另一個就不能裝載該位置了,需要換位置终抽。所以我們不能對實際地址進(jìn)行硬編碼戳表。
IMAGE_IMPORT_DESCRIPTOR
對于一個普通程序來說,需要導(dǎo)入多少個庫昼伴,就會存在多少個這樣的結(jié)構(gòu)體匾旭,這些結(jié)構(gòu)體組成數(shù)組,然后數(shù)組最后是以NULL 結(jié)構(gòu)體結(jié)束圃郊。其中有幾個重要的成員:
- OriginalFirstThunk INT Import Name Table 地址价涝,RVA
- Name 庫名稱字符串地址,RVA持舆,就是說該地址保存庫名稱
- First Thunk IAT 地址 RVA
- INT 中個元素的值是上邊那個IMAGE_IMPORT_BY_NAME 結(jié)構(gòu)體指針色瘩。
- INT 與 IAT 大小應(yīng)相同。
那么PE 是如何導(dǎo)入函數(shù)輸出到IAT 的:
- 讀取NAME 成員逸寓,獲取擴(kuò)名稱字符串
- 裝載相應(yīng)庫: LoadLibrary("kernel32.dll")
- 讀取OriginalFirstThunk成員居兆,獲取INT 地址
- 讀取INT 數(shù)組中的值,獲取相應(yīng)的 IMAGE_IMPORT_BY_NAME地址竹伸,是RVA地址
- 使用IMAGE_IMPORT_BY_NAME 的Hint 或者是name 項泥栖,獲取相應(yīng)函數(shù)的起始位置 GetProcAddress("GetCurrentThreadId")
- 讀取FistrThunk 成員,獲得IAT 地址佩伤。
- 將上面獲得的函數(shù)地址輸入相應(yīng)IAT 數(shù)組值聊倔。
- 重復(fù)4-7 到INT 結(jié)束。
這里就產(chǎn)生了一個疑惑生巡,OriginalFirstThunk 和 First Thunk 都指向的是函數(shù)耙蔑,為什么多此一舉呢?
首先孤荣,從直觀上說甸陌,兩個都指向了庫中引入函數(shù)的數(shù)組,魚C 畫的這張圖挺直觀:
OriginalFirstThunk 和 FirstThunk 他們都是兩個類型為IMAGE_THUNK_DATA 的數(shù)組盐股,它是一個指針大小的聯(lián)合(union)類型钱豁。
每一個IMAGE_THUNK_DATA 結(jié)構(gòu)定義一個導(dǎo)入函數(shù)信息(即指向結(jié)構(gòu)為IMAGE_IMPORT_BY_NAME 的家伙,這家伙稍后再議)疯汁。
然后數(shù)組最后以一個內(nèi)容為0 的 IMAGE_THUNK_DATA 結(jié)構(gòu)作為結(jié)束標(biāo)志牲尺。
IMAGE_THUNK_DATA32 結(jié)構(gòu)體如下:
因為是Union 結(jié)構(gòu),IMAGE_THUNK_DATA 事實上是一個雙字大小。
規(guī)定如下:
當(dāng) IMAGE_THUNK_DATA 值的最高位為 1時谤碳,表示函數(shù)以序號方式輸入溃卡,這時候低 31位被看作一個函數(shù)序號。
當(dāng) IMAGE_THUNK_DATA 值的最高位為 0時蜒简,表示函數(shù)以字符串類型的函數(shù)名方式輸入瘸羡,這時雙字的值是一個 RVA,指向一個 IMAGE_IMPORT_BY_NAME 結(jié)構(gòu)搓茬。
我們再看IMAGE_IMPORT_BY_NAME 結(jié)構(gòu):
結(jié)構(gòu)中的 Hint 字段也表示函數(shù)的序號犹赖,不過這個字段是可選的,有些編譯器總是將它設(shè)置為 0卷仑。
Name 字段定義了導(dǎo)入函數(shù)的名稱字符串峻村,這是一個以 0 為結(jié)尾的字符串。
現(xiàn)在重點來了:
第一個數(shù)組(由 OriginalFirstThunk 所指向)是單獨的一項系枪,而且不能被改寫雀哨,我們前邊稱為 INT。第二個數(shù)組(由 FirstThunk 所指向)事實上是由 PE 裝載器重寫的私爷。
PE 裝載器裝載順序正如上邊所講的那樣雾棺,我們再將它講詳細(xì)一點:
PE 裝載器首先搜索 OriginalFirstThunk ,找到之后加載程序迭代搜索數(shù)組中的每個指針衬浑,找到每個 IMAGE_IMPORT_BY_NAME 結(jié)構(gòu)所指向的輸入函數(shù)的地址捌浩,然后加載器用函數(shù)真正入口地址來替代由 FirstThunk 數(shù)組中的一個入口,因此我們稱為輸入地址表(IAT).
繼續(xù)套用魚C 的圖工秩,就能直觀的感受到了:
所以尸饺,在讀取一次OriginalFirstThunk 之后,程序就是依靠IAT 提供的函數(shù)地址來運行了助币。
EAT
搞清楚了IAT 的原理浪听,EAT 就好理解了,目前這篇總結(jié)的有點長了眉菱,我長話短說迹栓。IAT 是導(dǎo)入的庫和函數(shù)的表,那么EAT 就對應(yīng)于導(dǎo)出俭缓,它使不同的應(yīng)用程序可以調(diào)用庫文件中提供的函數(shù)克伊,為了方便導(dǎo)出函數(shù),就需要保存這些導(dǎo)出信息华坦。
回頭看PE 文件中的PE頭我們可以看到IMAGE_EXPORT_DIRECTORY 結(jié)構(gòu)體以的位置愿吹,他在IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress 的值就是 IMAGE_EXPORT_DIREDCTORY 的起始位置。
IMAGE_EXPORT_DIRECTORY結(jié)構(gòu)體如下:
這里邊同樣是這么幾個重要的成員:
- NumberOfFunctions 實際Export 函數(shù)的個數(shù)
- NumberOfNames Export 函數(shù)中具名的函數(shù)個數(shù)
- AddressOfFunctins Export 函數(shù)地址數(shù)組惜姐,數(shù)組個數(shù)是上邊的NOF
- AddressOfNames 函數(shù)名稱地址數(shù)組犁跪,個數(shù)是上邊的NON
- AddressOfNameOrdinals Ordinal 地址數(shù)組,個數(shù)等于上邊NON
- Name 一個RVA 值,指向一個定義了模塊名稱的字符串耘拇。如即使Kernel32.dll 文件被改名為”Ker.dll”撵颊。仍然可以從這個字符串中的值得知其在編譯時的文件名是”Kernel32.dll”。
-
Base:導(dǎo)出函數(shù)序號的起始值惫叛,將AddressOfFunctions 字段指向的入口地址表的索引號加上這個起始值就是對應(yīng)函數(shù)的導(dǎo)出 序號。
以kernel32.dll 為例逞刷,我們看一下:
從上邊這些成員嘉涌,我們實際上可以看出,是有兩種方式提供給那些想調(diào)用該庫中函數(shù)的夸浅,一種是直接從序號查找函數(shù)入口地址導(dǎo)入仑最,一種是通過函數(shù)名來查找函數(shù)入口地址導(dǎo)入。
先上一個魚C 的圖帆喇,方便理解:
上邊圖警医,注意一點,因為AddressOfNameOrdinals 的序號應(yīng)當(dāng)是從0開始的坯钦,不過圖中映射的是第二個函數(shù)指向的序號1预皇。
我們分別說一下兩種方式:
當(dāng)已知導(dǎo)出序號的時候
- Windows 裝載器定位到PE 文件頭,
- 從PE 文件頭中的 IMAGE_OPTIONAL_HEADER32 結(jié)構(gòu)中取出數(shù)據(jù)目錄表婉刀,并從第一個數(shù)據(jù)目錄中得到導(dǎo)出表的RVA 吟温,
- 從導(dǎo)出表的 Base 字段得到起始序號,
- 將需要查找的導(dǎo)出序號減去起始序號突颊,得到函數(shù)在入口地址表中的索引鲁豪,
- 檢測索引值是否大于導(dǎo)出表的 NumberOfFunctions 字段的值,如果大于后者的話律秃,說明輸入的序號是無效的用這個索引值在 AddressOfFunctions 字段指向的導(dǎo)出函數(shù)入口地址表中取出相應(yīng)的項目爬橡,這就是函數(shù)入口地址的RVA 值,當(dāng)函數(shù)被裝入內(nèi)存的時候棒动,這個RVA 值加上模塊實際裝入的基地址糙申,就得到了函數(shù)真正的入口地址
當(dāng)已知函數(shù)名稱查找入口地址時
- 從導(dǎo)出表的 NumberOfNames 字段得到已命名函數(shù)的總數(shù)赋焕,并以這個數(shù)字作為循環(huán)的次數(shù)來構(gòu)造一個循環(huán)
- 從 AddressOfNames 字段指向得到的函數(shù)名稱地址表的第一項開始男应,在循環(huán)中將每一項定義的函數(shù)名與要查找的函數(shù)名相比較但狭,如果沒有任何一個函數(shù)名是符合的疲迂,表示文件中沒有指定名稱的函數(shù)妹卿,如果某一項定義的函數(shù)名與要查找的函數(shù)名符合惹想,那么記下這個函數(shù)名在字符串地址表中的索引值缺厉,然后在 AddressOfNamesOrdinals 指向的數(shù)組中以同樣的索引值取出數(shù)組項的值桃犬,我們這里假設(shè)這個值是x
- 最后卜范,以 x 值作為索引值衔统,在 AddressOfFunctions 字段指向的函數(shù)入口地址表中獲取的 RVA 就是函數(shù)的入口地址
一般來說,做逆向或者是寫代碼都是第二種方法,我們以kernel32.dll 中的GetProcAddress 函數(shù)為例锦爵,其操作原理如下:
- 利用 AddressOfNames 成員轉(zhuǎn)到 『函數(shù)名稱數(shù)組』
- 『函數(shù)名稱數(shù)組』中存儲著字符串地址舱殿,通過比較字符串,查找指定的函數(shù)名稱险掀,此時數(shù)組所以為成為name_index
- 利用 AddressOfNameOrdinals 成員沪袭,轉(zhuǎn)到這個序號數(shù)組
- 在ordinal 數(shù)組中通過name_index 查找到相應(yīng)的序號
- 利用AddressOfFunctions 成員,轉(zhuǎn)到『函數(shù)地址數(shù)組』EAT
- 在EAT 中將剛剛得到的ordinal 作為索引樟氢,獲得指定函數(shù)的入口地址
寫了這么多冈绊,實際上算是對文件結(jié)構(gòu)有了一個入門的認(rèn)識,至少知道在程序運行過程中埠啃,系統(tǒng)是如何進(jìn)行操作和鏈接的死宣,而更加詳細(xì)的內(nèi)容注入運行時壓縮,DLL 注入碴开,API 鉤取等技術(shù)毅该,就需要在這個基礎(chǔ)之上繼續(xù)挖掘,所以PE 潦牛,ELF 文件結(jié)構(gòu)的分析是相當(dāng)重要的眶掌。
PS. 參考:
魚C 講解PE 文件格式之INT
《Windows PE 權(quán)威指南》
《逆向工程核心原理》
《程序員的自我修養(yǎng)-鏈接,裝載與庫》