Windows win32k.sys menus and some “close, but no cigar” bugs

Welcome after one of the more lengthy breaks in the blog’s activity. Today, I would like to discuss none other than several interesting weaknesses around the implementation of menus (like, window menus) in the core component of the Microsoft Windows kernel – the infamous win32k.sys driver, also known as the “Java of Windows” in terms of overall security posture.

Now, menus have been a part of the Windows graphical interface since the very beginning of the operating system existence. The implementation became part of the Windows kernel at the time of porting a majority of the Windows manager (User) subsystem to a ring-0 component during Windows NT 4.0 development. The functionality consists of user-facing (i.e. the NtUserThunkedMenuInfo and NtUserThunkedMenuItemInfo system calls) and rendering portions of code; I have found several bugs or problems in both areas.

First of all, let’s start with the win32k!xxxSetLPITEMInfo function, which can be generally reached through the two following call chains in Windows 7 x86:

NtUserThunkedMenuItemInfo → xxxInsertMenuItem → xxxSetLPITEMInfo
or
NtUserThunkedMenuItemInfo → xxxSetMenuItemInfo → xxxSetLPITEMInfo

The routine itself is responsible for setting up an ITEM structure, which describes a single menu item and is defined as follows for the previously stated platform:

2: kd> dt win32k!tagITEM
   +0x000 fType            : Uint4B
   +0x004 fState           : Uint4B
   +0x008 wID              : Uint4B
   +0x00c spSubMenu        : Ptr32 tagMENU
   +0x010 hbmpChecked      : Ptr32 Void
   +0x014 hbmpUnchecked    : Ptr32 Void
   +0x018 lpstr            : Ptr32 Uint2B
   +0x01c cch              : Uint4B
   +0x020 dwItemData       : Uint4B
   +0x024 xItem            : Uint4B
   +0x028 yItem            : Uint4B
   +0x02c cxItem           : Uint4B
   +0x030 cyItem           : Uint4B
   +0x034 dxTab            : Uint4B
   +0x038 ulX              : Uint4B
   +0x03c ulWidth          : Uint4B
   +0x040 hbmp             : Ptr32 HBITMAP__
   +0x044 cxBmp            : Int4B
   +0x048 cyBmp            : Int4B
   +0x04c umim             : tagUAHMENUITEMMETRICS

Among other characteristics, the structure stores a pointer to the string displayed on top of the menu item. The string associated with a new menu item being initialized is passed through a user-mode UNICODE_STRING structure pointer, the contents of which are then copied to kernel-mode memory using the following code (reverse-engineered pseudo code follows):

    if (input_string->Buffer) {
      menu_string = DesktopAlloc(menu->head.rpdesk, input_string->Length + sizeof(WCHAR), 8);
      if (!menu_string) {
        return 0;
      }
      memcpy(menu_string, input_string->Buffer, input_string->Length);
      string_length = input_string->Length / sizeof(WCHAR);
    }

As can be seen, the function allocates a buffer of size “Length + 2” but only initializes the first “Length” bytes. While nul termination is guaranteed by DesktopAlloc (which indeed zeroes out the allocation region before passing it back to the caller), the code still relies on the assumption that “Length” is an even value. Note that none of its top-level callers nor xxxSetLPITEMInfo explicitly enforces this assumption, enabling an attacker to specify a unicode string consisting of an odd number of bytes and have it processed by the code, potentially leading to the following layout of kernel-mode menu string allocation:

[[0x41][0x41]] [[0x41][0x41]] ... [[0x41][0x00]] [[0x00][???]] [[???][???]] ...

Note that the last two allocation bytes are still zero, but they span across two wide characters, while the second byte of the (supposedly) last character is not defined and doesn’t necessarily have to be 0x00. Therefore, we could provoke the allocation of a non-nul-terminated unicode string for the menu item, and although there is a “cch” field in the ITEM structure which specifies the actual length of the string, it is not taken into consideration while rendering the textual string. As a result, it is possible to get win32k.sys to disclose junk bytes from the Desktop Heap onto the display and into user-mode, as shown below:

