PE結構-導出表

在上一篇博客中說了導入表,所謂的導入表,其實相當於記錄程序所依賴的函數庫信息,類似於你要調用外部函數,總得記錄下這個函數在哪個庫中,名字或者序號是什麼。有了這些信息後,我們就可以LoadLibrary和GetProcAddress獲取函數地址了

那麼,操作系統是如何來獲取函數地址呢,也就是GetProcAddress的實現,這裏就涉及到了導出表。導出表,會記錄這個庫函數的地址是多少,所以簡單來說GetProcAddress就是查導出表來獲取地址,如何查就是下面的話題了。

導出意味着需要提供API給他人使用,一般來說會是一些DLL之類的,所以,我們先來寫一個DLL,再來分析其PE格式

.386
.model flat, stdcall  ;32 bit memory model
option casemap :none  ;case sensitive

include windows.inc
include user32.inc
includelib user32.lib

public g_nTest
.data
	g_nTest dd 87654321h
	
.code

ShowMsg proc szText:LPSTR,szTitle:LPSTR
	invoke MessageBox,NULL,szText,szTitle,MB_OK
	ret
ShowMsg endp

MySub proc x:UINT,y:UINT
	mov eax,x
	sub eax,y
MySub endp

MyAdd proc x:UINT,y:UINT
	mov eax,x
	add eax,y
MyAdd endp

DllMain proc hinstDLL:HINSTANCE,fdwReason:DWORD,lpvReserved:LPVOID
	mov eax,TRUE
	ret
DllMain endp	
end DllMain

def文件描述

EXPORTS
	ShowMsg @11
	MySub @5 noname
	MyAdd @7
	g_nTest @8

這裏我們先用Depends來觀察一下導出情況

其中序號爲5,7,8,9的就是我們自己導出的,那麼剩餘的是什麼,下面就可以通過導入表來解釋了

首先先來說一下如何定位到導入表,導入表位於數據目錄的第一項

2060這裏轉FA的話就是660,所以文件地址從660開始,大小爲77,這一整塊是導入表信息的總大小,這裏的總大小是包含所有信息的。

OK,下面先來看一下導入表結構,注意上面的0x77是全部信息的大小,而單純的導入表結構只佔40字節,也就是兩行半

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name; //動態庫名 用於說明性信息
    DWORD   Base; //導出序號中的最小值
    DWORD   NumberOfFunctions; //多少項導出函數
    DWORD   NumberOfNames;     //多少個根據名字導出的函數
    DWORD   AddressOfFunctions;     // 指向導出函數地址數組,數組大小由NumberOfFunctions確定
    DWORD   AddressOfNames;         // 指向名字導出數組,記錄函數名,數組由NumberOfNames確定
    DWORD   AddressOfNameOrdinals;  // 指向名稱對應序號數組,大小由NumberOfNames確定
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

前四個字段我們略過,因爲可以填寫任意值,沒有太大意義,開始討論後面的字段意義

Name字段,說明性信息,一般情況下會默認源碼文件名

OK,下面幾個字段相對會有那麼點繞,總的來說,站在GetProcAddress的角度去思考就會清爽很多

Base 全部導出函數中序號的最小值,首先來思考一下這個字段的意義?

首先我們先來想一下根據序號來找到函數地址,那麼此時肯定會有一張函數地址表(AddressOfFunctions),如何查找效率高?毫無疑問,直接將序號當成數組的下標來尋址,這樣找到函數地址的效率是最高的,

那麼問題來了,拿我們的導出例子來說,最小的序號爲5,數組下標來尋址意味着需要使得0~4項都是空的,所以Base字段的意義就來了,下標平移,節省空間。也就是說序號減去Base就是數組下標的位置,那麼序號5就會放在第0項,這樣子省去了一部分空間。

不過像我們中間出現的斷序的情況(序號5後面是序號7),這個就沒有辦法了,所以在上面Depends圖中0x6,0x9,0xA這幾項都是空的,只是用於佔位置罷了,畢竟數組是需要連續的。

NumberOfFunctions用於記錄總共有多少項導出函數地址

根據上圖中該結果是0x7,懂了上面的Base設計原理後,應該能明白爲什麼只有四項導出函數,這裏卻顯示7項。因爲還有好幾項只是用於數組中佔位置的,空間換時間的想法。

NumberOfNames用於記錄有多個是是根據名字導出的

