csaw ctf 2016 quals のwriteup

CSAW 2016 にTSGの面々(dai,moratorium08,satos)で参加してました。
14問解いて1226ptで126位でした。
自分が解いたり関わったりしたやつについてのWriteupっぽいものです。

The Rock

64bitのELF。stripped。
C++で書かれたやつだったので、objdumpをデマングルしてみたりする。c++filtというのでできるらしい。やったらわりと読みやすくなったので便利だった。

import os

def dem(s):
	os.system('echo "' + s + '" | c++filt > do.txt')
	with open('do.txt','r') as fp:
		res = fp.readlines()[0].split('\n')[0]
	#print res
	return res

with open('objd.txt','r') as fp:
	gs = ''.join(fp.readlines())
	ls = len(gs)
	ts = ""
	i = 0
	while i<ls:
		if gs[i]!='<':
			ts += gs[i]
			i += 1
		else:
			ts += gs[i]
			i += 1
			cs = ""
			while gs[i]!='>' and gs[i] != '@':
				cs += gs[i]
				i += 1
			ts += dem(cs)
			#print ts
	with open('demd.txt','w') as gp:
		gp.write(ts)

問題は正しい文字列を入れるとcorrect the flag is flag{入力文字列}を返してくれる系のやつ。
最初、適当なのを入れるとToo short or too long とか返ってくる。
lengthで調べると、

  4016d2:	e8 29 f7 ff ff       	call   400e00 <_ZNKSs6lengthEv@plt>
  4016d7:	48 83 f8 1e          	cmp    rax,0x1e  

というのがあったので、'a' * 30 をぶちこんでやると、passed 0 とか言われるようになる。
うまいことcorrectが出るようなパスを探すと、call 4017e6 の返り値をうまいことしてやればよいと分かる。4017e6は引数として自作謎構造体が与えられており、その中の2つの文字列を比較している模様。いろいろと入力を変えて調べてみると、片方は固定で、もう片方は入力に依存している。で、依存している方は'a' * 30 を入れると '^' * 30が、'a' * 15 + 'b' * 15 を入れると '^' * 15 + '_' * 15 が返ってくるのでどうやら各文字について変換してるっぽい。ここで'\x00' * 30 から '\xff' * 30 までを入力として走らせてみると、ひとつだけpassed 1 が返ってくるので順繰りに1文字ずつあわせていけばよいとわかる。

import os

bs = ""
while len(bs)<30:
	mbl = len(bs)
	for i in range(0,128):
		ns = bs + chr(i)
		ns += (30 - len(ns)) * 'a'
		with open('i.txt','w') as fp:
			fp.write(ns)
		os.system("./rock < i.txt > co.txt")
		
		ok = False
		with open('co.txt','r') as fp:
			rs = fp.readlines()[-1]
			rs = rs.split()[-1]
			#print rs
			if rs.isdigit():
				#print rs
				if int(rs,10)>len(bs):
					bs += chr(i)
					ok = True
		if ok:
			break
	print bs

	if mbl==len(bs):
		break

deedeedee

64bitのELF。not stripped。
実行してみると、

Your hexencoded, encrypted flag is: 676c60677a74326d716c6074325f6c6575347172316773616c6d686e665f68735e6773385e345e3377657379316e327d
I generated it at compile time. :)
Can you decrypt it for me?

と言われる。いわゆるコンパイル時実行というやつ(テンプレートとかのあれ)らしい。中を見てみると、どうやらD言語で書かれていたらしく、D言語的デマングルがされてる。実際に出力部を見てみると、変換後の文字列がそのままデータとして格納されてるようで元の文字列は影も形もない。どないするねんとなる。

うろうろしていると、_D9deedeedee7encryptFNaNfAyaZAya という関数が見つかる。中をのぞいてみると、

edi , esi = _D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya(edi,esi)
edi , esi = _D9deedeedee21__T3encVAyaa3_323232Z3encFNaNfAyaZAya(edi,esi)
...
こんなのがたくさん続く
...
edi , esi = _D9deedeedee33__T3encVAyaa9_343939343939343939Z3encFNaNfAyaZAya(edi,esi)

みたいな感じになってる。どうやら名前からするにそれぞれの関数はtemplate的なやつを用いてコンパイル時に作られたみたい、ということはこの関数が実行時にかまされたやつなのでは、と推測が立つ。

各関数を解析してみると、たとえば、_D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAyaは、

convx(){
	acc = s[0];
	res = "";
	for(p,q in zip(circle("111"),s)){
		res.append(acc ^ (p ^ q));
	}
	return res;
}

みたいになってることがわかる。(D言語を書いたことがないので文法は適当)

ここでaccの情報は落ちてしまうが、xor演算はモノイドになるので圧縮できて、最後に256パターンを試してやればよい。あとは関数名を抽出してやって復元してやればよい。

