LoginSignup
7
2

More than 3 years have passed since last update.

SECCON CTF 2019 write-up

Last updated at Posted at 2019-12-22

superflipは1,265点で10位。最初のほうは調子が良かったが……。

image.png

国内と国際があるけれど、順位表が違うだけで問題は同じ。

問題の形式

各大問ごとに攻撃点と防御点がある。

攻撃点は良くあるJeopardy形式と同じ。問題のファイルやサーバーの中からフラグを探して投稿すると点が入る。点数は全部同じで1本100点。

防御点は、各問題で指定されたURLに、チームごと5分ごとに変わるディフェンスキーワードが書かれていれば、5分ごとに「20/書かれているチーム数」の点数が入る。本来はサーバーの制御を奪ったら、他の参加者からサーバーを守れという意味での防御点だろうけど、まあ、その方式でやるのは無理があるだろうから、形式は問題によって様々。

各チーム(IPアドレスの上位12ビット)ごとにポートが指定される。指定されたポートにTCPで繋ぐと、「トークン」を聞かれ、トークンを答えるとフラグとともにUDPで指定されたポートに同じトークンを投稿しろと言われる。

Attack Flag: SECCON{P13453 +4K3 C4R3} - OK, Please send UDP packet with token in payload to game port, first port is 21854, time limit is 20 second

SECCON{P13453 +4K3 C4R3}

UDPで投稿すると別のポートにトークンを投げろと言われ、合計10回で、トークンが防御点用のファイルに書き込まれる。つまり、トークンをディフェンスキーワードにして、5分に1回以上これをやれば防御点が獲得できる。

何が難しいんだ?と言われそうだけど、問題文に不穏な一文がある。

Caution: If your service process goes to wrong state, we will not restart till all team's score goes to no count or any trouble is found.

この問題は接続ごとにサーバープログラムが起動するわけではなく、起動しっぱなし。タイムリミットを過ぎても10回繰り返した後でないと、TCPで待ち受ける初期状態に戻らない。UDPで投げるトークンが一致していない場合も同様にUDPモードのまま。スクリプトを書いていたりしてサーバーから送られてきたデータを読み捨ててしまったら終わり。まあ、プログラムを解析するとポートは1,000通りしかないから総当たりができるし、スクリプトを書いていたらトークンはスクリプトに残っていると思うのだけど。

コンテスト中の通信は全て記録しておけということだろうか。そうしているチームもあるらしい。

もう1個難しいのが、UDPの接続終了後に0.09秒のウエイトがあり、上記の制限時間は徐々に短くなっていって最後は1秒になる。余裕が0.1秒しかない。そもそも、UDPの通信のプログラムなんて書いたことがなくて、recvfrom に小さな値を指定したらエラーになったりして良く分からなかったけれど、次のプログラムで制限1秒になった後も10回に1回くらいは成功するようになった。この問題の状況では運営サーバーで試せないし、そもそも1日目の帰宅後だったので、ローカルで試行錯誤した。

solve.py
import time
import re
from socket import *
import select
import sys

#target = "127.0.0.1"
target = "10.1.1.1"

#index = 22  # 127.0.0.1
index = (192<<16|168<<8|41)%50

token = sys.argv[1]
print "Token: %s"%repr(token)

def tcp():
  s = socket(AF_INET, SOCK_STREAM)
  print "port", index*1000+10000
  s.connect((target, index*1000+10000))

  r = ""
  while len(r)==0 or r[-1]!=":":
    r += s.recv(1)
  print r

  s.send(token)

  r = ""
  while len(r)<6 or r[-6:]!="second":
    r += s.recv(1)
  print r
  s.close()

  return int(re.search(r"port is (\d+),", r).group(1))

port = tcp()
print port

