CVE-2010-0232の脆弱性を調べてみる

2010 年 1 月に Windows カーネルの権限昇格の脆弱性 CVE-2010-0232 が公開されました(セキュリティアドバイザリ 979682を参照)。発見者 Tavis Ormandy 氏が Full-Dsiclosure に投稿したアドバイザリ実証コード(Proof of Concept:PoC) を見てみると、この脆弱性が詳細に解説されていました。「これは学習用にぴったり!?」と思い、この脆弱性を色々と調べてみました。

なお、この日記を読んだからといって、この脆弱性をきちんと理解できるわけではありません。この脆弱性に興味がある方だけ読んでもらえばいいです(笑)。よりよい調査方法や間違い等があれば指摘ください。

実証コードを実行する

まずは論より証拠。実証コードを実行してみます。SecurityFocus から実証コードをダウンロードして展開します。展開されると、README や ADVISORY, 実証コードのソースファイルがありました。ビルド手順もきちんと記述されています。

// INSTRUCTIONS
//      C:\> nmake
//      C:\> vdmallowed.exe

nmake 等の開発ツールは Visual C++ に同梱されているようなので*1、Visual C++ 2008 Express Edition をインストールしました(インストール手順, Web インストーラ)。nmake 等の開発ツールにパスを通し、nmake を実行しました。ビルドが成功すると、vdmallowed.exe, vdmexploit.dll ができます。これを実行。あ、強制再起動。。。強制再起動がこの実証コードおよび動作環境(Windows XP SP3 の実機)の正しい挙動か確認するため、もう 1 度 vdmallowed.exe を実行してみます。やっぱり強制再起動しました。

試しに VMware Server 1.0.8 で動作している Windows XP SP3 を実行してみると、成功しました(下記画像を参照)。Users グループに所属する demo ユーザ(下記画像の青枠)で実証コードを実行すると、新たにコマンドプロンプトが立ち上がります。新しいコマンドプロンプトの実行ユーザが SYSTEM ユーザであることが分かります(下記画像の赤枠)。


実証コードを読む

実証コードのソースファイル vdmallowed.c, vdmexploit.c を MSDN を参照しながら、どんな処理が実施されるか確認します。ざっと読んだ限りでは、以下のような処理を実施するようです。

  1. vdmallowed.c(実行ファイル vdmallowed.exe のソース)
    1. SYSTEM 権限を付与させるプロセス(cmd.exe)を生成
    2. NTDLL の Ki386BiosCallReturnAddress 関数の最初の16バイトが実証コードの CodeSignatures 配列のものと一致するか確認(CodeSignatures 配列は OS 毎に用意されている)
    3. NTVDM subsystem で起動する 16bit アプリケーション(debug.exe)のプロセスを生成
    4. 16bit アプリケーションプロセスに DLL Injection を実行
    5. Injected DLL の終了ステータスから Exploit の成否を確認
  2. vdmexploit.c(DLL Injection する vdmexploit.dll のソース)
    1. 例外エラーが発生したときに実行させる関数(FirstStage)等を設定して、NTDLL の NtVdmControl 関数を実行
    2. FirstStage 関数が実行され、cmd.exe の実行権限を SYSTEM 権限に書き換える

カーネルデバッグしてみる

せっかくの ring0 exploit なので、デバッガで挙動を追っかけてみます。デバッガは windbg を使用します。Windows カーネルを直接デバッグすることになるため、デバッグするには準備が必要になります。今回は VMware Server 1.0.8 で動作する Windows XP SP3 に名前付きパイプ経由で windbg で接続して、カーネルデバッグします(下記画像)*2

事前準備:実証コードのシンボルファイルの作成

そのままカーネルデバッグすると、実証コード vdmallowed.exe, vdmexploit.dll の関数アドレスが分かりません。カーネルデバッグ時に実証コードである vdmallowed.exe, vdmexploit.dll の関数アドレスを調べるため、実証コードのシンボルファイルを作成します。

