SECCON 2019 国内決勝 writeup

12/21 ~ 22 で開催された SECCON 2019 の国際決勝にチーム yharima として参加してきました。結果は7位でした。上位のチームは基本的に defense point を多く獲得しているところばかりだったので、defense 大事だなとおもいました。

何問か解くことはできたので writeup を書いておきます。

サーバに画像を投げつけて一致度の高い画像を探すという問題。一致度 50%, 60%, 70% 以上でそれぞれ一個ずつフラグが出る。 4時間おきに一致度の計算用のパラメータが変わるということだったが、最序盤は全ピクセルで色が同じ白黒画像を投げつけただけで 89% 程度の一致度が出てフラグが全部そろってしまった。一応白黒以外の色も探索していくコードも書いてしばらく走らせたが、defense point が入ってるのかよくわからずすぐ放置してしまった。今考えるともう少し遊んでおくべきだったかもしれない。。。

↓白黒画像を生成してPOSTし、点を見るだけのソースコード

gen_uniform_image.py
#!/usr/bin/env python

from PIL import Image
import sys

if len(sys.argv) < 4:
    print 'input R G B'
    sys.exit(1)

r = int(sys.argv[1])
g = int(sys.argv[2])
b = int(sys.argv[3])

H = 400
W = 640

im = Image.new('RGB', (W, H))
for x in xrange(0, W):
    for y in xrange(0, H):
        im.putpixel((x, y), (r, g, b))

im.save('generated.png')
uniform.sh
#!/bin/sh

RESULT_DIR=uniform_result

for num in `seq 0 255`; do
  mkdir -p ${RESULT_DIR}
  ./gen_uniform_image.py ${num} ${num} ${num}
  result_file="${RESULT_DIR}/${num}.out"
  curl -s -c my.cookie -L -F photo=@generated.png http://10.1.2.1 > ${result_file}
  echo "[${num}]"
  fgrep "Statistics for each color" ${result_file}
  fgrep "Recognition rate" ${result_file}
  sleep 3
done

ジャンプしたときのtrue/falseと関数呼び出しの履歴から、それに沿った入力をこたえるという問題。予選でも同じ形式の問題が出ており、自分がその問題を解いていたので流れでやることになった。defenseは一瞬で解かれて太刀打ちできなかったのであきらめた

box1

予選と同じく、逆ポーランド記法の数式を受け取って計算するというプログラムだった。 基本的には1文字ずつパースして処理を進めるプログラムになっている。 a ~ f までの演算子もしうは数字が入力となるが、1文字パースする処理の最初の分岐(0x557a93890ddc)を起点として、後続の分岐の true / false の列を確認することで数字か a ~ f の演算子のどれかかということは判別できる。

実際使われていた演算子は a, b, c, e のみで、それぞれ加算、減算、乗算、min に対応していた。 入力によって分岐の仕方が変わるのは加算と乗算の2つで、加算の場合は第二引数(後からスタックに push した方)の数だけループを回して1ずつ足していく関係で、0x557a93890a31 の分岐が第二引数と同じ回数だけ true になる。 乗算の場合は第二引数の数だけループを回して第一引数を足し合わせていく実装になっているので、同じように 0x557a93890a94 の分岐が第二引数と同じ回数だけ true になる。

最終的には次のスクリプトを書いて、入力となった数式を特定した。

#!/usr/bin/env python

import json

HINTS = {
    'D': [True, False, True, True, True, True, True, True, False],
    'a': [True, False, False],
    'b': [True, False, True, False],
    'c': [True, False, True, True, False],
    'e': [True, False, True, True, True, True, False]
}


def get_chr(jump):
    p = None
    for c, a in HINTS.items():
        if len(jump) < len(a):
            continue
        success = True
        for i, b in enumerate(a):
            if jump[i][1] != a[i]:
                success = False
                break
        if success:
            p = c
            break
    return p

with open('box.trace') as f:
    traces = json.load(f)

first_found = False
jumps = []
formula = []
all_jumps = []
for i, trace in enumerate(traces):
    if trace["event"] == "image_load" or trace["event"] == "exit":
        continue
    if not first_found and trace["inst_addr"][-3:] != "ddc":
        continue

    jump = trace["branch_taken"]
    if trace["inst_addr"][-3:] == "ddc":
        if first_found:
            all_jumps.append(jumps)
            jumps = []
        else:
            first_found = True

        if jump == False:
            break

    jumps.append((trace["inst_addr"][-3:], trace["branch_taken"]))


formula = []
for i, jump in enumerate(all_jumps):
    c = get_chr(jump)
    formula.append(c)
    if c == 'a':
        t = 0
        for j in jump:
            if j[0] == 'a31' and j[1]:
                t = t + 1
        print i + 1, c, t
    elif c == 'c':
        t = 0
        for j in jump:
            if j[0] == 'a94' and j[1]:
                t = t + 1
        print i + 1, c, t
    else:
        print i + 1, c
    if c is None:
        print "Fail..."
        break
print ''.join(formula)

実行結果はこちらで入力となった数式を1文字ずつ出している。 加算 (a)、乗算 (c) の場合は第二引数の値も合わせて出力している。 D は 0~9 の数字を表す。

1 D
2 D
3 D
4 D
5 D
6 D
7 D
8 D
9 D
10 D
11 D
12 D
13 c 12
14 e
15 D
16 D
17 D
18 D
19 a 10
20 D
21 D
22 D
23 D
24 b
DDDDDDDDDDDDceDDDDaDDDDb

あとは、加算・乗算の引数に気をつけて適当に D の部分の数字を埋めれば良い。 次のような数式になるようにしてみた。

min(0, 0 * 12) + 10 - 0

APIへ数式を送りつけたところフラグが出た(input の部分が与えた入力)

[*] API /attack/1/submit with input = 000000000012ce0010a0000b
{'message': 'Good! Submit this flag :)', 'flag': 'SECCON{Good! Go on next :hugging-face: oUedlavRDOMzUhzu}', 'error': False}

box2

与えた入力を暗号化して出力するプログラムだった模様(CBCだったのかな?)。 鍵は入力によって変化することはないようで、実は入力によって分岐が変わる部分はごく僅かだった。 先頭から16byteずつごちゃごちゃと変換していくのだが、この16byteずつ読み込むためのループ以外は入力によって分岐が変化しない。 0x5579b80c8c61 の分岐が true になっている回数が、このループを回っている回数に対応し、これが12回だった。 つまり、16 * 12 = 192 byte の入力を与えてやれば良い

[*] API /attack/2/submit with input = 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
{'message': 'Good! Submit this flag :)', 'flag': 'SECCON{Brilliant! You know crypto function depends on input length :relaxed: gVtDHSPrBkoHdxiw}', 'error': False}

供養

挑戦したが解けなかった問題たち

  • 四 box3
    命令セットが x86 であることに気付いて適当にループを組めばいいところまでは分かったものの時間切れで終了。律儀に各命令の処理内容をバイナリから全て読み取ろうとして時間を浪費したのが痛かった…
  • 六 Hardware
    STM32 CubeMX Programmer なる開発環境をインストールして接続を試みるも DEV_UNKNOWN_MCU_TARGET など出てそもそも読み込みができず何もできなかった。つなぎ直すと出るエラーが変わったりしたのでそもそも物理的な接続がまずかったのかもしれない…

戦友

チーム yharima のメンバーの writeup