iT邦幫忙

2023 iThome 鐵人賽

DAY 23
0
Security

資安小白的密碼學從0到1-CryptoHack平台解題紀錄系列 第 24

【Day 23】Symmetric CryptoGraphy05 - ECB

  • 分享至 

  • xImage
  •  

前言

今天只解一題,因為ECB過程有點長,加上主要目標為ECB搞懂,和如何去進行暴力破解
https://ithelp.ithome.com.tw/upload/images/20231003/20162613VMIPqvElQt.png

解法詳細過程還未完成 完成惹,已更新!

Writeup

ECB Oracle

題目

網址 : https://cryptohack.org/courses/symmetric/ecb_oracle/
https://ithelp.ithome.com.tw/upload/images/20231003/20162613b2NnCRlELi.png

思路

題目告訴我們,這題與ECB有關!(廢話)
點進題目的網址: https://aes.cryptohack.org/ecb_oracle
一樣會看到source code跟可互動介面,還有右上角的ECB圖
先來懂source code
https://ithelp.ithome.com.tw/upload/images/20231003/20162613bMwDNla46k.png

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 = a
17,會添加東西讓它長度到32(16的倍數)

然後創建一個AES 模式為ECB的cipher
接下來進入try,
將(padded 使用cipher進行加密 並把結果存在encrypted
如果出錯就return error
如果順利進行,最後會return encrypted.hex()

看懂後,我們來了解甚麼是ECB

https://ithelp.ithome.com.tw/upload/images/20231003/20162613RffBTfg5Kn.png

ECB的安全性很低,它是把plaintext以明文直接切割成一個個block
之後用相同的key去加密,得到ciphertext,互相是「獨立的」
代表說如果兩個plaintext相同,那麼最後ciphertext也會相同

解法

先嘗試打一個東西在右邊encrypt

因為它只吃hex,所以記得先在下面轉成十六進制喔

我打了一個"a"
https://ithelp.ithome.com.tw/upload/images/20231004/20162613El2HufCMOW.png

好像沒什麼特別的?那如果打很多個呢,試試看

我打了十個"a"
https://ithelp.ithome.com.tw/upload/images/20231004/201626139AbYao7M2D.png

會發現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
    https://ithelp.ithome.com.tw/upload/images/20231004/201626139ZZW53k2HS.png

  • 7個a
    https://ithelp.ithome.com.tw/upload/images/20231004/20162613tPPTKUGuSc.png

  • 觀察ciphertext : "a82fc18acbef729706a8c965d8e94b6b0816236e3d31721c0247af3837d622df7e5ec9cb1daaa7baa4c1331b1291e634"

通常我們會定 16 bytes = 1 block
2位十六進位數字 = 1 bytes
所以
32位 = 16bytes = 1 block

而我們ciphertext則有64位
所以我們可以寫成

X = flag的某字元, P = 填充物

  • 輸入6個"a"
    https://ithelp.ithome.com.tw/upload/images/20231004/20162613ZRz4YMfxTe.png

  • 輸入7個"a"
    https://ithelp.ithome.com.tw/upload/images/20231004/20162613v4BEbQdgqT.png
    因為多了一個a,所以有一個X被擠到下面,多了一個block

  • 小總結
    每16個字節為一個block
    觀察後發現flag總共有26個字節,但因為flag經過encode
    所以有一個"b"的前墜,後面我們就直接把b扣掉
    所以後面我們就當 flag 有 25個字節

之後根據我們不久前得出的ECB危險的地方

兩個plaintext相同,那麼最後ciphertext也會相同

所以我們可以這樣暴力解

X = flag的某字元, P = 填充物

  • 輸入15個a
    https://ithelp.ithome.com.tw/upload/images/20231004/20162613XPsuxaShky.png

第一行 -> 得到ciphertext1
第二行 -> 得到ciphertext2
第三行 -> 得到ciphertext3

覺得疑惑的話可以再看一下這張圖,三行分別對應三組這樣,所以也得到ciphertext1、2、3
https://ithelp.ithome.com.tw/upload/images/20231004/20162613E2ZtLRsRQ8.png

  • 輸入16個a
    https://ithelp.ithome.com.tw/upload/images/20231004/20162613rCNq4wYF0p.png
    第一行 -> 得到ciphertext4
    第二行 -> 得到ciphertext5
    第三行 -> 得到ciphertext6

ciphertext後面數字單純為了區別用

如果「輸入15個"a"」那邊的第一行X = "a" 那麼ciphertext1 = ciphertext4
這樣我們就可以推出flag的第一個字元為a

兩個plaintext相同,那麼最後ciphertext也會相同
反過來說兩個ciphertext相同,那麼最後plaintext也會相同

那如何推出flag的第二個字元

  • 輸入14個a

https://ithelp.ithome.com.tw/upload/images/20231004/20162613Oip0bbzO0S.png

第一行 -> 得到ciphertext1
第二行 -> 得到ciphertext2
第三行 -> 得到ciphertext3

  • 輸入14個a + 已知字元x1 + chr(i)

已知字元用T代替

https://ithelp.ithome.com.tw/upload/images/20231004/20162613zx8iZ4Ws7J.png

第一行 -> 得到ciphertext4
第二行 -> 得到ciphertext5
第三行 -> 得到ciphertext6

之後可以利用迴圈跑T右邊那個值chr(i),直到ciphertext4 = ciphertext1
那麼就又求出flag的第二個字元了
以此類推
最後就可以成功求出flag

大概知道流程,那麼我們要怎麼求ciphertext
簡單啦~在頁面上輸入plaintext就可以惹
但,我們不可能每一次都手動去輸入吧
這樣會累死w
https://ithelp.ithome.com.tw/upload/images/20231004/201626133Fsi4tZPu9.png

看到那個FAQ了嗎,用力按下去,之後它會教你如何透過程式去輸入值

https://ithelp.ithome.com.tw/upload/images/20231004/201626130CeMmy0WfA.png

我這裡示範利用"requests"的方式

import requests
#發送請求
x = requests.get(url)
#輸出網頁內容
print(x.text)

從source code的這句可得知路徑

https://ithelp.ithome.com.tw/upload/images/20231004/20162613ONsxxRQ1sM.png
得出網址為"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字元開始到倒數第三個字元

之後來寫我們迴圈暴力解的程式

  • code
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
https://ithelp.ithome.com.tw/upload/images/20231004/20162613Rr3U4eZ4R1.png

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]

  • code
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
  • 完整code
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()

  • output
    https://ithelp.ithome.com.tw/upload/images/20231004/20162613yESNF3xdKr.png

flag : crypto{p3n6u1n5_h473_3cb}
這個flag我跑超久才跑出來,不知道你們是不是也是xd

統整

  • ECB很危險!安全性很低
    • 兩個plaintext相同,那麼最後ciphertext也會相同
  • requests用法
import requests
#發送請求
x = requests.get(url)
#輸出網頁內容
print(x.text)

小結

今天學了ECB模式的運行,發現其實沒有很難,昨天的我可能是因為太想睡所以想不出來XD
我們明天繼續解題!

參考資料


上一篇
【Day 22】小小小小~回顧
下一篇
【Day 24】Symmetric CryptoGraphy06 - ECB_CBC_WTF
系列文
資安小白的密碼學從0到1-CryptoHack平台解題紀錄31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言