PE文件結構詳解(五)延遲導入表&n…

本文轉自: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,來看看定義:

 

[cpp] viewplaincopy
  1. typedef struct ImgDelayDescr {  
  2.     DWORD           grAttrs;        // attributes  
  3.     RVA             rvaDLLName;     // RVA to dll name  
  4.     RVA             rvaHmod;        // RVA of module handle  
  5.     RVA             rvaIAT;         // RVA of the IAT  
  6.     RVA             rvaINT;         // RVA of the INT  
  7.     RVA             rvaBoundIAT;    // RVA of the optional bound IAT  
  8.     RVA             rvaUnloadIAT;   // RVA of optional copy of original IAT  
  9.     DWORD           dwTimeStamp;    // 0 if not bound,  
  10.                                     // O.W. date/time stamp of DLL bound to (Old BIND)  
  11. } ImgDelayDescr, * PImgDelayDescr;  
  12. typedef const ImgDelayDescr *   PCImgDelayDescr;  
grAttrs:用來區分版本,1是新版本,0是舊版本,舊版本中後續的rvaxxxxxx域使用的都是指針,而新版本中都用RVA,我們只討論新版本。

 

rvaDLLName:一個RVA,指向導入DLL的名字。

rvaHmod:一個RVA,指向導入DLL的模塊基地址,這個基地址在DLL真正被導入前是NULL,導入後纔是實際的基地址。

rvaIAT:一個RVA,表示導入函數表,實際上指向IAT,在DLL加載前,IAT裏存放的是一小段代碼的地址,加載後纔是真正的導入函數地址。

rvaINT:一個RVA,指向導入函數的名字表。

rvaUnloadIAT:延遲導入函數卸載表。

dwTimeStamp:延遲導入DLL的時間戳。

定義知道了,那他是怎麼被處理的呢?前面提到了,在延遲導入函數指向的IAT裏,默認保存的是一段代碼的地址,當程序第一次調用到這個延遲導入函數時,流程會走到那段代碼,這段代碼用來幹什麼呢?請看一個真實的延遲導入函數的例子:

 

[cpp] viewplaincopy
  1. .text:75C7A363 __imp_load__InternetConnectA@32:        ; InternetConnectA(x,x,x,x,x,x,x,x)  
  2. .text:75C7A363                 mov     eax, offset __imp__InternetConnectA@32  
  3. .text:75C7A368                 jmp     __tailMerge_WININET  

這段代碼其實只有兩行彙編,第一行把導入函數IAT項的地址放到eax中,然後用一個jmp跳轉走,那麼他跳轉到哪裏了呢?我們繼續跟蹤:

 

 

[cpp] viewplaincopy
  1. __tailMerge_WININET proc near             
  2. .text:75C6BEF0                 push    ecx  
  3. .text:75C6BEF1                 push    edx  
  4. .text:75C6BEF2                 push    eax  
  5. .text:75C6BEF3                 push    offset __DELAY_IMPORT_DESCRIPTOR_WININET  
  6. .text:75C6BEF8                 call    __delayLoadHelper  
  7. .text:75C6BEFD                 pop     edx  
  8. .text:75C6BEFE                 pop     ecx  
  9. .text:75C6BEFF                 jmp     eax  
  10. .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了。

延遲導入一次只會導入一個函數,而不是一次導入整個模塊的所有函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章