Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1078
We have discovered two bugs in the implementation of the win32k!NtGdiGetDIBitsInternal system call, which is a part of the graphic subsystem in all modern versions of Windows. The issues can potentially lead to kernel pool memory disclosure (bug #1) or denial of service (bug #1 and #2). Under certain circumstances, memory corruption could also be possible.
----------[ Double-fetch while handling the BITMAPINFOHEADER structure ]----------
At the beginning of the win32k!NtGdiGetDIBitsInternal system call handler, the code references the BITMAPINFOHEADER structure (and specifically its .biSize field) several times, in order to correctly calculate its size and capture it into kernel-mode memory. A pseudo-code representation of the relevant code is shown below, where "bmi" is a user-controlled address:
--- cut ---
ProbeForRead(bmi, 4, 1);
ProbeForWrite(bmi, bmi->biSize, 1); <------------ Fetch #1
header_size = GreGetBitmapSize(bmi); <----------- Fetch #2
captured_bmi = Alloc(header_size);
ProbeForRead(bmi, header_size, 1);
memcpy(captured_bmi, bmi, header_size); <-------- Fetch #3
new_header_size = GreGetBitmapSize(bmi);
if (header_size != new_header_size) {
// Bail out.
}
// Process the data further.
--- cut ---
In the snippet above, we can see that the user-mode "bmi" buffer is accessed thrice: when accessing the biSize field, in the GreGetBitmapSize() call, and in the final memcpy() call. While this is clearly a multi-fetch condition, it is mostly harmless: since there is a ProbeForRead() call for "bmi", it must be a user-mode address, so bypassing the subsequent ProbeForWrite() call by setting bmi->biSize to 0 doesn't change much. Furthermore, since the two results of the GreGetBitmapSize() calls are eventually compared, introducing any inconsistencies in between them is instantly detected.
As far as we are concerned, the only invalid outcome of the behavior could be read access to out-of-bounds pool memory in the second GreGetBitmapSize() call. This is achieved in the following way:
1. Invoke NtGdiGetDIBitsInternal with a structure having the biSize field set to 12 (sizeof(BITMAPCOREHEADER)).
2. The first call to GreGetBitmapSize() now returns 12 or a similar small value.
3. This number of bytes is allocated for the header buffer.
4. (In a second thread) Change the value of the biSize field to 40 (sizeof(BITMAPINFOHEADER)) before the memcpy() call.
5. memcpy() copies the small structure (with incorrectly large biSize) into the pool allocation.
6. When called again, the GreGetBitmapSize() function assumes that the biSize field is set adequately to the size of the corresponding memory area (untrue), and attempts to access structure fields at offsets greater than 12.
The bug is easiest to reproduce with Special Pools enabled for win32k.sys, as the invalid memory read will then be reliably detected and will yield a system bugcheck. An excerpt from a kernel crash log triggered using the bug in question is shown below:
--- cut ---
DRIVER_PAGE_FAULT_BEYOND_END_OF_ALLOCATION (d6)
N bytes of memory was allocated and more than N bytes are being referenced.
This cannot be protected by try-except.
When possible, the guilty driver's name (Unicode string) is printed on
the bugcheck screen and saved in KiBugCheckDriver.
Arguments:
Arg1: fe3ff008, memory referenced
Arg2: 00000000, value 0 = read operation, 1 = write operation
Arg3: 943587f1, if non-zero, the address which referenced memory.
Arg4: 00000000, (reserved)
Debugging Details:
------------------
[...]
TRAP_FRAME: 92341b1c -- (.trap 0xffffffff92341b1c)
ErrCode = 00000000
eax=fe3fefe8 ebx=00000000 ecx=00000000 edx=00000028 esi=00000004 edi=01240000
eip=943587f1 esp=92341b90 ebp=92341b98 iopl=0 nv up ei pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010246
win32k!GreGetBitmapSize+0x34:
943587f1 8b7820 mov edi,dword ptr [eax+20h] ds:0023:fe3ff008=????????
Resetting default scope
LAST_CONTROL_TRANSFER: from 816f9dff to 816959d8
STACK_TEXT:
9234166c 816f9dff 00000003 09441320 00000065 nt!RtlpBreakWithStatusInstruction
923416bc 816fa8fd 00000003 00000000 00000002 nt!KiBugCheckDebugBreak+0x1c
92341a80 816a899d 00000050 fe3ff008 00000000 nt!KeBugCheck2+0x68b
92341b04 8165af98 00000000 fe3ff008 00000000 nt!MmAccessFault+0x104
92341b04 943587f1 00000000 fe3ff008 00000000 nt!KiTrap0E+0xdc
92341b98 9434383e fe3fefe8 00000000 067f9cd5 win32k!GreGetBitmapSize+0x34
92341c08 81657db6 00000000 00000001 00000000 win32k!NtGdiGetDIBitsInternal+0x17f
92341c08 011d09e1 00000000 00000001 00000000 nt!KiSystemServicePostCall
[...]
--- cut ---
The out-of-bounds data read by GreGetBitmapSize() could then be extracted back to user-mode to some degree, which could help disclose sensitive data or defeat certain kernel security mitigations (such as kASLR).
Attached is a PoC program for Windows 7 32-bit (double_fetch_oob_read.cpp).
----------[ Unhandled out-of-bounds write to user-mode memory when requesting RLE-compressed bitmaps ]----------
The 5th parameter of the NtGdiGetDIBitsInternal syscall is a pointer to an output buffer where the bitmap data should be written to. The length of the buffer is specified in the 8th parameter, and can be optionally 0. The logic of sanitizing and locking the memory area is shown below ("Buffer" is the 5th argument and "Length" is the 8th).
--- cut ---
if (Length != 0 || (Length = GreGetBitmapSize(bmi)) != 0) {
ProbeForWrite(Buffer, Length, 4);
MmSecureVirtualMemory(Buffer, Length, PAGE_READWRITE);
}
--- cut ---
We can see that if the "Length" argument is non-zero, it is prioritized over the result of GreGetBitmapSize() in specifying how many bytes of the user-mode output buffer should be locked in memory as readable/writeable. Since the two calls above are supposed to guarantee that the required user-mode memory region will be accessible until it is unlocked, the call to the GreGetDIBitsInternal() function which actually fills the buffer with data is not guarded with a try/except block.
However, if we look into GreGetDIBitsInternal() and further into GreGetDIBitsInternalWorker(), we can see that if a RLE-compressed bitmap is requested by the user (as indicated by bmi.biCompression set to BI_RLE[4,8]), the internal EncodeRLE4() and EncodeRLE8() routines are responsible for writing the output data. The legal size of the buffer is passed through the functions' 5th parameter (last one), and is always set to bmi.biSizeImage. This creates a discrepancy: a different number of bytes is ensured to be present in memory (Length), and a different number can be actually written to it (bmi.biSizeImage). Due to the lack of exception handling in this code area, the resulting exception causes a system-wide bugcheck:
--- cut ---
KERNEL_MODE_EXCEPTION_NOT_HANDLED (8e)
This is a very common bugcheck. Usually the exception address pinpoints
the driver/function that caused the problem. Always note this address
as well as the link date of the driver/image that contains this address.
Some common problems are exception code 0x80000003. This means a hard
coded breakpoint or assertion was hit, but this system was booted
/NODEBUG. This is not supposed to happen as developers should never have
hardcoded breakpoints in retail code, but ...
If this happens, make sure a debugger gets connected, and the
system is booted /DEBUG. This will let us see why this breakpoint is
happening.
Arguments:
Arg1: c0000005, The exception code that was not handled
Arg2: 9461564b, The address that the exception occurred at
Arg3: 9d0539a0, Trap Frame
Arg4: 00000000
Debugging Details:
------------------
[...]
TRAP_FRAME: 9d0539a0 -- (.trap 0xffffffff9d0539a0)
ErrCode = 00000002
eax=00291002 ebx=00291000 ecx=00000004 edx=fe9bb1c1 esi=00000064 edi=fe9bb15c
eip=9461564b esp=9d053a14 ebp=9d053a40 iopl=0 nv up ei ng nz ac pe cy
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010297
win32k!EncodeRLE8+0x1ac:
9461564b c60300 mov byte ptr [ebx],0 ds:0023:00291000=??
Resetting default scope
[...]
STACK_TEXT:
9d052f5c 8172adff 00000003 17305ce1 00000065 nt!RtlpBreakWithStatusInstruction
9d052fac 8172b8fd 00000003 9d0533b0 00000000 nt!KiBugCheckDebugBreak+0x1c
9d053370 8172ac9c 0000008e c0000005 9461564b nt!KeBugCheck2+0x68b
9d053394 817002f7 0000008e c0000005 9461564b nt!KeBugCheckEx+0x1e
9d053930 81689996 9d05394c 00000000 9d0539a0 nt!KiDispatchException+0x1ac
9d053998 8168994a 9d053a40 9461564b badb0d00 nt!CommonDispatchException+0x4a
9d053a40 944caea9 fe9bb1c1 ff290ffc 00000064 nt!KiExceptionExit+0x192
9d053b04 944e8b09 00000028 9d053b5c 9d053b74 win32k!GreGetDIBitsInternalWorker+0x73e
9d053b7c 944d390f 0c0101fb 1f050140 00000000 win32k!GreGetDIBitsInternal+0x21b
9d053c08 81688db6 0c0101fb 1f050140 00000000 win32k!NtGdiGetDIBitsInternal+0x250
9d053c08 00135ba6 0c0101fb 1f050140 00000000 nt!KiSystemServicePostCall
[...]
--- cut ---
While the size of the buffer passed to EncodeRLE[4,8] can be arbitrarily controlled through bmi.biSizeImage (32-bit field), it doesn't enable an attacker to corrupt kernel-mode memory, as the memory writing takes place sequentially from the beginning to the end of the buffer. Furthermore, since the code in NtGdiGetDIBitsInternal() makes sure that the buffer size passed to ProbeForWrite() is >= 1, its base address must reside in user space. As such, this appears to be a DoS issue only, if we haven't missed anything in our analysis.
Attached is a PoC program for Windows 7 32-bit (usermode_oob_write.cpp), and a bitmap file necessary for the exploit to work (test.bmp).
Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/41879.zip