當前位置:
首頁 > 新聞 > Windows x86 Shellcode開發:尋找Kernel32.dll地址

Windows x86 Shellcode開發:尋找Kernel32.dll地址

前言

針對一個已經學習了Linux Shellcode開發,並開始在Windows上嘗試的研究人員來說,這一過程可能要比想像的更加艱難。Windows內核與Linux完全不同。儘管如此,但Linux內核要比Windows更容易理解,原因在於其開源的特性,並且與Windows相比,Linux只具有相當少的功能。另一方面,Windows在過去幾年中進行了重大的改進,由於這一改進,新版本與老版本相比已經發生了許多變化。在本文中,我們將專註於對Windows 10 x86進行分析,但其他舊版本可能與之相比沒有太多的不同。目前,已經有很多關於PEB LDR的博客文章,但我們還沒有看到有任何文章中展現了完整的邏輯,並闡述其本質原因的。大多數研究人員只是通過WinDBG進行分析,並要求讀者具備一定程度的後端基礎。我撰寫這篇文章的主要原因,是希望從C語言轉到ASM,並希望我們能共同了解在ASM x86中進行Shellcode開發時,後端的工作原理。

.data段

在我們開始處理Shellcode部分之前,我建議首先應該理解內存是如何工作的,因為我們即將做的一切操作都是在內存中。如果我們已經了解像LPWSTR、LPSTR這樣的Windows數據類型,那麼無疑是一個好消息,因為我們必須要知道:

標準的C語言並不等同於Windows C編程

接下來,唯一需要重點掌握的,就是基本的Assembly x86。默認情況下,除了系統調用或API調用之外,ASM在Linux或Windows中是相同的。因此,了解寄存器的工作原理就顯得非常重要。

最重要的是,我們應該了解如何對二進位文件進行反彙編。我主要使用x32dbg和WinDBG x86。我會同時使用這兩個工具進行調試,因為有一些我們不能在x32dbg中完成的事情,在WinDBG x86中是可以的,反之亦然。因此,我們將不斷切換使用這兩個工具。

.text段

在我們開始使用Shellcode之前,理解其在較低級別的工作方式,這一點非常重要。我們首先將從一個非常簡單的例子開始,找到系統的當前主機名。我們來看看下面是使用C語言編寫的Windows API示例:

在上圖中,我創建了兩個變數,分別是compName和compNameSize。這些將是提供給函數GetComputerNameA的參數。請記住,GetComputerNameA和GetComputerNameW有兩個相似的函數。W代表寬Unicode字元,而A代表ANSI CHAR字元串。我們將在整個博客系列中使用ANSI。下面是MSDN對GetComputerNameA函數的說明:

BOOL GetComputerNameA(LPSTR lpBuffer, LPDWORD nSize);

上面的代碼表示,GetComputerNameA接受LPSTR,表示長指針字元串,而LPDWORD則表示長指針雙字。一個字的大小是16位,因此DWORD在所有平台上都是32位。現在,如果使用g 編譯上述程序,我們將看到如下內容:

現在在這裡,在程序的最開始,有#include ,這也就意味著Windows庫將被引入到代碼中,它應該在這裡動態鏈接默認依賴項。但是,我們不能對ASM進行相同的操作。在ASM的場景中,我們需要動態地找到函數GetComputerNameA所在的地址,在堆棧上載入參數,並調用具有函數指針的寄存器。我們要知道的一件重要事情是,Windows的大多數功能,都是通過三個主要DLL訪問的:NTDLL.DLL、Kernel32.DLL和Kernelbase.DLL。因此,無論任何時間執行任何二進位文件,這些都是始終要載入的必要DLL。為了載入函數GetComputerNameA,我們必須找到這個函數所在的DLL,並在那裡找到它的基址。接下來,我們在x32dbg上嘗試載入任何x86二進位文件,看看能得到什麼。我將載入我們編譯的上述exe文件,但實際上,我們可以載入任何隨機的32位可執行文件,因為我們只會瀏覽上面提到的那些DLL。使用x32dbg打開exe文件,並導航到Log部分,可以看到載入了這三個DLL,以及其特定的地址:

接下來,我們將導航到突出顯示的Symbols部分,可以看到載入的不同DLL的名稱。在這裡,我們可以瀏覽DLL,並查看它們提供的所有功能。

現在,如果我們在搜索框中搜索函數GetComputerNameA,它將顯示Kernel32.DLL載入該函數。此外,還將列印出函數所在的地址0x74F69AC0。在理論和實際測試中,這一點都能夠很好地展現。接下來,讓我們通過C編程然後通過ASM來完成,我們要執行的步驟如下:

