iT邦幫忙

0

231解剖室:PE File Format - Part 3 軀幹

  • 分享至 

  • xImage
  •  

Data Directories & Sections


◀️ Previously

上一篇我們拆解了NT Headers的架構
了解他分成三個部分(Signature / File Header / Optional Header)
還有分別在硬碟和記憶體放置資料時的Alignment


📜 Overview

在這篇文章中,會先提到Optional Header的最後一個成員Data Directories
再來會講Data Directories和Section Headers還有Section之間的關係
最後偷偷講Export Directory來當例子


📋 Text

Data Directories

Data Directories是Optional Header的最後一個成員
因為他所提供的資訊跟PE檔的中後段比較有關,所以沒有在頭部的時候講
主要儲存的是各Data Directory的位置(entry)跟他的大小
底下是他的structure (defined in winnt.h)

typedef struct _IMAGE_DATA_DIRECTORY {
  DWORD VirtualAddress;
  DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

整個Data Directories總共有16個entries
從PE Bear可以看到全部的Directories

https://ithelp.ithome.com.tw/upload/images/20230127/20156936lQ1WdmVgO0.png

而順著VirtualAddress找到的各個Directory的Structure不會一樣

舉Debug Directory跟Export Directory為例

https://ithelp.ithome.com.tw/upload/images/20230127/20156936THmnSqR26E.png

每個Directory也會根據其性質被放在對應的Section

Sections

檔案中的資料(除了Header)都會放在Section裡面

以下是Section的列表

  • .text -- 放程式碼的地方
  • .data -- 已初始化資料
  • .bss --- 未初始化資料
  • .rdata - 唯獨資料
  • .idata - 引入函式資料
  • .edata - 引出函式資料
  • .reloc - 重定位資料
  • .rsrc -- 圖片、icon等資料
  • .tls -- 執行續的儲存區

有些Section會被併入其他的Section裡面,下面kernel32.dll就是一個例子

https://ithelp.ithome.com.tw/upload/images/20230128/20156936GJL9TDZAxp.png

在Section Headers找不到.edata區段
且Export Directory(0x97710 ~ 0xA5554)被包在.rdata區段(0x7D800 ~ 0xB0600)裡面
可以知道放引出函式的資料(.edata)被併入.rdata區段了

事實上,這些Section的名稱並不重要
Section的類別主要是被區段內存放的資料所定義

Section Headers

放置各個Section的Raw Address、Virtual Address、權限資訊(Characteristics)
還有Relocation和Debug相關(LineNumber)的資料

下面是Section Headers的structure

typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;
  } Misc;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD  NumberOfRelocations;
  WORD  NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
  • Name : section的名稱
  • Misc : section讀取進記憶體的大小
  • VirtualAddr : section的RVA
  • SizeOfRawData : section在磁碟上的大小
  • PointerToRawData : 指向section的指標
  • PointerToRelocations : 指向section relocation entry的指標
  • PointerToLinenumbers : 指向COFF line number entries的指標
  • NumberOfLinenumbers : relocation entries的數量
  • NumberOfLinenumbers : COFF line number entries的數量
  • Characteristics : 權限等額外資訊

如果Section內所存的值是固定的,則SizeOfRawData >= VirtualSize
因為SizeOfRawData必須要是FileAlignment的倍數,而VirtualSize則否

如果Section內所存的值是會變動的(.bss),則VirtualSize >= SizeOfRawData
在程式執行時,這些數值會被assign
因此在記憶體上的大小(VirtualSize)就會大於在磁碟上的大小(SizeOfRawData)

PointerToLinenumbers和NumberOfLinenumbers儲存的都是跟Debug相關的資訊
如果PE檔的Debug資訊有被strip掉,則這兩個欄位的值為 0

Export Directory

舉Export Directory來當例子看看
這個Directory存放的是在動態連結時引出函式的相關資訊

底下是他的structure

