CSAW CTF 2017にチームm1z0r3で出ていました.
Misc 100とCrypto 350を解いたのでそのWrite-upになります.
CVV ( Misc 100 pts)
CVVとはCard Verification Valueの略で,クレジットカード番号のこと.
netcatでサーバに接続すると,”I need a new Visa!” , “I need a new American Express!” のようにクレジットカード会社の番号を求められる.なお,newと付いているのは一度の接続で同じ数字を使いまわしてはいけないということを表している.
つまりこの問題は,指定された会社のクレジットカード番号として正しい番号を送り続ける問題である.では,クレジットカードの番号として正しい値がどのような値かというと,以下の二つを満たす値である.
- クレジットカード会社ごとのプレフィックスを満たす
- Luhnアルゴリズムを満たす
1.クレジットカード会社ごとのプレフィックスというのは決まっていて,Wikipediaにも記載されている.詳細はこちらを参照してほしい.
2.Luhnアルゴリズムというのはクレジットカード番号を決定する際に用いられるアルゴリズムである.1954年にハンス・ペーター・ルーンという当時IBMの研究者だった方が考案したアルゴリズムで,当初は特許化されていたが現在ではISOにより世界標準となっている(ISO/IEC 7812).
このアルゴリズムの説明は割愛するが,こちらもWikipediaに詳細が記載されているので参考にしてほしい.なんとPythonのコードまで記載されていたので拝借した.
スクリプトを書いて何度か試行していると,求められる条件が以下のように変動することがわかる.
- “I need a new …”: 特定のクレジットカード会社の正当な番号を答える
- “I need a new card that starts with …”: ある数列で始まる正当な番号を答える
- “I need a new card that ends with …”: ある数列で終わる正当な番号を答える
- “I need to know if … is valid! (0 = No, 1 = Yes)”: 与えられた番号が正当かどうか答える(16桁?)
これらによって条件分岐するようなスクリプトを書いた.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# coding:utf-8 from m1z0r3.crypro import * import string from random import randint import time def rand_num_str(n): ret = "" nums = string.digits for i in xrange(n): ret += nums[randint(0,9)] return ret # Reference: # https://ja.wikipedia.org/wiki/Luhnアルゴリズム#.E5.AE.9F.E8.A3.85.E4.BE.8B def check_number(digits): _sum = 0 alt = False if digits[0] == "0": return False for d in reversed(digits): d = int(d) assert 0 <= d <= 9 if alt: d *= 2 if d > 9: d -= 9 _sum += d alt = not alt return (_sum % 10) == 0 def main(): remotehost = "misc.chal.csaw.io" remoteport = 8308 s,f = sock(remotehost, remoteport) stage = 0 while True: stage += 1 print "Stage %s"%stage recv = read_until(f).strip() print recv send_item = "0" if "flag" in recv: break if "Visa" in recv: while not check_number(send_item): send_item = "4"+rand_num_str(15) elif "American" in recv: while not check_number(send_item): send_item = "3"+rand_num_str(14) elif "Master" in recv: while not check_number(send_item): send_item = "5"+rand_num_str(15) elif "Discover" in recv: while not check_number(send_item): send_item = "65"+rand_num_str(14) elif "starts with" in recv: starts_num = recv.split()[-1][:-1] print "[+] starts with %s"%starts_num send_item = starts_num+rand_num_str(16-len(starts_num)) while not check_number(send_item): send_item = starts_num+rand_num_str(16-len(starts_num)) elif "ends with" in recv: ends_num = recv.split()[-1][:-1] print "[+] ends with %s"%ends_num send_item = rand_num_str(16-len(starts_num)) + ends_num while not check_number(send_item): send_item = rand_num_str(16-len(starts_num)) + ends_num elif "need to know" in recv: verif_num = recv.split()[5] print "[+] verification number: %s"%verif_num if len(verif_num) %2 == 1: print "奇数やで" send_item = "0" elif check_number(verif_num): send_item = "1" else: send_item = "0" s.send(send_item+"\n") print "[+]",send_item print read_until(f).strip() print read_until(f).strip() print "----------------" time.sleep(0.1) continue s.send(send_item+"\n") print "[+]",send_item print read_until(f).strip() print "----------------" time.sleep(0.1) s.close() f.close() if __name__ == "__main__": main() |
The flag is “flag{ch3ck-exp3rian-dat3-b3for3-us3}”
baby_crypt ( Crypto 350 )
crypto.chal.csaw.io:1578 にnetcatにアクセスする.usernameを入力するとクッキーを発行してくれるというだけのサーバが動いている.
問題文に
The cookie is
input + flag
AES ECB encrypted with the sha256 of the flag as the key.
とあったみたいなのだが,私は完全に見落としていて入力検証から始めた.(途中で追記されたものと信じたい)
ひたすら入力検証をしていると以下のことが分かった.
- クッキーの長さは入力の長さに依存
- 長さは16文字おきに変化し,その差は32文字
- “a”*16個と,”a”*32個の時のクッキーを比較すると前のブロックに依存していないことが分かる
- 同じ長さの複数の入力を比較したとき,後ろ32文字が同じ
1.から,クッキーは入力を共通鍵暗号などで暗号化されて作られているのではないかということはわかる(自明という説もある).2.から,1ブロック16文字のブロック暗号ではないかと推測できる.3.によりモードはECBであることが分かる.4.により,パディング以外に後ろに何かくっついていることが分かる.
もう少し詳しく説明すると,3.は以下のような検証をした.
1 2 3 4 |
Enter your username (no whitespace): aaaaaaaaaaaaaaaa # "a"*16 Your Cookie is: 469ac6eba774ac471777f35c88d9dd6a / f9cc1330ae5830732a18d1a23211ffbce3725519adb9e6f10d658d87c80825ed Enter your username (no whitespace): aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # "a"*32 Your Cookie is: 469ac6eba774ac471777f35c88d9dd6a / 469ac6eba774ac471777f35c88d9dd6a / f9cc1330ae5830732a18d1a23211ffbce3725519adb9e6f10d658d87c80825ed |
もしもCBCモードのようにIVが使われていたり前の暗号化結果が依存するならこのような結果にはならない.”a”*16個のクッキーの1ブロック目と”a”*32個のクッキーの2ブロック目はまず同じにならない(きちんと検証するなら”b”*16個+”a”*16個などが分かりやすい).
そして,もしもブロック暗号だとすると,基本的にはパディングが施されるので,ブロックごとに分けられた入力の最後はブロック長にパディングされる.しかし,上の結果を見てみると,入力とは関係ない末尾の部分がパディングだけにしては長すぎる.従ってこの問題は,入力の末尾にflagをくっつけた後にパディングをしてAES ECBモードで暗号化してるのではないかという推測をした.
もしそうであれば,flagが”flag{…}”であるとき,以下の二つの入力のクッキーの1ブロック目は同じになるはずである.
- “aaaaaaaaaaaaaaaf” (“a”*15個+”f”) -> “aaaaaaaaaaaaaaaf / flag{…..}padpadpad….”
- “aaaaaaaaaaaaaaa” (“a”*15個) -> “aaaaaaaaaaaaaaaf / lag{…..}padpad….”
よって,仮に上のfの部分が未知でも,アルファベットを総当たりすることで1文字特定可能である.あとは以下のようにずらしていけば1文字ずつ特定することが可能である.
- “aaaaaaaaaaaaaaf?” (“a”*14個+”f”+”?”) -> “aaaaaaaaaaaaaaf? / flag{……}padpad…”
- “aaaaaaaaaaaaaa” (“a”*14個) -> “aaaaaaaaaaaaaafl / ag{……}padpad…”
?の部分を総当たりすればlで当たるはずである.
実際に推測できる”flag{“の部分を試すと同じになるので,この方針でスクリプトを書いた.なお,フラグが1ブロック長より長い可能性があるのでスクリプトでは”a”*32から始めた.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# coding:utf-8 from m1z0r3.crypro import * import time remotehost = "crypto.chal.csaw.io" remoteport = 1578 s,f = sock(remotehost, remoteport) def get_cookie(username): read_until(f, "whitespace):") s.send(username+"\n") read_until(f, "is: ") return read_until(f).strip() def main(): print 'get_cookie("a"*31+"f")[:64]:' print "",get_cookie("a"*31+"f")[:64] print 'get_cookie("a"*31)[:64]:' print "",get_cookie("a"*31)[:64] print "==== Start ====" ans = "" for i in xrange(1,33): check = get_cookie("a"*(32-i)) for c in readable: if check[:64] == get_cookie("a"*(32-i)+ans+c)[:64]: print "%s文字目は %s"%(i,c) ans+=c break #time.sleep(0.1) if c == "}": break print ans print "congrats!!" print ans if __name__ == "__main__": main() |
The flag is “flag{Crypt0_is_s0_h@rd_t0_d0…}”
まとめ
時間中に解いた二つの問題(CVV, baby_crypt)のWrite-upでした.Almost Xorが解けなかったので精進が必要です.