iT邦幫忙

2024 iThome 鐵人賽

0
Security

密碼學小白的學習之路系列 第 30

[Day 29] 題目(Symmetric-10-2)

  • 分享至 

  • xImage
  •  

ECB Oracle

https://cryptohack.org/courses/symmetric/ecb_oracle/

解題思路

這邊要先提一下 ECB 模式的弱點,之前的 ECB 小介紹有提到:
在ECB模式中每個明文塊都會單獨加密,產生相同長度的密文塊。相同的明文塊所產生的密文塊會完全相同。
因此,這種模式無法隱藏數據的結構或重複性,導致容易被分析。

所以可以這樣解密:
逐字節測試不同的明文,觀察密文的變化,從而推測出原始明文。

而我們收到的密文是怎麼產生的呢?
我們輸入的明文與 FLAG 串接後的字串會再使用 pad 函數來填充,使其長度成為AES的區塊大小(16 bytes)的倍數,最後再進行AES加密。

所以解密的過程是這樣的:

  1. 依序調整填充用的明文長度,使得待猜測的字元恰好位於區塊的最後一個位置
  2. 讓待猜測的字元遍歷所有可能的ASCII可打印字符(從33到127)
  3. 建構待測試的明文:填充用的明文 + 已知flag + 猜測字符
  4. 測試待測試的明文加密後是否與加密後的明文相同
  5. 如果是,則將該字元加到flag

第一部分的code:

上一篇已經提到可以透過request的方法向網頁端發送請求,來獲得加密後的密文。
而這部分就是向網頁發送加密的請求,並獲得密文的程式碼。

import requests
def encrypted(plaintext):
    url = "https://aes.cryptohack.org/ecb_oracle/encrypt/"
    hex_plaintext = plaintext.encode().hex()  # 將字串轉換為hex,因為網站的加解密輸出與輸入都要是hex格式
    response = requests.get(url + hex_plaintext)  # 發送GET請求並獲取響應
    ciphertext = response.text[15:-3]  
    # 提取密文,因為網站會回傳{"ciphertext":"53d03b61cae58e77b7cc33570b01ff9a022c13d750f4cb2f24e68c73ebb7a4d2"}這樣格式的內容,而我們只需要:後的密文就好,所以用[15:-3]來擷取第15個字元開始到倒數第3個字元
    return ciphertext

第二部分的code:

上上一篇文章有提到,flag總共有25個字元,而現在我們要找出前16個(1 block)。


def find_flag_1():
    flag = "crypto{" # 已知的前7個字元

    # 第一個區塊
    # 從第8個字元開始猜測,直到第16個字元(第一個區塊的最後一個字元)
    for i in range(7, 16):
        # 調整填充用的明文長度,使得待猜測的字元恰好位於區塊的最後一個位置
        plain = "a" * (15 - i)
        
        # 獲取只包含填充的密文(僅第一個區塊 1 block = 16 bytes = 32個hex字符)
        cipher = encrypted(plain)[:32]
        
        # 遍歷所有可能的ASCII可打印字符(從33到127)
        for test in range(33, 128):
            # 建構待測試的明文:填充用的明文 + 已知flag + 猜測字符
            plain_try = plain + flag + chr(test)
            
            # 加密待測試的明文並得到第一個區塊
            cipher_try = encrypted(plain_try)[:32]
            
            # 如果密文匹配,則找到了正確的字符
            if cipher == cipher_try:
                # 將找到的字符添加到flag中
                flag += chr(test)
                print(f"找到字符: {flag}")
                break  # 找到字符後跳出內層循環,繼續猜測下一個字符
    
    return flag

第三部分的code:

找出剩餘的flag字元
因為flag的最後一個字元是"}",所以為了省時間,只要找出第17-24個字符,最後返回的時候再加上"}"就好。

def find_flag_2(flag):
    # 第二個區塊的解密
    print("進入到第二階段")
    for i in range(len(flag), 24):  # 從第17個字符開始猜測,直到第24個字符
        # 調整填充長度,使待猜測的字符位於第二個區塊的最後一個位置
        plain = "a" * (31 - i)
        
        # 獲取只包含填充的密文的第二個區塊
        cipher = encrypted(plain)[32:64]
        
        # 遍歷所有可能的ASCII可打印字符(從33到127)
        for test in range(33, 128):
            # 構造待測試的明文:填充用的明文 + 已知flag + 猜測字符
            plain_try = plain + flag + chr(test)
            
            # 加密待測試的明文並獲取第二個區塊
            cipher_try = encrypted(plain_try)[32:64]
            
            # 如果密文匹配,則找到了正確的字符
            if cipher == cipher_try:
                # 將找到的字符添加到flag中
                flag += chr(test)
                print(f"找到字符: {flag}")
                break  # 找到字符後跳出內層循環,繼續猜測下一個字符
    
    # 返回完整的flag,包括結尾的 "}"
    return flag + "}"

