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}