def udp():
  s = socket(AF_INET, SOCK_DGRAM)
  #s.bind(("127.0.0.1", port+1))
  s.bind(("192.168.41.5", port+1))

  while not select.select([s], [], [], 0)[0]:
    s.sendto(token, (target, port))
    pass
  r, _addr = s.recvfrom(0x20)
  print r
  s.close()

  m = re.search(r"port is (\d+)", r)
  if m:
    return int(m.group(1))
  else:
    return 0

for _ in range(10):
  port = udp()

後はスコアサーバーからのディフェンスキーワードの取得とプログラムの実行を10秒に1回繰り返した。

attack.sh
while true
do
  date
  token=$(curl -sS -b "PHPSESSID=3838679e25c9df2babe71a6fc9dde5f88218e37c" http://score-dom.seccon/flagwords/ | grep '<th>superflip' | egrep -o '[0-9a-f]{32,32}')
  python -u solve.py $token
  sleep 10
done

これを書いたのが1日目帰宅後で、2日目は順調に防御点を稼いでいたけれど、14時くらいに止まっていた。UDPの各ポートにトークンを投げまくっても回復しなかった。懇親会で聞いたら、問題サーバーのプロセスが落ちたチームがあって、再起動はしなかったと言われた。同じスクリプトを動かしっぱなしなのだから状況が変わることも無さそうなのに……。

20191223_2.jpg

同じくらいの時間に止まっているチームは他にもいる。他のチームのIPアドレスからの接続を弾くのはファイアウォールではなくプログラムに実装されているので、何か脆弱性があれば他のチームから落とされることもありえそう。とはいえ、それなら攻撃を実行した1チーム以外は皆止まりそうなものだが……。謎。

SECCONはこれまで会場内の物理サーバーで運営していたが、時代の波に乗ってクラウド化したらしい。Pingで遅延が30ミリ秒くらいだったからけっこう際どい……と作問者に言ってみたら、乱数のポート番号を推測して応答が返る前にタイミングを合わせて送れば良いと返された。なるほど。srand(0), srand(rand())と乱数を初期化しているのはミスではなかったのか。

画像認識サーバーに画像を投げろという問題。画像を投稿すると、

Statistics for each color: 1480/12000
Recognition rate = 1.825%  (73/4000)

というような結果が返ってくる。「Statistics for each color」って何だろう。画像サイズは640x400と指定されているから分母は画素数でもないし。認識率の高い画像を得るのが目標。50%、60%、70%ごとに攻撃点用のフラグが返ってくる。45%以上で最大の認識率の画像を投稿しているチームが防御点を得られる。次の画像を投稿するまで3秒待たないといけない。「最大で1時間に1,200枚しか試せないよ」と問題文にも書かれていた。

4時間ごとに認識パラメタと認識率の最大値がリセットされる。ただし正解画像は変わらない。今これを書いていて気が付いたけれど、「正解画像」があるんだな。てっきり機械学習の物体識別のように、画像が犬の確率とか、猫の確率とかが計算されて、隠されている何かの確率が認識率だと思っていた。

「まずは色々試してみないとね」と白一色とか赤一色とか共に黒一色の画像を投稿したら、1個目のフラグが降ってきた。
black2.png

Statistics for each color: 9164/12000
Recognition rate = 50.125%  (2005/4000)

SECCON{S6BhhlE2v457zeyOzA2lBZQn8HwyMxZb} (2019-12-21 12:03:59)

#101010で塗りつぶした画像を投稿したら全てのフラグが手に入ってしまった。えぇ……。最初の4時間だったから「認識パラメタ」が甘いのか?

black2.png

Statistics for each color: 10491/12000
Recognition rate = 73.25%  (2930/4000)

SECCON{BaEkG068DzXENTu1fMtHo5Y1LwB4JaFa} (2019-12-21 12:20:59)
SECCON{GMUHjEGSgVcyt3psECE5x8jRprAMl2Kg} (2019-12-21 12:20:59)
SECCON{S6BhhlE2v457zeyOzA2lBZQn8HwyMxZb} (2019-12-21 12:03:59)

SECCON{BaEkG068DzXENTu1fMtHo5Y1LwB4JaFa}
SECCON{GMUHjEGSgVcyt3psECE5x8jRprAMl2Kg}
SECCON{S6BhhlE2v457zeyOzA2lBZQn8HwyMxZb}

ディープラーニングとか良く分からないし、そもそもディープラーニングの画像生成って微分ができるのが前提のところ各画像の認識率しか手に入らないし、ハイパーパラメタのチューニングライブラリも画素数の次元はやってくれないだろうなぁで防御点は諦めていた。正解画像とかとの単なる距離とかならエリアごとに徐々に色を変えていくとかやってみても良かったかもしれない。

追記

去年と同じ問題らしい。urandom vol.7に解説が載っている。サンプルでも一部が読める。

なるほど、この仕様だとたしかに閾値が広い最初はだいたい通ってしまうのか。正解画像がどんな絵だったのかは気になる。

色々なアーキテクチャでexploitを書けという問題だったかな。手を付けていない。

懇親会でチーム紹介があって、特に話すことが無いチームには司会者が「どの問題が面白かった?」と聞いていた。「4問目」と答えていたチームが多かった。

予選のfollow-meと同じように、Intel Pinを使って条件分岐ごとにアドレスをジャンプしたかどうかを記録したログから、同じログになるような入力を求める問題。攻撃フラグは全4問。

ある程度解析したら手元で実行して、問題のトレース結果と見比べ、辻褄を合わせる感じで進めると楽。トレース結果が同じになれば良いのであって、同一の入力にする必要は無い。

box1

数字と、各英字で処理が分かれている。000000000012ce0010a0000bで通った。12や10はループの回数。

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

SECCON{Good! Go on next :hugging-face: oUedlavRDOMzUhzu}

box2

入力データを暗号化だか復号だかしている。面倒。あと、プログラムを解析してみると、未初期化メモリを使っている。まあ、Cのコード上は未初期化でも実際に実行したときに値が決定的に決まっていれば良いのだけど、スタックのアドレスが書かれていた。ASLRで実行ごとに変わるぞ。どうするんだこれ……と思ったところで暗号化(復号?)の結果の値を一切見ていない事に気が付いた。それなら何でも良いな。

0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0が通った。

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

暗号アルゴリズムは値に処理が依存しないほうが望ましいということを分からせるための問題か?

単純電力解析 - Wikipedia

SECCON{Brilliant! You know crypto function depends on input length :relaxed: gVtDHSPrBkoHdxiw}

box3

x86の(一部の命令の)エミュレーター。最初に各オペランドに対応したサイズ256個の関数テーブルを作っている。ビルドし直すのが面倒で手元に動かすときに予選のものをそのまま使っていたら、予選のものは関数呼び出しに対応していなかった。決勝のものは関数の呼び出し先がコード中にあるのではなく、メモリから読んでいる場合は、アドレスを出力してくれる。手元でプログラムを動かして関数テーブルを作ったところでダンプして見比べれば、どのオペランドがどの順で並んでいるかはすぐに分かる。あとは各オペランドでのフラグの変化などを読めば良い。とはいえ、分量が多くてなかなかに面倒。

mov reg imm32
mov reg imm32

cmp r/m32 r32
jg rel8 (taken)
add r/m32 r32
jmp rel8

cmp r/m32 r32
jg rel8 (taken)
add r/m32 r32
jmp rel8

cmp r/m32 r32
jg rel8 (taken)
add r/m32 r32
jmp rel8

cmp r/m32 r32
jg rel8 (taken)
add r/m32 r32
jmp rel8

cmp r/m32 r32
jg rel8 (taken)
add r/m32 r32
jmp rel8

cmp r/m32 r32
jg rel8 (not taken)

hlt

という感じだったので、これで。

$ objdump -D -M intel -mi386 -b binary code

code:     file format binary


Disassembly of section .data:

00000000 <.data>:
   0:   b8 00 00 00 00          mov    eax,0x0
   5:   b9 04 00 00 00          mov    ecx,0x4
   a:   39 c8                   cmp    eax,ecx
   c:   7f 05                   jg     0x13
   e:   01 ff                   add    edi,edi
  10:   40                      inc    eax
  11:   eb f7                   jmp    0xa
  13:   f4                      hlt

この問題のプログラムの入力はx86の入力そのものなので、アセンブラの書き方が分からないときには、いちいちプログラムに食わせなくても逆アセンブルしてみれば良い。add r/m32でレジスタとメモリをどうやって切り替えるのかとか知らなかった。次の1バイトの上位2ビットが110xc0)ならば、残りの6ビットを3ビットずつ使ってオペランド2個のレジスタを指定する。

