摘要:根據(jù)PE文件導(dǎo)入表的結(jié)構(gòu)及系統(tǒng)加載導(dǎo)入表的原理,在外殼中自定義了導(dǎo)入表,將外殼中的導(dǎo)入表與PE文件的導(dǎo)入表合并,并用Win32匯編編程實(shí)現(xiàn)。利用Windows加載PE文件時(shí),將PE文件和外殼的導(dǎo)入表初始化,從而實(shí)現(xiàn)了在PE文件和外殼中正常調(diào)用API函數(shù)。
關(guān)鍵詞:PE文件;外殼;導(dǎo)入表;合并導(dǎo)入表
中圖分類號(hào):TP309 文獻(xiàn)標(biāo)識(shí)碼:A 文章編號(hào):1009-3044(2015)03-0117-04
The Use of Windows to Load the Import Table of the PE File and Shell
ZHANG Zhong
(Chongqing University of Technology, Chongqing 400054,China)
Abstract: Based on the import table structure of the PE file and the principle of system loading the import table,it is defined in the shell. the import table of the shell and that of the PE file are combined in one by using Win32 assembly program. Which is initialized while windows loads PE file, so as to realize normal calling API functions in the PE file and shell.
Key words: PE file; shell; import table; merging import table
PE文件(EXE)經(jīng)加殼后,由于程序功能上的需要,在外殼中或多或少地會(huì)用到API函數(shù),而要調(diào)用API函數(shù)就需要知道API函數(shù)的地址。由于外殼代碼是附加在編譯鏈接好的PE文件上,因此,外殼中調(diào)用的API函數(shù)地址只能自己想法解決。在外殼中獲得API函數(shù)地址常用的方法有二種:一種方法是在PE文件被加載的進(jìn)程中由外殼中的代碼自己動(dòng)態(tài)的獲取API函數(shù)的地址;另一種方法是在外殼中自定義一個(gè)所用API函數(shù)的導(dǎo)入表,利用Windows加載PE文件時(shí),由系統(tǒng)加載外殼的導(dǎo)入表,從而獲得API函數(shù)的地址。而PE文件的導(dǎo)入表就用外殼中的相關(guān)代碼來初始化或外殼中的相關(guān)代碼為PE文件重新構(gòu)造還原一個(gè)導(dǎo)入表并初始化。那么有沒有方法讓系統(tǒng)加載PE文件時(shí)同時(shí)完成對(duì)PE文件和外殼自定義的導(dǎo)入表進(jìn)行初始化呢?這就是本文所要討論的問題。
1 PE文件8導(dǎo)入表和外殼導(dǎo)入表合并的基本思路
1.1 PE文件導(dǎo)入表的結(jié)構(gòu)
圖1 PE文件磁盤映像中的導(dǎo)入表結(jié)構(gòu)(部分)
要自定義導(dǎo)入表和實(shí)現(xiàn)導(dǎo)入表的合并,首先要了解熟悉導(dǎo)入表的基本結(jié)構(gòu)組成。PE文件的導(dǎo)入表是由一系列的IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)組成的數(shù)組,每一個(gè)IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)對(duì)應(yīng)一個(gè)DLL,導(dǎo)入表的最后由一個(gè)內(nèi)容全為0 的IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)結(jié)束。該結(jié)構(gòu)的定義如下:
IMAGE_IMPORT_DESCRIPTOR STRUCT
union
Characteristics dd ?
OriginalFirstThunk dd ? ;指向?qū)朊Q表(INT)的RVA
ends
TimeDateStamp dd ?
ForwarderChain dd ?
Name1 dd ? ;指向DLL名稱(ANSI字符串,以0結(jié)尾)的RVA
FirstThunk dd ? ;指向?qū)氲刂繁恚↖AT)的RVA
IMAGE_IMPORT_DESCRIPTOR ENDS
字段OriginalFirstThunk所指的導(dǎo)入名稱表(Import Name Table,簡(jiǎn)稱INT)由若干個(gè)IMAGE_THUNK_DATA結(jié)構(gòu)組成的數(shù)組,每一個(gè)IMAGE_THUNK_DATA結(jié)構(gòu)對(duì)應(yīng)一個(gè)API導(dǎo)入函數(shù),數(shù)組的最后由一個(gè)內(nèi)容全為0的IMAGE_THUNK_DATA結(jié)構(gòu)結(jié)束。該結(jié)構(gòu)的定義如下:
IMAGE_THUNK_DATA STRUCT
union u1
ForwarderString dd ?
Function dd ? ;導(dǎo)入函數(shù)的入口地址
Ordinal dd ? ;導(dǎo)入API的序號(hào)
AddressOfData dd ? ;指向IMAGE_IMPORT_BY_NAME的RVA
ends
IMAGE_THUNK_DATA ENDS
從這個(gè)結(jié)構(gòu)的定義可看到,該結(jié)構(gòu)是一個(gè)共用體,實(shí)際上就是一個(gè)雙字。當(dāng)雙字的最高位是1時(shí),表示函數(shù)是以序號(hào)導(dǎo)入的,低31位就是函數(shù)的序號(hào)值;當(dāng)最高位是0時(shí),表示函數(shù)是以函數(shù)名稱(ANSI字符串,以0結(jié)尾)導(dǎo)入的,雙字表示是一個(gè)RVA,此時(shí)指向一個(gè)IMAGE_IMPORT_BY_NAME結(jié)構(gòu)。IMAGE_IMPORT_BY_NAME結(jié)構(gòu)定義如下所示。
IMAGE_IMPORT_BY_NAME STRUCT
Hint dw ? ;指示API函數(shù)在DLL導(dǎo)出表中的序號(hào),有些編譯器設(shè)為0
Name1 db ? ;導(dǎo)入函數(shù)的函數(shù)名(ANSI字符串,以0結(jié)尾)
IMAGE_IMPORT_BY_NAME ENDS
Windows在裝入PE文件時(shí),其工作之一是定位到導(dǎo)入表,根據(jù)導(dǎo)入表中說明的DLL,將DLL裝入內(nèi)存,在DLL中搜索導(dǎo)入表記錄的API函數(shù),找到后將對(duì)應(yīng)的函數(shù)地址(指針)寫入IAT,以方便程序正確調(diào)用API函數(shù)。
1.2 外殼中自定義的導(dǎo)入表(示例)
根據(jù)前面所敘的PE文件導(dǎo)入表結(jié)構(gòu),外殼中自定義的導(dǎo)入表如下(部分):
APPEND_CODE equ this byte ;外殼開始
ImportTableHeader label dword
;-----IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)數(shù)組-----
ImportTable dd _MessageBox-ImportTable ;OriginalFirstThunk
dd 0,0
dd DllUser32-ImportTable ;Name1
dd _MessageBox-ImportTable ;FirstThunk
dd 0,0,0,0,0 ;導(dǎo)入表結(jié)束符
;-------DLL名稱字符串---------
DllUser32 db 'user32.dll'
dw 0
;---------IMAGE_THUNK_DATA結(jié)構(gòu)數(shù)組------- _MessageBox dd Func1-ImportTable ;IMAGE_THUNK_DATA
_DialogBoxIndirectParam dd Func2-ImportTable
_EndDialog dd Func3-ImportTable
_GetDlgItemText dd Func4-ImportTable
_SetWindowText dd Func5-ImportTable
_SendDlgItemMessage dd Func6-ImportTable
_LoadIcon dd Func7-ImportTable
_SendMessage dd Func8-ImportTable
dd 0 ;結(jié)束符
;------IMAGE_IMPORT_BY_NAME結(jié)構(gòu)數(shù)組------
Func1 dw 0
db 'MessageBoxA',0
Func2 dw 0
db 'DialogBoxIndirectParamA',0
Func3 dw 0
db 'EndDialog',0
Func4 dw 0
db 'GetDlgItemTextA',0
Func5 dw 0
db 'SetWindowTextA',0
Func6 dw 0
db 'SendDlgItemMessageA',0
Func7 dw 0
db 'LoadIconW',0
Func8 dw 0
db 'SendMessageW',0
…………
ImportTableEnd label dword
1.3 PE文件的導(dǎo)入表和外殼的導(dǎo)入表的合并思路
為了讓W(xué)indows加載PE文件和外殼中的導(dǎo)入表,首先在外殼中要嚴(yán)格按照PE文件的導(dǎo)入表格式定義,然后將PE文件和外殼的導(dǎo)入表合并成一個(gè)IMAGE_IMPORT_DESCRIPTOR數(shù)組。由于在PE文件中的.idata節(jié)或.rdata節(jié)中空隙空間有限,不一定能裝下外殼中的整個(gè)導(dǎo)入表,所以在這里是將PE文件的導(dǎo)入表移動(dòng)到PE文件的原來最后一個(gè)節(jié)區(qū)的末尾處(新增加的.zzcode節(jié)區(qū)的開始處),然后再接上外殼的導(dǎo)入表,這樣就合并成了一個(gè)完整的導(dǎo)入表。這又有二種拼接方法:(a)PE文件的導(dǎo)入表放在前面,外殼的導(dǎo)入表放在后面;(b)外殼導(dǎo)入表放在前面,PE文件的導(dǎo)入表放在后面。如下圖2所示:
(a) (b)
圖2 PE文件導(dǎo)入表與外殼導(dǎo)入表合并后的磁盤映像
2 利用Windows加載PE文件和外殼的導(dǎo)入表的程序?qū)崿F(xiàn)
2.1 合并PE文件和外殼的導(dǎo)入表
這里用圖2(a)中所示的導(dǎo)入表合并方案來說明如何編程實(shí)現(xiàn)導(dǎo)入表的合并。
1)首先由API函數(shù)CreateFile、CreateFileMapping、MapViewOfFile創(chuàng)建PE文件的內(nèi)存映像,從PE開頭定位到NT映像頭IMAGE_NT_HEADERS,這里用ebx指向IMAGE_NT_HEADERS結(jié)構(gòu),由字段OptionalHeader、DataDirectory通過變量VirtualAddress,也就是 [ebx].OptionalHeader.DataDirectory[8].VirtualAddress定位到PE文件的導(dǎo)入表,然后用如下代碼片段計(jì)算出導(dǎo)入表的字節(jié)長(zhǎng)度。
xor ecx,ecx
assume esi:ptr IMAGE_IMPORT_DESCRIPTOR
.while [esi].OriginalFirstThunk || [esi].TimeDateStamp || \
[esi].ForwarderChain || [esi].Name1 || [esi].FirstThunk
inc ecx
add esi,sizeof IMAGE_IMPORT_DESCRIPTOR
.endw
mov eax,sizeof IMAGE_IMPORT_DESCRIPTOR
mul ecx
mov @IIDlength,eax
2)在PE文件中新增加一個(gè)節(jié)區(qū)如.zzcode,將PE文件的導(dǎo)入表寫入該文件的新增加節(jié)區(qū)的開頭處,然后將整個(gè)外殼(注意:要求外殼的導(dǎo)入表要放在外殼的最前面)寫在緊接PE文件的導(dǎo)入表的后面,這樣就實(shí)現(xiàn)了PE文件的導(dǎo)入表和外殼導(dǎo)入表的合并。其后就可用任意字節(jié)代碼履蓋掉PE文件原來位置的導(dǎo)入表。
3)修改PE文件導(dǎo)入表的指針使其指向合并后的導(dǎo)入表頭部,同時(shí)修改合并導(dǎo)入表的大小,以確保系統(tǒng)加載PE文件時(shí)初始化合并后的導(dǎo)入表。代碼片段如下:
mov eax,[ebx].VirtualAddress ;ebx指向PE文件新增加的節(jié)區(qū)
mov [edi].OptionalHeader.DataDirectory[8].VirtualAddress,eax
mov ecx,offset ImportTableEnd - offset ImportTableHeader
add ecx,@IIDlength
mov [edi].OptionalHeader.DataDirectory[8].isize,ecx
4)將外殼自定義的整個(gè)導(dǎo)入表讀入由函數(shù)GlobalAlloc申請(qǐng)的內(nèi)存塊中,然后對(duì)導(dǎo)入表IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)中的OriginalFirstThunk、Name1、FirstThunk字段的雙字地址進(jìn)行修改,同時(shí)對(duì)IMAGE_THUNK_DATA結(jié)構(gòu)中共用體u1中的AddressOfData字段的地址進(jìn)行修改,將字段中的相對(duì)于外殼導(dǎo)入表頭部的偏移offset轉(zhuǎn)換為RVA。這樣,當(dāng)Windows加載外殼導(dǎo)入表時(shí),通過內(nèi)存PE文件的映像基地址+字段的RVA,就能準(zhǔn)確定位需要查找DLL中的函數(shù),并將函數(shù)地址填寫入IAT,從而保證外殼中調(diào)用API函數(shù)時(shí)找到所對(duì)應(yīng)的函數(shù)地址。偏移地址修改為RVA完成后,再將內(nèi)存塊中的整個(gè)導(dǎo)入表寫回原來位置將原來的外殼導(dǎo)入表覆蓋掉,至此,合并導(dǎo)入表的工作就完成了。進(jìn)行這個(gè)地址轉(zhuǎn)換的程序代碼如下:
;修正外殼自定義導(dǎo)入表RVA子程序
DisposeImportTab proc _lphFile,_dwAddCodeFile,_dwAddCodeVirt
;_lphFile——文件句柄,_dwAddCodeFile——被加殼PE文件添加代碼的位置,_dwAddCodeVirt——內(nèi)存中添加代碼的位置
local @lpAlloc1,@dwReadByte,@ImpTablength
pushad
mov esi,offset ImportTableEnd-offset ImportTableHeader
mov @ImpTablength,esi
invoke GlobalAlloc,GPTR,@ImpTablength
mov @lpAlloc1,eax
mov edi,eax ;指向自定義的導(dǎo)入表頭部
mov ecx,_dwAddCodeFile
invoke SetFilePointer,_lphFile,ecx,NULL,F(xiàn)ILE_BEGIN
invoke ReadFile,_lphFile,edi,esi,addr @dwReadByte,NULL
.if @dwReadByte
;在此修正導(dǎo)入表RVA地址的代碼
assume edi:ptr IMAGE_IMPORT_DESCRIPTOR
mov eax,_dwAddCodeVirt
.while [edi].FirstThunk
add [edi].OriginalFirstThunk,eax
mov esi,@lpAlloc1
add esi,[edi].FirstThunk
add [edi].FirstThunk,eax
add [edi].Name1,eax
assume esi: ptr IMAGE_THUNK_DATA
.while [esi].u1.AddressOfData
add [esi].u1.Ordinal,eax
add esi,4
.endw
add edi,14h
.endw
assume edi:nothing,esi:nothing
mov ecx,_dwAddCodeFile
mov ebx,offset ImportTableEnd-offset ImportTableHeader
invoke SetFilePointer,_lphFile,ecx,NULL,F(xiàn)ILE_BEGIN
invoke WriteFile,_lphFile,@lpAlloc1,ebx,addr @dwReadByte,NULL
invoke GlobalFree,@lpAlloc1
popad
mov eax,1
.else
invoke MessageBox,NULL,addr szImportTabErr,addr szCaptionTip,MB_OK
popad
mov eax,0
.endif
ret
DisposeImportTab endp
2.2 合并導(dǎo)入表的測(cè)試與分析
1)在Windos 7 和Windows XP SP2環(huán)境下,對(duì)示例PE文件和多個(gè)PE文件進(jìn)行了加殼,對(duì)合并后的導(dǎo)入表進(jìn)行了測(cè)試,程序原有各項(xiàng)功能運(yùn)行正常,這說明PE文件的API函數(shù)調(diào)用,外殼中API函數(shù)調(diào)用工作正常,合并導(dǎo)入表達(dá)到預(yù)期目的。
2)用導(dǎo)入表查看工具軟件查看加殼后的示例PE文件導(dǎo)入表,如圖3所示是PE文件導(dǎo)入表(部分)磁盤映像,導(dǎo)入表字段OriginalFirstThunk指向INT,字段FirstThunk指向IAT;圖4所示是外殼導(dǎo)入表(部分)磁盤映像,導(dǎo)入表字段OriginalFirstThunk和字段FirstThunk指向同一個(gè)IMAGE_THUNK_DATA,當(dāng)被系統(tǒng)載入內(nèi)存后它就轉(zhuǎn)變成IAT了。
3 結(jié)束語
1)本文示例中外殼中的導(dǎo)入表和PE文件的導(dǎo)入表合并后放在外殼的最前面,其實(shí)合并后的導(dǎo)入表還可放置在外殼的最后面或外殼中的任意位置,只是這樣編程實(shí)現(xiàn)時(shí)要復(fù)雜一些。
2)測(cè)試和分析表明:除了可把PE文件的導(dǎo)入表IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)數(shù)組移動(dòng)到外殼中,實(shí)際上還可以把IMAGE_THUNK_DATA結(jié)構(gòu)也移動(dòng)到外殼中,不過這里就需要修正IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)中字段OriginalFirstThunk、FirstThunk的RVA值,以便正確的指向IMAGE_THUNK_DATA結(jié)構(gòu)數(shù)組,保證Windows加載PE文件導(dǎo)入表時(shí)正確尋址找到INT和IAT,但I(xiàn)MAGE_THUNK_DATA結(jié)構(gòu)中的共用體u1中的字段AddressOfData不必修正,因?yàn)镮MAGE_IMPORT_BY_NAME結(jié)構(gòu)的位置沒有變動(dòng)。同樣,PE文件的導(dǎo)入表IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)數(shù)組雖然移動(dòng)到外殼中,但由于IMAGE_THUNK_DATA結(jié)構(gòu)數(shù)組的位置沒有變化,所以不必修改其中的字段OriginalFirstThunk、FirstThunk的RVA值。
3)將PE文件的導(dǎo)入表IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)數(shù)組移動(dòng)到外殼中,而將IMAGE_THUNK_DATA結(jié)構(gòu)和IMAGE_IMPORT_BY_NAME結(jié)構(gòu)留在PE文件中,也就是把整個(gè)導(dǎo)入表分割成了二部分,這樣可以加強(qiáng)外殼與原程序的聯(lián)系,如果簡(jiǎn)單地把PE文件的外殼脫去會(huì)導(dǎo)致系統(tǒng)初始化PE文件的導(dǎo)入表失敗,從而使PE文件不能正常調(diào)用API函數(shù)而引發(fā)異常。
參考文獻(xiàn):
[1] 段鋼. 加密與解密[M]. 3版. 北京: 電子工業(yè)出版社, 2008.
[2] 羅云彬. Windows環(huán)境下32位匯編語言程序設(shè)計(jì)[M]. 2版. 北京: 電子工業(yè)出版社, 2006.
[3] 張鐘. 在遠(yuǎn)程進(jìn)程中注入DLL鉤掛IAT的方法[J]. 計(jì)算機(jī)與現(xiàn)代化, 2014(4).
[4] 戚利. WindowsPE權(quán)威指南[M]. 北京: 機(jī)械工業(yè)出版社, 2012.