This condition is one of the most typical errors found in the Windows kernel and related to unicode strings – it has been already discussed in my “A story of win32k!cCapString, or unicode strings gone bad” blog post, and was the root cause of at least one Denial of Service vulnerability in the implementation of Windows registry (CVE-2010-0235, advisory here). Causing the kernel to draw unicode artifacts over menu items is quite amusing itself, but it turns out that the contents of the Desktop Heap are not kept secret from user-mode applications; in fact, the heap is even mapped into the ring-3 virtual address space of GUI processes. The observation can prove useful in certain scenarios (e.g. while trying to map controlled bytes into a privileged process running within the same desktop, as shown in “CVE-2011-1281: A story of a Windows CSRSS Privilege Escalation vulnerability”), but here makes the bug a non-issue (as officially confirmed by MSRC). However, the problem was quite close to becoming very helpful in the exploitation of another potential vulnerability.

Signedness issue in win32k!xxxDrawMenuItemText

On the rendering side of things, the xxxDrawMenuItemText routine plays a very important role. Its overall declaration is rather complicated and not relevant to the discussed problem, but the one important point is that the 7th parameter stores the length of the string (in characters) to be displayed as a signed integer, and is used to determine whether a stack buffer or a dynamic pool allocation should be used to store the input string (using, of course, a signed comparison). The phenomenon is better illustrated in the following C-like pseudo code listing:

#define LOCAL_BUFFER_LENGTH 255

PVOID xxxDrawMenuItemText(..., signed int length, ...) {
  WCHAR local_buffer[LOCAL_BUFFER_LENGTH];
  PWCHAR buffer_ptr = local_buffer;

  if (length >= LOCAL_BUFFER_LENGTH) {
    buffer_ptr = ExAllocatePool((length + 1) * sizeof(WHCAR));
  }

  // operate on buffer_ptr.
}

The signedness of the integer is indeed confirmed by the assembly instruction used:

.text:0020C8E6                 cmp     ebx, 0FFh
.text:0020C8EC                 jl      short loc_20C927
.text:0020C8EE                 push    74727355h       ; Tag
.text:0020C8F3                 lea     ecx, ds:2[ebx*2]
.text:0020C8FA                 push    ecx             ; NumberOfBytes
.text:0020C8FB                 push    21h             ; PoolType
.text:0020C8FD                 call    ds:__imp__ExAllocatePoolWithTag@12 ; ExAllocatePoolWithTag(x,x,x)

In case you were wondering, 64-bit versions of Windows are similarly affected:

.text:FFFFF97FFF20C5FA                 cmp     edi, 0FFh
.text:FFFFF97FFF20C600                 jl      short loc_FFFFF97FFF20C637
.text:FFFFF97FFF20C602                 lea     ecx, [rdi+1]
.text:FFFFF97FFF20C605                 mov     edx, 74727355h
.text:FFFFF97FFF20C60A                 movsxd  rcx, ecx
.text:FFFFF97FFF20C60D                 add     rcx, rcx
.text:FFFFF97FFF20C610                 call    Win32AllocPool

At first glance, this sounds like the perfect situation with great potential for a stack-based buffer overflow right inside of win32k.sys, provided we are able to set the 7th function parameter to a negative value. In theory, this should not be possible due to the fact that the length of any menu item text is limited by the 16-bit width of the UNICODE_STRING.Length field used to set up the menu item in the first place (in this case, the limit is 32767 characters, which is nowhere near 2147483648 required to overflow the positive integer range). However, it turns out that the “Length” parameter of the affected function is not taken from the limited ITEM.cch field, but rather calculated in the following manner:

min(FindCharPosition(lpItem->lpstr, L'\8'), FindCharPosition(lpItem->lpstr, L'\t'))

where the FindCharPosition is a trivial wcschr-like function searching for a specific character from the beginning of a string, completing upon finding the desired character or encountering a unicode nul. This obviously opens up room for some potential abuse – by making use of the previous bug allowing lack of nul termination, we could potentially try to grow the Desktop Heap to 4GB+ (two billion wide characters) and hope that none of the {0x0000, 0x0008, 0x0009} words would occur at even offsets starting from the beginning of menu item text allocation. It is not clear whether one could increase the size of the desktop heap to as much as several gigabytes (if at all, this would only be possible on 64-bit platforms) and additionally satisfy the “no special characters inside” requirement, but even if that was possible, it still turns out that the bug would not be exploitable. Due to the fact that the GetPrefixCount function called by xxxDrawMenuItemText also operates on signed integers and does it in a way that prevents any kind of memory corruption:

DWORD GetPrefixCount(signed int length, PWCHAR buffer, ...) {
  if (length > 0) {
    // fill out "buffer"
  }

  *buffer = L'\0';
  // return
}

