從現在開始,面試的問題漸漸深入。這次的三個問題,都是對PE格式的不斷深入的提問。從最初的概念,到病毒對PE格式的利用,再到最后的殼的問題。這里需要說明的是,由于殼是一個比較復雜的概念,面試中也僅僅只能對原理性的東西進行提問,但是實際上,是要求面試者能夠熟練掌握脫殼乃至寫殼的技術的。我這里并沒有對此進行特別詳細的闡述,所以大家如果想深入理解,還是參考相應的資料,并加強實際的動手能力比較好。
以下是問題7——問題9:
**7、請說明PE文件由幾部分構成,并解釋什么是導入表、導出表以及附加數據。**
答:(以下內容選自**《Windows PE權威指南》**第3.3.3節——程序員眼中的PE結構)
在程序員眼中,PE文件格式是由許多數據結構組成的,數據結構是一系列有組織的數據的集合,如圖3-9所示。

圖3-9 程序員眼中的PE結構
如圖所示,一個標準的PE文件一般由四大部分組成:
□DOS頭
□PE頭(IMAGE_NT_HEADERS)
□節表(多個IMAGE_SECTION_HEADER結構)
□節內容
其中,PE頭的數據結構最為復雜。簡單來說,PE頭包含:
□4個字節的標識符號(Signature)
□20個字節的基本頭信息(IMAGE_FILE_HEADER)
□216個字節的擴展頭信息(IMAGE_OPTIONAL_HEADER32)
說明:如果按照“頭部+身體”的信息組織方式來看:
PE文件頭部 = DOS頭 + PE頭 + 節表
PE文件身體 = 節內容
節內容中會出現各種不同的數據結構,如導入表、導出表、資源表、重定位表等,關于這些數據的組織方式會在后面的章節中陸續接觸到。
(以下內容選自**《Windows環境下32位匯編語言程序設計 典藏版》**第17.2.1節——導入表簡介)
在Win32編程中常常用到“導入函數”(Import functions),導入函數就是被程序調用但其執行代碼又不在程序中的函數,這些函數的代碼位于一個或者多個DLL中,在調用者程序中只保留一些函數信息,包括函數名及其駐留的DLL名等。
對于存儲在磁盤上的PE文件來說,它無法得知這些導入函數會在內存的哪個地方出現,只有當PE文件被裝入內存,Windows裝載器才將DLL裝入,并將調用導入函數的指令和函數實際所處的地址聯系起來,這就是“動態鏈接”的概念。動態鏈接是通過PE文件中定義的“導入表”(Import Table)來完成的,導入表中保存的正是函數名和其駐留的DLL名等動態鏈接所必需的信息。
(以下內容選自**《Windows環境下32位匯編語言程序設計 典藏版》**第17.3節——導出表)
當PE文件被執行的時候,Windows裝載器將文件裝入內存并將導入表中登記的DLL文件一并裝入,再根據DLL文件中的函數導出信息對被執行文件的IAT表進行修正。在這些包含導出函數的DLL文件中,導出信息被保存在導出表中,通過導出表,DLL文件向系統提供導出函數的名稱、序號和入口地址等信息,以便Windows裝載器通過這些信息來完成動態鏈接的過程。
擴展名為.exe的PE文件中一般不存在導出表,而大部分的.dll文件中都包含導出表,但是這并不是必然的,比如,用作純資源的.dll文件就不提供導出函數,文件中也就不存在導出表;另外,偶爾也可以見到包含導出函數和導出表的.exe文件。
(以下內容選自**《加密與解密 第三版》**第13.6節——附加數據)
某些特殊的PE文件在各個區塊的正式數據之后還有一些數據,這些數據不屬于任何區塊。由于PE文件被映射到內存是按區塊映射的,因此這些數據是不能被映射到內存中的,這些額外的數據稱為附加數據(overlay)。
附加數據的起點可以認為是最后一個區塊的末尾,終點是文件末尾。用LordPE查看實例overlay.exe的區塊,如圖13.54所示。

