在上一篇博客中說了導入表,所謂的導入表,其實相當於記錄程序所依賴的函數庫信息,類似於你要調用外部函數,總得記錄下這個函數在哪個庫中,名字或者序號是什麼。有了這些信息後,我們就可以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;
}