実証コードの Makefile の 11-15 行目を以下のように修正します。具体的には、cl.exe の引数に「/link /debug」オプションを指定します。このオプションを指定することで、vdmallowed.exe, vdmexploit.dll のシンボルファイル vdmallowed.pdb, vdmexploit.pdb が生成されます。これらの pdb ファイルを windbg の symbol path に設定しておくことで、vdmallowed.exe, vdmexploit.dll の関数アドレスが確認できます。

vdmallowed.exe: vdmallowed.obj
	cl /Fe$(@F) $(**) /link /debug

vdmexploit.dll: vdmexploit.obj
	cl /Fe$(@F) /LD $(**) /link /debug
カーネルデバッグ

では、「Windowsダンプの極意」を片手に Let's kernel debug!

この脆弱性では #GP Trap Handler を意図的に発生させるようなので、例外トラップ nt!KiTrap0D が呼ばれる過程を調べてみます。

// 実証コードを実行する前に KiTrap0D 関数のアドレスにブレイクポイントを設定
kd> x nt!KiTrap0D
80541d70 nt!KiTrap0D = <no type information>
kd> bp 80541d70
kd> bl
 0 e 80541d70     0001 (0001) nt!KiTrap0D
kd> g

... ここで vdmallowed.exe をコマンドプロンプトから実行する ...

Breakpoint 0 hit
nt!KiTrap0D:
80541d70 f744240c00000200 test    dword ptr [esp+0Ch],20000h

// スタックの情報を表示する。
// nt!KiTrap0D のリターンアドレス(RetAddr)が 80543de2 であることが分かる
kd> kb 
ChildEBP RetAddr  Args to Child              
f5e88ddc 80543de2 f83e8b85 820cf748 00000000 nt!KiTrap0D
f5e88de0 f83e8b84 820cf748 00000000 0000027f nt!KiThreadStartup+0x16
f5e88de4 820cf748 00000000 0000027f 00000000 NDIS!___PchSym_+0xc
WARNING: Frame IP not in any known module. Following frames may be wrong.
f5e88de8 00000000 0000027f 00000000 00000000 0x820cf748

// メモリアドレス 80543dd0 から 80543de6 までをアセンブリコードで表示する
// nt!KiTrap0D のリターンアドレスが 80543de2 であることから、nt!Trap0D が
// 呼び出されたのは、1 つ前の 80543de0 の命令だと分かる
kd> u 80543dd0 80543de6
nt!KiThreadStartup+0x4:
80543dd0 33ff            xor     edi,edi
80543dd2 33ed            xor     ebp,ebp
80543dd4 b901000000      mov     ecx,1
80543dd9 ff151c974d80    call    dword ptr [nt!_imp_KfLowerIrql (804d971c)]
80543ddf 58              pop     eax
80543de0 ffd0            call    eax
80543de2 59              pop     ecx
80543de3 0bc9            or      ecx,ecx
80543de5 7407            je      nt!KiThreadStartup+0x22 (80543dee)

// レジスタを確認すると、eax の値は 56565656 となっている
kd> r
eax=56565656 ebx=00000000 ecx=00000001 edx=ffffffff esi=820e2020 edi=f5e88d64
eip=80541d70 esp=f5e88d84 ebp=f5e88d64 iopl=0         nv up di ng nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000082
nt!KiTrap0D:
80541d70 f744240c00000200 test    dword ptr [esp+0Ch],20000h ss:0010:f5e88d90=00010082

// メモリアドレス 56565656 を調べる
// このアドレスは使われていないことが分かる
kd> dc 56565656
56565656  ???????? ???????? ???????? ????????  ????????????????
56565666  ???????? ???????? ???????? ????????  ????????????????
56565676  ???????? ???????? ???????? ????????  ????????????????
56565686  ???????? ???????? ???????? ????????  ????????????????
56565696  ???????? ???????? ???????? ????????  ????????????????
565656a6  ???????? ???????? ???????? ????????  ????????????????
565656b6  ???????? ???????? ???????? ????????  ????????????????
565656c6  ???????? ???????? ???????? ????????  ????????????????
kd> !address 56565656
address 56565656 not found in any known Kernel Address Range ----