好了,上面兩個字段都是根據序號這一方面的考慮的,那麼使用函數名導出該這麼辦呢,我們可以這樣子來設計一下

typedef struct _IMAGE_EXPORT_BY_NAME {
    DWORD nameRva; //名字地址
    WORD index; //對應的地址表中的下標
}

看一下上面的結構體,其中一個字段是名字地址,根據其可以找到函數名,匹配函數名成功後可那下面的index下標字段去函數地址表中找地址。

當然Windows並不是這樣設計的,Windows把上面的兩項分別拆成了兩張表:名字導出表(AddressOfNames),名字對應下標表(AddressOfNameOrdinals),這兩張表的同下標是相互關聯的,也相當於上面的結構體。

所以這個字段就是用於描述這兩項有多少個的。

下面我們具體在WinHex中來看一下數值,可對照着上表的數據看,上表是一個概括

首先看AddressOfFunctions,其數組的大小爲NumberOfFunctions

VA2088對應FA爲688,下面在688位置查找7項

再來看一下AddressOfNames,其數組大小爲NumberOfNames,三項

這裏因爲是名字表,所以其對應是個名字的VA值,要找到名字字符串,我們還需要根據其值再找一次

再來看最後一張表了,這張表的大小也爲NumberOfNames,主要用於輔助名稱查地址,比如你知道了名稱,那麼這麼知道這個名稱在地址表的第幾項呢,所以這張表的目的就是用於映射該名稱在地址表中的下標值。需要注意的是因爲序號最大2字節,所以該表的每項長度也是2字節的

好了,分析完後可以再對照着上面的表看看,下面再來總結操作系統如何實現GetProcAddress

1.序號
	定位到數據目錄->導入表
    序號-Base = 下標(index)
    越界檢查,取函數總個數,檢查下標有沒有超過個數
    取函數地址表,取index項 -> RVA值 + 參數一實例句柄 = 函數地址
2.名稱
	定位到數據目錄->導入表
    根據名稱導出個數遍歷函數名稱數組查找 - 導出函數多
    	操作系統會折半查找,所以鏈接器會按照ascii碼順序排放
        查找不到則返回NULL
     查找到下標-index,找函數名稱和序號對應關係表
     	同在該表中取index項,取到內容->index2
     越界檢查,取函數總個數,檢查index2有沒有超過個數
     取函數地址表,取index2項 -> RVA值 + 參數一實例句柄 = 函數地址

下面是具體的代碼實現,當然這裏的代碼只是簡易版的,操作系統做的事情還有挺多的,這裏只是給了一個主體概要

void* MyGetProcAddress(HMODULE hModule, LPCSTR lpProcName)
{
    if(hModule == NULL || lpProcName == NULL)
        return NULL;

    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule;
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((DWORD)hModule + pDosHeader->e_lfanew);
    if (pDosHeader == NULL || pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
        return NULL;
    if (pNtHeaders == NULL || pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
        return NULL;
    PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)((DWORD)hModule +
                pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    PDWORD funTable = (PDWORD)((DWORD)hModule + pExport->AddressOfFunctions);
    if ((DWORD)lpProcName & 0xffff0000)
    {//Name
        PDWORD pNameTable = (PDWORD)((DWORD)hModule + pExport->AddressOfNames);
        PWORD pOrdinalTable = (PWORD)((DWORD)hModule + pExport->AddressOfNameOrdinals);
        for (int i = 0; i < pExport->NumberOfNames; ++i)
        {
            char *funName = (char*)((DWORD)hModule + pNameTable[i]);
            int funIndex=0,procIndex=0;
            while (funName[funIndex] && lpProcName[procIndex])
            {//比較名字字符串
                if(funName[funIndex] != lpProcName[procIndex])
                    break;
                ++funIndex;
                ++procIndex;
            }
            if(funName[funIndex] || lpProcName[procIndex])
                continue;
            WORD ordinal = pOrdinalTable[i];
            lpProcName = (LPCSTR)ordinal;
            break;
        }
    }
    if (!((DWORD)lpProcName & 0xffff0000))
    {//Ordinal
        WORD ordinal = (WORD)lpProcName;
        if(ordinal < 0 || ordinal >= pExport->NumberOfFunctions)
            return NULL;
        return (void*)((DWORD)hModule+funTable[ordinal]);
    }
    return NULL;
}

 

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