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”?
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.
- 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}