ts = ""
with open('objd.txt') as fp:
	s = fp.readlines()
	i = 0
	while s[i] != '  44cde0:	55                   	push   rbp\n':
		print i,
		i += 1
	while s[i] != '  44e36b:	0f 1f 44 00 00       	nop    DWORD PTR [rax+rax*1+0x0]\n':
		ts += s[i]
		i += 1
		
	with open('ed1.txt','w') as fp:
		fp.write(ts)


fs = []

with open('ed1.txt','r') as fp:
	ss = ''.join(fp.readlines())
	ss = ss.split('\n')
	for r in ss:
		#print 'x',r
		if len(r)>0 and r[-1] == '>':
			ads = r.split('<')[1][:-2]
			ads = ads.split('_')[-1]
			ads = ads.split('Z')[0]
			
			ts = ""
			ls = len(ads)
			for i in xrange(ls/2):
				#ts += ads[i*2:i*2+2] + ','
				ts += chr(int(ads[i*2:i*2+2],16))
			
			fs.append(ts)


#print fs
fs = fs[::-1]

bs = "gl`gzt2mql`t2_leu4qr1gsalmhnf_hs^gs8^4^3wesy1n2}"
bs = map(ord,bs)

for cv in fs:
	ts = []
	lv = len(cv)
	i = 0
	for c in bs:
		ts.append(c ^ ord(cv[i % lv]))
		i += 1
	bs = ts

#flag{t3mplat3_met4pr0gramming_is_gr8_4_3very0n3}

for i in xrange(256):
	ns = ""
	for c in bs:
		ns += chr(c ^ i)
	print ns

Sleeping

あんちくしょうシリーズその1。
ソースコードが与えられる。
len(key)==12ならbase64.b64encode(open("./sleeping.png",'rb').read()の値が返ってくる、とか書かれている。まずkeyが使われていないのが謎。また、ncして飛んでくる文字列をbase64デコードしてもpngの形式にならずただのdataだといわれる。なにこれ、エスパーでは...とかいいつつ放置していた。
ふと思い立ってPortable Network Graphics - Wikipediaを調べてみるとpngの先頭16byteは固定とのこと。ここでエスパー力が働き、len(key)==12のみ分かってるってことはkeyが繰り返しxorされているのでは!?となってこれをもとにkeyを算出してやるとなんか画像が復元されるので解ける。(この問題が溶けるとしたらこういう解法くらいしかないよなぁ...とか、ファイルの先頭の方のAscii文字度が高いのでなんかXOR的なあれでは...?くらいの推理はしていた)

Gametime

あんちくしょうシリーズその2。
音ゲーっぽいゲーム(Windows,32bit)が与えられるので、そのReversing...のはずだった。
「これパッチを当てるとこがたくさんあって面倒ですね。なんか1337と比較してるとこがあるし無限に時間がかかるのかな...?」「とりあえず遊んでみる?」
1~2分後...
「なんかThe key isとか出てるんだけどこれはなんなんですかね」「とりあえずSubmitしてみる?」 -> Accept
とかいうゲームを遊んでみるだけ問だったのでRev要素が0でした...(なんだったんだ)

Key

若干あんちくしょう。
Windows,32bitのReversing。
1. ふつうに起動すると、?W?h?a?t h?a?p?p?e?n?と言われる。調べてみると、C:\Users\CSAW2016\haha\flag_dir\flag.txtをOpenしようとしている部分が見つかるのでそれを作る。中に適当にflaghogrhogrとか書いておく。
2. 実行してみると、=W=r=o=n=g=K=e=y=と言われるので、見てみると、その直前あたりに判定ルーチン(sub_4020c0)があって、sub_4020c0(flag.txtの文字列,長さ,謎固定文字列(idg_cni~bjbfi|gsxb),長さ) みたいな呼び出され方をしている。
3. sub_4020c0はなんかめんどくさいことをやってるなー、とりあえず挙動でもみてみるかー、と思ってflag.txtの中身をidg_cni~bjbfi|gsxbにして動かすと、なんかYou Did It といわれる。
4. !? と思ってidg_cni~bjbfi|gsxbをsubmitするとCorrectと言われるのでおしまい。sub_4020c0はstrcmpを難読化したやつだったみたい。

途中めんどくさいなあと思って放っておいたのだけれど雑に手をつけたら雑に解けてしまった感じ。

Regexpire

Misc。実質PPC
javascriptのっぽい正規表現が飛んでくるので、それにmatchする文字列を返してやる。
途中、\dとか\Dとか\wとか\Wを勘違いしていてなかなか苦労した。あとパーザはちょっと雑でざる。下手に書いてしまった気がする。

import struct, socket, sys, telnetlib, os

import urllib, time

class mystr:
	def __init__(sl,s):
		print 'init',s
		sl.s = s
		sl.i = len(s)-1

	def getc(sl):
		if sl.i==0 or sl.s[sl.i-1]!='\\':
			res = sl.s[sl.i]
			sl.i -= 1
		else:
			if sl.s[sl.i]=='D':
				res = 'a'
			elif sl.s[sl.i]=='d':
				res = '3'
			elif sl.s[sl.i]=='w':
				res = 'a'
			elif sl.s[sl.i]=='W':
				res = '%'
			else:
				res = [sl.i]
			sl.i -= 2
		return res
	
	def getw(sl):
		print 'getw',sl.i,sl.s[sl.i]
		c = sl.s[sl.i]
		if c==')':
			g = ""
			sl.i -= 1
			while sl.s[sl.i]!='|':
				g = sl.s[sl.i] + g
				sl.i -= 1
			res = mystr(g).getma()
			while sl.s[sl.i]!='(':
				sl.i -= 1
			sl.i -= 1
		elif c=='*':
			sl.i -= 1
			sl.getw()
			res = ""
		elif c=='+':
			sl.i -= 1
			res = sl.getw()
		elif c==']':
			sl.i -= 1
			res = sl.getc()
			"""
			while res =='' or res == ' ':
				res = sl.getc()
			"""
			tc = ''
			while tc!='[':
				tc = sl.getc()
		elif c=='}':
			sl.i -= 1
			p = ""
			while sl.s[sl.i]!='{':
				p = sl.s[sl.i] + p
				sl.i -= 1
			x = int(p,10)
			sl.i -= 1
			pw = sl.getw()
			res = pw * x
		else:
			res = sl.getc()
		return res
	
	def getma(sl):
		res = ""
		while sl.i>=0:
			res = sl.getw() + res
			print sl.i,res

		print res
		return res


'''
q = '[sjN]*[a-zA-Z]0IN+[a-z][a-z]*u{9}\W(trump|clinton){6}[i4yCGL][i-r]*'

print mystr(q).getma()
exit(-1)
'''

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('misc.chal.csaw.io', 8001))

