Mô tả

Summary

ECB là chế độ đơn giản nhất, với mỗi khối bản rõ được mã hóa hoàn toàn độc lập. Trong trường hợp này, đầu vào của bạn được đặt trước cờ bí mật và được mã hóa và đó là tất cả. Chúng tôi thậm chí không cung cấp chức năng giải mã. Có lẽ bạn không cần một oracle đệm khi bạn có một “oracle ECB”?

Chơi tại https://aes.cryptohack.org/ecb_oracle

Mã nguồn:

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()}

Hướng dẫn giải

Khi đọc source code, ta thấy rằng input nhập vào sẽ được thêm vào trước flag. Ta có thể triển khai một cách tấn công dựa trên lỗ hổng này của ECB mode.

Find Block Size

Để tấn công theo cách này, trước tiên ta cần tìm ra block size của cipher. Tìm bằng cách thử gọi request tất cả các input có thể có đến khi nào ciphertext thay đổi kích thước:

curl -s "https://aes.cryptohack.org/ecb_oracle/encrypt/<input>"

Nhận thấy nếu input là từ 6 bytes (12 ký tự hexa) trở xuống:

curl -s "https://aes.cryptohack.org/ecb_oracle/encrypt/111111111111/"

Thì ciphertext có kích thước là 32 bytes (64 ký tự hexa):

{"ciphertext":"381c0b105d12bcf7617bf6cbb5871aae779f56d8fb3019c4a1e2cb0b74281657"}

Nếu input là 7 bytes (14 ký tự hexa):

curl -s "https://aes.cryptohack.org/ecb_oracle/encrypt/11111111111111/"

Thì ciphertext có kích thước là 48 bytes (96 ký tự hexa):

{"ciphertext":"e98947a94de24fedefe7d3506b9f6b33bed812700886882eade550f0f73192f267f7b4ed431d69a986c4e8a7fb810c97"}

Như vậy, để gây ra overflow thì ta cần tối thiểu 7 bytes.

Sự khác biệt kích thước của ciphertext chính là block size (48 - 32 = 16). Ngoài ra, ta thấy rằng với 6 bytes thì chưa bị overflow, ta đoán rằng plaintext sẽ có kích thước là 32 - 6 = 26 bytes (trong đó có 16 bytes đệm).

Generate Padding Bytes

Sau khi biết được block size thì ta tạo ra một chuỗi S có số bytes là

ceil(P/B) * B - 1

Với:

  • P là plaintext size và B là block size.
  • Theo dữ liệu ở trên, ta có được S = 31 bytes.
  • Các giá trị của S có thể là bất cứ thứ gì, ở đây ta chọn S là 1111 1111 1111 1111 1111 1111 1111 111.

Code tính kích thước block size và tạo S:

S_size = math.ceil(26 / 16) * 16 - 1
S = '1' * S_size

Brute-forcing

Trước khi đi đến bước 4 thì ta xét ví dụ sau:

Giả sử block size = 4, plaintext size = 4 và ta chọn S = 111 (3 bytes). Khi đó, ta gắn chuỗi S vào trước flag sẽ được các blocks như sau:

111x xxx

Với x là các ký tự chưa biết thuộc flag.

Giả sử kết quả mã hóa là:

aaab cde

Ta gọi kết quả này là expected ciphertext.

Lúc này, ta sẽ thử thay x ở sau 111 bằng một ký tự bất kỳ, chẳng hạn như:

111f xxx

Tiến hành mã hóa chuỗi này, nếu bytes đầu tiên có dạng là aaab giống với expected string thì ta biết rằng ký tự mà ta chọn chính là ký tự thuộc flag.

Điều này đúng bởi vì với chế độ ECB, cùng plaintext thì sẽ cho ra cùng một ciphertext. Do đó, ta cô lập 1 ký tự và thế vào các ký tự bất kỳ để tìm ra ký tự có trong flag.

Giá trị mà ta cần thay thế có thể là các ký tự abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_@}{ (tùy thuộc vào format của flag).

Sau khi tìm ra được một ký tự của flag thì ta giảm độ dài S xuống 1 giá trị và tiếp tục tìm kiếm giá trị tiếp theo.

  1. Theo ví dụ ở trên, ta cần gắn S vào trước flag rồi mã hóa để tạo ra expected ciphertext. Thực hiện điều này bằng cách gọi request và truyền vào S:
import requests
 
api = 'https://aes.cryptohack.org/ecb_oracle/encrypt/'
S = '1' * (S_size-len(flag))
url = f'{api}{S.encode().hex()}/'
expected = requests.get(url).json()['ciphertext']

Input dùng để brute-force sẽ là S + flag + c với c là ký tự cần brute-force.

Sử dụng một vòng lặp qua các ký tự ở trong chuỗi _@}{abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:

for c in characters:
	payload = (S + flag + c).encode().hex()
	url = f'{api}{payload}/'
	res = requests.get(url).json()['ciphertext']

Nếu block thứ 2 (ký tự 32 đến 63 ở trong chuỗi) trùng với block thứ 2 của expected ciphertext thì c là ký tự cần tìm.

import time
 
for c in characters:
	payload = (S + flag + c).encode().hex()
	url = f'{api}{payload}/'
	res = requests.get(url).json()['ciphertext']
 
	if res[32:64] == expected[32:64]:
		flag += c
		print(f'flag: {flag}')
		break
	time.sleep(1)

Tại sao là block thứ 2? Lý do là vì ký tự cần tìm của chúng ta là byte cuối cùng của block thứ 2.

Vì gửi request nhiều lần, ta cần gọi hàm time.sleep(1) để hạn chế tốc độ gửi request.

Sau khi tìm được một ký tự thì ta giảm S đi một ký tự và tiếp tục quá trình trên.

def bruteforce():
    flag = ''
    S_size = math.ceil(26 / 16) * 16 - 1
    characters = "_@}{abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    api = 'https://aes.cryptohack.org/ecb_oracle/encrypt/'
 
    while True:
        # Generate the expected ciphertext
        S = '1' * (S_size-len(flag))
        url = f'{api}{S.encode().hex()}/'
        expected = requests.get(url).json()['ciphertext']
 
        print('E: ', end='')
        print_bulk(expected, 32)
 
        # Bruteforce the character
        for c in characters:
            payload = (S + flag + c).encode().hex()
            url = f'{api}{payload}/'
            res = requests.get(url).json()['ciphertext']
 
            print(c, ' ', end='')
            print_bulk(res, 32)
 
            # Compare the second block of the ciphertext
            if res[32:64] == expected[32:64]:
                flag += c
                print(f'flag: {flag}')
                break
            time.sleep(1)
 
        if flag.endswith('}'):
            break
 
    print(flag)

Cờ

Success

crypto{p3n6u1n5_h473_3cb}

Tài liệu tham khảo