# 現代密碼學 序章 writeup :::spoiler 目錄 [toc] ::: ## Image Decode ### 題目 近期拉萊耶的考古團隊,發現了一張遠古時代遺留下來的文件 "output.txt",考古學家根據壁畫的提示判斷,該檔案內紀錄著歷代先知們傳承下來的「真理」,只要能解讀它就可以成為「神」!但是,沒有人知道如何解讀它,直到你出現。 你,一名聰明且富有冒險精神的 CTFer,決定要破解它!你在文件的附近還發現了一個使用上古程式語言「π森」撰寫的腳本 `task.py`,你猜測就是它產生了 `output.txt` ! 萬事俱備,因果之輪開始轉動 勇敢的 CTFer 啊,成為神吧! 提示 1:看清楚!好像不只 base64 喔? 提示 2:圖片是二進位資料還是一般字串呢? 寫檔應該要開什麼模式呢? `task.py`: ```python= from base64 import b64encode with open("flag.jpg", "rb") as img, open("output.txt", "w") as out: secret = b64encode(img.read().hex().encode()).decode() out.write(secret) ``` ### 解法 從 `secret = b64encode(img.read().hex().encode()).decode()` 可以看出這張圖片是先被 `.hex()` 轉成 Hex String 再被 `b64encode()` 編碼。 因此我們只要逆向操作,先從 base64 解碼,在用將 Hex String 轉回二進制資料即可!不過要記得因為圖片二進制資料,所以寫檔時的模式必須是 "wb" ```python= from base64 import b64decode with open("output.txt", "r") as f, open("result.jpg", "wb") as res: b64_string = f.read() original_content = bytes.fromhex(b64decode(b64_string).decode()) res.write(original_content) ``` ## XOR property ### 題目 在某個不具名的異世界,有一把可以淨化萬物的聖器「flag」。 因為 flag 的威力太強,合稱三大罪惡之源的恐懼之王迪亞波羅、憎恨之王墨菲斯托,以及毀滅之王巴爾齊心合力封印了 flag。 我是個專精封印術式的 CTFer,我在觀察 flag 的封印後發現,牠們是利用一種叫 XOR 的術式、將代表了三大罪惡的門之鑰跟 flag 混和在一起。 據我判斷,要解除封印需要利用 XOR 術式反轉的特性,然後只要再.... 哎呀!不知為何突然肚子好痛,這樣吧!我研究筆記就放在書房的書架上,你看到封面寫著 `task.py` 的那個就是了,不說了,我先去廁所了! `task.py`: ```python= # 提示:有工具就不需要自己動手 # 以下均為 hex string 表示,解密時需要轉回 bytes a_XOR_c = "144d696775452d35783c65220c276a714105560d6c152c3c11383c1078417272100705535d1631620456" b_XOR_c = "2249762906546d5807071b360d78557b60053c000500533a44415a187d1a4c0e4867437d52091b59270c" a_XOR_b_XOR_flag = "5576663e077e3b3a3742216c6e0d6059115f0f391a4c204f182642476a0479131704196f5b4072545127" ``` ### 解法 根據 XOR 的自反性,同樣的數字被 XOR 兩次會消失,所以 `(a XOR c) XOR (b XOR c) = a XOR b` ,此時同理,只要再 `(a XOR b XOR flag) XOR (a XOR b) = flag` ,即可得到 flag。 在 XOR 前記得要先將 Hex String 轉回 bytes 再進行運算;另外在這邊 pwn 的 xor 函數很方便,推薦直接使用! ```python= from task import a_XOR_c, a_XOR_b_XOR_flag, b_XOR_c from pwn import xor a_XOR_b_XOR_flag = bytes.fromhex(a_XOR_b_XOR_flag) a_XOR_c = bytes.fromhex(a_XOR_c) b_XOR_c = bytes.fromhex(b_XOR_c) flag = xor(a_XOR_b_XOR_flag, xor(a_XOR_c, b_XOR_c)) print(flag.decode()) ``` ## XOR decryption ### 題目 BlanHacker 日記: * 2023 年 11 月 30 日 我討厭題敘很多幹話的密碼學題目! 因為隔壁的 AlanHacker 出的題目題敘都長得不得了,而且幹話很多,所以為了教訓他,我從他電腦裡面偷出了他社課題目的 flag,把他用 XOR 加密後再放回去。呀哈哈!看你還敢不敢再亂寫題敘。 * 2023 年 11 月 31 日 那個 AlanHacker 就是遜啦!居然跪著求我解開加密ww根本笑死。 看在他態度誠懇,我就勉為其難幫他解密吧! * 2023 年 11 月 32 日 ....糟糕,我忘記當初加密的 key 是多少了... 怎麼辦?我只記得當初是用 1 byte 的整數作為 key 來加密 flag,但不知怎麼回事,key 居然不見了!? 有沒有誰?有沒有誰能幫幫我? 日記本旁邊是一個叫做 `task.py` 的文件,裡面寫著 BlanHacker 加密後的 flag 請你幫幫這可憐的小傢伙,解密出原來的 flag 吧! 保證 flag 格式為 crypto{XXX} `task.py`: ```python= # secret 為 hex string 表示,解密時需轉回 bytes secret = "e2f3f8f1f5eefaa0b4def5c9e8f2dee0deb6d8d1b2deb1e7dee2e0e4d2e0d3dec2e8f1c9b2f3fc" ``` ### 解法 1 byte 能表示的數字範圍為 $[0, 255]$ ,這其實很少!因此我們暴力計算每個組合 XOR 後的結果,再檢查開頭是否符合 flag 格式即可 ```python= from task import secret from pwn import xor secret = bytes.fromhex(secret) for i in range(256): if b"crypto" in xor(secret, bytes([i])): print(xor(secret, bytes([i])).decode()) ``` ## Hash Cipher ### 題目 道理我都懂,所以為什麼 Hash 不能拿來加密?人家 Base64 都可以加密了!! 所以我寫了一段程式來加密 flag,我相信它很安全,絕對不可能會被破解。 嗯?不信的話就來試試啊! 拿去!加密的程式在 `task.py` 裡面、輸出在 "output.txt" 裡面,都拿去!反正你也不可能破解的了。 `task.py`: ```python= from hashlib import sha3_256 with open("flag.txt", "r") as f, open("output.txt", "w") as out: flag = f.read() secret = "" for c in flag: secret += sha3_256(c.encode()).hexdigest()[:5] out.write(secret) ``` ### 解法 從 `task.py` 看得出它加密的方式只是把每個字做 sha3_256 後取 Hex String 的前 5 個字而已,而 ASCII 可顯示字元只有在整數 $[32, 127]$ 的範圍內,因此一樣暴力的計算所有可能並建表——這邊是用 dict 建 然後再對於被加密的每五個字,我從剛建的表中搜尋對應的字元,組合起來即可得到 flag ```python= from hashlib import sha3_256 with open("output.txt", "r") as f: cipher = f.read() prefix_map = dict() for i in range(32, 128): prefix_map[sha3_256(bytes([i])).hexdigest()[:5]] = chr(i) flag = "" for i in range(0, len(cipher), 5): flag += prefix_map[cipher[i:i + 5]] print(flag) ``` ## ECB ### 題目 嗚哇!你居然還認為自己能竄改我用 AES-ECB 加密的訊息?雜~魚♡ 看來有人不知道 AES 目前除了社交工程和旁道攻擊外還沒有發現其他有效攻擊方法呢~噗哧噗哧 雜魚~ 雜~魚♡ 好呀! "task.py" 裡面放著三組明文-密文對,想試就儘管試吧! 解不出來可別哭喔~ 真是沒用呢♡雜~魚 提示 1:把每個 block 印出來看會比較清楚喔! `task.py`: ```python= m1 = "Deposit amount: 5 dollars" c1 = "5797791557579e322e619f12b0ccdee8802015ee0467c419e7a38bd0a254da54" m2 = "One million dolls is quite the collection" c2 = "b1e952572d6b8e00b626be86552376e2d529a1b9cafaeb3ba7533d2699636323e7e433c10a9dcdab2ed4bee54da684ca" m3 = "Hey nice binoculars" c3 = "35d0c02036354fdf6082285e0f7bd6d2fdf526bd557b045bce65a3b3e300b55e" # 請設計出一個 payload,使解密後結果為: "Deposit amount: One million dollars" # 密文字串 c1、c2、c3 均是 16 進位字串、請轉回 bytes 運算 # flag:crypto{payload.hex()} ``` ### 解法 這題如同提示所說,印出來比較好理解! 印出來後,你會發現第一條明文存款的部分會被拆開到第二區塊、而第二條明文的第一區塊 "One million doll" 與我們要的 "One million dollars" 非常接近,此時我們只要在湊到 "doll" 後面的 "ars" 找到接在一起即可! 可以發現,第三條明文的最後區塊不就剛好是 "ars" 嗎?所以我們只要把對應位置的密文組合起來即為我們要的答案 ```python= from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from task import m1, m2, m3, c1, c2, c3 c1 = bytes.fromhex(c1) c2 = bytes.fromhex(c2) c3 = bytes.fromhex(c3) print(m1[:16], m1[16:], sep = "|===|") print(m2[:16], m2[16:32], m2[32:], sep = "|===|") print(m3[:16], m3[16:], sep = "|===|") payload = c1[:16] + c2[:16] + c3[16:] print(f"crypto{{{payload.hex()}}}") ``` ## Many Time Pad ### 題目 a:MTP b:OTP? a:M T P b:MTP! a:水啦! b:Is it good to encrypt? a:哩 encrypt 跨買都災 加密程式為 `task.py`、結果檔案為 "output.txt" 保證 flag 格式為 crypto{XXX} 提示 1:注意 flag 格式 提示 2:沒有什麼問題是暴力解決不了的 `task.py`: ```python= from pwn import xor from os import urandom with open("flag.txt", "r") as f: flag = f.read().encode() key = urandom(8) with open("output.txt", "w") as f: f.write(xor(flag, key).hex()) ``` ### 解法 `task.py` 中的 urandom 是設備隨機亂數,在密碼學 CTF 中看到它,在沒有其他外在因素的情況下,基本上不可能破解。 而提示有提到,注意 flag 格式。其實根據 XOR 的自反性,你會發現若我把明文拿去跟密文 XOR,我就能得到 key,而我們已知 flag 的前七個字是 "crypto{" ,因此我們可以解出部分的 key。 但是 urandom 產生的是 8 bytes 的 key 呀!還剩下 1 byte 怎麼辦?第二條提示就是用在這邊:嘗試暴力!如同 XOR decryption 那題一樣,我們一樣可以嘗試所有一個 byte 的可能,XOR 之後找出正確的 flag。 ```python= from pwn import xor with open("output.txt", "r") as f: cipher = f.read() cipher = bytes.fromhex(cipher) known_key = xor(cipher[:7], b"crypto{") for i in range(256): try: key = known_key + bytes([i]) plain = xor(cipher, key).decode() if plain.isprintable() and plain[-1] == "}": print(plain) except: pass ```