October 15, 2018

Flareon 2018 Challenge5 Web2point0 writeup, wasabi で wasm を動的解析する

ソードアート・オンライン アリシゼーション OP ……1億点!!!
SSSS.GRIDMAN #1 の洗面所……3億点!!!


Webセキュリティでこの先生き残るには WebAssembly もやらないとなぁとぼんやりしていたところ Flareon 2018 で入門にちょうど良さそうな wasm の crackme 的な問題が出ていたのでやってみた。

Web2point0

fetch("test.wasm").then(response =>
  response.arrayBuffer()
).then(bytes =>
  WebAssembly.instantiate(bytes
  ...
).then(results => {
    instance = results.instance;

    let a = new Uint8Array([
        0xE4, 0x47, 0x30, 0x10, 0x61, 0x24, 0x52, 0x21, 0x86, 0x40, 0xAD, 0xC1, 0xA0, 0xB4, 0x50, 0x22, 0xD0, 0x75, 0x32, 0x48, 0x24, 0x86, 0xE3, 0x48, 0xA1, 0x85, 0x36, 0x6D, 0xCC, 0x33, 0x7B, 0x6E, 0x93, 0x7F, 0x73, 0x61, 0xA0, 0xF6, 0x86, 0xEA, 0x55, 0x48, 0x2A, 0xB3, 0xFF, 0x6F, 0x91, 0x90, 0xA1, 0x93, 0x70, 0x7A, 0x06, 0x2A, 0x6A, 0x66, 0x64, 0xCA, 0x94, 0x20, 0x4C, 0x10, 0x61, 0x53, 0x77, 0x72, 0x42, 0xE9, 0x8C, 0x30, 0x2D, 0xF3, 0x6F, 0x6F, 0xB1, 0x91, 0x65, 0x24, 0x0A, 0x14, 0x21, 0x42, 0xA3, 0xEF, 0x6F, 0x55, 0x97, 0xD6

        //0xB6, 0xFF, 0x65, 0xC3, 0xED, 0x7E, 0xA4, 0x00,
        //                     0x61, 0xD3, 0xFF, 0x72, 0x36, 0x02, 0x67, 0x91,
        //0xD2, 0xD5, 0xC8, 0xA7, 0xE0, 0x6E
    ]);

    let b = new Uint8Array(new TextEncoder().encode(getParameterByName("q")));

    let pa = wasm_alloc(instance, 0x200);
    wasm_write(instance, pa, a);

    let pb = wasm_alloc(instance, 0x200);
    wasm_write(instance, pb, b);

    if (instance.exports.Match(pa, a.byteLength, pb, b.byteLength) == 1) {
        // PARTY POPPER
        document.getElementById("container").innerText = "🎉";
    } else {
        // PILE OF POO
        document.getElementById("container").innerText = "💩";
    }
});

問題の概要を雑にまとめると以下のようなもの。

  • 問題の HTML では main.js だけ読み込まれており、そのなかで test.wasm を読み込んでいる。
  • パラメータ q が正しければ🎉が表示される。
  • パラメータ q が正しいかどうかは instance.exports.Match(pa, a.byteLength, pb, b.byteLength) == 1 かどうか

wabt

wasm はバイナリなので人間が分かるレベルでいい感じに解析をしていく必要がある。
そのためのツールキットとして wabt というものがある。

例えば objdump のようなものがあり、関数一覧やディスアセンブルした結果を出力してくれたりする。

$ wasm-objdump -x -j Export ./test.wasm
test.wasm:      file format wasm 0x1

Section Details:

Export[6]:
 - func[48] <Match> -> "Match"
 - func[49] <writev_c> -> "writev_c"
 - table[0] -> "__wasabi_table"
 - memory[0] -> "memory"
 - global[1] -> "__heap_base"
 - global[2] -> "__data_end"
$ wasm-objdump -j Code -d ./test.wasm | grep -A20 Match
005ecf <Match>:
 005ed2: 4b 7f                      | local[0..74] type=i32
 005ed4: 41 0a                      | i32.const 10
 005ed6: 41 7f                      | i32.const 4294967295
 005ed8: 10 01                      | call 1 <begin_function>
 005eda: 23 00                      | get_global 0
 005edc: 41 0a                      | i32.const 10
 005ede: 41 00                      | i32.const 0
 005ee0: 41 00                      | i32.const 0
 005ee2: 23 00                      | get_global 0
 005ee4: 10 02                      | call 2 <get_global_i>
 005ee6: 21 04                      | set_local 4
 005ee8: 41 0a                      | i32.const 10
 005eea: 41 01                      | i32.const 1
 005eec: 41 04                      | i32.const 4
 005eee: 20 04                      | get_local 4
 005ef0: 10 03                      | call 3 <set_local_i>
 005ef2: 41 20                      | i32.const 32
 005ef4: 41 0a                      | i32.const 10
 005ef6: 41 02                      | i32.const 2
 005ef8: 41 20                      | i32.const 32

さて、調べる対象は Match 関数なのだが、ディスアセンブル結果を見ても骨が折れそう。
wabt には wasm2c と呼ばれる wasm から C言語 に変換してくれるツールが同梱されており、これを利用してみる。

$ wasm2c test.wasm -o test.c
$ cat test.c
...
static u32 Match(u32 p0, u32 p1, u32 p2, u32 p3) {
  u32 l0 = 0, l1 = 0, l2 = 0, l3 = 0, l4 = 0, l5 = 0, l6 = 0, l7 = 0,
      l8 = 0, l9 = 0, l10 = 0, l11 = 0, l12 = 0, l13 = 0, l14 = 0, l15 = 0,
      l16 = 0, l17 = 0, l18 = 0, l19 = 0, l20 = 0, l21 = 0, l22 = 0, l23 = 0,
      l24 = 0, l25 = 0, l26 = 0, l27 = 0, l28 = 0, l29 = 0, l30 = 0, l31 = 0,
      l32 = 0, l33 = 0, l34 = 0, l35 = 0, l36 = 0, l37 = 0, l38 = 0, l39 = 0,
      l40 = 0, l41 = 0, l42 = 0, l43 = 0, l44 = 0, l45 = 0, l46 = 0, l47 = 0,
      l48 = 0, l49 = 0, l50 = 0, l51 = 0, l52 = 0, l53 = 0, l54 = 0, l55 = 0,
      l56 = 0, l57 = 0, l58 = 0, l59 = 0, l60 = 0, l61 = 0, l62 = 0, l63 = 0,
      l64 = 0, l65 = 0, l66 = 0, l67 = 0, l68 = 0, l69 = 0, l70 = 0, l71 = 0,
      l72 = 0, l73 = 0, l74 = 0;
  FUNC_PROLOGUE;
  u32 i0, i1, i2, i3, i4, i5, i6, i7,
      i8, i9, i10, i11, i12;
  i0 = 10u;
  i1 = 4294967295u;
  (*Z___wasabi_hooksZ_begin_functionZ_vii)(i0, i1);
  i0 = g0;
  i1 = 10u;
  i2 = 0u;
  i3 = 0u;

...

が、このように、これもまた読めたものではない…
なので一度 gcc -m32 -shared -o flareon_level5 -I$PWD/wasm2c wasm2c/wasm-rt-impl.c test.c で ELF にして IDA にかけてしまえば超読みやすいC言語でデコンパイルしてくれるのだが、デコンパイル付きIDAを購入できるほど富豪ではない…

今回のような crackeme 的なものはだいたいメモリ上でアレコレしているので、私のような貧民は wasabi を使って動的解析します。はい。

wasabi がどのように動いているか理解できていないが、http://wasabi.software-lab.org/ を読む限り実行時にコードをインジェクション(フック?)してデバッグすることができるようになる。
例えば log-all.js を読み込むと命令に渡される引数などを console.log に表示してくれたりする。

まずは wasabi で test.wasm から2つのファイルを生成する。

$ wasabi test.wasm -o out/
$ ls out/
test.wasabi.js  test.wasm
  • test.wasm : 命令の間にフック関数が挿入された wasm バイナリ
  • test.wasabi.js : wasabi のローダ&ランタイム。一緒に生成された wasm バイナリに依存する JavaScript

これらと log-all.js を一緒に読み込んでみる。

<body>
  <script src="./main.js"></script>
  <script src="./test.wasabi.js"></script>
  <script src="./log-all.js"></script>
</body>

するとこんな感じで命令ごとにデバッグログが出力される。

{func: 10, instr: -1} "begin" "function"
log-all-all.js:97 {func: 10, instr: 0} "get_global" "global #" 0 "value =" 66592
log-all-all.js:93 {func: 10, instr: 1} "set_local" "local #" 4 "value =" 66592
log-all-all.js:65 {func: 10, instr: 2} "const, value =" 32
log-all-all.js:93 {func: 10, instr: 3} "set_local" "local #" 5 "value =" 32
log-all-all.js:93 {func: 10, instr: 4} "get_local" "local #" 4 "value =" 66592
log-all-all.js:93 {func: 10, instr: 5} "get_local" "local #" 5 "value =" 32
log-all-all.js:73 {func: 10, instr: 6} "i32.sub" "first =" 66592 " second =" 32 "result =" 66560
log-all-all.js:93 {func: 10, instr: 7} "set_local" "local #" 6 "value =" 66560
log-all-all.js:93 {func: 10, instr: 8} "get_local" "local #" 6 "value =" 66560
log-all-all.js:97 {func: 10, instr: 9} "set_global" "global #" 0 "value =" 66560
log-all-all.js:65 {func: 10, instr: 10} "const, value =" 0
log-all-all.js:93 {func: 10, instr: 11} "set_local" "local #" 7 "value =" 0
log-all-all.js:65 {func: 10, instr: 12} "const, value =" 11
log-all-all.js:93 {func: 10, instr: 13} "set_local" "local #" 8 "value =" 11
log-all-all.js:93 {func: 10, instr: 14} "get_local" "local #" 6 "value =" 66560
log-all-all.js:93 {func: 10, instr: 15} "get_local" "local #" 8 "value =" 11
log-all-all.js:73 {func: 10, instr: 16} "i32.add" "first =" 66560 " second =" 11 "result =" 66571
log-all-all.js:93 {func: 10, instr: 17} "set_local" "local #" 9 "value =" 66571
log-all-all.js:93 {func: 10, instr: 18} "get_local" "local #" 9 "value =" 66571
log-all-all.js:93 {func: 10, instr: 19} "set_local" "local #" 10 "value =" 66571

WebAssembly の opcode は WebAssembly/design にまとまっている。

今回は Match 関数内でパラメータ q が正しい文字列と一致しているかどうかという処理を見つければなんとかなりそうなので eqeqz あたりで探していけばよさそう。
探すと以下のようなものが出てくる。

{func: 9, instr: 297} "i32.eq" "first =" 46 " second =" 65 "result =" 0

今回パラメータ qAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA というランダムな文字列を与えていた。
man ascii すれば分かるが 65 = A なので、あたりっぽい。

というわけで opcode を i32.eq であるものでフィルタをかけると以下のような出力になる。

{func: 9, instr: 297} "i32.eq" "first =" 119 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 97 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 115 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 109 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 95 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 114 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 117 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 108 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 101 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 122 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 95 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 106 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 115 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 95 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 100 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 114 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 111 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 111 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 108 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 122 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 64 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 102 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 108 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 97 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 114 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 101 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 45 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 111 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 110 " second =" 65 "result =" 0
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 46 " second =" 65 "result =" 0

あとはコードポイントを変換する。

以下を flag.js として読み込む。

flag = '';

Wasabi.analysis = {
    binary(location, op, first, second, result) {
      if (op === "i32.eq") {
        console.log(location, op, "first =", first, " second =", second, "result =", result);
        flag += String.fromCharCode(first);
        console.log(flag);
    }
  },
};

すると以下のような出力となり flag を得ることができた。

{func: 9, instr: 297} "i32.eq" "first =" 119 " second =" 65 "result =" 0
log-all.js:8 w
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 97 " second =" 65 "result =" 0
log-all.js:8 wa
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 115 " second =" 65 "result =" 0
log-all.js:8 was
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 109 " second =" 65 "result =" 0
log-all.js:8 wasm
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 95 " second =" 65 "result =" 0
log-all.js:8 wasm_
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 114 " second =" 65 "result =" 0
log-all.js:8 wasm_r
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 117 " second =" 65 "result =" 0
log-all.js:8 wasm_ru
... (snip) ...
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 111 " second =" 65 "result =" 0
log-all.js:8 wasm_rulez_js_droolz@flare-o
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 110 " second =" 65 "result =" 0
log-all.js:8 wasm_rulez_js_droolz@flare-on
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 46 " second =" 65 "result =" 0
log-all.js:8 wasm_rulez_js_droolz@flare-on.
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 99 " second =" 65 "result =" 0
log-all.js:8 wasm_rulez_js_droolz@flare-on.c
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 111 " second =" 65 "result =" 0
log-all.js:8 wasm_rulez_js_droolz@flare-on.co
log-all.js:6 {func: 9, instr: 297} "i32.eq" "first =" 109 " second =" 65 "result =" 0
log-all.js:8 wasm_rulez_js_droolz@flare-on.com

というわけでパラメータ qwasm_rulez_js_droolz@flare-on.com を与えると🎉が出力されました。終わり。

このエントリーをはてなブックマークに追加

© Kouhei Morita 2018