2019-12-22に開催されたSECCONの国際決勝に、hiww , h_noson , st98 ともにHarekaze として参加しました。(昨年 は国内決勝に参加してました。)
順位はあまり振るわず、14チーム中11位でしたが、個人的には問題4の"box"のattackフラグを全部回収できたので大満足です。ということで、問題4の私なりの解き方についてまとめておきます。
"box"問題概要
今回は6つ大問がありましたが、そのうちの4問目でした。問題としては、stripされたx86 バイナリと、それに特定の入力を与えた際の分岐トレース結果が配布されるので、同一の分岐トレースが得られるような入力をみつける、というものです。
……あれ?この話前にも聞いたことがありますね。そうです、今年の予選では、完全に同様の形式の問題(follow-me)が出されていたのでした。
hikalium.hatenablog.jp
ということで、あとはやるだけです。
Attackフラグとしては、box1から4と名付けられた4つの問題がダウンロードできるので、それを解いて入力をAPI に投げるとフラグが得られます。
Defenceフラグとしては、box1に類似した問題が1時間ごとに出され、それをその時間内に解いてAPI に投げるとチームのtokenを設置できるのですが、ちょっと厳しかったので諦めました(後述)。
では、box1から4まで順に見ていきましょう。
box1
入力形式の特定
とりあえず普通のLinux バイナリなので実行してみます。
$ ./box
usage: ./box formula
なるほど…式を引数で渡すんですね。
$ ./box '1+3'
error: unhandled char '+'
うーん、だめですね。じゃああれかな、予選の時と同じ入力形式なのかな?
./box '1,3,+'
error: unhandled char ','
違うみたい…。(ここから試行錯誤することしばらく。)
$ ./box ''
error: stack is empty
$ ./box '0'
error: stack is empty
$ ./box '00'
error: stack is empty
$ ./box '000'
error: stack is empty
$ ./box '0000'
0
あー…なるほど?(さらにしばらく試行錯誤して。)
$ ./box '00010002a'
3
$ ./box '00010002b'
-1
$ ./box '00010002c'
2
$ ./box '00010002d'
1
$ ./box '00010002e'
2
$ ./box '00010002f'
5
$ ./box '00010002g'
error: unhandled char 'g'
なるほど、演算子 はa-fなんですね。(なぜアセンブリ を読まないのか(つかれてたので))。
トレースをみてみる
$ cat box.trace
[
{"event": "image_load", "image_name": "***/box1", "image_id": 1, "base_addr": "0x557a93890000", "image_size": "0x135f"},
{"event": "image_load", "image_name": "/lib64/ld-linux-x86-64.so.2", "image_id": 2, "base_addr": "0x7fb95ac8c000", "image_size": "0x26c23"},
{"event": "image_load", "image_name": "[vdso]", "image_id": 3, "base_addr": "0x7ffc905a6000", "image_size": "0x100a"},
{"event": "image_load", "image_name": "/lib/x86_64-linux-gnu/libc.so.6", "image_id": 4, "base_addr": "0x7fb9464a4000", "image_size": "0x3f0adf"},
{"event": "branch", "inst_addr": "0x557a9389064e", "branch_taken": true},
{"event": "branch", "inst_addr": "0x557a93890ed4", "branch_taken": false},
{"event": "branch", "inst_addr": "0x557a938907f5", "branch_taken": true},
{"event": "branch", "inst_addr": "0x557a93890786", "branch_taken": true},
{"event": "branch", "inst_addr": "0x557a93890ef4", "branch_taken": false},
{"event": "branch", "inst_addr": "0x557a938906b0", "branch_taken": true},
{"event": "branch", "inst_addr": "0x557a93890690", "branch_taken": true},
{"event": "branch", "inst_addr": "0x557a93890e3f", "branch_taken": true},
{"event": "branch", "inst_addr": "0x557a938906c0", "branch_taken": true},
...
]
こんな感じでJSON が書かれています。
base_addrは最初の要素に書いてあるので、ちゃんとやるなら引き算をすれば、適当にするなら下4桁をとればファイル内のアドレスになりそうです。
というわけで、objdumpと照らし合わせればすべてわかりそう…でもせっかくトレーサーのソースも与えられているし、あとのことも考えるとトレーサーの環境を用意した方がよくない?という気持ちになったので用意することに。
トレーサーの環境構築
問題で配布されているファイルの中には、トレース出力を得るために使ったトレーサーのソースも含まれていました。なので、これを実行したいのですが、これはIntel Pin というライブラリに依存しているので、適当にPinのソースコード を落としてきてビルドします。そして、Pinを用いたサンプルコードのディレクト リに行って、サンプルのうちひとつをコピーし、それのソースとMakefile を書き換えて、トレーサーのライブラリファイルをビルドしました。
/path/to/pin/source/tools/branchtrace $ ls
branchtrace.cpp branchtrace.out debugtrace.cpp makefile makefile.rules obj-intel64
/path/to/pin/source/tools/branchtrace $ ls obj-intel64/
branchtrace.o branchtrace.so
このbranchtrace.so
というのがトレーサーになります。
トレーサーの実行
branchtrace.so
を直接実行することはできないので、pinを介して呼び出します。以下のような感じで。
q4/box1$ /path/to/pin/pin -t /path/to/pin/source/tools/branchtrace/obj-intel64/branchtrace.so -- ./box '00010002a'
3
q4/box1$ cat branchtrace.out | head -n 7
[
{"event": "image_load", "image_name": "/vagrant/seccon2019df/q4/box1/box", "image_id": 1, "base_addr": "0x55aa08e61000", "image_size": "0x135f"},
{"event": "image_load", "image_name": "/lib64/ld-linux-x86-64.so.2", "image_id": 2, "base_addr": "0x7f4f2c012000", "image_size": "0x26c23"},
{"event": "image_load", "image_name": "[vdso]", "image_id": 3, "base_addr": "0x7fff31bb3000", "image_size": "0x100a"},
{"event": "image_load", "image_name": "/lib/x86_64-linux-gnu/libc.so.6", "image_id": 4, "base_addr": "0x7f4f1782a000", "image_size": "0x3f0adf"},
{"event": "branch", "inst_addr": "0x55aa08e6164e", "branch_taken": true},
{"event": "branch", "inst_addr": "0x55aa08e61ed4", "branch_taken": false},
できましたね!
結果を整形する
pinの出力してくれるtrace結果はまあまあ綺麗なのですが、各分岐のアドレスがイメージ内の相対ではないので、配布されたものと手元では値が異なり比較が面倒です。
ということで、ささっとスクリプト を書いて整形してくれるようにしましょう。私はNodejsが好きな変人なのでnodejsで書いたprint.js
がこちら。
const fs = require('fs');
const filename = process.argv[2];
const parseTrace =
(fileName) => {
const trace = JSON.parse(fs.readFileSync(fileName, 'utf-8'));
const base_addr = parseInt(trace[0].base_addr, 16);
const branches = trace.filter(e => (e.inst_addr != undefined)).map(e => {
if (e.event === 'call') {
return {
'addr': (parseInt(e.inst_addr, 16) - base_addr).toString(16),
'target': (parseInt(e.target_addr, 16) - base_addr).toString(16)
};
}
return {
'addr': (parseInt(e.inst_addr, 16) - base_addr).toString(16),
'taken': e.branch_taken
};
});
return branches;
}
const print =
(branches) => {
for (var i = 0; i < branches.length;) {
const b = branches[i];
var count = 0;
while (i < branches.length && branches[i].addr == b.addr &&
branches[i].taken == b.taken) {
count++;
i++;
}
console.log(`${JSON.stringify(b)} * ${count}`);
}
console.log(branches.length);
}
const branches = parseTrace(filename);
print(branches);
最初のほうでは気づかなかったのですが、同じ分岐をぐるぐる回る際もけっこうあって、それを *N
と表示した方がわかりやすかったので、そうしていたりします。(色々適当に工夫しながらできあがった完成形。)
また、あとのほうでjmp ccだけでなくcallも出てきたので、それにも一応対応してます。
実行するとこんな感じです。
q4/box1$ node print.js box.trace | head -n 5
{"addr":"64e","taken":true} * 1
{"addr":"ed4","taken":false} * 1
{"addr":"7f5","taken":true} * 1
{"addr":"786","taken":true} * 1
{"addr":"ef4","taken":false} * 1
よいよい、みやすくなった。
入力と分岐の対応関係を調べる
本当ならファジングなり、静的解析でツールを使うなり、ちゃんとやったほうが早いと思うのですが、私はobjdumpしかわからないので、バイナリのobjdumpを横においておきながら、入力と分岐を変化させて、トレーサーをぶん回しながら、どういう挙動をしているのか調べます。
メモの断片:
XXXXYYYYa
{"addr":"ddc","taken":true},
{"addr":"bac","taken":false},
{"addr":"bb6","taken":false},
{"addr":"964","taken":true},
{"addr":"964","taken":true},
{"addr":"a21","taken":true},
{"addr":"a31","taken":true},* Y+1 times
{"addr":"9c4","taken":true},
{"addr":"bf5","taken":true},
00020001b
{"addr":"ddc","taken":true},
{"addr":"bac","taken":false},
{"addr":"bb6","taken":true},
{"addr":"bfe","taken":false},
{"addr":"964","taken":true},
{"addr":"964","taken":true},
{"addr":"9c4","taken":true},
{"addr":"c3d","taken":true},
00200010c
{"addr":"ddc","taken":true},
{"addr":"bac","taken":false},
{"addr":"bb6","taken":true},
{"addr":"bfe","taken":true},
{"addr":"c46","taken":false},
{"addr":"964","taken":true},
{"addr":"964","taken":true},
{"addr":"a5c","taken":true},
{"addr":"a82","taken":true},
{"addr":"a94","taken":true}, // opR + 1
{"addr":"9c4","taken":true},
{"addr":"c85","taken":true},
なるほど、結局のところ
XXXXYYYYa -> XXXX+YYYY
XXXXYYYYb -> XXXX-YYYY
XXXXYYYYc -> XXXX*YYYY
XXXXYYYYd -> min(XXXX, YYYY)
XXXXYYYYe-> max(XXXX, YYYY)
という感じの逆ポーランド記法 電卓なんですねー(X, Yは数字)
あと、addとmulでは、右辺の値に対応して、途中の分岐の呼ばれる数が変わるんですねーということがわかりました。
ということで、適当に合いそうな入力を考えて、トレース……よし、合っていそう。では、投げましょう。私はこんな入力をえらびました。
001000000012ce0010a0000b
わーい!次にいきましょうー
box2
なんだ楽勝じゃん、次もどうせ入力が違うだけなのでは?と思ったのですが
$ ./box
usage: ./box input
どうも入力はformura、つまり式ではないようです。色々入れてみましょう。
$ ./box hoge
_t
$ ./box hogehoge
#^L8
$ ./box hogehogehoge
ɿAV
$ ./box hogehogehogefuga
@%Zyr
$ ./box hogehogehogefugassssssssssssssssssssssssss
-2"qC
$ ./box hogehogehogefugasssssssssssssssssssssssssssssssssssssss
N?sG>*AA^8>PwvƴP
$ ./box hogehogehogefugassssssssssssssssssssssssssssssssssssssssssss
z:R
Rc-OgGOx
おお?なんか印字不能 な文字を出してくるときもありますね。それに、入力と出力の対応も不明です。困った。
とりあえずobjdump...と。
8c9: c6 45 e0 27 movb $0x27,-0x20(%rbp)
8cd: c6 45 e1 51 movb $0x51,-0x1f(%rbp)
8d1: c6 45 e2 90 movb $0x90,-0x1e(%rbp)
8d5: c6 45 e3 79 movb $0x79,-0x1d(%rbp)
8d9: c6 45 e4 66 movb $0x66,-0x1c(%rbp)
8dd: c6 45 e5 b7 movb $0xb7,-0x1b(%rbp)
8e1: c6 45 e6 25 movb $0x25,-0x1a(%rbp)
8e5: c6 45 e7 61 movb $0x61,-0x19(%rbp)
8e9: c6 45 e8 45 movb $0x45,-0x18(%rbp)
8ed: c6 45 e9 63 movb $0x63,-0x17(%rbp)
8f1: c6 45 ea c3 movb $0xc3,-0x16(%rbp)
8f5: c6 45 eb f8 movb $0xf8,-0x15(%rbp)
8f9: c6 45 ec f4 movb $0xf4,-0x14(%rbp)
8fd: c6 45 ed 96 movb $0x96,-0x13(%rbp)
901: c6 45 ee a5 movb $0xa5,-0x12(%rbp)
905: c6 45 ef 2e movb $0x2e,-0x11(%rbp)
909: 48 8b 85 00 ff ff ff mov -0x100(%rbp),%rax
910: 48 8b 40 08 mov 0x8(%rax),%rax
914: 48 89 85 18 ff ff ff mov %rax,-0xe8(%rbp)
91b: 48 8b 85 18 ff ff ff mov -0xe8(%rbp),%rax
922: 48 89 c7 mov %rax,%rdi
925: e8 86 fd ff ff callq 6b0 <strlen@plt>
なんか16回movしてそのあとstrlenしている…16という数字があやしいな?
と思いながらトレースをしていたところ、以下の事実を発見した。
$ node trace.js ./box '0' | tail -n 1
821
$ node trace.js ./box '0123' | tail -n 1
821
$ node trace.js ./box '0123456789abcdef' | tail -n 1
821
$ node trace.js ./box '0123456789abcdefg' | tail -n 1
1535
$ node trace.js ./box '0123456789abcdefghi' | tail -n 1
1535
$ node trace.js ./box '0123456789abcdef0123456789abcdef' | tail -n 1
1535
$ node trace.js ./box '0123456789abcdef0123456789abcdef0' | tail -n 1
2249
ここでtrace.jsは、私が適当に書いたトレーサーをラップするスクリプト で、最後にトレースの総分岐数が出てくるのですが、なぜか入力が16文字増えるごとに段階的に分岐数が増える。しかも、トレース結果をみると、同じ分岐回数なら、分岐の内容は一切変わっていない。
16文字の境界を超えるごとに、分岐数は1535-821 == 2249 - 1535 == 714
増えるから、つまるところ16*N+1
以上16*N+16
文字(N >=0)
の入力を与えた時、その総分岐数は821+N*714
になるというわけです。
さて、与えられたtraceの総分岐数は8675であったから、この式で逆算すると(8675-821)/714 => 11
となるわけです。おお、まじか、ぴったり整数じゃん!(ここで嬉しい気分になる。)
というわけで、気軽に16*11+16 => 192
文字のファイルを生成して送りつけました。わーい!
box3
box2がさくっと終わったので、どんどんやっていこうーとなった流れで到達したbox3は、めっちゃ楽しかった。だって
$ ./box
usage: ./box filename
と出たので、適当にtouchしたファイルを食べさせたら
./box empty.txt
[!] Not implemented: code=0
EAX = 00000000
ECX = 00000000
EDX = 00000000
EBX = 00000000
ESP = 00007c00
EBP = 00007c00
ESI = 00000000
EDI = 00000000
EIP = 00007c00
見慣れたレジスタ 名が出てきたんですもの。しかも、0x7c00!!!!!(この嬉しさを理解したい方は30日でできるOS自作入門かIntel SDMでも読んでおいてください。)
これは十中八九エミュレーター だろう、と思ったので、早速NOPを食べさせると
$ echo '90' | xxd -r -p > nop.bin
$ ./box nop.bin
[!] Not implemented: code=0
EAX = 00000000
ECX = 00000000
EDX = 00000000
EBX = 00000000
ESP = 00007c00
EBP = 00007c00
ESI = 00000000
EDI = 00000000
EIP = 00007c01
たしかにさっきより1バイト進んでから死んでますね。やったね!
どんな命令が実装されているのか特定する
そうきたら、あとはこの子の実装状況と動作のようすを解明してあげるだけです。
最初は、Intel SDM Vol.2のopcode表をみて適当にあたりをつけていたのですが、あまりにもきつかったので方針転換しました。
幸運にもNOPが実装されていることはわかったので、NOPの数を変えてトレースしてみましょう。
トレース結果は長いのですが、着目すべき点はただ一つ: call命令のトレース結果だけです。私のprint.jsでは、call命令の場合はtargetというメンバを記録するようにしているので、それでgrep をかけます。
$ echo '90' | xxd -r -p > nop1.bin
$ echo '90 90' | xxd -r -p > nop2.bin
$ node trace.js ./box nop1.bin | grep target
{"addr":"3b2d","target":"2e44"} * 1
$ node trace.js ./box nop2.bin | grep target
{"addr":"3b2d","target":"2e44"} * 1
{"addr":"3b2d","target":"2e44"} * 1
なるほど…つまるところ、+0x2e44のところがNOPの命令を処理しているようですね。
どうしてcall命令にのみ着目すればいいのかというと、たいていのCPUエミュレータ は、動作を高速にするために、入力された命令のバイトを配列の添字として、関数ポインタの配列にアクセスして各処理を呼び出すからです。(作ってみるとわかるこの気持ち。)
というわけで、call命令のtargetを見れば、どの命令が実行されているのかわかりそうですね。
では、求めるべきトレース結果から、どの命令が使われているか抽出してみましょう。とりあえずcall命令だけを抜き出してみると、
$ node print.js box.trace | grep target
{"addr":"3b2d","target":"2ebd"} * 1
{"addr":"3b2d","target":"2ebd"} * 1
{"addr":"3b2d","target":"2159"} * 1
{"addr":"3b2d","target":"28fd"} * 1
{"addr":"3b2d","target":"1ead"} * 1
{"addr":"3b2d","target":"2389"} * 1
{"addr":"3b2d","target":"317d"} * 1
{"addr":"3b2d","target":"2159"} * 1
{"addr":"3b2d","target":"28fd"} * 1
{"addr":"3b2d","target":"1ead"} * 1
{"addr":"3b2d","target":"2389"} * 1
{"addr":"3b2d","target":"317d"} * 1
{"addr":"3b2d","target":"2159"} * 1
{"addr":"3b2d","target":"28fd"} * 1
{"addr":"3b2d","target":"1ead"} * 1
{"addr":"3b2d","target":"2389"} * 1
{"addr":"3b2d","target":"317d"} * 1
{"addr":"3b2d","target":"2159"} * 1
{"addr":"3b2d","target":"28fd"} * 1
{"addr":"3b2d","target":"1ead"} * 1
{"addr":"3b2d","target":"2389"} * 1
{"addr":"3b2d","target":"317d"} * 1
{"addr":"3b2d","target":"2159"} * 1
{"addr":"3b2d","target":"28fd"} * 1
{"addr":"3b2d","target":"1ead"} * 1
{"addr":"3b2d","target":"2389"} * 1
{"addr":"3b2d","target":"317d"} * 1
{"addr":"3b2d","target":"2159"} * 1
{"addr":"3b2d","target":"28fd"} * 1
{"addr":"3b2d","target":"3269"} * 1
おー、やはりindirectなcall命令は1箇所しかないんですね。で、このtargetの一意なリストは以下です。
$ node print.js box.trace | grep target | cut -d \" -f 8 | sort | uniq
1ead
2159
2389
28fd
2ebd
317d
3269
さて、ここからどうやって命令オペコードを知ればよいでしょうか。
さきほど言ったように、このエミュレータ は命令オペコードをテーブルの添字にすることで関数ポインタを得て、それをcallしているはずです。
では、そのテーブルはどこにあるのか…objdumpをみるとあやしいところを見つけました。
340b: 55 push %rbp
340c: 48 89 e5 mov %rsp,%rbp
340f: 48 83 ec 10 sub $0x10,%rsp
3413: ba 00 08 00 00 mov $0x800,%edx
3418: be 00 00 00 00 mov $0x0,%esi
341d: 48 8d 3d 7c 2c 20 00 lea 0x202c7c(%rip),%rdi # 2060a0 <__cxa_finalize@plt+0x2055f0>
3424: e8 f7 d5 ff ff callq a20 <memset@plt>
3429: 48 8d 05 7d ea ff ff lea -0x1583(%rip),%rax # 1ead <__cxa_finalize@plt+0x13fd>
3430: 48 89 05 71 2c 20 00 mov %rax,0x202c71(%rip) # 2060a8 <__cxa_finalize@plt+0x2055f8>
3437: 48 8d 05 0c eb ff ff lea -0x14f4(%rip),%rax # 1f4a <__cxa_finalize@plt+0x149a>
343e: 48 89 05 7b 2c 20 00 mov %rax,0x202c7b(%rip) # 2060c0 <__cxa_finalize@plt+0x205610>
3445: 48 8d 05 02 ec ff ff lea -0x13fe(%rip),%rax # 204e <__cxa_finalize@plt+0x159e>
344c: 48 89 05 c5 2c 20 00 mov %rax,0x202cc5(%rip) # 206118 <__cxa_finalize@plt+0x205668>
3453: 48 8d 05 62 ec ff ff lea -0x139e(%rip),%rax # 20bc <__cxa_finalize@plt+0x160c>
345a: 48 89 05 87 2d 20 00 mov %rax,0x202d87(%rip) # 2061e8 <__cxa_finalize@plt+0x205738>
3461: 48 8d 05 f1 ec ff ff lea -0x130f(%rip),%rax # 2159 <__cxa_finalize@plt+0x16a9>
3468: 48 89 05 f9 2d 20 00 mov %rax,0x202df9(%rip) # 206268 <__cxa_finalize@plt+0x2057b8>
346f: 48 8d 05 8b ed ff ff lea -0x1275(%rip),%rax # 2201 <__cxa_finalize@plt+0x1751>
3476: 48 89 05 fb 2d 20 00 mov %rax,0x202dfb(%rip) # 206278 <__cxa_finalize@plt+0x2057c8>
347d: 48 8d 05 25 ee ff ff lea -0x11db(%rip),%rax # 22a9 <__cxa_finalize@plt+0x17f9>
3484: 48 89 05 f5 2d 20 00 mov %rax,0x202df5(%rip) # 206280 <__cxa_finalize@plt+0x2057d0>
348b: 48 8d 05 89 ee ff ff lea -0x1177(%rip),%rax # 231b <__cxa_finalize@plt+0x186b>
3492: 48 89 05 ef 2d 20 00 mov %rax,0x202def(%rip) # 206288 <__cxa_finalize@plt+0x2057d8>
3499: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
何やらばんばんleaして代入してますね。しかも、関数のアドレスっぽいです。もう少し下の方をみると
3677: 48 8d 05 c6 f7 ff ff lea -0x83a(%rip),%rax # 2e44 <__cxa_finalize@plt+0x2394>
367e: 48 89 05 9b 2e 20 00 mov %rax,0x202e9b(%rip) # 206520 <__cxa_finalize@plt+0x205a70>
NOP命令だとわかっている、0x2e44の関数ポインタを代入しているところをみつけました!
どうもこのテーブルの先頭は、ソースの流れ的に
341d: 48 8d 3d 7c 2c 20 00 lea 0x202c7c(%rip),%rdi # 2060a0 <__cxa_finalize@plt+0x2055f0>
曰く0x2060a0のようです。では、この予想が正しいか調べてみましょう。
NOPのオペコードは0x90
NOPの関数ポインタはオフセット0x206520に格納されている
ポインタの大きさは8bytes
関数ポインタのテーブルは0x2060a0から始まっている?
関数ポインタのテーブルをop_tableとしたとき、
op_table[0x90] == <+0x2e44のアドレス>
になっていてほしいわけです。これはつまり
*(op_table + 8 * 0x90) == <+0x2e44のアドレス>
というのと等価ですから、格納されるべきアドレスは、テーブルの先頭op_tableから8 * 0x90 == 1152離れているはずです。op_tableを0x2060a0とすれば、その結果は…
0x2060a0 + 8 * 0x90 => 0x206520
ビンゴ!NOPの関数ポインタが代入されているアドレスと一致しますね!!
というわけで、これでテーブルを生成するコードで各関数がテーブルのどのオフセットに格納されているか調べれば、対応するオペコードがわかりそうです。
…とりあえず、テーブル内の各オフセットと対応するオペコードはスクリプト でさくっと生成しました。
[
[ '2060a8', '1' ], [ '2060c0', '4' ],
[ '206118', 'f' ], [ '2061e8', '29' ],
[ '206268', '39' ], [ '206278', '3b' ],
[ '206280', '3c' ], [ '206288', '3d' ],
[ '2063e0', '68' ], [ '2063f0', '6a' ],
[ '206420', '70' ], [ '206428', '71' ],
[ '206430', '72' ], [ '206438', '73' ],
[ '206440', '74' ], [ '206448', '75' ],
[ '206460', '78' ], [ '206468', '79' ],
[ '206480', '7c' ], [ '206490', '7e' ],
[ '206498', '7f' ], [ '2064b8', '83' ],
[ '2064e0', '88' ], [ '2064e8', '89' ],
[ '2064f0', '8a' ], [ '2064f8', '8b' ],
[ '206508', '8d' ], [ '206520', '90' ],
[ '2066b8', 'c3' ], [ '2066d8', 'c7' ],
[ '2066e8', 'c9' ], [ '206708', 'cd' ],
[ '2067c0', 'e4' ], [ '2067e0', 'e8' ],
[ '2067e8', 'e9' ], [ '2067f8', 'eb' ],
[ '206800', 'ec' ], [ '206810', 'ee' ],
[ '206840', 'f4' ], [ '206898', 'ff' ]
]
そして、あとはobjdumpの結果とIntel SDM Vol.2をじーっと見つめると…
1ead -> 2060a8 -> 0x01 -> ADD Ev,Gv
2159 -> 206268 -> 0x39 -> CMP Ev,Gv
2389 -> INC r32
28fd -> 206498 -> 0x7f -> JNLE / JG
2ebd -> -> mov r32, imm32
317d -> 2067f8 -> 0xeb -> short Jb
3269 -> 206840 -> 0xf4 -> HLT
はい!
incとmovに関しては、オペコード内にレジスタ がエンコーディング されている(つまり、複数のオペコードが同じ操作を表現する)ので、バイナリ中でもループを回して代入されていました。なのでちょっと歯抜けです。(INCとかがオフセットいくつで実装されているかを調べたりして突き止めた。)
さて、あとはprint.jsを拡張して、トレース結果からオペコードを表示してみるか…となったんですが
PROLOGUE
mov r32, imm32
mov r32, imm32
CMP Ev,Gv
{"addr":"bdf","taken":false} * 1
{"addr":"be6","taken":true} * 1
{"addr":"a20","taken":true} * 1
{"addr":"c7d","taken":true} * 1
{"addr":"cbf","taken":true} * 1
{"addr":"cd6","taken":true} * 1
{"addr":"d0e","taken":true} * 1
{"addr":"13a1","taken":false} * 1
{"addr":"13bc","taken":true} * 1
{"addr":"1cb5","taken":false} * 1
{"addr":"1cca","taken":true} * 1
{"addr":"1cf1","taken":true} * 1
{"addr":"1d2d","taken":false} * 1
{"addr":"1d41","taken":true} * 1
{"addr":"1e86","taken":true} * 1
{"addr":"1d67","taken":true} * 1
{"addr":"21f8","taken":true} * 1
JNLE / JG
ADD Ev,Gv
INC r32
...
INC r32
short Jb
CMP Ev,Gv
{"addr":"bdf","taken":false} * 1
{"addr":"be6","taken":true} * 1
{"addr":"a20","taken":true} * 1
{"addr":"c7d","taken":true} * 1
{"addr":"cbf","taken":true} * 1
{"addr":"cd6","taken":true} * 1
{"addr":"d0e","taken":true} * 1
{"addr":"13a1","taken":false} * 1
{"addr":"13bc","taken":true} * 1
{"addr":"1cb5","taken":true} * 1
{"addr":"1cf1","taken":true} * 1
{"addr":"1d2d","taken":true} * 1
{"addr":"1e86","taken":true} * 1
{"addr":"1d67","taken":true} * 1
{"addr":"21f8","taken":true} * 1
JNLE / JG
JNLE / JG taken
HLT->END
444
これはCMP Ev, Gvの分岐列だけベタで、あとは命令の名前で表示してみたんですが、なぜか微妙に長さが違います。考えられるのは…演算結果によって分岐が変わるということです。
CMP命令は、引き算の結果に合わせてフラグレジスタ を設定しますから、いかにもありそうなかんじです。また、分岐命令についても分岐列の長さが1命令程度変動する時があり、これは分岐が行われたか否かを示しているようでした。
というわけで、うまくつじつまを合わせるとこんな感じのアセンブリ 列をバイナリにして食べさせてあげたトレース結果を投げると(長い)フラグが降ってきました。うれしいね!
.intel_syntax noprefix
mov eax, 1
mov ebx, 5
cmp eax, ebx
jg out
add ecx, edx
inc eax
jmp a
a:
cmp edx, ebx
jg out # false
add ecx, edx
inc eax
jmp b
b:
cmp edx, ebx
jg out # false
add ecx, edx
inc eax
jmp d
d:
cmp edx, ebx
jg out # false
add ecx, edx
inc eax
jmp e
e:
cmp eax, ebx
jg cccc
cccc:
add ecx, edx
inc eax
jmp g
g:
cmp ebx, edx
jg out # true
add ecx, edx
inc eax
jmp f
f:
out:
hlt
コンパイル とバイナリの抽出は以下のような感じで。(out.binがおいしいバイナリです。)
gcc -m16 -c -o test.o test.S && objcopy -O binary test.o out.bin
さあ、次もいきましょう!!
box4
さて、次は何かな、と思ったら
$ md5sum box3/box box4/box
d8614ad07b9efbb87a6049bd7b5da1c7 box3/box
d8614ad07b9efbb87a6049bd7b5da1c7 box4/box
同じバイナリじゃないですか!やったね、スクリプト が流用できるよ!
追加の命令列を特定
2c33 -> 2064e8 -> 0x89 -> MOV Ev,Gv
23e7 -> -> DEC r32
24c3 -> -> POP r32
251a -> 2063e0 -> 0x68 -> PUSH lz
2645 -> 206440 -> 0x74 -> JZ/JE
2695 -> 206448 -> 0x75 -> JNZ/JNE
3144 -> 2067e8 -> 0xe9 -> JMP near Jz
かんたんだね!
print.jsを拡張
分岐のtaken/not taken
OF(オーバーフローフラグ)の表示を追加
Parser for SECCON 2019 final q4 box4
このスクリプト を、与えられたトレース結果に適用すると、こんなかんじの出力が得られます。
$ node print.js box.trace
PROLOGUE
PUSH imm32
PUSH imm32
PUSH imm32
PUSH imm32
POP r32
POP r32
DEC r32 ZF=false OF=false
MOV r/m32,r32
ADD Ev,Gv
DEC r32 ZF=false OF=false
JNZ/JNE
JNZ/JNE taken?
ADD Ev,Gv
DEC r32 ZF=true OF=false
JNZ/JNE
CMP Ev,Gv ZF=false
JZ/JE
JMP near Jz
POP r32
DEC r32 ZF=false OF=false
MOV r/m32,r32
ADD Ev,Gv
DEC r32 ZF=true OF=false
JNZ/JNE
CMP Ev,Gv ZF=false
JZ/JE
JMP near Jz
POP r32
DEC r32 ZF=false OF=false
MOV r/m32,r32
ADD Ev,Gv
DEC r32 ZF=false OF=false
JNZ/JNE
JNZ/JNE taken?
ADD Ev,Gv
DEC r32 ZF=false OF=false
JNZ/JNE
JNZ/JNE taken?
ADD Ev,Gv
DEC r32 ZF=true OF=false
JNZ/JNE
CMP Ev,Gv ZF=true
JZ/JE
JZ/JE taken
HLT->END
613
もうほとんどアセンブリ だね!
入力バイナリを錬成
メモ書き込みですがこんな感じでとけた。
.intel_syntax noprefix
//PUSH lz
//PUSH lz
//PUSH lz
//PUSH lz
.byte 0x68, 0x02, 0x00, 0x00, 0x00
.byte 0x68, 0x02, 0x00, 0x00, 0x00
.byte 0x68, 0x02, 0x00, 0x00, 0x00
.byte 0x68, 0x02, 0x00, 0x00, 0x00
//POP r32
//POP r32
//DEC r32 ZF=false : r32 != 1
//MOV Ev,Gv
//ADD Ev,Gv
//DEC r32 ZF=false : r32 != 1
//JNZ/JNE
//JNZ/JNE taken?
pop edx // edx = 1
pop esi // esi = 1
dec edx // op != 0
mov ecx,edx
add edx,eax
dec esi
jne fake
fake:
//ADD Ev,Gv
//DEC r32 ZF=true : r32 == 1
//JNZ/JNE
//CMP Ev,Gv ZF=false
//JZ/JE
//JMP near Jz
add ecx,ecx
dec esi // op should be 1
jnz c
c:
cmp eax,ecx
jz d
.byte 0xe9, 0x00, 0x00, 0x00, 0x00
d:
//POP r32
//DEC r32 ZF=false
//MOV Ev,Gv
//ADD Ev,Gv
//DEC r32 ZF=true
//JNZ/JNE
pop ebx
dec ebx
mov ecx,ebx
add edi,ecx
dec ebx
jne e
e:
//CMP Ev,Gv ZF=false
//JZ/JE
//JMP near Jz
cmp eax,ecx
jz p
.byte 0xe9, 0x00, 0x00, 0x00, 0x00
p:
//POP r32
//DEC r32 ZF=false
//MOV Ev,Gv
//ADD Ev,Gv
//DEC r32 ZF=false
//JNZ/JNE
//JNZ/JNE taken
pop ebx
dec ebx
mov ecx,ecx
add edi,ecx
dec edi
jne q
q:
//ADD Ev,Gv
//DEC r32 ZF=false
//JNZ/JNE
//JNZ/JNE taken
add edi,ecx
dec edi
jnz m
m:
//ADD Ev,Gv
//DEC r32 ZF=true
//JNZ/JNE
add ecx,ecx
dec ebx
jne n
n:
//CMP Ev,Gv ZF=true
//JZ/JE
//JZ/JE taken
cmp eax,eax
jz s
s:
//HLT->END
hlt
あれ、Defenceポイントはどうしたの?
box1のvariantってことだったので、演算子 とかは一緒かなと思っていたのですが、完全に変わっていたのでつらかった。自動化するにはちゃんとファジングとかデコンパイラ を活用する必要がありそうだなあという気持ちになりました。強くなりたいです。
他に何をやりましたか?
MimuraのフラッシュをダンプしてFATを読む試みをしたりした。でもうまくいかなかった。st98さんが実機を持って帰ったので朗報を期待している。
Bad mouseをst98さんが解いてくれたのだが、実機を持っていなかったようなので、去年かどこかで配られたのを家に置いてあった私が代わりに実行しました。
マウスが動いてフラグが塗られるのは楽しかった。しかし、待ち時間が長かった…(st98さん曰く、もっとwaitを短くできたらしいのでそうするといいと思います。)
以下、塗られたフラグの実際の画像です。
FLAG for Bad Mouse
まとめ
問題4はめっちゃ楽しかった。作問者の友利奈緒 さん、いい問題をありがとうございます!
坂井さんの問題は、ちょっとやる気が出なかったです。私のバイナリ鍛錬が足りないのも要因ですが、単純にあまり問題がおもしろくなかったのと、ディフェンスポイントの入りかたにゲーム性がないなどの問題もあったと私は思います。来年はアーキの数で殴るタイプではない、面白く解けるマルチアーキテクチャ 問題を期待しています…。
とはいえ、全体的には楽しめたのでよかったです。SECCONはSECCONなので!と私は思っています。
運営のみなさま、ありがとうございました。