今天只解一題,因為ECB過程有點長,加上主要目標為ECB搞懂,和如何去進行暴力破解
解法詳細過程還未完成完成惹,已更新!
網址 : https://cryptohack.org/courses/symmetric/ecb_oracle/
題目告訴我們,這題與ECB有關!(廢話)
點進題目的網址: https://aes.cryptohack.org/ecb_oracle
一樣會看到source code跟可互動介面,還有右上角的ECB圖
先來懂source code
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
KEY = ?
FLAG = ?
@chal.route('/ecb_oracle/encrypt/<plaintext>/')
def encrypt(plaintext):
plaintext = bytes.fromhex(plaintext)
padded = pad(plaintext + FLAG.encode(), 16)
cipher = AES.new(KEY, AES.MODE_ECB)
try:
encrypted = cipher.encrypt(padded)
except ValueError as e:
return {"error": str(e)}
return {"ciphertext": encrypted.hex()}
會先把讀進來的plaintext轉為bytes之後
padded = pad(plaintext + FLAG.encode(), 16)
這句解釋為
把plaintext跟FLAG字串相加,之後將結果填充到最接近的16的倍數
最後把結果存到padded裡
比如說如果plaintext + flag = a15,那麼就會幫它添加一個東西(通常可能是0)讓它長度為16
如果plaintext + flag = a17,會添加東西讓它長度到32(16的倍數)
然後創建一個AES 模式為ECB的cipher
接下來進入try,
將(padded 使用cipher進行加密 並把結果存在encrypted
如果出錯就return error
如果順利進行,最後會return encrypted.hex()
看懂後,我們來了解甚麼是ECB
ECB的安全性很低,它是把plaintext以明文直接切割成一個個block
之後用相同的key去加密,得到ciphertext,互相是「獨立的」
代表說如果兩個plaintext相同,那麼最後ciphertext也會相同
先嘗試打一個東西在右邊encrypt
因為它只吃hex,所以記得先在下面轉成十六進制喔
我打了一個"a"
好像沒什麼特別的?那如果打很多個呢,試試看
我打了十個"a"
會發現output變長了!為甚麼會這樣?
padded = pad(plaintext + FLAG.encode(), 16)
還記得它嗎
因為它會讓ciphertext長度保持在16的倍數,如果我們剛好讓它超過一點點,那麼它就會直接多一個block
例子 原長度13 pad後 -> 16, 原長度15 pad後 -> 16, 原長度17 pad後 -> 32
所以我們先來找看看plaintext + flag 原長度為16的plaintext
經過測試發現,輸入6個"a"會剛好是16,因為輸入7個"a"的話會直接多一個block
6個a
7個a
觀察ciphertext : "a82fc18acbef729706a8c965d8e94b6b0816236e3d31721c0247af3837d622df7e5ec9cb1daaa7baa4c1331b1291e634"
通常我們會定 16 bytes = 1 block
2位十六進位數字 = 1 bytes
所以
32位 = 16bytes = 1 block
而我們ciphertext則有64位
所以我們可以寫成
X = flag的某字元, P = 填充物
輸入6個"a"
輸入7個"a"
因為多了一個a,所以有一個X被擠到下面,多了一個block
小總結
每16個字節為一個block
觀察後發現flag總共有26個字節,但因為flag經過encode
所以有一個"b"的前墜,後面我們就直接把b扣掉
所以後面我們就當 flag 有 25個字節
之後根據我們不久前得出的ECB危險的地方
兩個plaintext相同,那麼最後ciphertext也會相同
所以我們可以這樣暴力解
X = flag的某字元, P = 填充物
第一行 -> 得到ciphertext1
第二行 -> 得到ciphertext2
第三行 -> 得到ciphertext3
覺得疑惑的話可以再看一下這張圖,三行分別對應三組這樣,所以也得到ciphertext1、2、3
ciphertext後面數字單純為了區別用
如果「輸入15個"a"」那邊的第一行X = "a" 那麼ciphertext1 = ciphertext4
這樣我們就可以推出flag的第一個字元為a
兩個plaintext相同,那麼最後ciphertext也會相同
反過來說兩個ciphertext相同,那麼最後plaintext也會相同
那如何推出flag的第二個字元
第一行 -> 得到ciphertext1
第二行 -> 得到ciphertext2
第三行 -> 得到ciphertext3
已知字元用T代替
第一行 -> 得到ciphertext4
第二行 -> 得到ciphertext5
第三行 -> 得到ciphertext6
之後可以利用迴圈跑T右邊那個值chr(i),直到ciphertext4 = ciphertext1
那麼就又求出flag的第二個字元了
以此類推
最後就可以成功求出flag
大概知道流程,那麼我們要怎麼求ciphertext
簡單啦~在頁面上輸入plaintext就可以惹
但,我們不可能每一次都手動去輸入吧
這樣會累死w
看到那個FAQ了嗎,用力按下去,之後它會教你如何透過程式去輸入值
我這裡示範利用"requests"的方式
import requests
#發送請求
x = requests.get(url)
#輸出網頁內容
print(x.text)
從source code的這句可得知路徑
得出網址為"https://aes.cryptohack.org/ecb_oracle/encrypt/"
之後後面要在+我們要encrypt的十六進制字串
所以可以這樣寫
def get_encrypt(cipher):
url = "https://aes.cryptohack.org/ecb_oracle/encrypt/"
x = requests.get(url + cipher)
ciphertext = x.text[15:-3]
return ciphertext
x.text[15:-3]原因是因為
只有x.text回傳的內容為
{"ciphertext":"53d03b61cae58e77b7cc33570b01ff9a022c13d750f4cb2f24e68c73ebb7a4d2"}
而我們不需要大括號跟雙引號,所以從第15字元開始到倒數第三個字元
之後來寫我們迴圈暴力解的程式
def solve_part1():
flag = "crypto{"
for i in range(0, 8):
cipher = "a" * (8-i)
ciphertext = get_encrypt(cipher.encode().hex())[:32]
for guess in range(32,128):
cipher_1 = "a" * (8-i) + flag + chr(guess)
try_flag = get_encrypt(cipher_1.encode().hex())[:32]
if try_flag == ciphertext :
flag += chr(guess)
break
return flag
因為我們已經知道flag前面一小部分的文字了,所以可以讓迴圈少跑幾次
我們實際模擬第一遍怎麼跑
cipher = "a" * 8
傳進get_encrypt得到ciphertext而我們只要ciphertext1也就是前32字元
所以加上[:32]
之後讓guess跑32~127
參考ascii table
cipher_1 = a*8 + flag +chr(guess)
之後傳進get_encrypt得到ciphertext,也是只要前32字元,然後把值存到try_flag
最後看try_flag 是否等於 ciphertext
是的話就把該字元加到flag後面
最後我們可以獲得flag的前15個字元
後面的因為flag後面的字元會跑到第二行(一行最多16個字元),進入到第二個block,所以程式碼要稍微改一下
把[:32]改成[32:64]
def solve_part2(flag):
for i in range(0, 10):
cipher = "a" * (16-i)
ciphertext = get_encrypt(cipher.encode().hex())[32:64]
for guess in range(32,128):
cipher_1 = "a" * (16-i) + flag + chr(guess)
try_flag = get_encrypt(cipher_1.encode().hex())[32:64]
if try_flag == ciphertext :
flag +=chr(guess)
break
return flag
import requests
#str to hex
#strings = "abc"
#print(strings.encode().hex())
def get_encrypt(cipher):
url = "https://aes.cryptohack.org/ecb_oracle/encrypt/"
x = requests.get(url + cipher)
ciphertext = x.text[15:-3]
return ciphertext
def solve_part1():
flag = "crypto{"
for i in range(0, 8):
cipher = "a" * (8-i)
ciphertext = get_encrypt(cipher.encode().hex())[:32]
for guess in range(32,128):
cipher_1 = "a" * (8-i) + flag + chr(guess)
try_flag = get_encrypt(cipher_1.encode().hex())[:32]
if try_flag == ciphertext :
flag += chr(guess)
break
return flag
def solve_part2(flag):
for i in range(0, 10):
cipher = "a" * (16-i)
ciphertext = get_encrypt(cipher.encode().hex())[32:64]
for guess in range(32,128):
cipher_1 = "a" * (16-i) + flag + chr(guess)
try_flag = get_encrypt(cipher_1.encode().hex())[32:64]
if try_flag == ciphertext :
flag +=chr(guess)
break
return flag
def main():
flag = solve_part1()
print(solve_part2(flag))
if __name__ == "__main__":
main()
flag : crypto{p3n6u1n5_h473_3cb}
這個flag我跑超久才跑出來,不知道你們是不是也是xd
import requests
#發送請求
x = requests.get(url)
#輸出網頁內容
print(x.text)
今天學了ECB模式的運行,發現其實沒有很難,昨天的我可能是因為太想睡所以想不出來XD
我們明天繼續解題!