CTFするぞ

あたまよくないけどがんばります

WebKit Exploitに対する防御機構と回避法

はじめに

少し前のFireShell CTFでwebkitのexploitを書いたのですが、一部作問ミスを利用して解いたのでexploitを書き直しました。 その際、webkitのセキュリティ機構を2つ回避する必要があったので、それらの回避法について調べました。 この辺は日本語記事が皆無なのでまとめようと思います。 Goyotanくんがstructure idのリークに関する非常に分かりやすい記事を書いてくれました。

goyotan.hateblo.jp

※今回紹介するStructure IDのリーク手法は今から2週間程前の以下のコミットで対策されたそうです。(Thank you @y0ny0ns0n for the information!)

github.com

基本知識

だいたい経験則で書いてるから間違ってるかも。

WebKitのオブジェクト

webkitのオブジェクトの構造はliveoverflowの動画が一番分かりやすいと思うので、そちらを参照してください。 butterflyという特徴的な構造以外は特に変なことはしていません。 v8と違ってポインタもちゃんと8バイトで保持しています。 (liveoverflowでは古いバージョンで検証しており、今回紹介する防御機構は一切入っていないので注意。)

butterfly

webkitで特徴的なのはbutterflyというポインタです。 butterflyは配列の先頭を指しています。配列はアドレスが高い方に向かって伸びていきます。 butterflyの指すアドレスの1つ前に配列の長さがあり、さらにアドレスが低い方に向かってプロパティが入ります。

f:id:ptr-yudai:20200422181851p:plain

JSCellにはオブジェクトの状態や型などの情報が含まれています。 JSCellの先頭4バイトはstructure idというオブジェクトを区別するユニークな番号が書かれています。 これについては後述しますが、exploitを書く上で非常に重要となります。

CopyOnWrite

次のように配列などを作ると、CopyOnWriteという状態になります。

var a = [1.1, 2.2, 3.3];

これは配列への書き込みが発生していない状態で、constantな配列です。 書き込みが発生するとオブジェクトはコピーされ、それに伴いアドレスも変わります。 そのためfakeobj等に使う配列は、作った直後に

a.p0 = 3.14

のように書き込んでCopyOnWriteを外す必要があります。

ArrayWithInt32 / ArrayWithDouble / ArrayWithContiguous

次のように、配列に特定の型の値のみが入っている場合、その型のみを格納できる配列として認識されます。

var a = [1, 2, 3] // ArrayWithInt32
var b = [1.1, 2.2, 3.3] // ArrayWithDouble

いずれもbutterflyが配列の要素を指しているという点は変わりません。

一方、次のようにオブジェクトやnullなどが入ると、ArrayWithContiguousという型になります。

var a = [3.14, 2.71, {}] // ArrayWithContiguous

これもbutterflyに配列のアドレスが入りますが、配列の要素は整数だったり小数だったりポインタだったりします。 いずれも8バイトですが、webkitはこれらを次のような規則で区別します。