圖13.54 查看區塊信息
從圖13.54可以計算出最后一個區塊末尾的文件偏移值為3200h+600h=3800h。用十六進制工具打開目標文件,跳到3800h,會發現后面還有一段數據,這就是附加數據,如圖13.55所示。

圖13.55 附加數據
用PEiD分析實例overlay.exe,會給出結果“Nothing found [Overlay]*”,其中Overlay就表明有附加數據的存在。帶有附加數據的文件脫殼時,必須將附加數據粘貼回去,如果文件有訪問附加數據的指針,也要修正。
本節實例overlay.exe實際是用UPX加殼了,由于附加數據的存在,干擾了PEiD分析。用OllyDbg打開實例,來到OEP處。
~~~
00401436 55 push ebp
00401437 8BEC mov ebp,esp
00401439 6AFF push -1
~~~
此時,抓取內存映像保存到磁盤中,然后用ImportREC重建輸入表,最終文件為dumped_.exe。
運行實例原文件,然后單擊菜單“File/Open”,程序將會讀取附加數據并在編輯框中顯示出來,如圖13.56所示。而運行脫殼后的文件dumped_.exe,不能將原來的文字顯示出來,如圖13.57所示。

圖13.56 讀取附加數據

圖13.57 脫殼后讀取附加數據
由于附加數據沒有被映射到內存里,因此抓取的映像文件里也沒有附加數據。現在將原文件的附加數據移到脫殼后的文件里。用十六進制工具打開overlay.exe,將3800h后的附加數據追加到dumped_.exe文件末尾E000h處。
運行已有附加數據的dumped_.exe,但執行“File/Open”仍不能正確讀取數據。用OllyDbg分析一下實例是如何讀取自身附加數據的。用CreateFileA設斷,執行“File/Open”功能后,會中斷到這段代碼處。
~~~
00401040 push ecx
00401041 push 0
00401043 call dword ptr [<GetModuleFileNameA>] ;取自身文件名
00401049 push 0
……
00401062 call dword ptr [<&CreateFileA>] ;打開自身
00401068 mov dword ptr [ebp-11C],eax
……
004010DC push 0
004010DE push 0
004010E0 push 3800 ;注意這個值
004010E5 mov edx,dword ptr [ebp-11C]
004010EB push edx
004010EC call dword ptr [<SetFilePointer>] ;移動讀寫指針
……
0040110E call dword ptr [<&ReadFile>]
00401127 mov ecx,dword ptr [ebp-108]
0040112D push ecx
0040112E mov edx,dword ptr [ebp-10C]
00401134 push edx
00401135 call dword ptr [<SetWindowTextA>] ;將附加數據顯示到文本框里
~~~
用CreateFileA打開一個文件后,文件指針默認是指向文件的第一個字節的。程序用SetFilePointer設置指針,指向附加數據,然后用ReadFile將附加數據讀取出來。這里SetFilePointer函數比較關鍵,其原型如下:
~~~
DWORD SetFilePointer(
HANDLE hFile, //文件句柄
LONG lDistanceToMove, //移動的距離,這個是低32位
PLONG lpDistanceToMoveHigh, //移動的距離,這個是高32位
DWORD dwMoveMethod //移動方式
);
~~~
由于脫殼后,文件大小發生變化,追加后的附加數據地址已改變,此處變為E000h,因此需要修正SetFilePointer的參數,將其指向附加數據。
~~~
004010DC push 0
004010DE push 0
004010E0 push 0E000 ;將此處指向附加數據E000h
004010E5 mov edx,dword ptr [ebp-11C]
004010EB push edx
004010EC call dword ptr [<SetFilePointer>]
~~~
也就是說,對于帶有附加數據的程序,抓取內存映像后,必須將附加數據追加到脫殼文件的最后,同時修正讀取附加數據的相應指針。
**知識擴展:**
(以下內容選自**《加密與解密 第三版》**第10.15.1節——文件格式檢查)
文件格式可以通過PEheader開始的標志Signature來檢測。也許讀者會說,檢測DOS Header的Magic Mark不是也可以檢測此PE文件是否合法嗎?這個想法沒有錯,但是檢測Magic Mark不一定能確定就是PE文件,如果某文本文件正好在開始就是“MZ”字符串,就會誤判斷。
(1)判斷文件開始的第一個字段是否為IMAGE_DOS_SIGNATURE,即5A4Dh。
(2)再通過e_lfanew找到IMAGE_NT_HEADERS,判斷Signature字段的值是否為IMAGE_NT_SIGNATURE,即00004550h,如果是IMAGE_NT_SIGNATURE,就可以認為該文件是PE格式。
具體實現的代碼如下:
~~~
BOOL IsPEFile(LPVOID ImageBase)
{
PIMAGE_DOS_HEADER pDH=NULL;
PIMAGE_NT_HEADERS pNtH=NULL;
if(!ImageBase) //判斷映像基址
return FALSE;
pDH=(PIMAGE_DOS_HEADER)ImageBase;
if(pDH->e_magic!=IMAGE_DOS_SIGNATURE) //判斷是否為MZ
return FALSE;
pNtH=(PIMAGE_NT_HEADERS32)((DWORD)pDH+pDH->e_lfanew);
if(pNtH->Signature!=IMAGE_NT_SIGNATURE) //判斷是否為PE格式
return FALSE
return TRUE;
}
~~~
(以下內容選自**《C++黑客編程揭秘與防范》**第4.2.7節——3種地址的轉換)
某數據的文件偏移=該數據所在節的起始文件偏移+(某數據的RVA-該數據所在節的起始RVA)。
除了上面的計算方法以外,還有一種計算方法,把節的起始RVA的值減去節的起始文件偏移值,得到一個差值。然后再用RVA減去這個得到的差值就可以得到其所對應的FileOffset了。
**8、請解釋一下病毒一般如何感染PE文件。**
答:(以下內容選自**《C++黑客編程揭秘與防范》**第4.6節——添加節區)
添加節區在很多場合都會用到,比如在加殼中,在免殺中都會經常使用到對PE文件添加一個節區。添加一個節區的方法有4個步驟,第1個步驟是在節表的最后面添加一個IMAGE_SECTION_HEADER,第2個步驟是更新IMAGE_FILE_HEADER中的NumberOfSections字段,第3步是更新IMAGE_OPTIONAL_HEADER中的SizeOfImage字段,最后一步是添加文件的數據。當然了,前3個步驟是沒有先后順序的,但是最后一個步驟一定要明確如何改變。
(以下內容選自**《C++黑客編程揭秘與防范》**第6.2節——簡單病毒剖析)
大部分病毒都有感染的功能,病毒會把自身當中的或者需要其他程序來完成的指定功能的代碼感染給其他的正常文件。就像人類的流行感冒,辦公室中只要有一個人攜帶感冒病毒,就有可能所有人都會被傳染。如果沒被傳染就說明已經預防過了,因此在機器上安裝殺毒軟件還是非常有必要的。
前面說了,病毒要感染其他文件也就是把病毒本身的攻擊代碼或者病毒期望其他程序要完成的功能代碼寫入到其他程序當中,而想要對其他程序寫入代碼就必須要有寫入代碼的空間。除了把代碼寫入到其他程序中以外,還必須讓這些代碼有機會被執行到。就上面兩個問題而言都是比較容易解決的,下面分別來討論一下。
病毒要對其他程序寫入代碼,必須確定目標程序有足夠的空間讓他把代碼寫入。通常情況下有兩種比較容易實現的方法,第一種在前面的章節介紹過,就是添加一個節區,添加一個節區后就有足夠的空間讓病毒來寫入了。第二種方法是縫隙查找,然后寫入代碼。何為縫隙?在每個節與節之間,必然有沒有使用到的空間,這個空間就叫縫隙。只要確定要寫入代碼的長度,然后根據這個長度來查找是否有滿足該長度的縫隙就可以了。
**9、請說說你對于殼的理解。**
答:(以下內容選自**《加密與解密 第三版》**第12.1.1節——殼的概念)
在自然界中,植物用殼來保護種子,動物用殼來保護身體等。同樣,在一些計算機軟件里面也有一段專門負責保護軟件不被非法修改或反編譯的程序。它們附加在原程序上通過Windows加載器載入內存后,先于原程序執行,得到控制權,執行過程中對原始程序進行解密、還原,還原完成后再把控制權交還給原始程序,執行原來的代碼的部分。加上外殼后,原始程序代碼在磁盤文件中一般是以加密后的形式存在的,只在執行時在內存中還原,這樣就可以比較有效地防止破解者對程序文件的非法修改,同時也可防止程序被靜態反編譯。由于這段程序和自然界的殼在功能上有很多相同的地方,基于命名的規則,就把這樣的程序稱為“殼”了。
……
加殼軟件一般都有良好的操作界面,使用也比較簡單。除了一些商業殼,還有一些個人開發的殼,種類較多。殼對軟件提供了良好保護的同時,也帶來了兼容性問題,選擇一款殼保護軟件后,要在不同硬件和系統上多測試。由于殼能保護自身代碼,因此許多木馬或病毒都喜歡用殼來保護和隱藏自己。對于一些流行的殼,殺毒引擎能對目標軟件脫殼,再進行病毒檢查。而大多數私人殼,殺毒軟件不會專門開發解壓引擎,而是直接把殼當成木馬或病毒處理。
有加殼就一定會有脫殼。一般的脫殼軟件多是專門針對某加殼軟件而編的,雖然針對性強、效果好,但收集麻煩。因此掌握手動脫殼技術十分必要。
**知識擴展:**
(以下內容選自**《加密與解密 第三版》**第13.1.1節——殼的加載過程)
殼和病毒在某些方面比較類似,都需要比原程序代碼更早地獲得控制權。殼修改了原程序的執行文件的組織結構,從而能夠比原程序的代碼提前獲得控制權,并且不會影響原程序的正常運行。這里簡單說說殼的常見加載過程。
(1)保存入口參數
加殼程序初始化時保存各寄存器的值,外殼執行完畢,再恢復各寄存器內容,最后再跳到原程序執行。通常用pushad/popad、pushfd/popfd指令對來保存與恢復現場環境。
(2)獲取殼自己所需要使用的API地址
一般外殼的輸入表中只有GetProcAddress、GetModuleHandle和LoadLibrary這幾個API函數,甚至只有Kernel32.dll以及GetProcAddress。如果需要其他的API函數,則通過LoadLibraryA(W)或LoadLibraryExA(W)將DLL文件映像映射到調用進程的地址空間中,函數返回的HINSTANCE值用于標識文件映像映射到的虛擬內存地址。
LoadLibrary函數的原型如下:
~~~
HINSTANCE LoadLibrary(
LPCTSTR lpLibFileName //DLL文件名的地址
);
//返回值:成功返回模塊的句柄,失敗返回NULL。
~~~
如果DLL文件已被映射到調用進程的地址空間里,可以調用GetModuleHandleA(W)函數獲得DLL模塊句柄。函數的原型如下:
~~~
HMODULE GetModuleHandle(
LPCTSTR lpModuleName //DLL文件名地址
);
~~~
一旦DLL模塊被加載,線程就可以調用GetProcAddress函數獲取輸入函數的地址。函數原型如下:
~~~
FARPROC GetProcAddress(
HMODULE hModule, //DLL模塊句柄
LPCSTR lpProcName //函數名
);
~~~
參數hModule是調用LoadLibrary(Ex)或GetModuleHandle函數的返回值。參數lpProcName可以采用兩種形式:第一種是以0結尾的字符串地址;第二種形式是調用地址的符號的序號(微軟公司非常反對使用序號)。
讀者必須熟練掌握這三個函數的用法,外殼中用到其他函數就是用這三個函數來調用的。現在有些殼,為了提高強度,甚至連系統提供的GetProcAddress函數都不用,而是自己寫個相同功能的函數代替GetProcAddress,以提高函數調用的隱蔽性。
(3)解密原程序的各個區塊數據
殼出于保護原程序代碼和數據的目的,一般都會加密原程序文件的各個區塊。在程序執行時外殼將會對這些區塊數據解密,以讓程序能正常運行。殼一般是按區塊加密的,那么在解密時也按區塊解密,并且把解密的區塊數據按照區塊的定義放在合適的內存位置。
(4)IAT的初始化
IAT的填寫,本來應該由PE裝載器實現。但由于加殼時,自己構建了一個輸入表,并讓PE頭中的輸入表指針指向了自建的輸入表。所以,PE裝載器就將對自建的輸入表進行了填寫。那么原來PE的輸入表的填寫,只好由外殼程序實現了。外殼要做的就是將這個新輸入表結構從頭到尾掃描一遍,對每一個DLL引入的所有函數重新獲取地址,并填寫在IAT表中。
(5)重定位項的處理
文件執行時將被映射到指定的內存地址中,這個初始內存地址稱為基址。當然這只是程序文件中聲明的,程序運行時能夠保證系統一定滿足其要求嗎?
對于EXE的程序文件來說,Windows系統會盡量滿足。例如某EXE文件的基地址為400000h,而運行時Windows系統提供給程序的基地址也同樣是400000h。在這種情況下就不需要進行地址“重定位”了。由于不需要對EXE文件進行“重定位”,所以加殼軟件把原程序文件中用于保存重定位信息的區塊干脆也刪除了,這樣使得加殼后的文件更加小巧。有些工具提供“Wipe Reloc”的功能,其實就是這個作用。
不過對于DLL的動態鏈接庫文件來說,Windows系統沒有辦法保證每一次DLL運行時提供相同的基地址。這樣“重定位”就很重要了,此時殼中也需要提供進行“重定位”的代碼,否則原程序中的代碼是無法正常運行起來的。從這點來說,加殼的DLL比加殼的EXE修正時多了一個重定位表。
(6)HOOK-API
程序文件中的輸入表的作用是讓Windows系統在程序運行時提供API的實際地址給程序使用。在程序的第一行代碼執行之前,Windows系統就完成了這個工作
殼一般都修改了原程序文件的輸入表,然后自己模仿Windows系統的工作來填充輸入表中相關的數據。在填充過程中,外殼就可填充HOOK-API的代碼的地址,這樣就間接地獲得程序的控制權。
(7)跳轉到程序原入口點(OEP)
從這個時候起殼就把控制權交還給原程序了,一般的殼在這里會有明顯的一個“分界線”。當然現在越來越多的加密殼將OEP一段代碼搬到外殼的地址空間里,然后將這段代碼清除掉。這種技術稱為Stolen Bytes。這樣,OEP與外殼間就沒那條明顯的分界線了,增加了脫殼的難度。
(以下內容選自**《加密與解密 第三版》**第16.3.1節——外殼的加載過程)
Windows的PE加載器加載可執行程序時,首先根據輸入表獲取所有API調用的地址,并填寫到IAT中,再重定位所有的重定位項,最后調用WinMain(HINSTANCE hInstance,HINSTANCEhPrevInstance,PSTR szCmdLine,int iCmdShow)執行;如果是DLL,則調用DllMain(HINSTANCE hInstance,DWORDdwReason,LPVOID lpReserved)。加殼后,這個加載過程就由外殼來模擬了。
**本篇文章參考資料:**
1、戚利,**《Windows PE權威指南》**,機械工業出版社。
2、羅云彬,**《Windows環境下32位匯編語言程序設計(典藏版)》**,電子工業出版社。
3、段鋼(主編),Blowfish、沈曉斌、丁益青、單海波、王勇、趙勇、唐植明、softworm、afanty、李江濤、林子深、印豪、馮典、羅翼、林小華、郭春楊(編委),**《加密與解密(第三版)》**,電子工業出版社。
4、冀云,**《C++黑客編程揭秘與防范》**,人民郵電出版社。