読者です 読者をやめる 読者になる 読者になる

ヾノ*>ㅅ<)ノシ帳

技術ブログに見せかけて、ジャンル制限のないふりーだむなブログです。

SECCON2016 大阪大会(アツマレバイナリアン)ひとり反省会

アツマレトモリナオということで友利奈緒ちゃんたちと「イーグルジャンプ」というチームで出場しました。

メンバー:

  • ぴんく (@PINKSAWTOOTH)
  • たけまる (@tkmru)
  • しゅうすい (@syusui_s)

結果は、

結果はたぶん8位 自分は全然だめだった・・・ 自動化まで完成させて点を取ってくれたチームメイトに感謝。

SECCON2016 大阪大会参加してきた・・・(だめ) - Twitterに書ききれないこと

圧倒的実装力不足!(普段pythonを書かなすぎている…><)
自動化して得点を入れ始めたのが終了40分前だったので、submitできたのは800点くらい。
# 競技時間は3時間

チームメイトの参加記はこちら:

pinksawtooth.hatenablog.com

問題構成

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 ; '.'

結果だけ書くと、rsigood_knownecxに文字列長、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値で保存することで、ファイルを受け渡すことなく一緒になって同じバイナリを解析することが容易だった
    • 同じタイミングでバイナリを落とせば、md5が一致することを根拠に同じバイナリを落とせたことを確認できる
  • 大会前にangrを入門できた(のに…)

反省点(やらかし列伝)

無能ミス連発

  • Hopperからでangrを動かせるプラグインを事前に作ったものの、正答の入力が印字可能文字の範囲内という制約式を付けてしまっていたため、angrで簡単には解けないとミスジャッジ
    • 実際には簡単に解ける(gif)
  • angrで解くpythonスクリプトを書いたが、ファイル名をangr.pyにして、スクリプト内のimport angrで自分をimportさせる馬鹿をした
    • これをチームメイトのぴんくもやっていた
    • 数日潰してまでめっちゃangrに入門したのに〜ぃ
  • 普段、objdump -Mintelobjdumpに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.pyangr.pycというファイルがあると、import angrに無言の死を遂げる

おまけ

本番1週間前ぐらいに、angrで解いてくれるHopper Script書きました。
Ping-Micのtool置き場(GitHub)に上げてあります。

Hopperでbackdoor(hard)を秒殺(?)してみた

youtu.be

他の人にひとこと

angrの出力を見て、成功したか成功しなかったのかを判断するのはangrの流儀に反してると思います。(確かにそれでも解けますけど…)
PathGroup.explore()でfindを指定しない、State.add_constrains()しないでdeadendするまで動かしてその時の標準出力を見るのは探索効率が非常に悪いです。


あんだけ本番でガンガンangr使えるように練習したのに、ファイル名が原因でangr使えなかったのは、本当に勝機を逃しすぎてる…
angr.pyというファイル名はダメ、絶対!