Pointer {  0000:PPPP:PPPP:PPPP
         / 0001:****:****:****
Double  {         ...
         \ FFFE:****:****:****
Integer {  FFFF:0000:IIII:IIII

nullやundefinedは特定の値(ポインタとして有り得ない値)が割り当てられています。 詳しい仕様はソースコードを参考にしてください。 doubleの上位16ビットが0000やFFFFにならないよう、webkitはdouble値に0x200000000000を足した値を格納します。 これはexploitを書く上で厄介で、type confusionが起きてもdouble値をポインタとして認識させられません。

Float64Array / Uint64Array

Float64ArrayやUint64ArrayはArrayWithDoubleなどとは別なので注意してください。

var a = new Float64Array(1) // Float64Array
var b = new Uint64Array(1) // Uint64Array

これを使って作られた配列は、butterflyではなくvectorに配列のポインタが入ります。 配列の長さはlengthに入ります。

Inline Slot

先に示した図ではbutterflyの次にvectorやmode, lengthなどがあり、Float64Arrayなどで使われました。 一方、次のようなプロパティのみを持つオブジェクトはどうでしょうか。

var a = {}
a.p0 = 3.14
a.p1 = 2.71

このようなオブジェクトはNonArrayとして認識され、p0の値はvectorの部分に、p1の値はmodeとlengthの部分に、という具合に値がJSObjectにそのまま入ります。 ややこしいですが、この構造はstructure idのリークでも使う重要な仕組みです。

addrof / fakeobj primitive

addrofとはその名の通り、指定したオブジェクトのアドレスを取得するものです。 一方でfakeobjは、指定した値をアドレスとするJSObjectを作るものです。 つまり、

addr == addrof(fakeobj(addr))
obj == fakeobj(addrof(obj))

が成り立ちます。

Structure ID Randomization

WebKitはすべてのオブジェクトにStructure IDが付いています。 プロパティなどはこのIDを元に参照され、型の情報なども含まれています。 そのため、fakeobjで作ったオブジェクトのStructure IDが正しくないと使い物になりません。 私は最近Browser Exploitを始めたので実情は知りませんが、従来はこのIDを予測することができました。 というのも、IDは1から(?)順に割り当てられるので、例えば最初に500個オブジェクトを作って、IDを250くらいにすれば大抵当たるので問題ありませんでした。 しかし、Structure IDが最近ランダムになったので、IDのguessはできなくなり、リークする必要が出てきました。

この辺の歴史も含めてGoyotan先生がまとめているのでそっちを参考にすると良いと思います。

Symbolを利用したリーク

元ネタはここです。 GCが働くまで不正なIDのオブジェクトは別にクラッシュを起こす訳ではありません。 ここで、Symbolオブジェクトとかいう謎の型(?)があるのですが、これは次のように文字列を格納します。

f:id:ptr-yudai:20200422202931p:plain

嬉しいことにSymbolはJSCellのtypeを使って型を判別するので、偽のSymbolオブジェクトに対してtoStringが適用できます。 つまり、次のようなリンクを作れば任意のオブジェクトのIDをリークできることが分かります。

f:id:ptr-yudai:20200422203620p:plain

なのですが、何故か上手く動かなかったです。 この方法でリークできた人いたら教えて。

JSFunctionを利用したリーク

JSFunctionはソースコードを文字列として保持しています。 これもSymbolと同じようにstructure idはチェックされないので、toStringが適用できます。

f:id:ptr-yudai:20200422204949p:plain

Symbolと同じような原理で、これを使ってもstructure idがリークできることが分かるでしょう。 文献のスライドでは次のようなリンクでbutterflyに対象のオブジェクトを置いています。

f:id:ptr-yudai:20200422205255p:plain

sprayよりも綺麗なのでこの手法は普通に便利だと思います。

Gigacage

無事structure idを取得したので、TypedArrayのidとfakeobj primitiveを使って作ればAAR/AAWが実現できそうです。 しかし、最近のwebkitにはgigacageと呼ばれるセキュリティ機構が付いており、これが上手くいきません。

仕組み

gigacageではオブジェクトの種類によって別のヒープ領域(HeapKinds)を使います。 Gigacageを提案するこの文献では3つのHeapKindsがあります。

  • Primary: gigacageにより保護されない通常の領域
  • PrimitiveGigacage: primitiveで連続な配列用のメモリ(よく分からん)
  • JSValueGigacage: butterflyの指すデータを格納する領域

アクセス前に領域が正しいか検証・修正されるため、別の領域へのアクセスはできません。 悲しいかな。

私がソースコードを眺めた限り、PrimaryとJSValueは少なくとも実装されているようです。 Primitiveは見つからず、NumberOfKindsというのがありましたが、使われているのかよく分かりません。 イメージはこんな感じです。

f:id:ptr-yudai:20200422213303p:plain

しかし、すべてのJSObjectのbutterflyが分離されるわけではなく、あくまで「危険」なオブジェクトのみ分離しているそうです。

回避法1:Objectのbutterflyを共有する

Objectのbutterflyはgigacageにより分離されません。 そのため、次のようなリンクが可能になります。

f:id:ptr-yudai:20200422223751p:plain

fakeのbutterflyがvictimを指しているので、fakeの要素を書き換えるとvictimのbutterflyを書き換えることができます。 なお、書き込み先アドレス(偽装したbutterfly)のlengthが正しい値とは限らないので、実際にはbutterflyを書き込みたいアドレス+0x10とし、プロパティを読み書きします。 また、これを利用して新たにaddrofやfakeobj primitiveを作る場合はunboxed, boxedという2つのオブジェクトを用意します。 これはそれぞれDoubleArray, ContiguousArrayで、2つともbutterflyが同じアドレスを指すようにします。 アドレスを書き込む際(fakeobj)はboxedを使い、アドレスを読み込む際(addrof)はunboxedを使います。

回避法1(改):butterflyをリークする

たぶんこれが一番簡単だと思います。

前の節でstructure idのリーク方法を説明しました。 structure idはJSCellに入っていますが、その次のqwordはbutterflyポインタです。 したがって、同じ方法で任意のオブジェクトのbutterflyをリークできます。 オブジェクトのbutterflyポインタを持っていると次のような偽のオブジェクトを作って簡単にAAR/AAWが実現できます。

f:id:ptr-yudai:20200422232100p:plain

ArrayWithDoubleなevilオブジェクトを用意しておき、そのstructure idをリークするついでにbutterflyも読みます。 fakeobjで同じbutterflyの指すアドレスにArrayWithContiguousなvictimオブジェクトを作ります。

evil[1]を書き換えることでvictimのbutterflyを任意の値に変えられます。 victimのstructure idが正しい必要があるので、evil[0]を書き換えてstructure idをevilと同じものにします。 すると、victimもArrayWithDoubleとして認識され、butterflyの指す先を操作できます。

回避法2:WebAssembly.Memoryを利用する

WebAssemblyのMemoryも同様にgigacageで分離されません。 Memoryはポインタを持つので、それを書き換えることで任意のアドレスを読み書きできます。

f:id:ptr-yudai:20200422232856p:plain

眠くてだんだん適当になりつつありますが、両方ともfakeobjです。 vectorやmemoryなどはdeleteして0にします。(webkitではundefinedは0と定義されている。) sizeToReadやsize, initialSizeは適当に大きな値にします。

このwasmBufferをmemに持ち、いい感じにAAR/AAWを実装したwasm moduleを作ってやれば良いです。 若干面倒ですね。

まとめ

WebKitのsecurity mitigationとしてStructureID RandomizationとGigacageがありますが、両方とも回避が可能です。 FireShell CTF 2020の"The Return of the Side Effect"というJITのバグを突く問題で知ったセキュリティ機構で、他にもあるのかは知りません。 回避法は作問者の方に教えていただきました。 たった数行を消しただけでRCEができるという非常に面白い問題なので、是非試してみてください。

参考文献