AES là một loại block cipher (mã hoá từng khối dữ liệu). Trong AES lại chia ra thành các mode mã hóa như ECB, CBC, CTR,... Bài hôm nay mình sẽ nói rõ hơn về ECB, cách mã hóa và các lỗ hổng tồn tại xung quanh mode này (mà mình đã tìm hiểu được). Các hàm, toán tử trong bài này và các bài crypto sau sẽ được tiêu chuẩn hóa về ngôn ngữ Python (cụ thể là version 3). Như sơ đồ bài trước thì từ một P (Plaintext) ban đầu sẽ được chia ra làm nhiều Pi con có độ dài là 16 bytes với 1 < i < n: P = P1 + P2 + ... +Pn Nếu độ dài P không chia hết cho 16 thì sẽ có thêm một bước gọi là Padding (thêm một số byte vào cuối, thường thì dù độ dài có chia hết cho 16 hay không ta vẫn sẽ có thêm bước Padding này) để cho P có độ dài thỏa mãn, được mô tả như sau: Giả sử len(P) = x, ta đặt y = (x // 16 + 1) * 16 là độ dài của P sau khi Padding. Đặt z = y - x (số bytes cần thêm vào cuối). Như vậy ta sẽ thêm vào cuối của P ban đầu z bytes có giá trị z. Mục đích của việc này là để P có độ dài chia hết cho 16, sau khi decrypt C (Ciphertext) ra P' (là P sau khi Padding) thì máy tính sẽ đọc giá trị của byte cuối cùng trong P' là z và cắt đi z bytes cuối cùng trong P' để ra P. Ví dụ: P = b"Off To Gulag Now" => x = len(P) = 16 => y = 32 => z = 16 ==> P' = b"Off To Gulag Now\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10" Chia P' ra làm 2 đoạn dài 16 bytes là P1 và P2 C1 = E(P1, key) C2 = E(P2, key) => C = C1 + C2 Sau khi decrypt C ra P' thì chỉ cần đọc byte cuối là b"\x10"' == 16 và sau đó cắt đi 16 bytes cuối là ra P. Một service chỉ cho ta chạy hàm mã hóa chạy mode ECB và trả về C, nó có thể bị leak toàn bộ P (thường thì là flag trong CTF) với điều kiện sau: C = E(M + P, key) Service nhận input từ người dùng là M sau đó trả về E(M+P, key). Trong ví dụ này mình sẽ lấy một challenge nhỏ trên cryptohack.org. Link: http://aes.cryptohack.org/ecb_oracle/ Quá trình khai thác:
- Ta cần biết được độ dài của P. Nhập bừa input, đến khi nhập n-1 bytes input, server trả về C' có độ dài m, tiếp tục n bytes input, server trả về C' có độ dài m+16 thì độ dài P được tính: x = m/16 - n Đối với chall trên thì x = 25.
- Ý tưởng phá mã:
- Do mã hóa hóa từng block riêng biệt nên không làm mất tính tổng quát, ta chỉ xét block trong đó chứa cả phần input người dùng và một phần của P. Giả sử P có dạng 'secret42xxxxxx'
- Khi ta nhập vào k bytes (k < 16) thì block đó sẽ chứa 16 - k ký tự đầu tiên của P ở cuối. Nhập vào 15 bytes A thì sẽ là AAAAAAAAAAAAAAAs Nhập vào 14 bytes A thì sẽ là AAAAAAAAAAAAAAse Nhập vào 13 bytes A thì sẽ là AAAAAAAAAAAAAsec ... Và server sẽ trả về ciphertext của block đó.
- Như vậy ta sẽ nhập vào 16t-1 bytes A (x < 16t) để server trả về mã hóa của các block gồm: ciphertext của các block có 16 bytes A và ciphertext của block AAAAAAAAAAAAAAAs (Hình 1. Ta chỉ cần cái này, đặt là C0). Sau đó ta lại nhập vào 16*t-1 bytes A + 1 bytes i bất kỳ từ 0 đến 255, server sẽ lại trả về ciphertext các block có 16 bytes A và ciphertext block AAAAAAAAAAAAAAAi (đặt là C0') rồi ta đem so sánh C0 và C0', nếu trùng nhau thì ký tự đầu tiên của P chính là i
- Sau khi tìm được ký tự đầu là i rồi thì nhập vào 16*t-2 bytes A + i và lặp lại bước trước ta sẽ tìm được bytes thứ 2 là j (Hình 2)... Như vậy ta đã leak được toàn bộ P mà không cần biết key hay hàm giải mã. Code để giải challenge phía trên ở hình 3