SECCON2016 大阪大会(アツマレバイナリアン)ひとり反省会
アツマレトモリナオということで友利奈緒ちゃんたちと「イーグルジャンプ」というチームで出場しました。
友利奈緒!!!! pic.twitter.com/SQPd3Q6Kls
— たけまる (@tkmru) October 2, 2016
メンバー:
- ぴんく (@PINKSAWTOOTH)
- たけまる (@tkmru)
- しゅうすい (@syusui_s)
結果は、
結果はたぶん8位 自分は全然だめだった・・・ 自動化まで完成させて点を取ってくれたチームメイトに感謝。
SECCON2016 大阪大会参加してきた・・・(だめ) - Twitterに書ききれないこと
SECCON大阪の最終結果です。 #seccon pic.twitter.com/bmCCNnxsWU
— ツモり四限遅刻 (@ymduu) 2016年10月2日
圧倒的実装力不足!(普段pythonを書かなすぎている…><)
自動化して得点を入れ始めたのが終了40分前だったので、submitできたのは800点くらい。
# 競技時間は3時間
チームメイトの参加記はこちら:
SECCON大阪大会 2016 30000の問題を自動化するスクリプト、出来てしまった https://t.co/dxtGix9LQh
— Сюусуи (@syusui_s) 2016年10月3日
問題構成
reversing、pwnの2種類×難易度で2種類の4種類。フラグ1つ100点。
resersingパートはbackdoorをモチーフにした問題。特定の入力をするとサーバーがフラグを教えてくれる。 pwnパートはほぼやってないので説明を省略する。
難易度が低い方は、5分に1回バイナリとフラグが変わる。 難易度が高い方は、1秒に1回バイナリとフラグが変わる。 1秒以内に解かないといけないというけではなく、コネクションが維持できている一定時間内であればサーバーに入力を与えることができる。
解いた問題
backdoor (easy)
概要
ぴんくと一緒に解析した。
入力文字列の後ろから1文字ずつxorしてgood_known
と比較しているだけ
(ここで例に挙げるバイナリのmd5sumは242cecd67fc91dc1457c045d23e5baa9
)
00000000004001b0 lea rdi, qword [ss:rbp-0xb] 00000000004001b4 push 0xb 00000000004001b6 push rdi 00000000004001b7 push 0x0 00000000004001b9 call sub_400239 00000000004001be add rsp, 0x18 00000000004001c2 mov ecx, 0xb 00000000004001c7 lea rdi, qword [ss:rbp-0xb] 00000000004001cb movabs rsi, 0x400212 ; Basic Block Registers Used: rcx rsi rdi - Defined: rax rbx rip CPAZSO - Killed: <nothing> - LiveIn: rcx rsp rbp rsi rdi - LiveOut: rcx rsp rbp rsi rdi rip - AvailIn: rax rcx rip CPAZSO - AvailOut: rax rcx rbx rip CPAZSO 00000000004001d5 mov al, byte [ds:rdi+rcx-1] ; XREF=EntryPoint+89 00000000004001d9 mov bl, byte [ds:rsi+rcx-1] 00000000004001dd xor al, 0x5a 00000000004001df cmp al, bl 00000000004001e1 jne 0x4001f4
0000000000400212 db 0xc7 ; '.' ; XREF=EntryPoint+62 0000000000400213 db 0xc9 ; '.' 0000000000400214 db 0x4a ; 'J' 0000000000400215 db 0x5f ; '_' 0000000000400216 db 0xa0 ; '.' 0000000000400217 db 0x28 ; '(' 0000000000400218 db 0xb2 ; '.' 0000000000400219 db 0x61 ; 'a' 000000000040021a db 0x97 ; '.' 000000000040021b db 0x23 ; '#' 000000000040021c db 0xab ; '.'
結果だけ書くと、rsi
にgood_known
、ecx
に文字列長、rdi
にユーザー入力が入るのでreversing技術とobjdumpゴリ押しで自動化可能と分かった。
できた自動回答プログラム
backdoor.py
:
from pwn import * from hexdump import hexdump import os import hashlib import re import base64 r = remote("10.0.1.2", 10000) # r.sendline("") data = r.recvrepeat(1).split('\n') info = data[0] print info binary_b64 = data[1].split(':')[1] # print binary_b64 binary = binary_b64.decode("base64") hexdump(binary) file_name = hashlib.md5(binary).hexdigest() log.info(file_name) with open(file_name, 'wb') as f: f.write(binary) os.system("chmod +x " + file_name) proc = subprocess.Popen(["./find.sh", file_name], stdin=subprocess.PIPE, stdout=subprocess.PIPE) out, _ = proc.communicate() print out good_known = int(out, 16) print "good_known = %#x" % good_known proc = subprocess.Popen(["./xor.sh", file_name], stderr=subprocess.PIPE, stdout=subprocess.PIPE) out, _ = proc.communicate() print "xor out = %r" % out xor_key = int(out, 16) print "xor key = %x" % xor_key proc = subprocess.Popen(["./length.sh", file_name], stderr=subprocess.PIPE, stdout=subprocess.PIPE) out, err = proc.communicate() length = int(out.split('\n',)[0], 16) print "length = %x" % length _from = good_known & 0xfff print "offset = %#x" % _from lbinary = list(binary) print "good_known = " + str(lbinary[_from:_from+length]) good_known = lbinary[_from:_from+length] import string flag = "" # good_known = "\xa5\x59\x2e\x80\x0a\x19\xfc\x33\x5b" # ↓ xorしてるから同じ手順で平文を得られるのになんで全探索してるんですかねぇ… length = len(good_known) # ←はじめに文字列長をベタ書きしていて、手動で解こうとしてたぴんくの時間を無駄にしてしまった for i in reversed(range(length)): # for c in string.printable: # ←誤った思い込み良くない for c in range(256): # al = ord(c) al = c bl = ord(good_known[i]) if al ^ xor_key == bl: flag = chr(c) + flag # print "%r" % flag break if len(flag) == length: print "flag = %r" % flag else: print "[!] incorrect flag. exit" exit() payload = base64.b64encode(flag+'\n') r.sendline(payload) flag_output = r.recvrepeat(1) FLAG = flag_output.split(':')[1] rs = remote('10.0.1.1', 10000) rs.sendline("EagleJump " + FLAG) print rs.recvrepeat(1) # r.interactive()
find.sh
:
#!/bin/sh objdump -Mintel -d $1 | grep -B 2 "mov al,BYTE" | head -n 1 | awk -F, '{print $2}'
# objdump -Mintelと書くことの重要性!(alias対策)
xor.sh
:
#!/bin/sh objdump -Mintel -d $1 | egrep "xor" | awk -F, '{print $2}'
length.sh
:
#!/bin/sh objdump -Mintel -d $1 | egrep "mov ecx" | awk -F, '{print $2}'
backdoor (hard) ※本番では解けていない
(敗因)secretがstringsで引っかかるのに気づかなかった&angr.pyというファイル名を付けて自爆
方針:
strings -tx
でsecretのアドレスを調べる- objdumpとgrepをゴリ押しして、secretを文字列として格納しているBasic Blockを探す
- 正確にはangrに渡すfindのアドレスはBasic Blockの先頭アドレスでもないが、exploreでは問題にならない
import logging logging.basicConfig(level=logging.ERROR) import angr import subprocess import os, sys def main(BIN): # BIN = "30506f9aa41ca3067d7568539a7267e8" with open('find_secret.sh', 'w') as f: f.write(""" str_addr=`strings -tx $1 | grep secret | egrep -o "[0-9]+"` objdump -Mintel -d $1 | grep 0x400${str_addr} | awk -F: '{print $1}' | tr -d ' ' """) p = angr.Project(BIN, load_options={'auto_load_libs': False}) proc = subprocess.Popen(["bash", "find_secret.sh", BIN], stdin=subprocess.PIPE, stdout=subprocess.PIPE) out, _ = proc.communicate() find_addr = int(out, 16) print "[*] find address = %#x" % find_addr initial_state = p.factory.entry_state() pg = p.factory.path_group(initial_state) ex = pg.explore(find=(find_addr)) if len(ex.found): INPUT = ex.found[0].state.posix.dumps(0) # dump stdin print "[*] found: %r" % INPUT print "[*] self check" with open("secret", 'w') as f: content = "this_is_secret" print "[*] content of secret is '%s'" % content f.write(content) os.system("python2 -c \"print('%s')\" | ./%s" % (INPUT, BIN)) else: print "[!] not found" print "" if __name__ == '__main__': if len(sys.argv) != 2: print "usage: %s binary_file" % sys.argv[0] exit() main(sys.argv[1])
実行例:
% time python2 backdoor2-angr.py 30506f9aa41ca3067d7568539a7267e8 [*] find address = 0x400271 [*] found: '\xacD\xd5.c:\xd0\xe5' [*] self check [*] content of secret is 'this_is_secret' this_is_secret python2 backdoor2-angr.py 30506f9aa41ca3067d7568539a7267e8 8.21s user 0.16s system 99% cpu 8.395 total
感想
- 問題は面白かった。簡単だが、問題がコロコロ変わるの最高ー!
よかったこと
とあるプロにとっては当たり前なのかもしれないけど
- 競技説明中に、落としてきたバイナリを保存するプログラムを書いておけた
- pwntoolsでサーバーとやり取りするという安定の方法をとれた
- バイナリの名前をmd5のsum値で保存することで、ファイルを受け渡すことなく一緒になって同じバイナリを解析することが容易だった
- 大会前にangrを入門できた(のに…)
反省点(やらかし列伝)
無能ミス連発
- Hopperからでangrを動かせるプラグインを事前に作ったものの、正答の入力が印字可能文字の範囲内という制約式を付けてしまっていたため、angrで簡単には解けないとミスジャッジ
- 実際には簡単に解ける(gif)
- angrで解くpythonスクリプトを書いたが、ファイル名を
angr.py
にして、スクリプト内のimport angr
で自分をimportさせる馬鹿をした- これをチームメイトのぴんくもやっていた
- 数日潰してまでめっちゃangrに入門したのに〜ぃ
- 普段、
objdump -Mintel
をobjdump
にaliasしていて、スクリプトobjdump
と読んでもintel記法にはならない→grepで期待したアセンブリが引っかからない - pythonで配列のスライスは
arr[from:to]
というように、開始から終了までのインデックスを指定するものなのに、arr[offset:length]
というようなオフセットから数文字とると勘違いしていた - シェルスクリプトで解析対象のファイル名を引数で取るつもりが、ファイル名をベタ書きしていて時間を無駄にした
- HopperやIDA Proの解析ミスり気味だった。それで粘るのではなく、objdumpやstringsゴリ押しで効率よく解析すべきだったか?(
grep xor
とか?) - xorで全探索で求めてるの無能気味では
学んだこと
- pythonスクリプトを、そこでimportするモジュールと同じ名前にしてはならない
- シェルでバイナリから特定の情報を取るなど複雑なことをしたいときは、シェルスクリプトで作っておくとチェックとコーディングが楽(今回でいうとfind.sh, xor.sh, length.sh) or 小学生に煽られてでもpythonでcommands(python2のみ)のようなモジュールを使うとよい
- pwntoolsのような特殊なモジュールを使うと他のチームメイトが実行できない可能性がある or 事前にチームメイト間の環境を揃える
- aliasを普段のシェルで無効化したほうが事故を防げそう
- grepの
-A
、-B
オプションクソ便利 - カレントディレクトリに
angr.py
、angr.pyc
というファイルがあると、import angr
に無言の死を遂げる
おまけ
本番1週間前ぐらいに、angrで解いてくれるHopper Script書きました。
Ping-Micのtool置き場(GitHub)に上げてあります。
Hopperでbackdoor(hard)を秒殺(?)してみた
他の人にひとこと
angrの出力を見て、成功したか成功しなかったのかを判断するのはangrの流儀に反してると思います。(確かにそれでも解けますけど…)
PathGroup.explore()でfindを指定しない、State.add_constrains()しないでdeadendするまで動かしてその時の標準出力を見るのは探索効率が非常に悪いです。
あんだけ本番でガンガンangr使えるように練習したのに、ファイル名が原因でangr使えなかったのは、本当に勝機を逃しすぎてる…
angr.pyというファイル名はダメ、絶対!