完整code:



import requests
def encrypted(plaintext):
    url = "https://aes.cryptohack.org/ecb_oracle/encrypt/"
    hex_plaintext = plaintext.encode().hex()  # 將字串轉換為十六進位表示
    response = requests.get(url + hex_plaintext)  # 發送GET請求並獲取響應
    ciphertext = response.text[15:-3]  # 提取密文
    return ciphertext

def find_flag_1():
    flag = "crypto{" # 已知的前7個字元

    # 第一個區塊
    # 從第8個字元開始猜測,直到第16個字元(第一個區塊的最後一個字元)
    for i in range(7, 16):
        # 調整填充長度,使得待猜測的字元恰好位於區塊的最後一個位置
        plain = "a" * (15 - i)
        
        # 獲取只包含填充的密文(僅第一個區塊)
        cipher = encrypted(plain)[:32]
        
        # 遍歷所有可能的ASCII可打印字符(從33到127)
        for test in range(33, 128):
            # 構造待測試的明文:填充 + 已知flag + 猜測字符
            plain_try = plain + flag + chr(test)
            
            # 加密待測試的明文並獲取第一個區塊
            cipher_try = encrypted(plain_try)[:32]
            
            # 如果密文匹配,則找到了正確的字符
            if cipher == cipher_try:
                # 將找到的字符添加到flag中
                flag += chr(test)
                print(f"找到字符: {flag}")
                break  # 找到字符後跳出內層循環,繼續猜測下一個字符
    
    # 返回找到的flag(不包括最後的 "}")
    return flag
            
def find_flag_2(flag):
    # 第二個區塊的解密
    print("進入到第二階段")
    for i in range(len(flag), 24):  # 從第17個字符開始猜測,直到第24個字符
        # 調整填充長度,使待猜測的字符位於第二個區塊的最後一個位置
        plain = "a" * (31 - i)
        
        # 獲取只包含填充的密文的第二個區塊
        cipher = encrypted(plain)[32:64]
        
        # 遍歷所有可能的ASCII可打印字符(從33到127)
        for test in range(33, 128):
            # 構造待測試的明文:填充 + 已知flag + 猜測字符
            plain_try = plain + flag + chr(test)
            
            # 加密待測試的明文並獲取第二個區塊
            cipher_try = encrypted(plain_try)[32:64]
            
            # 如果密文匹配,則找到了正確的字符
            if cipher == cipher_try:
                # 將找到的字符添加到flag中
                flag += chr(test)
                print(f"找到字符: {flag}")
                break  # 找到字符後跳出內層循環,繼續猜測下一個字符
    
    # 返回完整的flag,包括結尾的 "}"
    return flag + "}"

if __name__ == "__main__":
    result = find_flag_2(find_flag_1())
    if result:
        print(f"完整的 flag 是: {result}")
    else:
        print("無法完成 flag 的解密"),

crypto{p3n6u1n5_h473_3cb}
penguins_hate_ecb(可能與linux企鵝圖經過ECB加密後仍然很清楚有關)

參考資料:

Ascii table: https://www.rapidtables.com/code/text/ascii-table.html
前人的文章: https://ithelp.ithome.com.tw/m/articles/10333803

後話:

當初debug了很久,然後又因為晚了幾分鐘發文,所以連更記錄沒了qq。
今天要來把之前還沒補完的內容補全。
備註:因為這題的程式碼要用暴力猜解法一個個去試,所以歷時較久,要很有耐心。
當初因為他跑了很久都沒有動靜想說是不是程式碼有什麼bug,所以我讓他在每一個階段只要找到flag的字符就要print出來,後來發現程式碼沒什麼問題,只是跑得太慢了。


上一篇
[Day 29] 填充模式(Padding)
下一篇
[Day 30] 題目(Symmetric-11)
系列文
密碼學小白的學習之路31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言