Posted by Tavis Ormandy, Security Research Over-Engineer.
“Sometimes, hacking is just someone spending more time on something than anyone else might reasonably expect.”[1]
I often find it valuable to write simple test cases confirming things work the way I think they do. Sometimes I can’t explain the results, and getting to the bottom of those discrepancies can reveal new research opportunities. This is the story of one of those discrepancies; and the security rabbit-hole it led me down.
It all seemed so clear..
Usually, windows on the same desktop can communicate with each other. They can ask each other to move, resize, close or even send each other input. This can get complicated when you have applications with different privilege levels, for example, if you “Run as administrator”.
It wouldn’t make sense if an unprivileged window could just send commands to a highly privileged window, and that’s what UIPI, User Interface Privilege Isolation, prevents. This isn’t a story about UIPI, but it is how it began.
The code that verifies you’re allowed to communicate with another window is part of win32k!NtUserPostMessage. The logic is simple enough, it checks if the application explicitly allowed the message, or if it’s on a whitelist of harmless messages.
BOOL __fastcall IsMessageAlwaysAllowedAcrossIL(DWORD Message) { BOOL ReturnCode; // edx BOOL IsDestroy; // zf ReturnCode = FALSE; if... switch ( Message ) { case WM_PAINTCLIPBOARD: case WM_VSCROLLCLIPBOARD: case WM_SIZECLIPBOARD: case WM_ASKCBFORMATNAME: case WM_HSCROLLCLIPBOARD: ReturnCode = IsFmtBlocked() == 0; break; case WM_CHANGECBCHAIN: case WM_SYSMENU: case WM_THEMECHANGED: return 1; default: return ReturnCode; } return ReturnCode; } |
Snippet of win32k!IsMessageAlwaysAllowsAcrossIL showing the whitelist of allowed messages, what could be simpler.... ? |
I wrote a test case to verify it really is as simple as it looks. If I send every possible message to a privileged window from an unprivileged process, the list should match the whitelist in win32k!IsMessageAlwaysAllowedAcrossIL and I can move onto something else.
![]() |
What the...?! Scanning which messages are allowed across IL produces unexpected results. |
The tool showed that unprivileged applications were allowed to send messages in the 0xCNNN range to most of the applications I tested, even simple applications like Notepad. I had no idea message numbers even went that high!
Message numbers use predefined ranges, the system messages are in the range 0 - 0x3FF. Then there’s the WM_USER and WM_APP ranges that applications can use for their own purposes.
This is the first time I’d seen a message outside of those ranges, so I had to look it up.
The following are the ranges of message numbers.
| ||||||||||||
This is a snippet from Microsoft’s WM_USER documentation, explaining reserved message ranges. |
Uh, string messages?
The documentation pointed me to RegisterWindowMessage(), which lets two applications agree on a message number when they know a shared string. I suppose the API uses Atoms, a standard Windows facility.
My first theory was that RegisterWindowMessage() automatically calls ChangeWindowMessageFilterEx(). That would explain my results, and be useful information for future audits.... but I tested it and that didn’t work!
...Something must be explicitly allowing these messages!
I needed to find the code responsible to figure out what is going on.
Tracking down the culprit...
I put a breakpoint on USER32!RegisterWindowMessageW, and waited for it to return one of the message numbers I was looking for. When the breakpoint hits, I can look at the stack and figure out what code is responsible for this.
$ cdb -sxi ld notepad.exe Microsoft (R) Windows Debugger Version 10.0.18362.1 AMD64 Copyright (c) Microsoft Corporation. All rights reserved. CommandLine: notepad.exe (a54.774): Break instruction exception - code 80000003 (first chance) ntdll!LdrpDoDebuggerBreak+0x30: 00007ffa`ce142dbc cc int 3 0:000> bp USER32!RegisterWindowMessageW "gu; j (@rax != 0xC046) 'gc'; ''" 0:000> g 0:000> r @rax rax=000000000000c046 0:000> k Child-SP RetAddr Call Site 0000003a`3c9ddab0 00007ffa`cbc4a010 MSCTF!EnsurePrivateMessages+0x4b 0000003a`3c9ddb00 00007ffa`cd7f7330 MSCTF!TF_Notify+0x50 0000003a`3c9ddc00 00007ffa`cd7f1a09 USER32!CtfHookProcWorker+0x20 0000003a`3c9ddc30 00007ffa`cd7f191e USER32!CallHookWithSEH+0x29 0000003a`3c9ddc80 00007ffa`ce113494 USER32!_fnHkINDWORD+0x1e 0000003a`3c9ddcd0 00007ffa`ca2e1f24 ntdll!KiUserCallbackDispatcherContinue 0000003a`3c9ddd58 00007ffa`cd7e15df win32u!NtUserCreateWindowEx+0x14 0000003a`3c9ddd60 00007ffa`cd7e11d4 USER32!VerNtUserCreateWindowEx+0x20f 0000003a`3c9de0f0 00007ffa`cd7e1012 USER32!CreateWindowInternal+0x1b4 0000003a`3c9de250 00007ff6`5d8889f4 USER32!CreateWindowExW+0x82 0000003a`3c9de2e0 00007ff6`5d8843c2 notepad!NPInit+0x1b4 0000003a`3c9df5f0 00007ff6`5d89ae07 notepad!WinMain+0x18a 0000003a`3c9df6f0 00007ffa`cdcb7974 notepad!__mainCRTStartup+0x19f 0000003a`3c9df7b0 00007ffa`ce0da271 KERNEL32!BaseThreadInitThunk+0x14 0000003a`3c9df7e0 00000000`00000000 ntdll!RtlUserThreadStart+0x21 |
So.... wtf is ctf? A debugging session trying to find who is responsible for these windows messages. |
The debugger showed that while the kernel is creating a new window on behalf of a process, it will invoke a callback that loads a module called “MSCTF”. That library is the one creating these messages and changing the message filters.
The hidden depths reveal...
It turns out CTF[2] is part of the Windows Text Services Framework. The TSF manages things like input methods, keyboard layouts, text processing and so on.
If you change between keyboard layouts or regions, use an IME like Pinyin or alternative input methods like handwriting recognition then that is using CTF.

The only discussion on the security of Text Services I could find online was this snippet from the “New Features” page:
Posting Komentar