ここまでの windbg の結果から、使用されていないメモリアドレス 56565656 を call したことで、例外エラー 0Dh:General Protection Fault*3が生じたと考えます。80543dd9 に call 命令があり、その直後に pop 命令でスタックの値を eax に設定しています。80543dd9 で call した nt!_imp_KfLowerIrql での処理がくさいですね。

この nt!_imp_KfLowerIrql にブレイクポイントを設定しようとしましたが、この関数のメモリアドレスが分かりませんでした。例外エラーが生じた nt!KiThreadStartup は頻繁に呼び出されている関数であるため、ブレイクポイントを設定すると、すぐに windbg に処理がうつってしまうため、実証コードの実行どころではなくなります:( ここでアプローチを変えてみます。vdmexploit!FirstStage にブレイクポイントを設定して、FirstStage 関数が呼ばれる瞬間をとらえてみます。

ちなみに、この時点から「g」コマンドを複数回実行すると、STOP エラー(いわゆる Blue Screen of Death)が生じます。windbg でブレイクポイント(BreakPoint)を設定しないで、実証コードを実行すると STOP エラーが生じませんでした。STOP エラーを生じるのは、windbgデバッグしている影響だと思います。

... まずは vdmallowed.exe をコマンドプロンプトから実行する ...

// 実証コードに成功すると、以下のデバッグ情報が windbg に出力される。
// vdmexploit.dll の FirstStage 関数で呼び出している DbgPrint 関数が
// このデバッグ情報を出力している
FirstStage() Loaded, CurrentThread @821FD468 Stack F5AA5000 - F5AA1000
Repairing references to 0217D2A4-0217E2A0 in CurrentThread@821FD468...
PsLookupProcessByProcessId(940) => 821C1928
PsInitialSystemProcess @823C2830
PsReferencePrimaryToken(821C1928) => E21389D0
PsReferencePrimaryToken(823C2830) => E10017E8
watchdog!WdUpdateRecoveryState: Recovery enabled.

// DbgPrint 関数のメモリアドレスを調べて、そのアドレスに
// ブレイクポイントを設定する
kd> x nt!DbgPrint
80529e72 nt!DbgPrint = <no type information>
kd> bp 80529e72
kd> bl
 0 e 80529e72     0001 (0001) nt!DbgPrint
kd> g

... 再度 vdmallowed.exe をコマンドプロンプトから実行する ...

Breakpoint 0 hit
nt!DbgPrint:
80529e72 8bff            mov     edi,edi

// シンボルファイルを再読み込みする
kd> !reload
Connected to Windows XP 2600 x86 compatible target at (Sat Feb 27 13:34:39.656 2010 (GMT+9)), ptr64 FALSE
Loading Kernel Symbols
...............................................................
.......................................
Loading User Symbols
............
Loading unloaded module list
....

// 実証コードの Payload に該当する vdmexploit.dll の
// FirstStage 関数のアドレスを調べる
kd> x vdmexploit!FirstStage
10001050 VDMEXPLOIT!FirstStage (void)

// スタック情報を表示する。vdmexploit.dll の FistStage 関数が
// 呼ばれていることが分かる。
kd> kb
ChildEBP RetAddr  Args to Child              
0217d268 100010ad 1000d0bc 820e77f8 f5a0d000 nt!DbgPrint
0217d2c4 00000000 00000000 00000000 00000000 VDMEXPLOIT!FirstStage+0x5d [c:\download\37864\kitrap0d\vdmexploit.c @ 97]

// 「g」コマンドで実証コードの実行を継続する
kd> g
FirstStage() Loaded, CurrentThread @81FF9DA8 Stack F5914000 - F5910000Breakpoint 0 hit
nt!DbgPrint:
80529e72 8bff            mov     edi,edi
kd> g
Repairing references to 0217D2A4-0217E2A0 in CurrentThread@81FF9DA8...Breakpoint 0 hit
nt!DbgPrint:
80529e72 8bff            mov     edi,edi
kd> g
PsLookupProcessByProcessId(812) => 821AEA70Breakpoint 0 hit
nt!DbgPrint:
80529e72 8bff            mov     edi,edi
kd> g
PsInitialSystemProcess @823C2830Breakpoint 0 hit
nt!DbgPrint:
80529e72 8bff            mov     edi,edi
kd> g
PsReferencePrimaryToken(821AEA70) => E1FEC640Breakpoint 0 hit
nt!DbgPrint:
80529e72 8bff            mov     edi,edi
kd> g
PsReferencePrimaryToken(823C2830) => E10017E8
kd> g
watchdog!WdUpdateRecoveryState: Recovery enabled.

...(ここで、実証コードの実行が成功する)...

// vdmexploit.dll の FirstStage 関数にブレイクポイントを
// 設定して、再度実証コードを実行する
kd> bl
 0 e 80529e72     0001 (0001) nt!DbgPrint
kd> bc 0
kd> bl

kd> bp 10001050
kd> bl
 0 e 10001050     0001 (0001) VDMEXPLOIT!FirstStage
kd> g

... 再び、vdmallowed.exe をコマンドプロンプトから実行する ...

FirstStage() Loaded, CurrentThread @81FF9DA8 Stack F58F4000 - F58F0000
Repairing references to 0217D2A4-0217E2A0 in CurrentThread@81FF9DA8...
PsLookupProcessByProcessId(1916) => 822501F8
PsInitialSystemProcess @823C2830
PsReferencePrimaryToken(822501F8) => E1F2D5A8
PsReferencePrimaryToken(823C2830) => E10017E8

... あれ、ブレイクせずに実証コードの実行が成功した ...

実証コードの Payload に該当する vdmexploit.dll の FistStage 関数が呼ばれたことまでは確認できましたが、nt!KiTrap0D から vdmexploit!FirstStage が呼び出される過程は分かりませんでした。

まとめ

今回いろいろ調べた限りでは、nt!KiTrap0D を実行した後、例外エラーが生じたメモリアドレスを eip に設定する際に、本来設定すべきメモリアドレスではなく、FistStage 関数のメモリアドレスが設定されるのだと理解しました。しかし、まだまだ知識不足で vdmexploit!FirstStage を呼び出すまでの過程が追えませんでした。結局、実機の Windows XP SP3 で実証コードを実行すると再起動してしまう原因も不明のまま・・・

実証コードのデバッグは面白い!ただ、まだまだ分からないことが多いですね。今後もデバッグ技術は調べていこうと思います。

参考:「Windowsダンプの極意」における例外に関する記述

Windowsダンプの極意」に例外に関する記述があったため、引用します。

これらの例外が発生すると、OS はその例外をトラップ(罠)します。専用の関数が呼び出され、発生した例外に基づいてさまざまな回復処理を行い、修復できなければ STOP エラーを発生させたりするのです。後で説明しますが、トラップする関数の名前の中に発生した例外のコードも含まれています。

p.35 ”2.12 例外とトラップ”,「Windows ダンプの極意」

「後で説明します」の記述が該当するのは、おそらく以下になります。

スタックトレース上部に、nt!KiTrap0E という関数が表示されていることがわかります。これがトラップフレームです(Trap という文字の後ろに 0E ちおう文字が含まれていますが、これは例外コードになります)。

p.130 ”8.1.1 トラップフレームとエラー発生コードの確認”,「Windows ダンプの極意」

*1:Windows 環境でのプログラミング経験がないから、いつも exploit コードのビルド方法を忘れる

*2:カーネルデバッグ環境の準備については、この日記の本筋ではないため、別途メモとしてまとめる予定です

*3:例外エラーについては、Microsoft のサポート技術情報KB406272を参照