To date, I believe that none of the functions called subsequently by xxxDrawMenuItemText can cause stack corruption and write beyond the local buffer; however, my feeling is that the impossibility of exploitation is purely accidental, and a very slight change of a parameter type in any of the involved functions may suddenly introduce a very severe vulnerability. In other words, this is something the security community should definitely keep an eye on across new versions of Windows. :-)

One last potential problem with the function is the calculation of a dynamic buffer size in the line:

.text:0020C8F3                 lea     ecx, ds:2[ebx*2]

For Length=0x7fffffff (and above), the allocation size overflows and becomes 0x0 on 32-bit platforms; however, it is physically impossible to allocate 4GB of kernel memory (required for a large enough string length) on x86 CPUs. On 64-bit platforms, the calculation is performed using 64-bit variables based on a sign-extended 32-bit length. As a result, the final ExAllocatePoolWithTag parameter becomes 0xffffffff00000000, which when casted back to an unsigned long long (or size_t, rather) is too large for the allocator to handle.

Overall, my take is that Microsoft has been pretty lucky with the current shape of menu implementation – although there is a number of issues in the code, none of them are currently exploitable due to various architecture and system-specific limitations (likely not intentional or considered by the original win32k.sys developers). A subtle modification of the code path can potentially result in enabling practical exploitation of some or all of the problems; however, Microsoft has decided that the current lack of security impact renders the bugs not worth fixing.

And that’s it for today. As a final word, it is worth mentioning that actual (exploitable) vulnerabilities were discovered in the menu implementation in the past, see Tavis Ormandy’s “Microsoft Windows win32k!xxxRealDrawMenuItem() missing HBITMAP bounds checks” advisory. Comments and feedback are welcome, especially if I happened to miss something in the analysis and any of the problems actually are exploitable. :-)

Take care!

Proof of Concept

The source code of a Proof of Concept program demonstrating the first issue (odd unicode string length) for Microsoft Windows 7 SP1 64-bit is shown below:

#include <cstdio>
#include <cstdlib>
#include <string>
#include <windows.h>
#include <uxtheme.h>

#pragma comment(lib, "GDI32")
#pragma comment(lib, "USER32")
#pragma comment(lib, "UXTHEME")

//---------------------------------------------------------------------------
#ifndef MFS_CACHEDBMP
# define MFS_CACHEDBMP 0x20000000L
#endif
//---------------------------------------------------------------------------
typedef struct _LSA_UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} LSA_UNICODE_STRING, *PLSA_UNICODE_STRING, UNICODE_STRING, *PUNICODE_STRING;

extern "C" {
VOID WINAPI RtlInitUnicodeString(
  PUNICODE_STRING DestinationString,
  PCWSTR SourceString
);
}  // extern "C"
//---------------------------------------------------------------------------
#define __NR_NtUserThunkedMenuItemInfo 0x1098
#define SYSCALL_ARG(x) ((__int64)(x))

BYTE SyscallCode[] = "\x4C\x8B\xD1"          // MOV R10, RCX
                     "\xB8\x00\x00\x00\x00"  // MOV EAX, 
                     "\x0F\x05"              // SYSENTER
                     "\xC3";                 // RET
PBYTE SyscallCodePtr = SyscallCode;
ULONG (*SystemCall)(__int64 Argument1, __int64 Argument2, __int64 Argument3, __int64 Argument4,
                    __int64 Argument5, __int64 Argument6, __int64 Argument7, __int64 Argument8);
