PE文件導入表

   在上一篇文章裏,我使用一個 TreeList 控件,展示了 PE 文件的內容。在那裏可充分了解PE的文件頭的信息,但是對 section(備註:常見譯文爲節,段,塊)的一些信息我們還沒有涉及。比如全局變量等數據,代碼,資源,導入表等信息都位於相應的 section 中,有些 section 通常具有特定的名字,例如資源通常位於 .rsrc,代碼通常位於 .text,導入表通常位於 .idata 段,等等。文本講述的是把一個PE文件的導入表打印出來。我注意到 MS 提供了一個比較有用的函數,ImageRvaToVa,我們稍後主要藉助這個函數去從RVA定位我們的目標數據。

 

    在開始之前,我們先做一些概念的定義和說明。

    Image: PE格式鏡像文件,這通常就是我們的exe,dll文件。

 

    下面我們定義一些地址相關的概念,因爲PE文件位於磁盤上,同時文件又可以被映射到虛擬內存中,在運行PE文件時它也被系統的Loader加載到內存中。所以這裏就有了三個空間,如果我們不做一個清楚的說明,在後面我們很容易混淆。

 

    (1)磁盤空間:這裏我們使用的地址叫做文件地址(距離文件頭部的偏移)。在PE頭的相關屬性名稱中,文件中的數據稱爲原始數據 (rawData),文件中的數據使用的對齊稱爲 FileAlignment。

 

    (2)虛擬內存空間:在這裏的地址稱爲虛擬地址(Virtual Address)。同時PE文件的數據裝載/映射到內存中後又分以下兩種情況:

 

      (a)PE文件的內存視圖,即PE文件被映射到內存(MapViewOfFile):

      BaseAddress:內存映射文件的基礎地址,從這裏看過去,就和從編輯器打開看到的文件內容完全一致。

      內存映射通常是處理大文件的一種有效方式。映射後我們在內存中看到的內容就是磁盤文件的一個視圖。

      因此我們吧PE文件映射到內存以後,通過某個數據的RVA,調用 ImageRvaToVa 可得到某個數據的VA,再減去映射文件的起始地址,就是文件地址。

 

      (b) 進程空間,即在被調度之前,被loader裝載到內存的時刻(例如雙擊執行一個exe)。

      這裏和前者的視圖方式不同,屬於一種地址映射關係,文件中的節內容根據NT文件頭的信息被加載到進程空間的相應位置。

      ImageBase:映射到進程空間的基礎地址。

      RVA:相對ImageOfBase的偏移。它加上ImageBase就是進程空間的VA。

      在PE文件中的DataEntry,Section表中的VirutalAddress基本都屬於RVA。

 

    下面介紹以下這個函數:ImageRvaToVa:(注意這個函數要求XP和win2000系統以上,在VC6自帶的SDK中沒有。。。)

    PVOID ImageRvaToVa(
        PIMAGE_NT_HEADERS NtHeaders,
        PVOID Base,
        ULONG Rva,
        PIMAGE_SECTION_HEADER* LastRvaSection
      );

 

    這個函數在(2).(a)即內存映射文件後使用,它把RVA根據NT頭的信息,換算成內存映射文件中的實際VA。看起來不是在進程空間使用 的,因爲在進程空間中,ImageBase + Rva 就是VA了,裝載後NT頭等信息也不再重要了。最後一個參數是可選的,或許是因爲如果調用方主動提供,此 API 可提高一定效率(可以直接在節中的地址信息去判斷)。第二個參數也可以提供一個假的地址給這個函數,這個函數也能計算。即這個函數不去校驗Base是否是 一個有效地址,因此實際上我們可以讀取出NtHeader的信息後,用一個假地址傳給這個函數,再把結果減去這個假地址,即可換算出文件地址。

 

    好了,下面介紹下導入表的定位,這方面的資料和文章在看雪論壇的文集裏面有很多。我再這裏只做一個比較簡潔的介紹。導入表本質上就是位於某 個節中的一些數據,這些數據主要是一些C字符串(以0字符爲結尾的dll和函數名稱)以及一些指針(RVA地址),所以我們主要是需要了解如何定位到導入 表,從而打印出導入表的信息。

    首先導入表的RVA地址,就在optional Header的DataDirectory的第二個元素中。通過它我們定位到導入表。

    導入表類似一個二級索引。一級是一個模塊目錄(IMAGE_IMPORT_DESCRIPTOR數組,這裏把目錄理解爲一個以全0字節爲結 束的數組),它的每個元素代表了一個DLL。二級是導入地址表IAT(即 IMAGE_THUNK_DATA 數組,一個指針數組),每個元素指向一個 IMAGE_IMPORT_BY_NAME結構(該結構含有一個函數序號和一個函數名稱字符串)。

    總結一下,我們的定位過程:

    (1)通過 NtHeaders.OptionalHeader.DataDirectory[1].VirtualBase --> 定位到導入表(IID Table)。

    (2)遍歷每個 IID,直到遇到全0爲止。

        通過 IID.Name -> 定位到 DLL 名字。

        通過 IID.OriginalFirstThunk 或者 FirstThunk -> 定位到IAT ( image_thunk_data32[] );

          遍歷指針目錄,知道遇到NULL爲止。

            通過 thunk_data.AddressOfData -> 定位到一個 IMAGE_IMPORT_BY_NAME 的地址,再根據它尋址到真正的函數序號和函數名稱。

 

    爲什麼存在二級指針呢,這是很容易解釋的。所以我們需要一個DLL目錄,由於DLL名稱的長度和函數個數不固定,所以向下擴展了一級,而每個函數的函數名稱又是不固定的,所以又要向下擴展一級,這樣要找到真正的函數名稱必須經過這樣兩級定向。

 

    因此這個二級索引的導入表就是這樣的定位方式(每個數組都是一個高地址方向半開口的樣子, C字符串也是這樣的字符數組),如下圖所示(注意每個數組的元素的size是固定的,但由於數組是半開口,所以數組本身屬於size不固定):

 

    

 

    特點就是,每次遇到長度無法預測的成員,就用指針把它從元素中擴展出去(用一個指針指向它),這樣我們就保證每個數組的元素都是固定的 size,這樣它才能成爲線性表結構(滿足用指針的加減或者[]操作符進行元素讀取)。例如DLL的名稱是可變長度的,因此它被扔到元素定義的外面去,在 元素中保留爲一個指針。每個DLL的函數目錄也是可變長度的(函數個數是不確定的),因此它在元素中也是一個指針。而函數目錄中函數信息又被扔出去,用指 針指向它。

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