HCTF 2018 Writeup
2018/11/9 21:00 - 11/11 21:00 の 48時間、 insecure (ptr-yudai, yoshiking, theoldmoon0602) として、 HCTF に参加していました。日付と時間帯が大変都合がよく、食事睡眠お風呂炊事以外の時間はずっとCTFに取り組み、およそここまでの人生で最もCTFに染まった2日間になりました。
私は3問を解き、yoshikingが1問、残りをptr-yudaiが解いてinsecureは3032.49点を獲得。得点 292チーム中 22位でした。輝かしい功績ですが、突っ込んだリソースから言えばもう少し稼ぎたかったところです。ちなみにWebとPwnはまるでわからなくて、MiscとCrypto、少しのBin+BlockChainで稼ぎました。
このCTFは解いたチーム数に応じて問題の得点が変化するタイプ*1で、小数点までついてます。
[Misc 424.63] Easy Dump
Miscの皮をかぶったForensic問題です。mem.data
という名前の256MBほどのバイナリが与えられます。 strings
などをした結果、どうやらWindowsのファイルらしいぞということがわかりました。まずディスクダンプを疑ったのでSleuth Kitにかけてみたのですが反応がなく、続いてメモリダンプを疑ってVolatilityを試すとこちらが当たりでした。
volatility -f mem.data pstree
としたところ、以下のようになりました。
Name Pid PPid Thds Hnds Time -------------------------------------------------- ------ ------ ------ ------ ---- 0xfffffa80003a5040:System 4 0 108 373 2018-11-07 08:12:31 UTC+0000 (略) .. 0xfffffa8002c62060:vmtoolsd.exe 1356 472 9 225 2018-11-07 08:12:37 UTC+0000 ... 0xfffffa8000cfeb30:cmd.exe 2824 1356 0 ------ 2018-11-07 08:26:51 UTC+0000 (略) 0xfffffa8001d64b30:explorer.exe 1696 1548 20 661 2018-11-07 08:12:38 UTC+0000 . 0xfffffa8003344390:wordpad.exe 1804 1696 3 120 2018-11-07 08:15:35 UTC+0000 . 0xfffffa8000d9ab30:MineSweeper.ex 312 1696 9 208 2018-11-07 08:15:39 UTC+0000 . 0xfffffa8002de1560:mspaint.exe 2768 1696 6 122 2018-11-07 08:16:05 UTC+0000 . 0xfffffa8002d0cb30:vmtoolsd.exe 2028 1696 9 199 2018-11-07 08:12:39 UTC+0000 0xfffffa8001fe1060:csrss.exe 424 408 11 197 2018-11-07 08:12:33 UTC+0000 0xfffffa8002638670:winlogon.exe 504 408 3 114 2018-11-07 08:12:33 UTC+0000
cmd.exe
を疑って cmdscan
をしたのですが何も得られず、続いて mspaint.exe
にあたりをつけました。volatility -f mem.data --profile=Win7SP1x64 memdump -p 2768 --dump-dir=dump
としてメモリダンプを抽出し、それをgimpで開くと一部文字のようなものが見える部分がありました。頑張って幅を調整して、画像を反転させるとフラグが書いてありました。
問題名のとおり簡単でしたが、ここまでたどり着くのに時間を大きく奪われてしまったので反省です。
[Misc 432.97] Difficult Programming Language
pcapファイルが渡されます。中身をみるとUSBのパケットで、キーボードっぽいなと思いました。むしろUSBのパケットを渡されてキーボード以外だったことがまだない。
とりあえず tshark -Tfields -e usb.capdata -r difficult_programming_language.pcap > data
のようなことをしてデータ部を取り出します。tsharkの使い方は私のブログの過去記事に助けられました。dataは以下のようになります。
02:00:00:00:00:00:00:00 02:00:07:00:00:00:00:00 02:00:00:00:00:00:00:00 00:00:00:00:00:00:00:00 00:00:34:00:00:00:00:00 00:00:00:00:00:00:00:00 00:00:35:00:00:00:00:00 00:00:00:00:00:00:00:00 ...
さて、このデータから何が入力されていたのかを辿りたいのですが、真面目にキーストロークと文字コードを変換するのは大変なので、インターネットの力↓に頼りました。ありがとうございます。
データのフォーマットを反映して少しスクリプトを変更しました。
keymap = { 0x04: ('a','A'), 0x05: ('b','B'),0x06: ('c','C'), 0x07: ('d','D'), 0x08: ('e','E'),0x09: ('f','F'), 0x0a: ('g','G'), 0x0b: ('h','H'),0x0c: ('i','I'), 0x0d: ('j','J'), 0x0e: ('k','K'),0x0f: ('l','L'), 0x10: ('m','M'), 0x11: ('n','N'),0x12: ('o','O'), 0x13: ('p','P'), 0x14: ('q','Q'),0x15: ('r','R'), 0x16: ('s','S'), 0x17: ('t','T'),0x18: ('u','U'), 0x19: ('v','V'), 0x1a: ('w','W'),0x1b: ('x','X'), 0x1c: ('y','Y'), 0x1d: ('z','Z'),0x1e: ('1','!'), 0x1f: ('2','@'), 0x20: ('3','#'),0x21: ('4','$'), 0x22: ('5','%'), 0x23: ('6','^'),0x24: ('7','&'), 0x25: ('8','*'), 0x26: ('9','('),0x27: ('0',')'), 0x28: (' [Enter] ',' [Enter] '), 0x29: ('\x1b','\x1b'), 0x2a: (' [del] ',' [del] '), 0x2b: ('\x09','\x09'), 0x2c: ('\x20','\x20'), 0x2d: ('-','_'), 0x2e: ('=','+'), 0x2f: ('[','{'),0x30: (']','}'), 0x31: ('\\','|'), 0x33: (';',':'),0x34: ('\'','\"'), 0x35: ('`','~'), 0x36: (',','<'),0x37: ('.','>'), 0x38: ('/','?'), 0x51:(' [downArrow] ',' [downArrow] '), 0x52: (' [upArrow] ',' [upArrow] '),0x32: ('\\','|') } def analyze_usb_data(usb_data): flag = "" for d in usb_data: if d[2] == 0 or not(0 in d[3:8]): #No Event continue if d[0] == 0 or d[0] == 0x20: #press shift #binary -> int c = keymap[d[2]][0] flag += c else: #binary -> int c = keymap[d[2]][1] flag += c print flag def main(): data = list(map(lambda x: list(map(lambda y: int(y, 16), x.split(":"))), open("data").read().splitlines())) analyze_usb_data(data) if __name__ == '__main__': main()
ちなみに何かを途中で間違えたらしく、シフトキーを押したかどうかの判定が逆になっており、途中で時間をとられました。正しく変換してやると、次のような文字列になります。
D'`;M?!\mZ4j8hgSvt2bN);^]+7jiE3Ve0A@Q=|;)sxwYXtsl2pongOe+LKa'e^]\a`_X|V[Tx;:VONSRQJn1MFKJCBfFE>&<`@9!=<5Y9y7654-,P0/o-,%I)ih&%$#z@xw|{ts9wvXWm3~C
これであっているのかと不安になる記号列ですが、問題名に従ってMalblogeというプログラミング言語のソースコードであるとあたりをつけました。インタプリタがないか探したところ以下がヒットしたので実行し、フラグを得られました。
https://zb3.me/malbolge-tools/
[Crypto 424.63] xor?rsa
次のようなソースコードと、サーバの動作情報が与えられます。 verify
なる関数が謎過ぎて悩んでいましたが、ptr-yudai が「チームトークンだよ」と教えてくれました(このCTFではチーム毎に、「チームトークン」なる文字列が与えられ、時々問題の最初に入力を求められました)。
from Crypto.Util.number import * import SocketServer import string import hashlib import random import requests import json from flag import * class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): pass class RSATCPHandler(SocketServer.BaseRequestHandler): def handle(self): self.request.sendall("Welcome to flag getting system\ngive me your token > ") token = self.request.recv(1024).strip() if not verify(token): self.request.sendall("token error\n") else: p = getStrongPrime(1024) q = getStrongPrime(1024) n = p * q e = 5 nbits = size(n) kbits = nbits // (2 * e * e) m1 = getRandomNBitInteger(nbits) m2 = m1 ^ getRandomNBitInteger(kbits) c1 = pow(m1, e, n) c2 = pow(m2, e, n) self.request.sendall("n=" + str(n) + "\n") self.request.sendall("c1=" + str(c1) + "\n") self.request.sendall("c2=" + str(c2) + "\n") self.request.sendall("now give me you answer\n") ans1 = self.request.recv(2048).strip() ans2 = self.request.recv(2048).strip() if str(ans1) == str(m1) and str(ans2) == str(m2): self.request.sendall(FLAG) else: self.request.sendall("wrong answer\n") if __name__ == "__main__": HOST, PORT = "0.0.0.0", 10086 server = ThreadedTCPServer((HOST, PORT), RSATCPHandler) server.serve_forever()
ソースコードをみるとわかるように、 m2は小さめのデータを生成したあと、m1とのxorをとっています。はじめはeが小さかったのでLow Public Exponent Attackかなと思っていましたがはずれで、m1とm2の上位ビットが共通することからFranklin-Reiter Related Message Attackをやることにしました。
Franklin-Reiter Related Message Attackではこちら↓の記事が、(多分同じ出典ということでしょうけど)そっくりのスクリプトを使っていたので、同じコードで攻撃できると判断してソースコードを拝借しました。
import sys def short_pad_attack(c1, c2, e, n): PRxy.<x,y> = PolynomialRing(Zmod(n)) PRx.<xn> = PolynomialRing(Zmod(n)) PRZZ.<xz,yz> = PolynomialRing(Zmod(n)) g1 = x^e - c1 g2 = (x+y)^e - c2 q1 = g1.change_ring(PRZZ) q2 = g2.change_ring(PRZZ) h = q2.resultant(q1) h = h.univariate_polynomial() h = h.change_ring(PRx).subs(y=xn) h = h.monic() kbits = n.nbits()//(2*e*e) diff = h.small_roots(X=2^kbits, beta=0.5)[0] # find root < 2^kbits with factor >= n^0.5 return diff def related_message_attack(c1, c2, diff, e, n): PRx.<x> = PolynomialRing(Zmod(n)) g1 = x^e - c1 g2 = (x+diff)^e - c2 def gcd(g1, g2): while g2: g1, g2 = g2, g1 % g2 return g1.monic() return -gcd(g1, g2)[0] if __name__ == '__main__': n = sage_eval(sys.argv[1]) e = 5 c1 = sage_eval(sys.argv[2]) c2 = sage_eval(sys.argv[3]) diff = short_pad_attack(c1, c2, e, n) m1 = related_message_attack(c1, c2, diff, e, n) print m1 print m1 + diff
接続用のスクリプトを組んで、フラグがでました。 str(int(res[0]))
と一見無駄っぽいことをしていますが、これを挟まないと wrong answer
が出ます。ちょっと謎挙動で時間を取られた。この問題はSageMathのインストールとこれが時間泥棒でした。
import sys import subprocess from pwn import * def main(): p = remote('rsa.2018.hctf.io', 10086) p.recvuntil('> ') p.sendline('M3SEdoxFHNsGWj5Gc6pN1vKqbP5jfKmn') n = p.recvline().split("=")[1] c1 = p.recvline().split("=")[1] c2 = p.recvline().split("=")[1] res = subprocess.check_output(["/home/user/Downloads/SageMath/sage", "attack.sage", n, c1, c2]).splitlines() print(p.recvuntil('answer\n')) p.sendline(str(int(res[0]))) p.sendline(str(int(res[1]))) p.interactive() main()
感想など
- 1282.23 / 3032.49 ということなのでいい具合かもしれない。しかし私が解いた問題はSolve数が多いものばかりで差をつけることができてない。もっと難しい問題も解けるようになって得点源になりたい。
- Lucky Star☆は解きたかった。途中まで解析して諦めてptr-yudaiに投げたら解いていました。絶対Writeupに「おっはラッキー☆」って書きたかったのに。
- 同様にSpiralは「天元突破グレンラガン」だったので解きたかった。このRev激ムズでは、解いたチーム本当に解析したんですか。どうやって。Writeup読もう
- xor_gameはptr-yudaiが解いてたけど最初の方時間を溶かして全ての方針を折られたので何をやればよかったのか気になる。
- adminはなんで解けなかったのかわからない。ソースコードを見るところまでは行けて(なんでコメントにGitHubのURLが書かれることになるんだろう)、SECRET_KEYがそのままだからセッション書き換えればいいのねって書き換えても弾かれ、それがだめならremember_tokenと思ってこちらをやっても反応なし。なんでだろう。この問題ちゃんと実装されていない箇所もあって嫌い
- PolitishDuckも解けなかった。つら
- その他のWeb問は全部エスパーでしょ。なんもわからん。特にshareはヒント0で解ける人絶対0人だって
- yoshikingがBlockChain問題解いててすごい。今度教えてもらいます。
- このCTFのPlatform、セッションがすぐ切れてログインを求められるんだけどそのたびにyoshikingのメールアドレスを打ち込むのがかなり負担。つらい。
- 22位はちょっとすごいと思ってる。ptr-yudaiはそのうち去ってしまうのでそれまでに力をつけて、ptr-yudaiなしでもこのくらい頑張れるようになりたい。
*1:この形式に名前ついてないんですか