[*] API /attack/3/submit with input = ?    ?   9ネ?@?E
{'message': 'Good! Submit this flag :)', 'flag': 'SECCON{Cool! You know how CPU emulator works :sunglass: SogtZbFVCnyPrmMA}', 'error': False}

SECCON{Cool! You know how CPU emulator works :sunglass: SogtZbFVCnyPrmMA}

box4

バイナリはbox3と同じ。入力だけが違う。

push imm32
push imm32
push imm32
push imm32
pop reg

pop reg
dec reg (>0)
mov r/m32 r32
add r/m32 r32
dec reg (>0)
jnz rel8 (taken)
add r/m32 r32
dec reg (=0)
jnz rel8 (not taken)
cmp r/m32 r32
jz rel8 (not taken)
jmp rel32

pop reg
dec reg (>0)
mov r/m32 r32
add r/m32 r32
dec reg (=0)
jnz rel8 (not taken)
cmp r/m32 r32
jz rel8 (not taken)
jmp rel32

pop reg
dec reg (>0)
mov r/m32 r32
add r/m32 r32
dec reg (>0)
jnz rel8 (taken)
add r/m32 r32
dec reg (>0)
jnz rel8 (taken)
add r/m32 r32
dec reg (=0)
jnz rel8 (not taken)
cmp r/m32 r32
jz rel8 (taken)

