
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 に到達します。extraData == 0cbwndExtra を使用してウィンドウクラスを登録するときに設定できます。ExtraData は tagWND 構造体の直後に追加されます(私は、逆コンパイルされたコードを少し見やすくするために、このフィールドを IDA の tagWND 構造体に sizeof(tagWND) オフセットの QWORD として追加しました)。その値は SetWindowLongPtr の呼び出しで設定できます。これらの条件がすべて満たされると、ウィンドウの 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 の投稿で言及されている特別なスイッチウィンドウを作成すると、まさにそれが実現されることがわかりました。
- ウィンドウが破棄されていないかどうかを確認。
このケースでは既に満たされています。