SECCON 2015 Online CTF Writeup
Crypto 100 (Unzip the file)
パスワード付きのzipファイルが渡されます。
中身のファイル名がbacknumber08.txt, backnumber09.txt, flagの3つなので、それでググるとsecconメールマガジンのバックナンバーが引っかかります。
http://2014.seccon.jp/mailmagazine/backnumber08.txt
http://2014.seccon.jp/mailmagazine/backnumber09.txt
あとはpkcrack(https://www.unix-ag.uni-kl.de/~conrad/krypto/pkcrack/pkcrack-readme.html)でKnown-plaintext attackするとWordのファイルが出てきます。
開くと真っ白ですがフォントの色を変えると答えが出ます。
$ zip backnumber08.zip backnumber08.txt $ ./pkcrack-1.2.2/src/pkcrack -C unzip -c backnumber08.txt -P backnumber08.zip -p backnumber08.txt -d decrypted.zip
Flag: SECCON{1s_th1s_passw0rd_ weak?}
Binary 400 (Reverse-Engineering Hardware 1)
Raspberry Pi 2とブレッドボードで作った回路の写真と動画、pythonのコードが渡されます。
回路の解読のためにはGPIOのピン配置を知る必要があります。
http://qiita.com/aryoa/items/3f6d82b8c63761cef087
74HC74(Dフリップフロップ)が使われているのでデータシートも読みます。
http://www.marutsu.co.jp/contents/shop/marutsu/datasheet/TC74HC74A.pdf
回路は次のようなブロック7つからできています。
ここでですが、
プルアップ抵抗なのでです。
以上に注意して解読すると次のような回路であることがわかります。
適当にエミュレータを作って動かせば答えが出ます。
#!/usr/bin/python #import RPi.GPIO as GPIO import time CLR = 4 CLK = 5 DA = 6 DB = 12 X1 = 13 X2 = 19 X3 = 20 X4 = 21 X5 = 26 X6 = 16 Q1 = 0 Q2 = 1 class VirtualGPIO: array = [False] * 27 def input(self, pin): if pin in [X1, X2, X3, X4, X5, X6]: self.update() return self.array[pin] def output(self, pin, val): self.array[pin] = val if pin == CLK: self.array[Q1] = self.array[DA] self.array[Q2] = self.array[DB] def update(self): self.array[X1] = self.array[Q1] self.array[X2] = self.array[Q2] self.array[X3] = not (self.array[DA] or not self.array[Q2]) self.array[X4] = not ((not self.array[DA] and self.array[Q2]) or not self.array[Q1]) self.array[X5] = not (self.array[DB] or self.array[Q2]) self.array[X6] = not (self.array[Q1] != self.array[Q2]) GPIO = VirtualGPIO() #GPIO.setmode(GPIO.BCM) #GPIO.setup(CLR, GPIO.OUT) #GPIO.setup(CLK, GPIO.OUT) #GPIO.setup(DA, GPIO.OUT) #GPIO.setup(DB, GPIO.OUT) #GPIO.setup(X1, GPIO.IN) #GPIO.setup(X2, GPIO.IN) #GPIO.setup(X3, GPIO.IN) #GPIO.setup(X4, GPIO.IN) #GPIO.setup(X5, GPIO.IN) #GPIO.setup(X6, GPIO.IN) GPIO.output(CLK, False) GPIO.output(DA, False) GPIO.output(DB, False) #GPIO.output(CLR, False) #GPIO.output(CLR, True) flagString = "" def encoder(x6,x5,x4,x3,x2,x1): v = 0 v = GPIO.input(x1); v = 2*v + GPIO.input(x2) v = 2*v + GPIO.input(x3) v = 2*v + GPIO.input(x4) v = 2*v + GPIO.input(x5) v = 2*v + GPIO.input(x6) return v c = '@' flag = "" try: for i in range(10) : if c == 'Y' : GPIO.output(DA, False) GPIO.output(DB, True) else: if (i & 1) == 0 : GPIO.output(DA, False) else : GPIO.output(DA, True) if (i & 2) == 0 : GPIO.output(DB, False) else : GPIO.output(DB, True) #time.sleep(0.1) c = chr(encoder(X6,X5,X4,X3,X2,X1)+32) flag = flag + c GPIO.output(CLK, True) GPIO.output(CLK, False) #time.sleep(0.1) flag = flag + chr(encoder(X6,X5,X4,X3,X2,X1)+32) except KeyboardInterrupt: print("stop\n") #GPIO.cleanup() print("The flag is SECCON{"+flag+"}")
Flag: SECCON{###FD80UY#!8880UY#!8}
Unknown 100 (4042)
問題文が「謎の文章が2005年に古代遺跡から発見された。
これは何を意味している?」だったので、"4042 2005"で検索するとRFC4042というジョークRFCが出てきます。
http://www.ietf.org/rfc/rfc4042.txt
これで与えられた8進列をUTF-9 -> UTF-16にデコードします。
#!/usr/bin/env python3 with open('no-network.txt') as f: text = f.read() text = ''.join(text.split('\n')) nonets = [] for i in range(len(text) // 3): nonets.append(int(text[i * 3:i * 3 + 3], 8)) i = 0 result = b'\xfe\xff'#utf-16be bom while i < len(nonets): if (nonets[i] & 0x100) == 0: result += b'\xff' + (nonets[i] & 0xff).to_bytes(1, byteorder='big') else: result += (nonets[i] & 0xff).to_bytes(1, byteorder='big') + (nonets[i+1]).to_bytes(1, byteorder='big') i += 1 i += 1 if i % 27 == 0: result += b'\x00\x0a' with open('ans' ,'wb') as f: f.write(result)
頑張って読むと答えがわかります。
Flag: SECCON{A_GROUP_OF_NINE_BITS_IS_CALLED_NONET}
Binary 500 (Reverse-Engineering Hardware 2)
1と同様、Raspberry Pi 2の回路の写真とpythonコードが渡されます。
74HC161を使っているとのことなのでデータシートを取ってきます。
http://toshiba.semicon-storage.com/info/lookup.jsp?pid=TC74HC161AP&lang=ja
回路を解読するとだいたい次のようになります。
与えられたpythonコードをよく読むと、生成されるマスクは平文によらないので、暗号文をそのまま入力すれば平文が復元できます。
#!/usr/bin/env python #import RPi.GPIO as gpio import time import sys import struct rfd = open(sys.argv[1], 'rb') wfd = open(sys.argv[2], 'wb') len = int(sys.argv[3]) load = 4 clock = 10 reset = 9 p0=21 p1=20 p2=16 p3=12 p4=25 p5=24 p6=23 p7=18 pns = [p7,p6,p5,p4,p3,p2,p1,p0] q0=19 q1=13 q2=6 q3=5 q4=22 q5=27 q6=17 q7=26 class Virtual74HC161: qa = qb = qc = qd = False pre_clk = False def set(self, rst, clk, ld, a, b, c, d): if rst == False: self.qa = self.qb = self.qc = self.qd = False else: if ld == False: self.qa = a self.qb = b self.qc = c self.qd = d else: if self.pre_clk == False and clk == True: if self.qa == False: self.qa = True elif self.qb == False: self.qa = False self.qb = True elif self.qc == False: self.qa = False self.qb = False self.qc = True elif self.qd == False: self.qa = False self.qb = False self.qc = False self.qd = True else: self.qa = False self.qb = False self.qc = False self.qd = False self.pre_clk = clk def getQA(self): return self.qa def getQB(self): return self.qb def getQC(self): return self.qc def getQD(self): return self.qd def getCO(self): return self.qa and self.qb and self.qc and self.qd class VirtualGPIO: IC1 = Virtual74HC161() IC2 = Virtual74HC161() LOW = False HIGH = True array = [False] * 28 def output(self, pins, val): if isinstance(pins, int): self.array[pins] = val else: for pin in pins: self.array[pin] = val self.IC1.set(self.array[reset], self.array[clock], self.array[load], self.array[p0], self.array[p1], self.array[p2], self.array[p3]) self.IC2.set(self.array[reset], self.IC1.getCO(), self.array[load], self.array[p4], self.array[p5], self.array[p6], self.array[p7]) def input(self, pin): self.array[q7] = self.IC1.getQA() self.array[q0] = self.IC1.getQB() self.array[q1] = self.IC1.getQC() self.array[q2] = self.IC1.getQD() self.array[q3] = self.IC2.getQA() self.array[q4] = self.IC2.getQB() self.array[q5] = self.IC2.getQC() self.array[q6] = self.IC2.getQD() return self.array[pin] gpio = VirtualGPIO() def pulse(pin): gpio.output(pin, gpio.LOW) gpio.output(pin, gpio.HIGH) def init(): #gpio.setwarnings(False) #gpio.setmode(gpio.BCM) #gpio.setup([clock,load,reset], gpio.OUT) gpio.output([clock,load,reset], gpio.HIGH) pulse(reset) #gpio.setup(pns, gpio.OUT) gpio.output(pns, gpio.LOW) #for q in [q7, q6, q5, q4, q3, q2, q1, q0]: #gpio.setup(q, gpio.IN) def setValue(n): pulse(reset) for i in range(n): pulse(clock) def a2v(a): return a[7]+2*a[6]+4*a[5]+8*a[4]+16*a[3]+32*a[2]+64*a[1]+128*a[0] # main init() for i in range(len): value = a2v([gpio.input(q7), gpio.input(q6), gpio.input(q5), gpio.input(q4), gpio.input(q3), gpio.input(q2), gpio.input(q1), gpio.input(q0)]) # file convert v = rfd.read(1) d = b'' d += struct.pack('B', ord(v) ^ value) wfd.write(d) setValue(value) #time.sleep(0.1) pulse(clock) pulse(clock) pulse(clock) #gpio.cleanup()
復元したgzipを解凍するとflagが得られます。
Flag: SECCON{7xgxUbQYixmiJAvtniHF}
Exercises 50 (Last Challenge (Thank you for playing))
やるだけ。
#!/usr/bin/env python cipher = 'PXFR}QIVTMSZCNDKUWAGJB{LHYEOEV}ZZD{DWZRA}FFDNFGQO' plain = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ{}{HELLOWORLDSECCONCTF}' s = 'A}FFDNEA}}HDJN}LGH}PWO' res = '' for c in s: res += plain[cipher.find(c)] print(res)
Flag: SECCON{SEEYOUNEXTYEAR}
SECCON CTF 2014 決勝戦 六(6) Writeup
2015/02/07,2015/02/08にteam 0x0(@nash_fs, @superbacker, @waidotto, @wafrelka)でSECCON CTF 2014決勝戦に参加していました。
結果は5位/24で、3連覇達成はなりませんでした。
以下、サーバ六(6)のWriteupです。
概要
サーバ6はCrypt系で、次のような内容でした。
1.Your team can upload a python code(enc/dec pair).
2.After your team uploads your code, you will not be allowed to upload again for 1 hour.
3.You will be able to get a keyword for AP if you break ID1-4 codes.
4.All codes will be ranked by its code size.
5.Your team will gain DP while your code keep 1st place.
6.Your team's defense flags are written automatically into the flag page.
7.The flag page is here(フラグページヘのリンク).8.INPUT will consist of 64 characters (A-Za-z0-9_/)
9.OUTPUT must consist of 64 characters (A-Za-z0-9_/)(*) AP=ATTACK POINT, DP=DEFENCE POINT
ID: 00000001
単純な換字式暗号です。
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/
を入力すればテーブルが得られます。
s = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/' table = 'b1gkVL0PTaQwprGUvK5mtnZBMD8SI3oHqACRzJE_7y6iOud2hW9jflNFxeX4/sYc' cipher = 'U8OOh7i3YI8_YJo8i_Yi7IzY37Y5u9JoY97dYOCud8uC7_YHC_3YbY8_3YhiCuoYu7YhACJoYE8C_J9Y7IIdiio3YA8OYA8IzoiYuAoY7iYdOoYhi7uoYio83YiCqAuY' plain = '' for c in cipher: plain += s[table.find(c)] print plain
ID: 00000002
各文字の変換結果は入力の文字数 mod 4にのみ依存しており、暗号文が64文字なので、やはり
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/
を入力すればテーブルが得られます。
s = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/' table = 'RhW4NCBlFexfymtp/oELivM69cQSqYgIjkdurJz28AwP0U5D1Zb3HaXKTnOV_7sG' cipher = 'pQ0018PYsa7_nskQqrgP0szQrgsSg0Usa77OsdYg2UdIdqQUd82sQskQqrgPsd0sQYzd2d0UPQUd82szb0gJIs1kbsUkgsUdzgs1gJJszQbsd0sUkgs0k85JYsUdUJgs' plain = '' for c in cipher: plain += s[table.find(c)] print plain
ID: 00000003
@wafrelkaが解いてくれました。
文字ごとにテーブルをローテートしているらしい?です。
ID: 00000004
試しにABを投げてみると、次の出力が得られます。
AAAAAlBoSAB
明らかにBase64なのでデコードします。
00 00 00 02 50 68 48 00
他にもいくつか投げてみると、次のようなことがわかります。
- 単一の文字からなる場合のみランレングス圧縮
- 複数の文字がある場合、はじめの4バイトは文字列長
- なんかハフマン符号っぽい(勘)
既存の圧縮アルゴリズムかと思いましたが、よくわからなかったので自力で解きました。
入力をもう少し長くしてbbbbbaaaddddcceeeeeとしてBase64デコードして先頭4バイトを取り除いて2進数にすると、次が得られます。
00101100 01010110 01010101 10010001 01100001 10110001 10000000 00011011 01101010 10101111 11010101 01010000 00000000
このビット列をしばらく眺めると次のように見えます。
001 01100010 #'b' 1 01100101 #'e' 01 01100100 #'d' 01 01100001 #'a' 1 01100011 #'c' 00 00 00 00 00 110 110 110 10 10 10 10 111 111 01 01 01 01 01 000000000000
これから'b'->00, 'a'->110, 'd'->10, 'c'->111, 'e'->01という符号が得られます。
これをハフマン木で書いてみると次のようになります。
これから次のようなアルゴリズムで復号できると推測できます。
- 連続する0の数だけ左下へ行く(行きがけに'*'で埋める)
- 1が来たら、その後ろ8ビットを文字として置く
- 一つ上る
- 現在いる場所の右下が空になるまで上る
- 木が埋まっていなければ1.に戻る
- 出来上がったハフマン木に従って文字数ぶん復号する
次のようなコードで復号できました。
import math s = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/' cipher = 'AAAAgBX7KsK03QWS3Leud1FmWe5LyodjFstpL6Eo1JFKpioWItFrXBdrv6Vrf9ZwOK3TmBqriKkisDVXD/E45TcNOQUwNumw8L8pNiO4vLfxzsz6FYGDMB25IvYwO2ZI6qzbhOMi4dfSGkQfd8h5YAB==' bc = cipher.decode('base64') length = int(bc[:4].encode('hex'), 16) bc = ''.join(['{0:08b}'.format(ord(x)) for x in bc[4:]]) tree = [None] * (2**16) def downLeft(p): return p * 2 + 1 def downRight(p): return p * 2 + 2 def up(p): return (p - 1) // 2 def depth(p): return math.floor(math.log(ptr + 1, 2)) ptr = 0 break_flag = False while True: while bc[0] == '0': bc = bc[1:] tree[ptr] = '*' ptr = downLeft(ptr) bc = bc[1:] tree[ptr] = chr(int(bc[:8], 2)) bc = bc[8:] ptr = up(ptr) while tree[downRight(ptr)] != None: ptr = up(ptr) if ptr == 0 and tree[downRight(ptr)] != None: break_flag = True break if break_flag: break ptr = downRight(ptr) plain = '' while 0 < len(bc): ptr = 0 while tree[ptr] == '*': if bc[0] == '0': ptr = downLeft(ptr) if bc[0] == '1': ptr = downRight(ptr) bc = bc[1:] plain += tree[ptr] if length <= len(plain): break print plain
Defence Point
プレイヤーはenc.pyとdec.pyをアップロードすることができ、plain == decode(encode(plain))ならば受理されます。
len(enc.py) + len(dec.py)が小さいほどランキング上位に入れます。
最短は、enc.py, dec.pyをともに
INPUT=OUTPUT
にすればOKです。
ただし、他のプレイヤーが暗号文を復号してcipher == encode(plain)にできればそのコードをランキングから落とすことができます。
しかしplain == decode(encode(plain))のチェックはアップロード時にしか行われないため、
例えばenc.pyとdec.pyを
OUTPUT=`id(0)`+INPUT
OUTPUT=INPUT[8:]
とすることで、36バイトのコードで実質的にcipher == encode(plain)にできなくなってしまいます。
これによってDefence Pointを稼いでいたチームが多かったようです。
その他
このサーバはどうやらcode golfをするのが正攻法(?)だったようです。
enc.pyでなんとかしてリバールシェルを起動させたりファイルを読んだりしようとしましたが、サンドボックス内で動いていたためダメでした。
アップロード時にSyntaxErrorなどが起きる場合、エラーメッセージが返されるので、例えば
raise NameError('hoge')
などとするとhogeが返ってきます。
これを利用して得られた情報をいくつか記します。
repr(os.uname())
('Linux', 'ubuntu', '3.13.0-32-generic', '#57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014', 'x86_64')
sys.version
2.7.6 (default, Mar 22 2014, 22:59:56) [GCC 4.8.2]
os.getcwd()
/usr/lib/cgi-bin
repr(os.environ)
{'HTTP_REFERER': 'http://6.finals.seccon.jp/', 'CONTEXT_DOCUMENT_ROOT': '/usr/lib/cgi-bin/', 'SERVER_SOFTWARE': 'Apache/2.4.7 (Ubuntu)', 'CONTEXT_PREFIX': '/cgi-bin/', 'SERVER_SIGNATURE': '<address>Apache/2.4.7 (Ubuntu) Server at 6.finals.seccon.jp Port 80</address>\n', 'REQUEST_METHOD': 'POST', 'SERVER_PROTOCOL': 'HTTP/1.1', 'QUERY_STRING': '', 'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'CONTENT_LENGTH': '1156', 'HTTP_USER_AGENT': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 Firefox/35.0', 'HTTP_CONNECTION': 'keep-alive', 'SERVER_NAME': '6.finals.seccon.jp', 'REMOTE_ADDR': '192.168.2.6', 'SERVER_PORT': '80', 'SERVER_ADDR': '10.100.6.1', 'DOCUMENT_ROOT': '/var/www/html', 'SCRIPT_FILENAME': '/usr/lib/cgi-bin/cg.cgi', 'SERVER_ADMIN': 'webmaster@localhost', 'HTTP_HOST': '6.finals.seccon.jp', 'SCRIPT_NAME': '/cgi-bin/cg.cgi', 'HTTP_CACHE_CONTROL': 'max-age=0', 'REQUEST_URI': '/cgi-bin/cg.cgi', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'GATEWAY_INTERFACE': 'CGI/1.1', 'REMOTE_PORT': '58079', 'HTTP_ACCEPT_LANGUAGE': 'ja,en-us;q=0.7,en;q=0.3', 'REQUEST_SCHEME': 'http', 'CONTENT_TYPE': 'multipart/form-data; boundary=---------------------------743837628893702018431594614', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate'}
traceback.format_exc()
Traceback (most recent call last): File "/usr/lib/cgi-bin/pysbox.py", line 105, in _child_main OSError: [Errno 9] Bad file descriptor
repr(sys.modules)
長いので/cgi-bin/内のものだけ示します。
{'hadoken': <module 'hadoken' from '/usr/lib/cgi-bin/hadoken.py'> 'cryptolog': <module 'cryptolog' from '/usr/lib/cgi-bin/cryptolog.py'> 'acctrl2': <module 'acctrl2' from '/usr/lib/cgi-bin/acctrl2.py'> 'pysbox': <module 'pysbox' from '/usr/lib/cgi-bin/pysbox.py'> '__main__': <module '__main__' from '/usr/lib/cgi-bin/cg.cgi'> 'pysqldb': <module 'pysqldb' from '/usr/lib/cgi-bin/pysqldb.pyc'>}
/cgi-bin/pysbox.pyなどのファイルにアクセスしてもすべてInternal Server Errorでダメでした。
__main__.get_chars()が'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/'を返すなどはわかりましたが、pythonに詳しくないので他のクラスや関数の中身はわかりませんでした。
hadoken.pyという名前が謎だったので競技終了後に愛甲さんに聞いてみたところ、
「えっ、なんでそれ知ってるの」という反応でした(「波動拳システム(=NIRVANA)」のことらしいです)。
反省
Attack PointはいつSubmitしても同じなので、ホテルに帰ってから挑戦するべきでした。
初日はDefence Pointを確実に稼いだほうが良いです。
実際に、優勝したTOEFL Beginnerは挑戦者の少ないサーバを狙って確実にDefence Pointを稼いでいたようです。
おわりに
今年もとても楽しかったです。
運営の皆様ありがとうございました。
No cON Name CTF Quals 2014 Writeup
MISCall(Misc 100)
.tar.gzのファイルを展開すると、gitリポジトリが出てきます。
ファイルはflag.txtのみで、Nothing to see here, moving along...とか書いてあります。
git logを見てもinitial commitのみ。
そこで.git/objects/を見ます。
$ git cat-file -p <ハッシュ値>でファイルが見られるので、いろいろ漁るとs.pyというファイルがあります。
#!/usr/bin/env python from hashlib import sha1 with open("flag.txt", "rb") as fd: print "NCN" + sha1(fd.read()).hexdigest()
あとはもとのflag.txtをダンプしてsha1をとれば終了です。
ただし、gitのハッシュはファイルのsha1そのものではないのでそこは注意が必要です。
参考:http://stackoverflow.com/questions/5290444/why-does-git-hash-object-return-a-different-hash-than-openssl-sha1
Flag:
NCN4dd992213ae6b76f27d7340f0dde1222888df4d3
BackdoorCTF 2014 Write up
Crypto 10
問題文
H4x0R(jpgファイルへのリンク) recently went missing. An investigating team specializing in hacking was deployed to search around his place. All they found was this file. Please help them obtain secret 32characters string that can lead to him
Submit the flag as: flag_obtained
答え
crypto10.jpgをforemostにかけるとzipファイルが出てきて、解凍するとgot2.jpgが出てきます。
got2.jpgをforemostにかけるとzipファイルが出てきて、解凍するとtxt.txtが出てきます。
txt.txtに答えが書かれています。
Flag:
6307834008eb8edbe18c7a20ee4a909d
Crypto 100
問題文
H4x0R got this weird code while coming back from school. can you get a 32char code flag that can make him happy ?
1f8b08089c452c530003737465703900edd85b6ec3300c44d1ffae86dcffe61ac7e1437403e42b1a171745d02a946c890713383537f3c7cb3c7e8e37ec7c99d7fb39cbe3afaa5bac89f1735e5c3597bf263e571ef52ab549d6d759bb5eed2de6d7c56bf76d3f6de47d76ac8e9567c9bdefb0d7dbe16c3d67ded997bbb71eae3d8d7db8d796d6fed7fda317f808fb5ceaf848f9b48ee1a3e833ebf868f9901f6d9f59c747cb87fca8faacfbc1079fbbf8ccfa469f1ce323999f4b1d9f6b7d8f4f1e161ff2732b1ff2a3eda3911f7cb47d78bed6f6213fef7c667da3cfa58e0ff9c1071f7cf0f986cface3830f3e1f7fffb9d4f1c147dd67d6f7f8f43ee0d37d34f29387c547d6a7c6f828faf48ee1e335191f611ff283cf673e35c647d1a7770c1f2d9fb33ff8a8fa909f3bf8cc3a3e5a3ee447d9a7c6f8a8f9f8da157c14ff7f80cf1f3e1af9895ee0a3e973cec147d5270f8b8fa40fcf07da3ed10b7ca6cfacefcc8f8d3a3e2af9e1f3ed9d8f467ef2b0f8e8e6071f599fe8053e9a3ee4e7063e39c647d287fce0830f3effd327f6808fa6cfd2317cb47c7a1ff0d1f3213fda3ee447db87fc68fbb4fbe123e8e36b57f0499f59df989fd6317ce4f2137bc067f1d1c8cfd99f65ddebe0a976fcfef9059996b432616b0000
Hint: A decade worth of encryption layers
答え
問題文をhexデコードするとgzipが出てきます。
gzipを解凍すると0と1だけが大量に書かれたファイルが出てきます。
8文字ごと区切って2進数として読むとBase64の書かれたファイルが出てきます。
Base64をデコードすると-GASYADT*&-25-GASYADT*&-33-……などと大量に書かれたファイルが出てきます。
アルファベットや記号は気にせず数字だけをhexデコードするとURLエンコードされた文字列が出てきます。
URLデコードするとhexが出てきます。
hexデコードするとuggc://cnfgr.hohagh.pbz/7130554/というのが出てきます。
ROT13デコードするとURLになり、アクセスするとフラグが得られます。
Flag:
5d3144233c46404dba4afc766601b997
Miscellaneous 150
問題文
This(ファイルへのリンク) wierd file was found by H4XOR when trying to search for his flags. Can you get him his flag ?
Submit flag as flag_obtained
Hint: Brush upon your history lessons.
答え
fileコマンドにかけるとzipのようなので、unzipします。
misc1504.zipが出てくるので、unzipします。
misc1503.zipが出てくるので、unzipします。
misc1502.zipが出てくるので、unzipします。
misc1501.zipが出てくるので、unzipします。
misc150.zipが出てくるので、unzipします。
Misc150というファイルが出てきます。fileコマンドにかけるとLinux rev 1.0 ext2 filesystem dataとのことなので、mountします。
mountするとpdfやらwavやらkey.txt(ダミー)やらゲームやらが出てきますが、
% diff .bash_history lost+found/\#23 330d329 < curl http://paste.ubuntu.com/7130279/ 355c354 < --- > curl http://paste.ubuntu.com/7130279/
としたら答えが出てきました。
Flag:
934360b5b4901b727471b39455949a47
Miscellaneous 200
問題文
>A military troop was caught sending over a microprocessor chip along with a memory chip to a family in Scotland.
The program on the microprocessor can be found here(ファイルへのリンク).
The Memory Chip had the following state: 103 110 117 95 115 107 105 108 108 95 97 99 116 101 100 95 114 97 119 starting from memory address 0x0031, ie the value in the memory location 0x0032 was 110.
Submit the flag as: md5(secret_msg)
答え
元々のメモリのアドレス0x31からの内容を文字列に直すと"gnu_skill_acted_raw"となります。
ファイルには次のような8085のアセンブリが書かれていました。
; This is the default program that ships with ; every 8085 that we at Intel manufacture ; ; Cheers! jmp start ;code start: lxi H,0041h lxi D,0044h mvi B,03h loop: mov A,M xchg mov M,A xchg inx H inx D dcr B mov A,B sbi 0 jnz loop lxi H,0041h lxi D,0042h mvi A,69h mov M,A xchg mvi A,6Eh mov M,A inx H mvi A,5Fh mov M,A lxi H,0044h lxi D,0046h mov A,M xchg mov B,M mov M,A xchg mov M,B lxi H,0032h lxi D,0033h mov A,M xchg mov B,M mov M,A xchg mov M,B lxi H,003Bh lxi D,003Ch mov A,M xchg mov B,M mov M,A xchg mov M,B lxi H,003Dh lxi D,003Fh mov A,M xchg mov B,M mov M,A xchg mov M,B lxi H,0036h lxi D,0035h mvi B,04h loopagain: mov A,M xchg mov M,A xchg inx H inx D dcr B mov A,B sbi 0 jnz loopagain lxi H,0039h mvi A,73h mov M,A ;halt hlt
http://ja.wikipedia.org/wiki/Intel_8085を見てみると、8080とほとんど同じだということなので、http://ja.wikipedia.org/wiki/Intel_8080の命令セットの欄を参照しながら読むと、以下のような処理をしているだけだとわかります。
[0x44] = [0x41] [0x45] = [0x42] [0x46] = [0x43] [0x41] = 0x69 [0x42] = 0x6e [0x43] = 0x5f [0x44] <=> [0x46] [0x32] <=> [0x33] [0x3b] <=> [0x3c] [0x3d] <=> [0x3f] [0x35] = [0x36] [0x36] = [0x37] [0x37] = [0x38] [0x38] = [0x39] [0x39] = 0x73
実際にやってみると、"gun_kills_cadet_in_war"という文字列が得られるので、md5を取ると答えが得られます。
Flag:
f57f4973ce9eb1c07c71ad3be3752c79
Miscellaneous 250-2
問題文
Username and password based login seemed a bit too monotonous. We developed an indigenous image based login system.
The login service is available here(ログインページへのリンク).
The image below can be used to login as the backdoor user. Unfortunately that doesn't serve any purpose.
Login as the sdslabs user for a change.
Submit the flag as: md5(flag_obtained)
Hint: Are you kidding me? Character recognition seriously?
答え
言われたとおりログインページでbackdoorの画像をアップロードすると、backdoorユーザとしてログインできます。
画像をgimpなどのペイントソフトで開いて黒い部分を白で塗りつぶすと、左上のほうに黒いドット(#010101)が残ります。
画像の一番左上から#000000を0、#010101を1として、右に向かって2進数として解釈すると"backdoor"の文字コードの2進数表示が得られます。
そこで、backdoorの代わりにsdslabsの2進数表示を左上に作った画像をアップロードすることで、sdslabsとしてログインできます。
sdslabsとしてログインすると、Congrats the flag is practice_makes_one_perfectと表示されます。
Flag:
c16a3c8504985a8c91956c29f7338184
Web 250-1
問題文
Web250(Webページヘのリンク) Flag format: md5_string
答え
リンクを開くと、MarkdownのソースをHTMLに変換するWebアプリが出てきます。
https://github.com/backdoor-ctf/web250からソースが得られます。
js-yamlの使い方をググってみると、かなり自由に色々できるようです。
次のようなデータを送信するとフラグが得られます。
--- flag: !!js/function > function() { return process.env.FLAG; } --- {{flag}}
Flag:
fb1f85e4f37eb3bf31141cb1dcce1caf
感想
MD5どんだけ好きなんですか
RuCTF Quals 2014 hardware 200 Write up
問題文
What is the shortest valid code?Files(zipファイルへのリンク). Attention: you have 70 attempts.
答え
zipファイルをダウンロードすると、次の3枚の画像と1つの動画が入っています。
動画には上の画像の回路が動作している様子が写っています。
はじめに赤いLEDが点灯していて、カメラの撮影範囲外でパスコードを入力すると緑のLEDが点灯します。
回路の表に写っているICのうち、2つは74HC32(OR回路が4つ入っている、超有名なロジックIC)で、残り2つはK561というよくわからないICです。
K561のデータシートを探したところ、次のようなロシア語のものしか見つかりませんでした。
http://radio-hobby.org/uploads/datasheets/k/k561tm2.pdf
これをGoogle翻訳のロシア語手書き入力で翻訳した(キリル文字は立体と斜体で形が大きく違うので、Wikipediaを参照しながら書いた)ところ、Два D-триггера с динамическим управлениемは2つの動的制御のDフリップフロップというような意味であることがわかりました。
よくわかりませんがとにかくDフリップフロップだということなので、データシートのピンアサインを見ながら作成した大まかな論理回路の図を以下に示します。
ここで、注意すべきことが2つあります。
- 抵抗の配置から、この抵抗はプルダウン抵抗であることがわかるので、ボタンを押したときにHIGH、離したときにLOWの信号がICに入力されます。
- LEDにつながっているトランジスタはおそらくPNP型なので、信号がLOWのときに光ります。
こういうわけで、フリップフロップのCLOCKにつながっている9,8,3,1を順番に押すとトランジスタへの信号がLOWになり、緑のLEDが点灯します。
Flag:
9831
RuCTF Quals 2014 hardware 100 Write up
問題文
問題タイトル: IR dump
What credit card number has been typed?Dump(zipファイルへのリンク)
答え
zipをダウンロードして解凍すると、次のようなdump.txtが出てきます。
RMC364GY00000000000000000000000000000000000000000000000000000000 ...(ひたすら0か1が続く)
先頭のRMC364GYで検索すると、JVCというメーカのテレビのリモコンがヒットします。
問題タイトルがIR dumpなので、このファイルはリモコンの信号のダンプだと推測できます。
JVCリモコンの仕様を調べると、次のようなページがヒットします。
http://www.sbprojects.com/knowledge/ir/jvc.php
http://users.telenet.be/davshomepage/jvc.htm
これらの情報をもとに、デコードを行えばOKです。
まず、dump.txt連続する文字とその個数で表したファイルに変換しました。
R 1 M 1 C 1 3 1 6 1 4 1 G 1 Y 1 0 572686 1 1584 0 767 1 110 0 271 1 112 0 277 1 111 ...
0と1の数から大体どれがスタートコードでどれがリピートコードか、などのあたりをつけ、作成したデコーダが以下です。
#!/usr/bin/python2 f = open('converted.txt', 'r') data = f.read() array = data.split('\n') array = array[9:] repeating = False count = 0 starting = False address = 0 command = 0 for line in array: if line != '': c, n = line.split(' ') n = int(n) if starting: starting = False continue if repeating: if c == '0' and 5000 < n: repeating = False else: if c == '1' and 1000 < n < 2000: starting = True elif count == 16: print hex(address), hex(command) address = 0 command = 0 count = 0 repeating = True elif c == '0': if n < 150: v = 0 else: v = 1 if count < 8: address += v << count else: command += v << (count - 8) count += 1 f.close()
実行結果
% ./decode.py 0x3 0x24 0x3 0x24 0x3 0x28 0x3 0x25 0x3 0x22 0x3 0x24 0x3 0x28 0x3 0x24 0x3 0x20 0x3 0x29 0x3 0x24 0x3 0x29 0x3 0xa5 0x3 0x29 0x3 0x29 0x3 0x22
JVCリモコンのコマンドの詳細な仕様書にキーコードテーブルがあるので、それを参照します。
http://support.jvc.com/consumer/support/documents/RemoteCodes.pdf
0x20〜0x29が数字のボタンらしいので、一番下の桁を読めばOKです(0xa5はおそらくデコードミス)。
Flag:
4485248409495992