typedef struct _IMAGE_EXPORT_DIRECTORY {
	DWORD	Characteristics;
	DWORD	TimeDateStamp;
	WORD	MajorVersion;
	WORD	MinorVersion;
	DWORD	Name;
	DWORD	Base;
	DWORD	NumberOfFunctions;
	DWORD	NumberOfNames;
	DWORD	AddressOfFunctions;
	DWORD	AddressOfNames;
	DWORD	AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

這邊簡單講一下比較重要的member

  • NumberOfFunctions : 引出Function的數量 (by name + by ordinal)
  • NumberOfNames : 引出Function的數量 (by name)
  • AddressOfFunctions : 存放Function位址的位址
  • AddressOfNames : 存放Function名字的位址
  • Base : Ordinal的偏移量
  • AddressOfNamesOrdinals : Ordinal的位址

補充資訊:

  • 可以用名稱和Ordinal兩種方式引出,用名稱引出在執行上比較容易
  • Ordinal就是引出Function的序號,大多從 1 開始
  • 每個DLL只會有一個export directory
  • forwarded export是一種重新導向的引入方式,主要是在告訴loader某些function不在這個DLL裡面,並提供DLL的名字讓loader去找
  • Stuxnet蠕蟲利用forwarded export的技巧來達成DLL hijacking,這邊附上相關文章

小實驗環節

在這個部分,我們會嘗試實作出PE Bear在Export Directory底下的資訊欄

https://ithelp.ithome.com.tw/upload/images/20230128/20156936jooGzAcgXd.png

附上layout當作參考
https://ithelp.ithome.com.tw/upload/images/20230128/201569367DsGXLBK7Q.jpg

詳細的步驟:

  • 找到Optional Header (from NT Headers)
  • 找到Export Descriptor (from DataDiretory in Optional Header)
  • 得出Section總數,並找出Export Descriptor是在哪個Section裡面
  • 找到Export Directory (calculate via RVA & section)
  • 算出各個數值
  • 直接print出來
// 找到Optional Header (from NT Headers)
IMAGE_DOS_HEADER *dosHeader = (IMAGE_DOS_HEADER *)fbuf;
IMAGE_NT_HEADERS *ntHeaders = (IMAGE_NT_HEADERS *)((size_t)dosHeader + dosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER optHeader = &ntHeaders->OptionalHeader;
// 找到Export Descriptor (from DataDiretory in Optional Header)
IMAGE_DATA_DIRECTORY exportDescriptor = optHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
// 得出Section總數,並找出Export Descriptor是在哪個Section裡面
PIMAGE_SECTION_HEADER sectionHeader = (PIMAGE_SECTION_HEADER)(((PBYTE)optHeader) + ntHeaders->FileHeader.SizeOfOptionalHeader);
WORD numberOfSections = ntHeaders->FileHeader.NumberOfSections;

int i;
DWORD VirtualAddr, VirtualSize;
for (i = 0; i < numberOfSections; i++) {
        VirtualAddr = sectionHeader->VirtualAddress;
        VirtualSize = sectionHeader->Misc.VirtualSize;

        if (VirtualAddr <= exportDescriptor.VirtualAddress &&
            exportDescriptor.VirtualAddress < VirtualAddr + VirtualSize) {
                break;
        }
        sectionHeader = (PIMAGE_SECTION_HEADER)(((PBYTE)sectionHeader) + sizeof(IMAGE_SECTION_HEADER));
}

RVA轉File Offset
轉換步驟:

  1. 找到directory所在的section
  2. 用RVA減去section的VirtualAddress
  3. 加上section offset

總而言之就是
File Offset = RVA - section在記憶體上的開始位置 + section在硬碟上的開始位置

// RVA轉成File Offset
DWORD RVA2FileOffset(DWORD RVA, PIMAGE_SECTION_HEADER sectionHeader)
{
        return RVA - sectionHeader->VirtualAddress + sectionHeader->PointerToRawData;
}

算出各個數值
https://ithelp.ithome.com.tw/upload/images/20230128/20156936mQcgiKiB5n.png

找forwarder的方式
forwarder會緊接在function name後面
如果發現下一個兩個function name之間的大小 != function name長度 + 1
就可以知道中間有插一個forwarder

https://ithelp.ithome.com.tw/upload/images/20230128/201569363BFzDdKaoW.png

以上面這個例子:

  • 兩個function name之間的大小 : 0x9CF34 - 0x9CEFB = 57
  • function name長度 : len("AcquireSRWLockExclusive") == 23
  • 57 != 23 + 1 --> 中間有forwarder
// 列出全部的引出函式
for (i = 0; i < exportDirectory->NumberOfFunctions; i++) {

        int addrDiff;
        char *functionName, *forwarderName;

        functionName = (char *)((size_t)dosHeader + RVA2FileOffset(*(AddressOfNames + i), sectionHeader));

        if (i < exportDirectory->NumberOfFunctions - 1) {
                addrDiff = *(AddressOfNames + i + 1) - *(AddressOfNames + i);
                if (addrDiff != strlen((char *)functionName) + 1) {
                        forwarderName = functionName + strlen(functionName) + 1;
                } else {
                        forwarderName = NULL;
                }
        } else {
                addrDiff = strlen((char *)functionName);
        }
        
        printf("%08x%8s%8x%8s%012x%8s%08x%8s%-50s%-50s\n",
        RVA2FileOffset(exportDirectory->AddressOfFunctions + i * 4, sectionHeader), " ",
        *(AddressOfOrdinals + i) + exportDirectory->Base, " ",
        *(AddressOfFunctions + i), " ",
        *(AddressOfNames + i), " ",
        functionName,
        forwarderName == NULL ? " " : (char *)forwarderName);
}

完成後dump出kernel32.dll的export directory,結果如下
https://ithelp.ithome.com.tw/upload/images/20230128/20156936WFOvBBeyqH.png

原本以為可以收工,結果試另外一個Imagehlp.dll就烙賽了 = =
https://ithelp.ithome.com.tw/upload/images/20230128/20156936bC8oShWpaz.png

檢查了一下發現他的Ordinal跟Name RVA不是照順序排下來的,哭了
https://ithelp.ithome.com.tw/upload/images/20230128/20156936J5vsU44BDa.png

所以修正過後的步驟是:

  • 找到Optional Header (from NT Headers)
  • 找到Export Descriptor (from DataDiretory in Optional Header)
  • 得出Section總數,並找出Export Descriptor是在哪個Section裡面
  • 找到Export Directory (calculate via RVA & section)
  • 算出各個數值
  • 把各數值存到一個struct裡面
  • 算出forwarder
  • 然後這邊才能print出來,吐血
// 用來存數值的struct
// 其實functionNameAddr不用assign成指標
struct _exportTable {
        DWORD offset;
        WORD ordinal;
        DWORD *functionPtr;
        DWORD *functionNameAddr;
        char *functionName;
        char *forwarderName;
};
// 挖空間給struct
struct _exportTable **exportTable =
    malloc(sizeof(struct _exportTable *) * exportDirectory->NumberOfFunctions);
for (i = 0; i < exportDirectory->NumberOfFunctions; i++)
    exportTable[i] = malloc(sizeof(struct _exportTable));
// print出來的function
void printExportTable(struct _exportTable **exportTable, size_t len)
{
        printf("Offset%10sOrdinals%8sFunction RVA%8sName RVA%8s%-50s%-50s\n",
                " ", " ", " ", " ", "Name", "Forwarder");

        for (int i = 0; i < len; i++) {
                printf("%08x%8s%8x%8s%012x%8s%08x%8s%-50s%-50s\n",
                exportTable[i]->offset, " ",
                exportTable[i]->ordinal, " ",
                *(exportTable[i]->functionPtr), " ",
                *(exportTable[i]->functionNameAddr), " ",
                exportTable[i]->functionName,
                exportTable[i]->forwarderName);
        }

        return;
}

改完之後就照順序排下來了

https://ithelp.ithome.com.tw/upload/images/20230128/20156936ZbG64al04C.png

source code (successfully built on Windows11 w/ gcc 11.2.0)


🦛 Conclusion

這篇我們介紹了Data Directories (AKA 各個Directory的Entry跟Size的Container)
還提到各種Section和用來存Section開始位置的Section Headers
最後寫了一個小程式來重現PE Bear的export functions欄位

下一篇我們會講到Import Dir / Resource Dir / Base Relocation Table


References

0xrick's PE file format
https://0xrick.github.io/win-internals/pe5/
https://0xrick.github.io/win-internals/pe6/

Official Documentation
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_section_header

Sections
https://stackoverflow.com/questions/19012300/whats-the-difference-between-rdata-and-idata-segments

RVA to File Offset
https://stackoverflow.com/questions/9955744/getting-offset-in-file-from-rva

Stuxnet
https://css.csail.mit.edu/6.858/2014/readings/stuxnet.pdf
https://nixhacker.com/fixing-dll-exports-for-dll-hijacking/


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言