ソードアート・オンライン アリシゼーション 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
が正しい文字列と一致しているかどうかという処理を見つければなんとかなりそうなので eq
や eqz
あたりで探していけばよさそう。
探すと以下のようなものが出てくる。
{func: 9, instr: 297} "i32.eq" "first =" 46 " second =" 65 "result =" 0
今回パラメータ q
に AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
というランダムな文字列を与えていた。
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
というわけでパラメータ q
に wasm_rulez_js_droolz@flare-on.com
を与えると🎉が出力されました。終わり。