hlt

このエミュレータはaddではフラグが変化しないから適当で良いのだが、decはフラグが変化する(=トレース結果が変わる)のできちんと合わせないといけない。

   0:   68 03 00 00 00          push   0x3
   5:   68 01 00 00 00          push   0x1
   a:   68 02 00 00 00          push   0x2
   f:   68 03 7c 00 00          push   0x7c03
  14:   59                      pop    ecx
  15:   58                      pop    eax
  16:   49                      dec    ecx
  17:   89 c0                   mov    eax,eax
  19:   01 ff                   add    edi,edi
  1b:   48                      dec    eax
  1c:   75 fb                   jne    0x19
  1e:   39 cd                   cmp    ebp,ecx
  20:   74 05                   je     0x27
  22:   e9 ee ff ff ff          jmp    0x15
  27:   f4                      hlt

外側のループは3回回るので、最初はpush 0x7c03push 0x03にしてcmp ebp,ecxの部分で値が0になっているeaxと比較していた。これだとdecの条件を満たさなくてダメだった。ebp0x7c00で初期化されている。

まあ、考えてみれば、律儀にループにする必要も無かったかもしれない。ジャンプで条件を満たしても満たさなくても次の命令に飛ばせば良い。

[*] API /attack/4/submit with input = h   h   h   h|  YXI佳Hu・ヘt鴃
{'message': 'Good! Submit this flag :)', 'flag': 'SECCON{Good job! You are emperor of x86-64 :pray: tAXnPJEpyLoLKsQR}', 'error': False}

フラグの中身を今読んだ。x86だと思っていたけどx64だったのか。まあ、通ったから良いか。

SECCON{Good job! You are emperor of x86-64 :pray: tAXnPJEpyLoLKsQR}

防御点

1時間ごとにbox1を元にした問題が出題される。最初に解いた1チームディフェンスキーワードが書き込まれる(1時間そのチームに点数が入る)。