ULONG CallService(DWORD ServiceId, __int64 Argument1, __int64 Argument2, __int64 Argument3,
                  __int64 Argument4, __int64 Argument5, __int64 Argument6, __int64 Argument7,
                  __int64 Argument8) {
  memcpy(&SyscallCode[4], &ServiceId, sizeof(DWORD));
  return SystemCall(Argument1, Argument2, Argument3, Argument4,
                    Argument5, Argument6, Argument7, Argument8);
}
//---------------------------------------------------------------------------
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
//---------------------------------------------------------------------------
DWORD WINAPI CreateHostWindow(LPVOID lpParameter) {
  CONST CHAR class_name[] = "TEST_CLASS_NAME";
  CONST CHAR wnd_title[] = "TEST_WND_TITLE";
  HINSTANCE instance = GetModuleHandle(NULL);
  WNDCLASSEX wndclsex;
  MSG msg;

  wndclsex.cbSize        = sizeof(WNDCLASSEX);
  wndclsex.style         = CS_HREDRAW | CS_VREDRAW;
  wndclsex.lpfnWndProc   = WndProc;
  wndclsex.cbClsExtra    = 0;
  wndclsex.cbWndExtra    = 0;
  wndclsex.hInstance     = instance;
  wndclsex.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
  wndclsex.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wndclsex.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
  wndclsex.lpszMenuName  = NULL;
  wndclsex.lpszClassName = class_name;
  wndclsex.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);

  RegisterClassEx(&wndclsex);

  // Possibly disable themes for current session
  if (IsThemeActive()) {
    EnableTheming(FALSE);
  }  

  // An equivalent of in the form of a system call, which enables
  // us to pass a raw UNICODE_STRING structure follows:
  //
  // AppendMenu(menu, MF_STRING, kMenuId, "menu test");
  //
  HMENU menu = CreateMenu();
  MENUITEMINFOW info = { sizeof MENUITEMINFOW, // UINT cbSize
                         MIIM_STRING,          // UINT fMask
                         MFT_STRING,           // UINT fType
                         MFS_ENABLED,          // UINT fState
                         0,                    // UINT wID
                         NULL,                 // HMENU hSubMenu
                         NULL,                 // HBITMAP hbmpChecked
                         NULL,                 // HBITMAP hbmpUnchecked
                         0,                    // ULONG_PTR dwItemData
                         NULL,                 // LPTSTR dwTypeData (ignored)
                         0,                    // UINT cch (ignored)
                         NULL };               // HBITMAP hbmpItem

  UNICODE_STRING item;
  RtlInitUnicodeString(&item, L"menu test");
  item.Length = 0xf - 0x2;

  CallService(__NR_NtUserThunkedMenuItemInfo,
              SYSCALL_ARG(menu),       // HMENU hMenu
              SYSCALL_ARG(1),          // UINT nPosition
              SYSCALL_ARG(TRUE),       // BOOL fByPosition
              SYSCALL_ARG(TRUE),       // BOOL fInsert
              SYSCALL_ARG(&info),      // LPMENUITEMINFOW lpmii
              SYSCALL_ARG(&item),      // PUNICODE_STRING pstrItem
              0, 0);

  HWND hwnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW,
                             class_name,
                             wnd_title,
                             WS_OVERLAPPEDWINDOW | WS_VISIBLE,
                             0, 0, 640, 480,
                             NULL,
                             menu,
                             instance,
                             NULL);

  UpdateWindow(hwnd);

  while (GetMessage(&msg, NULL, 0, 0)) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  return 0;
}
//---------------------------------------------------------------------------
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
  switch(msg) {
    case WM_DESTROY:
      PostQuitMessage(WM_QUIT);
      break;

    default:
      return DefWindowProc(hwnd, msg, wparam, lparam);
  }

  return 0;
}
//---------------------------------------------------------------------------
int main() {
  // Set syscall stub permissions.
  DWORD OldProtect;
  VirtualProtect(SyscallCode, sizeof(SyscallCode), PAGE_EXECUTE_READWRITE, &OldProtect);
  memcpy(&SystemCall, &SyscallCodePtr, sizeof(PVOID));

  // Create host window.
  HANDLE hthread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CreateHostWindow, NULL, 0, NULL);
  CloseHandle(hthread);

  // Terminate local thread.
  ExitThread(EXIT_SUCCESS);
  return EXIT_SUCCESS;
}

6 thoughts on “Windows win32k.sys menus and some “close, but no cigar” bugs”

  1. a) Most of USER and GDI should not be in the kernel at all.
    b) The kernel code is too low-level. It looks like C programming was done 20 years ago. Modern C++ would squash many errors because it supports safer and higher level patterns.

  2. Wouldn’t the code in the first example include a double fetch bug since they use the userland provided input_string->Length to allocate a buffer and then do a memcpy without copying it to kernel land first?

  3. “Modern C++ would squash many errors because it supports safer and higher level patterns.”
    I think most of that stuff was designed for user mode not kernel mode.

  4. Nice review. I Personally tried to take a glance at xxxInsertMenuItem function of Windows 7 64 Win32k, exploiting it with the help of your source code, and it seems that suffer for the bug you have described.
    Very funny and interesting! Thank you for sharing this…

    Great work!
    Andrea

Comments are closed.