1. 使用函數LoadLibraryA WinAPI在內存中載入Kernel32.dll;

2. 使用GetProcAddress在Kernel32.dll中找到函數GetComputerNameA的地址;

3. 將GetProcAddress返回值類型轉換為接受2個參數的WinAPI函數(因為GetComputerNameA接受2個參數);

4. 為ComputerName及其Length創建緩衝區。

將Address作為函數指針來執行。

訪問LoadLibraryA的MSDN頁面,可以發現它返回一個HMODULE,這意味著它將一個句柄返回到一個被載入的模塊。因此,我們創建了一個變數hmod_libname。類似地,GetProcAddress返回從DLL載入的函數的地址。我們需要將GetProcAddress返回的地址類型轉換為GetComputerNameA函數,以使其能夠正常工作。為此,我們創建了一個typedef,它基本上複製了函數GetComputerNameA的結構。在上圖中,我們載入庫Kernel32.dll,並使用GetProcAddress查找函數GetComputerNameA的基址,將地址存儲在GetComputerNameProc中。最後,我們創建兩個變數CompName和CompNameSize,並使用(*GetComputerNameProc)作為函數指針,執行存儲在GetComputerNameProc中的地址,並為其提供所需的變數。上面的代碼中,還列印了函數GetComputerNameA的地址。我們嘗試對其進行編譯,看看結果如何:

不錯!地址0x74F69AC0與上面用x32dbg調試時發現的地址一致。

_start

我們接下來進入到有趣的部分。所有DLL及其函數的地址在重新啟動時都會發生變化,並且在每個其他系統中都會有所不同。這就是我們無法對ASM代碼中的任何地址進行硬編碼的原因。但是,主要問題仍然存在,那就是我們如何找到kernel32.dll自身的地址?

我在一開始說過,每個exe都載入了Kernel32.dll、NTDLL.DLL和Kernelbase.dll。事實上,這些DLL是操作系統中非常重要的一部分,每次在執行任何操作時,都會載入這些DLL。因此,這些DLL到內存中的載入順序總是相同的。然而,這可能因操作系統而異。這就意味著,在Windows XP與Windows 10之間可能有所不同,但所有Windows 10中的載入順序將保持不變。

所以,我們在繼續下一步之前,需要完成下面的工作:

1. 找到Kernel32.dll的載入順序;

2. 找到Kernel32.dll的地址;

3. 找到GetComputerNameA的地址;

4. 在棧上載入GetComputerNameA的參數;

5. 調用GetComputerNameA函數指針。

可能聽起來很容易?我們來實際嘗試一下。

查找kernel32.dll的地址並不簡單。當我們執行任何exe時,在操作系統中首先創建的就是TEB(線程環境塊)和PEB(進程環境塊)。

我們的主要關注點在於PEB結構(稱為LDR),因為這是與進程相關的所有信息都被載入的地方。從流程參數到流程ID的所有內容都存儲在這個位置。在PEB中,有一個名為PEB_LDR_DATA的結構,它包含三個關鍵部分。這些被稱為鏈接列表(Linked Lists)。

1. InLoadOrderModuleList - 載入模塊(exe或dll)的順序;

2. InMemoryOrderModuleList - 模塊(exe或dll)存儲在內存中的順序;

3. InInitializationOrderModuleList - 在進程環境塊中初始化模塊(exe或dll)的順序。

在鏈表中載入模塊的順序是固定的。這意味著,我們如果能夠在上面的列表中找到kernel32.dll的順序,就可以搜索kernel32.dll的地址,並繼續進行。現在,我們啟動WinDBG x86。如果各位還沒有安裝WinDBG及其依賴項,你可以在SLAER上找到一篇關於WinDBG的文章。一旦安裝WinDBG之後,就可以像我們之前那樣打開任意的exe文件。

在WinDBG中載入exe文件後,會顯示一些輸出。限制,我們將忽略輸出內容,並在下面的命令提示符中輸入.cls以清除屏幕並重新開始。現在,我們在命令提示符下輸入!peb,看看在這裡能夠得到什麼:

如大家所見,我們得到了LDR(PEB結構)的地址,即779E0C40。這非常重要,因為我們要使用該地址來計算前進的地址。接下來,我們輸入命令dt nt!_TEB,以查找PEB結構的偏移量。

如我們所見,_PEB位於偏移量0x030的位置。以類似的方式,我們可以使用dt nt!_PEB查看_PEB結構的內容。