国内で準優勝したkimiyukiさんはこれを自動化して点数を稼いでいたらしい。一応1日目に別の時間の問題を持って帰って中を見てみたけど、バイナリ自体も変わっていた。トレース結果が変わるくらいなら頑張ろうと思ったけど、これはつらい。

ゲームのAIを対戦させて勝ったチームに防御点が入るらしい。攻撃点は無し。面白そうだったけれど、手を付ける余裕が無かった。

Jeopardy。小問が(たしか)5個。防御点は無し。大問の問題ページをメモってくるのを忘れたので、小問の順番は適当。

mimura

コンテスト開始前に謎の電子部品が配られた。

20191223.jpg

「記念品かな? ラッキー」と思っていたけど、問題だったわ。問題文には

It's a hardware challenge.

としか書かれていなかったがこれでしょう。手つかず。

Bad Mouse

たぶん上の写真の左下のやつ。指してみたけどエラーで認識されなかった。

こちらはIntel HEX形式のファームウエアが与えられた。冒頭に

=>?@aBcDAUiHkJKLMNQPOaS\UVWXY`kemgoiAqcdEywLyN{P]Mopqrstuvwxyz{|
}~OHrQCLEFGHiN[U]W_YqXSTUVWXYZ[\]^_`}fElGnIpmqklk}QxSzU|w~wxuA}D
?FAHEFCDAMIPKRMTQROPMYU\W^Y`Ua[\[mc`ibsdcughIjKlK}p@rBstwzY@kBeD
CE?@CBERgGaHOJKLINQYS[UYQYWXyakemgoiAgcdelwqys{uM}opw~KwUvO{?F{|
IDQIcJUMQLGHgP]T_VaXsYSTSeg`ibkd}e_`cbedcuihkjklmt?yA{C}UEwxuz}E
?GAE}ECDAFIQKSMQIQOPqYc\e^g`y^[\UeshsjulEmghejmuowqumustS|I@KBMD
_E?@?QSLUNWPiQKLORqXCZ}\[]WXQaodofqhAicdqlyqKr}uytopOxE|G~I@[A{|
[DQHSJULgMGHIJ[T~]OXQRSTuYoZ_\Yk]`_`gb{dEu?hojklMu?xAzC|UzwxYz[|
yMa@cBCDELWQYS[Um]OPoXe\g^i`{a[\[mohqjslEmghGp}t?vAxSystvFxHWI[|
]~?@B

という文字列があり、ある程度規則的に値が増えているけれどそうでないところもあり、ここにマウスの動きをエンコードしているのかな?と、ファームウエアを読んでみた。

AVR形式なので、AVR形式をダンプできるようにビルドしたobjdumpならば、

$ objdump -D -m avr:25 -b binary firm.bin

で逆アセンブルできる。AVRの解説をしているサイトを読みながら処理を追ってみたけれど良く分からず。終了1時間前くらいに「Ghidraが対応しているんじゃない?」と思って試してみたら対応していた。まだ難しいがもう少し早く気が付いていれば何とかなったかもしれん。

factortheflag

I hid the SECCON{} flag in a big prime number.
1401111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111112220791111111111111111
1111111111111111111111111111122207911223089031988903088023088023088920012001200
2319889030879222080230880230890319887911122318879211992120012999912120013000013
0000131008920012001199121200120022089200130000119912119911121200120011992119912
1199121199121199121200130101000012001199121200120930009200130000119921199111121
2001200119921199121199121199121199121200130010208012002318879112120929999112120
9299991212103188892001200119912230890318889199121199121200130000131007911112119
9212092091991211992119912120013010111188791222079112129999121199121199121200130
0001200119911121200120012091992119921299992120013010099991112119911112129999121
1991211991212001300001200120012001209199223198889199212001209199213010099991112
1199212001299991212001300001300001300001200120012001299991111212091991212001209
1992130100999911121199122308903198890308802308802308892001200119922308903198879
2121031988903088011992130100999911121199111111111111111111111111111111111111111
2220791111111111111111111111111111111111111111111112220791111111111111111111111

「SECCONで素数の性質とかをガチで使った問題が出るか……? :thinking: 」と思って、メモ帳に貼り付けてメモ帳の幅を変えると何か周期が見える。色々試すと幅97文字が一番良い感じ。開始位置をちょっとズラして、

140
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111112220791111111111111111111111111111111111111111111112220791122
3089031988903088023088023088920012001200231988903087922208023088023089031988791112231887921199212
0012999912120013000013000013100892001200119912120012002208920013000011991211991112120012001199211
9912119912119912119912120013010100001200119912120012093000920013000011992119911112120012001199211
9912119912119912119912120013001020801200231887911212092999911212092999912121031888920012001199122
3089031888919912119912120013000013100791111211992120920919912119921199121200130101111887912220791
1212999912119912119912120013000012001199111212001200120919921199212999921200130100999911121199111
1212999912119912119912120013000012001200120012091992231988891992120012091992130100999911121199212
0012999912120013000013000013000012001200120012999911112120919912120012091992130100999911121199122
3089031988903088023088023088920012001199223089031988792121031988903088011992130100999911121199111
1111111111111111111111111111111111112220791111111111111111111111111111111111111111111112220791111
111111111111111111

スクショをペイントに貼り付けてバケツツール。

image.png

最後の2文字が読めなかったが、524287はメルセンヌ素数。で、mPだった。

SECCON{524287mP}

追記

想定解法はまずは素因数分解らしい。もっと楽に読める。大きな数に小さな数を掛けても、大きな数の雰囲気(?)はあまり変わらないから解けたのか。

SECCON 2019 Final write-up (Factor the flag, Bad Mouse) - 竹迫の近況報告

QR Decoder

ウェブサーバーのURLとバイナリファイルが与えられる。バイナリファイルを見ると、zbarimgというプログラムを実行して結果をQR-Code:Hello, world!と比較している。Hello, world!をQRコードにしてサーバーに投稿したら1個目のフラグゲット。

The decoded string is: Hello, world!
Congratulations!
The first flag is SECCON{8182 means decimal ASCII code of Q and R}.
Next, read ./flag2.txt in the server.

SECCON{8182 means decimal ASCII code of Q and R}

素直に http://10.1.5.1:8182/cgi-bin/flag2.txt を開いたら2個目のフラグが読めた。終わり。コマンドラインインジェクションか何かができるのかな?

追記

バッファオーバーフローがあったらしい。たしかに。これでアドレスのリークとか無しに解けという問題ならば、ファイル名が与えられるのも納得(libcを利用してシェルが取れるならファイル名は要らない)。が、後続の変数を壊すと落ちるというのと、PIEが有効というのがあり、ホントにこれで解けるのか謎。

SECCON{U R QR C0DE H4CK3R}

syzbot panic

syzbotに関する問題で、さらに小問が5問あって繋げて1個のフラグにする。2問目以降は「これこれこういう(たぶんsyzbot自体やLinuxの)コミットのSHA-1の先頭10文字は?」という形式。1問目は、

(Q1) What is FQDN (in all lower characters) of a website that manages the state of bugs syzbot has found?

で、まずこれが分からん。フラグの全体の長さが与えられているから計算すると23文字になるはず。syzkaller.appspot.com(21文字)じゃないのか?

この日本語のページが読めるかどうかで大きく差が付いて国際大会としては良くないと思うんだよなぁ……。結局解けていないからこのページに書かれているコミットに正解があるのかは知らないけど。

The SYZBOT CTF

追記

SECCON 2019 国内本選 writeup | にろきのメモ帳

あれ、syzkaller.appspot.comで良いの? だって文字数……。テキストエディタのルーラーで確認したり構成要素ごとに分けて計算したりしていたけど……。と見返したら、これはひどい:innocent: さて、どこが間違っているでしょうか?

image.png

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2