天天看點

棧溢出筆記1.6 位址問題(1)

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

前面的Shellcode中,我用到了如下幾個寫死位址,它們的含義如下:

棧溢出筆記1.6 位址問題(1)

其中,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機器上的該結構:

棧溢出筆記1.6 位址問題(1)

圖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;  
/*****************************************************************************/
           

同樣,在我的機器上為:

棧溢出筆記1.6 位址問題(1)

前面三個連結清單對應于該結構挂入的三個連結清單,重點在于第四個成員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基位址的整個流程如下:

棧溢出筆記1.6 位址問題(1)

圖38

寫代碼之前,先通過調試器順着該順序看一看,我用windbg加載的是example_1,輸入:

d fs:[0x30]檢視目前peb的位址:

棧溢出筆記1.6 位址問題(1)

圖39

位址為0x7ffda000,輸入:!peb,驗證一下:

棧溢出筆記1.6 位址問題(1)

圖40

是相同的。輸入: d 7ffda000+0x0c,檢視PEB_LDR_DATA結構的位址:

棧溢出筆記1.6 位址問題(1)

圖41

輸入: d 00251ea0+0x1c,檢視InInitializationOrderModuleList連結清單:

棧溢出筆記1.6 位址問題(1)

圖42

先列一下LIST_ENTRY結構,Flink 指向下一個節點:

/*****************************************************************************/
typedef struct _LIST_ENTRY 
{
     struct _LIST_ENTRY *Flink; 
     struct _LIST_ENTRY *Blink; 
} LIST_ENTRY, *PLIST_ENTRY;
/*****************************************************************************/
           

是以,0x00251f58是第一個元素。輸入:d 0x00251f58檢視以下其内容:

棧溢出筆記1.6 位址問題(1)

基位址為0x7c920000,但是右邊顯示為kernel32.dll???第一個元素不是ntdll.dll嗎?

來看看下一個元素:

棧溢出筆記1.6 位址問題(1)

基位址為0x7c800000,顯示為uer32.dll,看來是出了一些問題,輸入!peb來看看:

棧溢出筆記1.6 位址問題(1)

這裡的基位址和名稱對應才是對的。

下面來寫擷取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;
}
/*****************************************************************************/
           

結果如下:

棧溢出筆記1.6 位址問題(1)

圖43

這裡是一種方法,其它方法(包括Windows 7)請看:

http://blog.harmonysecurity.com/2009_06_01_archive.html

繼續閱讀