def getunt(c):
	res = ""
	while res=='' or res[-len(c):]!=c:
		res += sock.recv(1)
	print res
	return res

getunt('?\n')

while True:
	s = getunt('\n')
	s = s[:-1]
	if s=='Irregular':
		break
	res = mystr(s).getma()
	sock.send(res + '\n')

Tutorial

前の記事参照。

感想

Reversing面倒くさいなあと思いつついろいろと解いていた感じ。

エスパー問がたくさんあってエスパー力が身についたのでよかった(よくない)。エスパー問は撲滅されるべき。

はじめて生のCTF中にpwn問題を通すことができたのでいい記念になった。

追記

Neo

大会中にとけなかったやつ。Padding Oracle Attackだと予想はついたものの、自分で一度も書いたことがなく、また他のとこから引っ張ってきたコードが動かなかったので解けずじまいだった。
大会終了後もしばらくサービスが動いていたので解いた。

#coding: utf-8

import urllib


from HTMLParser import HTMLParser

class TestParser(HTMLParser):
	def __init__(self):
		HTMLParser.__init__(self)
		self.findederr = False
	def handle_starttag(self,tagname,attribute):
		if tagname.lower() == "div":
			if attribute[0][1]=='alert alert-error':
				self.findederr = True

def oracle(gs):
	#print gs
	url = 'http://crypto.chal.csaw.io:8001/'
	params = urllib.urlencode({'matrix-id':gs})

	#print 'send',
	#print params
	f = urllib.urlopen(url, params)

	s = f.read()
	#print s
	#print 'recv',
	
	parser = TestParser()
	parser.feed(s)
	res = parser.findederr
	parser.close()
	
	return not res


import base64
#bs = "pDjx7VaOqCkjk/AniTtGezPvSfBjXzy6oWg7PjGrJxvnAHbQPXwU7Zm87abBv4OIcQN+mFeG7mwIOb+cg+8//Ud0HpJXowsgLHkE/CEIrag="

bs = "nfUgtYJZpGxz7mlMKFaGIuPmDBSiO7DrtuULgAawfNULJyjnYRP4AzZp7Mwi/ZvFDLGKKVvHv0TqhdcklTi0ygwfQM+XRYCo+dFLq8qEkQ4="
bs = base64.b64decode(bs)


blocks = []
for i in xrange(len(bs)/16):
	blocks.append(map(ord,bs[i*16:(i+1)*16]))
print blocks

plain = ""

idx = 0
for nb in blocks[1:]:
	#b1 = map(ord,bs[80-16:80])
	npl = [0] * 16

	for n in range(0,16)[::-1]:
		pn = 16-n
		for c in xrange(256):
			#print c,
			npl[n] = c
			sv = [npl[i] ^ pn for i in xrange(16)]
			sv += nb
			sv = ''.join(map(chr,sv))
			sv = base64.b64encode(sv)
			if oracle(sv):
				print 'n .. ', n, 'c .. ',c
				break
	
	npl = map(lambda (p,q): p^q,zip(npl,blocks[idx]))
	npl = ''.join(map(chr,npl))
	print npl
	plain += npl
	idx += 1

print plain

#flag{what_if_i_told_you_you_solved_the_challenge}