本文轉自:http://blog.csdn.net/evileagle/article/details/12718845 感謝作者的原創。
PE文件結構詳解(四)PE導入表講了一般的PE導入表,這次我們來看一下另外一種導入表:延遲導入(DelayImport)。看名字就知道,這種導入機制導入其他DLL的時機比較“遲”,爲什麼要遲呢?因爲有些導入函數可能使用的頻率比較低,或者在某些特定的場合纔會用到,而有些函數可能要在程序運行一段時間後纔會用到,這些函數可以等到他實際使用的時候再去加載對應的DLL,而沒必要再程序一裝載就初始化好。
這個機制聽起來很誘人,因爲他可以加快啓動速度,我們應該如何利用這項機制呢?VC有一個選項,可以讓我們很方便的使用到這項特性,如下圖所示:
在這一項後面填寫需要延遲導入的DLL名稱,連接器就會自動幫我們將這些DLL的導入變爲延遲導入。
現在我們知道如何使用延遲導入了,那這個看上去很厲害的機制是如何實現的呢?接下來我們來探索一番。在IMAGE_DATA_DIRECTORY中,有一項爲IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT,這一項便延遲導入表,IMAGE_DATA_DIRECTORY.VirtualAddress就指向延遲導入表的起始地址。既然是表,肯定又是一個數組,每一項都是一個ImgDelayDescr結構體,和導入表一樣,每一項都代表一個導入的DLL,來看看定義:
- typedef struct ImgDelayDescr {
- DWORD grAttrs; // attributes
- RVA rvaDLLName; // RVA to dll name
- RVA rvaHmod; // RVA of module handle
- RVA rvaIAT; // RVA of the IAT
- RVA rvaINT; // RVA of the INT
- RVA rvaBoundIAT; // RVA of the optional bound IAT
- RVA rvaUnloadIAT; // RVA of optional copy of original IAT
- DWORD dwTimeStamp; // 0 if not bound,
- // O.W. date/time stamp of DLL bound to (Old BIND)
- } ImgDelayDescr, * PImgDelayDescr;
- typedef const ImgDelayDescr * PCImgDelayDescr;
rvaDLLName:一個RVA,指向導入DLL的名字。
rvaHmod:一個RVA,指向導入DLL的模塊基地址,這個基地址在DLL真正被導入前是NULL,導入後纔是實際的基地址。
rvaIAT:一個RVA,表示導入函數表,實際上指向IAT,在DLL加載前,IAT裏存放的是一小段代碼的地址,加載後纔是真正的導入函數地址。
rvaINT:一個RVA,指向導入函數的名字表。
rvaUnloadIAT:延遲導入函數卸載表。
dwTimeStamp:延遲導入DLL的時間戳。
定義知道了,那他是怎麼被處理的呢?前面提到了,在延遲導入函數指向的IAT裏,默認保存的是一段代碼的地址,當程序第一次調用到這個延遲導入函數時,流程會走到那段代碼,這段代碼用來幹什麼呢?請看一個真實的延遲導入函數的例子:
- .text:75C7A363 __imp_load__InternetConnectA@32: ; InternetConnectA(x,x,x,x,x,x,x,x)
- .text:75C7A363 mov eax, offset __imp__InternetConnectA@32
- .text:75C7A368 jmp __tailMerge_WININET
這段代碼其實只有兩行彙編,第一行把導入函數IAT項的地址放到eax中,然後用一個jmp跳轉走,那麼他跳轉到哪裏了呢?我們繼續跟蹤:
- __tailMerge_WININET proc near
- .text:75C6BEF0 push ecx
- .text:75C6BEF1 push edx
- .text:75C6BEF2 push eax
- .text:75C6BEF3 push offset __DELAY_IMPORT_DESCRIPTOR_WININET
- .text:75C6BEF8 call __delayLoadHelper
- .text:75C6BEFD pop edx
- .text:75C6BEFE pop ecx
- .text:75C6BEFF jmp eax
- .text:75C6BEFF __tailMerge_WININET endp
其中最重要的是push了一個__DELAY_IMPORT_DESCRIPTOR_WININET,這個就是上文中看到的ImgDelayDescr結構,他的DLL名字是wininet.dll。之後,CALL了一個__delayLoadHelper,在這個函數裏,執行了加載DLL,查找導出函數,填充導入表等一系列操作,函數結束時IAT中已經是真正的導入函數的地址,這個函數同時返回了導入函數的地址,因此之後的eax裏保存的就是函數地址,最後的jmpeax就跳轉到了真實的導入函數中。
這個過程很完美,也很靈巧,但是如果仔細觀察就會發現什麼地方有點不對勁,你發現了嗎?__delayLoadHelper的參數中只有IAT項的偏移和整個模塊的延遲導入描述__DELAY_IMPORT_DESCRIPTOR_WININET,但是參數中並沒有要導入函數的名字。也許你說,名字在__DELAY_IMPORT_DESCRIPTOR_WININET的名字表中,是的,那裏確實有名字,但是別忘了,那是個表,裏面存的是所有要從該模塊導入的函數名字,而不是“當前”這個被調用函數的函數名。或許你覺得參數中應該有個索引號,用來表示名字列表中的第幾項是即將被導入的那個函數的名字,不幸的是我們也沒有看到參數中有這樣的信息存在,那Windows執行到這裏是如何得到名字的呢?MS在這裏使用了一個巧妙的辦法:__DELAY_IMPORT_DESCRIPTOR_WININET中有一項是rvaIAT,前面提到了,這裏實際上就是指向了IAT,而且是該模塊第一個導入函數的IAT的偏移,現在我們有兩個偏移,即將導入的函數IAT項的偏移(記作RVA1)和要導入模塊第一個函數IAT項的偏移(記作RVA0),(RVA1-RVA0)/4=導入函數IAT項在rvaIAT中的下標,rvaINT中的名字順序與rvaIAT中的順序是相同的,所以下標也相同,這樣就能獲取到導入函數的名字了。有了模塊名和函數名,用GetProcAddress就可以獲取到導入函數的地址了。
上述流程用一張圖來總結一下:
最後還有兩點要提醒大家:
延遲導入的加載只發生在函數第一次被調用的時候,之後IAT就填充爲正確函數地址,不會再走__delayLoadHelper了。
延遲導入一次只會導入一個函數,而不是一次導入整個模塊的所有函數。