KOSEN セキュリティコンテスト 2017 に参加したおはなし

はじめに

KOSENセキュリティコンテスト2017にPwnPwnPainとして参加してきました。
参加は2度目で、今年は去年とは打って変わって「Jeopardy」と「KoH(King of Hill)」形式でのCTFでした(去年はサーバーの脆弱性を潰すHardening的な形式?でした)。 レベル感は全体的には低めに感じましたが、まだまだ力及ばずな問題もありました。

とりあえず、備忘録、復習の記録ってのも兼ねて、競技時間中に解けた問題と、終わってから解けた問題のWriteupをカテゴリーごとにゆるーく書きたいと思います。

また、Writeupにて、出力や入力が極端に長い場合、...として省略させていただきます。

f:id:ush1ken:20171023000720p:plain

目次

Sample

00 _ サンプル _ 100

これはサンプル問題である。 今回出題される問題は、全て答えとなる「フラグ」が含まれている。
フラグは、必ずSCKOSEN{hoge}の形式になっている。
この問題のフラグはSCKOSEN{Let's enjoy}である。入力しろ。

Flag: SCKOSEN{Let's enjoy}

Binary

01 _ フラグを答えろ _ 100

正しいフラグを書けば正しいかどうかを判定してくれる、便利なアプリを開発した。
フラグを調べて解答せよ

a.outという実行ファイルが配布されるので、何も考えずにstringsコマンド...

$ strings a.out 
/lib64/ld-linux-x86-64.so.2
{e#yd
libc.so.6
puts
__stack_chk_fail
stdin
printf
strtok
fgets
strcmp
__libc_start_main
__gmon_start__
GLIBC_2.4
GLIBC_2.2.5
UH-`
SCKOSEN{H
h1dden_fH
1ag}

一応実行してみる。

# ./a.out    
Enter the FLAG: SCKOSEN{h1dden_f1ag}   
correct! the flag is SCKOSEN{h1dden_f1ag}

Flag: SCKOSEN{h1dden_f1ag}

02 _ ファイル名を探せ _ 100

フラグは簡単だ、ファイル名に隠した。

qというファイルが渡されるので、何も考えずにfileコマンド...

$ file q
q: POSIX tar archive (GNU)

tarファイルということなので、解凍。

$ tar xvf tmp 
x q/
x q/xNUMaohRhJu8EJTLRx0ivRfwXk3cc
x q/y2Ce56H56EVYwO7tWgOXo726O0uXp
x q/h14PySClySuaKQ6kxjLYWhq99ibhm
x q/2bTq1nVLWdgX6aypu018Yl84ubSsE
x q/VgiiZE4hEprvEWdLnsO4G8KbOHUTi
x q/G0wmRiMSrE1uArCttTbnd1wefAFad
x q/8Q0VABE4ellLGwRVQtb640YcDXCB2
x q/arRyqJbDkwFhzfSvDJEbpvlGYJy3q
x q/BNpv17Z6xGJhMatl91SZbU5MRErTa
...

なんかqっていうディレクトリにファイルがたくさん(18888個)でてくる。
ファイルの中には何も書かれていないので、とりあえずファイル名をbase64デコードしてみるも、Incorrect Paddingと言われる。
冷静になって考えたら、18888個の中にフラグがあるのでは、と思ってgrepしてみる。

$ ls q | grep SCKOSEN
SCKOSEN{ki_ha_mori_ni_kakuse}

Flag: SCKOSEN{ki_ha_mori_ni_kakuse}

03 _ ボスを倒せ _ 200

とあるゲームを見つけたのだが、ボスがあまりにも強すぎて一切倒せそうにない。
どうにか倒す方法はないだろうか?
ゲームへの接続方法: nc [hostname] [port] 例: nc www.ctfkit 8050

この問題は、チームメンバーが適当にBoFしようとしたら解けた問題。。
ちゃんとロジックを考えるために、競技終了間際に挙動をメモったので復習。

とりあえずncで接続。

$ nc score.kosensc2017.tech 40048
Input your name: AAAAA
Player's HP: 1000
Boss's HP: 12345678
Next round=>^Z

たぶん、最初の入力にBoFがあるんだろうなあ、と考えてAをいっぱい入れてみる。

$ nc score.kosensc2017.tech 40048
Input your name: AAAAAAAAAAAAAAAAAAAAAA 
Player's HP: 1094795585
Boss's HP: 16705
Next round=>^Z

お、減った。ということで、16075をpythonhex(16075)してみると見事'0x4141'という出力に。
ここで、入力したAは22個なので、1個減らせばボスのHPは0x41で65になるのでは、と思うものの65だとちょっとまだ強い()ので、スペースBoF。(なぜかecho -eで\x01とかが流し込めなかった、、)

$ nc score.kosensc2017.tech 40048
Input your name: AAAAAAAAAAAAAAAAAAAA 
Player's HP: 1094795585
Boss's HP: 32
Next round=>1

Round 1/10
    Player's Attack!
    Boss's HP: 21 (-11)
    Boss's Attack!
    Player's HP: 1094795419 (-166)
Next round=>
Round 2/10
    Player's Attack!
    Boss's HP: 12 (-9)
    Boss's Attack!
    Player's HP: 1094795244 (-175)
Next round=>1

Round 3/10
    Player's Attack!
    Boss's HP: 3 (-9)
    Boss's Attack!
    Player's HP: 1094795069 (-175)
Next round=>
Round 4/10
    Player's Attack!
    Boss's HP: 0 (-10)
    Boss's Attack!
    Player's HP: 1094794937 (-132)
You win!
Flag is SCKOSEN{buffer_over_flow!}

Flag: Flag is SCKOSEN{buffer_over_flow!}

04 _ OreNoFS _ 500

このファイルraw.dmgには、どうも独自のファイルシステムが構築されているらしい。 このファイルシステムに格納されたデータは1つ、そのデータを復元しよう。
このファイルシステムは、クラスタ単位で管理されており、1クラスタ=4096byteであることは分かっている。
また、AllocationTableというFATファイルシステムでいうFAT領域とディレクトリエントリ、データ格納領域に分けられているようだ。
また、GUID Partition Table(GPT)でフォーマットされているので、その分はクラスタの管理外である点に注意しなければならない。
ATは2byte*8192、ディレクトリエントリは32byteで下記の構造体で定義されているらしい。

 struct directory_entry {
         unsigned char magic;(1byte) 
         char filename[8];
         char extension[3];
         unsigned int size;(4byte) 
         unsigned long offset_of_cluster;(2byte) 
         char attribute[12];
         unsigned long reserved;(2byte) 
};

解けなかったので他の方のwriteupを読んで解きたい。。

Crypto

05 _ 簡単な暗号化2 _ 100

ファイルからフラグを探せ

チームメンバーが、1日目の夜に解いてくれた問題。

問題ファイルにアクセスすると、base64っぽい文字列がずらーっと並んでいたので、コピペして、pythonに流し込んでdecodeしてみる。

$ python
>> import base64
>> input = '''
NGI1MDA0MDMwMDE0MDgwODAwMDgyYzU2NGI1MzAwMDAwMDAwMDAwMDAwMDAwMDAw...
'''
>>> base64.b64decode(("".join(input.split('\n'))))
'4b5004030014080800082c564b53000000000000000000000000000b0000725f6c652f73722e6c65ad73dd92034a0c41ef...'
>>> len(base64.b64decode(("".join(input.split('\n')))))
8520

decodeしてみると、何かのファイルのバイナリっぽい何かが出てくる。
チームメンバーが解いたときリトルエンディアンうんぬん言っていたので、頑張ってリトルエンディアンにしてファイルに出力してみる。

>>> b = base64.b64decode(("".join(input.split('\n'))))
>>> packed = "".join([pack("<L",int("0x"+b[i:i+8], 16)) for i in range(0, len(b), 8)])
>>> "".join([pack("<L",int("0x"+b[i:i+8], 16)) for i in range(0, len(b), 8)])
'\x03\x04PK\x08\x08\x14\x00V,\x08\x00\x00\x00SK\x00\x00\x00\x00...'
>>> f = open("out", "wb")

出力したものをfileコマンドで調査。

$ file out
out: data

なんかうまくいっていない。。
また冷静になって考えてみると、先頭4バイトが\x03\x04PKとなっていて、なんかみたことあるなあ、、とggると、ZIPファイルのシグネチャ0x504B0304(PK\003\004)となっていた。
なるほど。ということで、2バイトでリトルエンディアン取り直すことに。

>>> "".join([pack("<H",int("0x"+b[i:i+4], 16)) for i in range(0, len(b), 4)])
'PK\x03\x04\x14\x00\x08\x08\x08\x00V,SK\x00\x00\x00\x00...'

お、今度はちゃんとZIPファイルと同じシグネチャになった。ということで、ファイルとして出力してみる。

>>> packed = "".join([pack("<H",int("0x"+b[i:i+4], 16)) for i in range(0, len(b), 4)])
>>> f = open("out", "wb")
>>> f.write(packed)

出力したファイルを調査すると、Microsoft社のWordファイルだということが分かるので開いてみる。

$ file out 
out: Microsoft Word 2007+
$ mv out.doc out.docx
$ open out.docx

f:id:ush1ken:20171022183858p:plain Flag: SCKOSEN{TEXT_BUT_NOT_PLAIN}

06 _ 解凍して解答せよ _ 100

ファイルからフラグを読み取れ!

flag.zipというファイルが渡されるので、解凍すると、flagというディレクトリの中にpngが2つ展開される。

$ unzip tmp.zip 
Archive:  tmp.zip
   creating: flag/
  inflating: flag/masks.png          
  inflating: flag/xor.png 

開いてみると、次のような画像が表示される。 f:id:ush1ken:20171022184929p:plain 画像をみると、xorしたらなんか得られそうだし、画像のファイル名もmasksとxorだし、ということで画像のxorをとってみる。

import cv2
masks = cv2.imread('./flag/masks.png')
xor = cv2.imread('./flag/xor.png')
flag = cv2.bitwise_xor(masks, xor)
cv2.imwrite('flag.png', flag)

f:id:ush1ken:20171022192038p:plain Flag: SCKOSEN{simple_visual_cryptography}

07 _ 簡単な符号化 _ 100

U0NLT1NFTntiYXNlNjRfaXNfdmVyeV9lYXN5fQ

base64デコードするだけ。

$ echo "U0NLT1NFTntiYXNlNjRfaXNfdmVyeV9lYXN5fQ" | base64 -D
SCKOSEN{base64_is_very_easy

Flag: SCKOSEN{base64_is_very_easy}

Network

15 _ 寝坊気味のコンピュータ _ 100

ここにある通信をキャプチャしたファイルがある。この中から、フラグを見つけ出せ!

problem.pcapngというファイルが渡されるのでとりあえずstringsコマンド...

$ strings problem.pcapng 
Linux 4.10.0-37-generic
Dumpcap (Wireshark) 2.2.6 (Git Rev Unknown from unknown)
wlp4s0
Linux 4.10.0-37-generic
SCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSESCKOSE
N{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wakeN{wake
_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la_on_la
n_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_an_is_a
1~_z
lerm}
lerm}
lerm}
lerm}
lerm}
lerm}
lerm}
lerm}
lerm}
lerm}
lerm}
lerm}
lerm}
lerm}
lerm}
lerm}

フラグっぽいのがみえるので手動で抽出。
ちなみに、Wake-on-LANパケットは、起動したいマシンのMACアドレスを16回繰り返したデータを含むそうです。
この問題では、フラグが6文字ずつMACアドレスとしてWoLパケットに埋め込まれているという問題でした。
Flag: SCKOSEN{wake_on_lan_is_alerm}

16 _ ログインしたいんだ! _ 100

ここにある通信をキャプチャしたファイルがある。この中から、フラグを見つけ出せ!

これもまたproblem.pcapngというファイルが渡されるのでまた何も考えずstringsコマンド...

$ strings problem.pcapng
...
GET / HTTP/1.1
Host: 172.27.132.124
Connection: keep-alive
Authorization: Basic YWRtaW46U0NLT1NFTntiYXNpY19pc191bnNlY3VyZX0=
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.102 Safari/537.36 Vivaldi/1.94.971.8
...

ざーっと眺めるとしたのほうにbase64っぽい文字列を発見。 よく見るとBasic認証をしているので、base64デコードしてみる。

$ echo "YWRtaW46U0NLT1NFTntiYXNpY19pc191bnNlY3VyZX0=" | base64 -D
admin:SCKOSEN{basic_is_unsecure}

Flag: SCKOSEN{basic_is_unsecure}

別解

f:id:ush1ken:20171022231009p:plain たぶんこっちが正攻法。
WireSharkなどのパケットキャプチャツールでproblem.pcapngを開くと、401 Unauthorizedで認証に何度も失敗しているHTTP通信が見られるので、認証に成功している(200 OKな)パケットを探す。
48パケット目に200 OKなパケットがあり、その直前のGET / HTTP/1.1なパケットを見ると、Basic認証の情報があるので、パスワードがフラグになっている。 f:id:ush1ken:20171022232423p:plain Flag: SCKOSEN{basic_is_unsecure}

17 _ ファイル送信 _ 200

これはファイルを送受信しているpcapです

file.pcapというファイルが与えられるので、とりあえずstrings...してもあまり嬉しい情報は見つからなかった。
素直にWireSharkで開くと、Lenna.pngと、lock.zipがHTTP通信で送受信されているのがわかる。
ということで、WireSharkFile -> Export Objects -> HTTP -> Save Allでファイルをエクスポート。

それぞれのfileコマンドの結果を見てみる。

$ file Lenna.png lock.zip 
Lenna.png: PNG image data, 512 x 512, 8-bit/color RGB, non-interlaced
lock.zip:  Zip archive data, at least v2.0 to extract

普通のファイルっぽいので、普通にunzipしてみる。

$ unzip lock.zip 
Archive:  lock.zip
[lock.zip] Lenna.png password: 
password incorrect--reenter: 
password incorrect--reenter: 
   skipping: Lenna.png               incorrect password
[lock.zip] flag.txt password: 
password incorrect--reenter: 
password incorrect--reenter: 
   skipping: flag.txt                incorrect password

普通にパスワードがかかっててunzipできない。
ここでみんなで数時間悩んだ。
「Lenna.pngにパスワードが隠されているのでは?」「TCPパケットにパスワードが隠されているのでは?」「BruteForceでパスワードクラックするのでは?」
結論からいえば、既知平文攻撃でした。
1日目終了後、おうちに帰って冷静になってctf zip パスワードとかでwriteupを適当にggっていたら、あ。これだ。

SECCON 2015 Online CTF Writeup

ということで、pkcrackをインストールし、早速実行。

$ zip Lenna.zip Lenna.png
$ pkcrack -C lock.zip -c Lenna.png -P Lenna.zip -p Lenna.png -d flag.zip
...
Done. Left with 90 possible Values. bestOffset is 298362.
Stage 1 completed. Starting stage 2 on Sun Oct 22 23:51:16 2017
Ta-daaaaa! key0=e15a792b, key1=591e8679, key2=9dee3dba
Probabilistic test succeeded for 175560 bytes.
Ta-daaaaa! key0=e15a792b, key1=591e8679, key2=9dee3dba
Probabilistic test succeeded for 175560 bytes.
Stage 2 completed. Starting zipdecrypt on Sun Oct 22 23:51:19 2017
Decrypting Lenna.png (155edefb6ebeec35536b5a3d)... OK!
Decrypting flag.txt (f38434531be49de337a96b0e)... OK!
$ cat flag.txt
SCKOSEN{k_p_t_a}

Flag: SCKOSEN{k_p_t_a}

おわりに

今回のKOSENセキュコンでは、圧倒的知識不足を感じました(毎回感じてる気がする)。
というのも、解きたい問題のメインテーマについてそこそこ知識が無いと解けない問題というものにどんどん直面してきてるように感じるので、もっと広く深く勉強したいです。
解けなかった問題についても、プロ各位のwriteupを見て勉強したいです。。

楽しい大会をありがとうございました!