1. 程式人生 > 其它 >羽夏殼世界—— PE 結構(下)

羽夏殼世界—— PE 結構(下)

寫在前面

  此係列是本人一個字一個字碼出來的,包括程式碼實現和效果截圖。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏殼世界——序 ,方便學習本教程。

概述

  匯入表和重定位表是一個比較複雜的結構,匯入表相對複雜,當然還有更復雜的資源表,不過這裡並不會介紹它。
  匯入表是用來做什麼的?比如你在寫程式碼的時候,總會呼叫一些WinAPI,而這些函式都是通過DLL匯出的。匯入表的作用就是告訴作業系統載入該PE

檔案的時候需要找哪些DLL,使用了哪些DLL所提供的函式。
  重定位表可能稍微難理解一些,不過如果學習上篇應該就不太難了。我們在介紹ImageBase這個成員的時候,曾說它是PE檔案傾向於要載入的地址,但是如果開了基址隨機或者是DLL的話通常不會使用該地址,而這個成員就是用來進行重定位的,我們可以看看為什麼需要重定位:

  如果執行上述程式碼,這個push是所謂的死地址,也就是要puts的字串,如果載入的基址並不是我們所謂的ImageBase,而這個是作為硬編碼的一部分的,如果不改變的話,這地址是錯誤的,如果不進行重定位,就會導致列印的字串不對甚至報0xC0000005錯誤,這個就是重定位的意義,我們來看示意圖:

  下面我們來介紹匯入表和重定位表的結構:

匯入表

  在介紹匯入表之前,我們先放個示意圖:

  匯入表相關資訊是放到IMAGE_IMPORT_DESCRIPTOR結構體中的,它的結構如下:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  //時間戳
    DWORD   ForwarderChain;                 //不使用
    DWORD   Name;                           //指向Ascii字串
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

  這結構體一個挨著一個,以空結構體為結尾(內容全為0的IMAGE_IMPORT_DESCRIPTOR)。用一句話概括就是一個IMAGE_IMPORT_DESCRIPTOR不定長陣列,通過最後一個是空表示結束。
  OriginalFirstThunk包含指向輸入名稱表INTRVAINT是一個IMAGE_THUNK_DATA結構的陣列,陣列中的每個IMAGE_THUNK_DATA結構都指向IMAGE_IMPORT_BY_NAME結構,陣列以一個內容為0的IMAGE_THUNK_DATA結構結束。
  TimeDateStamp是一個32位的時間標誌,可以忽略。
  ForwarderChain是第1個被轉向的API的索引,一般為0,在程式引用一個DLL中的API,而這個API又在引用其他DLLAPI時使用,但這樣的情況很少出現。
  Name是DLL名字的指標。它是一個以\0結尾的ASCII字元的RVA地址,該字串包含輸入的DLL名,例如KERNEL32.DLL
  FirstThunk包含指向輸入地址表IATRVAIAT是一個IMAGE_THUNK_DATA結構的陣列。
  OriginalFirstThunkFirstThunk指向IMAGE_THUNK_DATA陣列結構,結構是相似的,它的結構體如下所示:

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
typedef struct _IMAGE_THUNK_DATA64 {
    union {
        ULONGLONG ForwarderString;  // PBYTE 
        ULONGLONG Function;         // PDWORD
        ULONGLONG Ordinal;
        ULONGLONG AddressOfData;    // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA64;
typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64;

  上面的結構體32位和64位的區別不大,就是成員大小的問題。它是一個共用體的結構體,這個比較複雜,在不同的時刻具有不同的含義。
  當IMAGE_THUNK_DATA值的最高位為1時,表示函式以序號方式輸人,這時低31位,或者一個64位可執行檔案的低63位,被看成一個函式序號。當雙字的最高位為0時,表示函式以字串型別的函式名方式輸入,這時的值是一個RVA,指向一個IMACE_IMPORT_BY_NAME結構。
  下面我們繼續介紹IMAGE_IMPORT_BY_NAME,它的結構如下:

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    CHAR   Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

  Hint是本函式在其所駐留DLL的輸出表中的序號。該域被PE裝載器用來在DLL的輸出表裡快速查詢函式。該值不是必需的,一些連結器將它設為·。
  Name含有輸入函式的函式名。函式名是一個ASCII字串,以'\0'結尾。注意,這裡雖然將Name的大小以位元組為單位進行定義,但其實它是一個可變尺寸域,是不定長的。
  為了更好的理解,我們來看一個OriginalFirstThunk指向的結構示意,FirstThunk也是一樣的。

  那麼IATINT到底有啥區別,我們來看個圖:

PE載入前

PE載入後

  可以看出,當PE檔案在磁碟的時候,這兩個儲存的東西是一模一樣的,但是被載入後,IAT變為了地址表,而INT被廢棄掉,不再使用。
  由於匯入表的結構十分複雜,可能你看第一遍該博文的時候可能會犯糊塗,建議使用自己熟悉的程式語言手動解析PE的匯入表,當你能夠比較輕鬆的解析它的時候,你就會明白匯入表的設計。
  下面我們來看一下在二進位制檔案中相應的內容:

重定位表

  重定位表的結構相對比較簡單,在學習之前請看下面的示意圖:

  匯入表也是一個不定長陣列,用與匯入表相同的方式表示結束位置,每一個成員開頭描述都是一個IMAGE_BASE_RELOCATION結構:

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

  VirtualAddress是指這組重定位資料的開始RVA地址。各重定位項的地址加這個值才是該重定位項的完整RVA地址。
  SizeOFBlock是當前重定位結構的大小。因為VirtualAddressSizeOfBlock的大小都是固定的4位元組,所以這個值減8就是TypeOffset陣列的大小。
  TypeOffset是一個數組。陣列每項大小為2位元組,共16位。這16位分為高4位和低12位。高4位代表重定位型別,低12位是重定位地址,它與VirtualAddress相加就是指向PE映像中需要修改的地址資料的指標。
  對於常見的重定位型別,如下所示:

型別 含義
IMAGE_REL_BASED_ABSOLUTE 沒有具體含義,只是為了讓每個段4位元組對齊
IMACE_REL_BASED_HIGHLOW 重定位指向的整個地址都需要修正,實際上大部分情況下都是這樣的
IMAGE_REL_BASED_DIR64 出現在64位 PE 檔案中,對指向的整個地址進行修正

  基址重定位資料採用類似按頁分割的方法組織,是由許多重定位塊串接成的,每個塊中存放4KB的重定位資訊,每個重定位資料塊的大小必須以4位元組對齊。
  下面我們來看一下一個64位程式的重定位表:

  在二進位制檔案的位置和內容:

地址轉化

  如果想要通過VA得到RVA,這個十分簡單:記憶體地址 – ImageBase
  如果我們向通過RVA得到FOA,那麼怎麼樣呢?首先我們得判斷RVA是否位於PE頭中,如果是FOA == RVA,因為此時並沒有進行記憶體展開。如果RVA不在的話,我們就得判斷RVA位於哪個節。若RVA >= 節.VirtualAddress && RVA <= 節.VirtualAddress +當前節記憶體對齊後的大小,那麼差值 = RVA - 節.VirtualAddress,再加上相應的該節在檔案中的FOA,就可以得到了真正的FOA
  如何通過FOA得到RVA呢?這裡我就不多說了,原理是一樣的,只是判斷的東西不太一樣,正確答案將會在實現篇進行揭曉。

下一篇

  羽夏殼世界——基礎篇小結