前面的Shellcode中,我使用的都是自己XP機器上的寫死位址。任何時候在Shellcode中使用寫死位址都不是個好主意,這一點與動态庫的重定位類似,一旦系統環境和程式編譯設定發生變化,Shellcode幾乎肯定會失效。是以,我們要找到更好一點的方法。
前面的Shellcode中,我用到了如下幾個寫死位址,它們的含義如下:

其中,LoadLibraryA的作用比較特殊,我們用它來加載user32.dll庫。
現在我們要換掉這些寫死位址。那麼,如何得到這些API函數的位址呢?在動态連結庫中擷取函數位址有一個專門的函數——GetProcAddress,這個函數的原型如下:
/*****************************************************************************/
FARPROC WINAPI GetProcAddress(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName
);
/*****************************************************************************/
這個函數第一個參數為子產品的句柄,可以調用LoadLibraryA獲得,第二個參數為函數名稱。這樣的話,MessageBoxA和ExitProcess都可以使用這種方式來擷取。但是這依賴于兩個函數:LoadLibraryA和GetProcAddress,那這兩個函數的位址又怎麼得到呢?
這兩個函數都位于kernel32.dll,kernel32.dll肯定會加載,不需要我們自己加載。是以,現在的問題就是如何從kernel32.dll中找到LoadLibraryA和GetProcAddress的位址?
接下來就需要一點Windows核心和PE檔案格式的知識了。我們知道,kernel32.dll為PE格式的動态連結庫,要導出的函數放在PE檔案的導出表中,是以,為了擷取LoadLibraryA和GetProcAddress的位址,我們需要手動解析kernel32.dll的導出表。但在這之前,我們需要知道kernel32.dll在記憶體中的位置,也就是kernel32.dll的基位址。是以,問題總結為以下:
(1)如何擷取kernel32.dll的基位址?
(2)如何在kernel32.dll的導出表中找到LoadLibraryA和GetProcAddress的位址?
先來解決第一個問題。我們知道,一個程序運作的時候,除了加載exe檔案外,所依賴的.dll也會映射到程序的虛拟位址空間中,那麼,這些加載的dll也是程序的财産,是以,程序會儲存它們的資訊。程序的資訊都儲存在PEB結構中,其中的Ldr(偏移為0xc0)指向一個PEB_LDR_DATA結構,這個結構儲存的程序已加載子產品的資訊。這個結構如下:
/*****************************************************************************/
typedef struct _PEB_LDR_DATA
{
ULONG Length; // +0x00
BOOLEAN Initialized; // +0x04
PVOID SsHandle; // +0x08
LIST_ENTRY InLoadOrderModuleList; // +0x0c
LIST_ENTRY InMemoryOrderModuleList; // +0x14
LIST_ENTRY InInitializationOrderModuleList; // +0x1c
} PEB_LDR_DATA,*PPEB_LDR_DATA; // +0x24
/*****************************************************************************/
Windows并未完全公開(文檔化)此結構。這是網上的版本,我們也可以通過Windbg來得到,這是我XP SP3機器上的該結構:
圖37
這個結構的重點在于後面三個連結清單:InLoadOrderModuleList、InMemoryOrderModuleList和InInitializationOrderModuleList。從名稱上看,這三個隊列都是子產品連結清單,第一個是按加載的先後順序,第二個是按在虛拟空間中的位置,第三個是按初始化的順序。第二個容易了解,但是第一個和第三個有什麼差別呢?加載是先于初始化的,加載就是完成虛拟空間的映射和與exe的連結,加載完成後的DLL會挂入InInitializationOrderModuleList,進行初始化,初始化就是調用其DLLMain。可以看到的一點是InLoadOrderModuleList中有exe本身的子產品,而InInitializationOrderModuleList中隻有exe依賴的DLL。(當然,這随着系統版本在發生變化)
我們要使用的是InInitializationOrderModuleList,挂入該連結清單中的為LDR_DATA_TABLE_ENTRY,也就是說,每個加載的子產品對應于一個LDR_DATA_TABLE_ENTRY,該結構如下:
/*****************************************************************************/
typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
DWORD SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
DWORD Flags;
WORD LoadCount;
WORD TlsIndex;
LIST_ENTRY HashLinks;
PVOID SectionPointer;
DWORD CheckSum;
DWORD TimeDateStamp;
PVOID LoadedImports;
PVOID EntryPointActivationContext;
PVOID PatchInformation;
}LDR_DATA_TABLE_ENTRY,*PLDR_DATA_TABLE_ENTRY;
/*****************************************************************************/
同樣,在我的機器上為:
前面三個連結清單對應于該結構挂入的三個連結清單,重點在于第四個成員DllBase,這是載入子產品的基位址。是以,我們隻需要順着InInitializationOrderModuleList連結清單找到kernel32.dll的PLDR_DATA_TABLE_ENTRY,然後通過其DllBase成員,就知道了kernel32.dll載入的位址。那麼InInitializationOrderModuleList連結清單中哪一個kernel32.dll呢?最保險的方法是解析FullDllName成員,這樣代碼會比較複雜。實際上在特定版本的系統中,動态庫初始化的順序是一定的,第一個為ntdll.dll,第二個就是kernel32.dll。Vista 以後第二個是kernelbase.dll,第三個是kernel32.dll。是以,可以避免解析FullDllName成員。
現在,我們要找到程序的PEB結構位址,PEB結構儲存于線程的TEB結構中的peb成員,而Windows系統中,寄存器fs總是指向目前線程的TEB。是以,擷取kernel32.dll基位址的整個流程如下:
圖38
寫代碼之前,先通過調試器順着該順序看一看,我用windbg加載的是example_1,輸入:
d fs:[0x30]檢視目前peb的位址:
圖39
位址為0x7ffda000,輸入:!peb,驗證一下:
圖40
是相同的。輸入: d 7ffda000+0x0c,檢視PEB_LDR_DATA結構的位址:
圖41
輸入: d 00251ea0+0x1c,檢視InInitializationOrderModuleList連結清單:
圖42
先列一下LIST_ENTRY結構,Flink 指向下一個節點:
/*****************************************************************************/
typedef struct _LIST_ENTRY
{
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;
/*****************************************************************************/
是以,0x00251f58是第一個元素。輸入:d 0x00251f58檢視以下其内容:
基位址為0x7c920000,但是右邊顯示為kernel32.dll???第一個元素不是ntdll.dll嗎?
來看看下一個元素:
基位址為0x7c800000,顯示為uer32.dll,看來是出了一些問題,輸入!peb來看看:
這裡的基位址和名稱對應才是對的。
下面來寫擷取kernel32.dll基位址的代碼:
/*****************************************************************************/
// example_8 擷取kernel32.dll的基位址
#include <stdio.h>
// 擷取kernel32.dll的基位址
int get_kernel32_base()
{
__asm
{
mov eax, fs:[0x30] // PEB
mov eax, [eax+0x0c] // PEB->Ldr
mov eax, [eax+0x1c] // PEB->Ldr.InInitializationOrderModuleList.Flink(指向第一個元素)
mov eax, [eax] // 指向第二個元素
mov eax, [eax+0x08] // kernel32.dll基位址
}
}
int main()
{
printf("0x%x\n", get_kernel32_base());
return 0;
}
/*****************************************************************************/
結果如下:
圖43
這裡是一種方法,其它方法(包括Windows 7)請看:
http://blog.harmonysecurity.com/2009_06_01_archive.html