_PEB_LDR_DATA的偏移量為0x00c。接下來,我們嘗試查找_PEB_LDR_DATA結構中的內容。我們可以用類似的方式實現這一點:

dt nt!_PEB_LDR_DATA

在這裡,我們可以看到InLoadOrderModuleList位於偏移量0x00c處,InMemoryOrderModuleList位於偏移量0x014處,InInitializationOrderModuleList位於偏移量0x01c處。此外,如果要查看每個列表所在的地址,可以使用我們此前找到的地址779E0C40(LDR的地址)以及命令dt nt!_PEB_LDR_DATA 779E0C40。這將向我們顯示鏈接列表的相應起始地址和結束地址,如下所示:

有一個地方,可能會被一些人誤解,就是上圖中展示出InMemoryOrderModuleList的類型為_LIST_ENTRY,但在MSDN上已經另有說明:

因此,MSDN聲明它是LDR_DATA_TABLE_ENTRY類型而不是_LIST_ENTRY類型。我們嘗試查看結構中載入的模塊,並指定該結構的起始地址為0x7041e8,以便可以看到載入的模塊的基址。需要注意的是,0x7041e8是此結構的地址,因此第一個條目將比此地址少8個位元組。因此,我們的命令是:

dt nt!_LDR_DATA_TABLE_ENTRY 0x7041e8-8

第一個出現的BaseDllName是gethost.exe。這就是我之前執行的exe文件。此外,我們可以看到現在InMemoryOrderLinks的地址是0x7040e0。偏移量0x018處的DllBase中包含BaseDllName的基址。現在,我們下一個載入的模塊必須距離0x7040e0有8個位元組,也就是0x7040e0-8。

dt nt!_LDR_DATA_TABLE_ENTRY 0x7040e0-8

所以,我們的第二個模塊是ntdll.dll,它的地址是0x778c000,下一個模塊位於0x704690之後的8個位元組。所以,我們的下一個命令是:

dt nt!_LDR_DATA_TABLE_ENTRY 0x704690-8

由此,就得到了第三個模塊Kernel32.dll,其地址是0x74f50000,其偏移量時0x018。模塊載入的順序總是固定的,至少這適用於Windows 10、Windows 7、Windows 8(包括8.1)。因此,當我們編寫ASM時,我們可以遍歷整個PEB LDR結構體,並找到Kernel32.dll的地址,並將其載入到我們的Shellcode中。以類似的方式,我們還可以找到Kernelbase.dll的地址,這是第四個模塊。

現在,我們總結一下需要進行的工作:

1. PEB位於距離文件段寄存器偏移量為0x030的位置;

2. LDR位於偏移量為PEB 0x00C的位置;

3. InMemoryOrderModuleList位於偏移量LDR 0x014的位置;

4. 第一個模塊入口是exe本身;

5. 第二個模塊入口是ntdll.dll;

6. 第三個模塊入口是kernel32.dll;

7. 第四個模塊入口是Kernelbase.dll。

我們現在最感興趣的,就是Kernel32.dll。每次載入DLL時,地址都將存儲在DllBase的偏移量0x018的位置。我們鏈接列表的起始地址將存儲在InMemoryOrderLinks的偏移量中,即0x008。因此,偏移量之間的關係將是 DllBase – InMemoryOrderLinks = 0x018 – 0x008 = 0x10。因此,Kernel32.dll的偏移量將是LDR 0x10。更詳細的理解,可以在下圖中看到,這張圖是我從這裡偷過來的。

現在,如果我們在ASM中做同樣的工作,將會是如下所示:

我們使用NASM來編譯,並在x32dbg中載入它。大家可以從這裡下載NASM。

實際上,一旦我們的最後一條指令被運行,就應該在EAX寄存器中載入Kernel32.dll的地址。我們來看看它在x32dbg中看起來是否相同。

如我們所見,在最後一條指令之後,載入到EAX中的地址與我們在下面使用lm命令在WinDBG中看到的地址相同,都是74F50000,這是Kernel32.dll的地址。

現在,我們已經有了Kernel32.dll的地址,下一步就是使用LoadLibraryA找到GetComputerNameA的地址,並調用該函數。我們將在下一篇文章中重點討論這一問題,完善ASM代碼,實現獲取計算機名稱並將其列印在屏幕上,然後列印到Shellcode部分。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 嘶吼RoarTalk 的精彩文章:

攻擊者是如何利用SSRF漏洞讀取本地文件並濫用AWS元數據的
OceanLotus隱寫術分析報告

TAG:嘶吼RoarTalk |