# 現代密碼學 序章 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
```