Symmetric ciphers - Mật mã đối xứng AES (phần 6)
V. Padding trong block cipher và AES
1. Kỹ thuật padding
AES-128 thực hiện mã hóa dữ liệu theo từng khối (block), mỗi khối có đầu vào là một chuỗi gồm ký tự và đầu ra cũng là một chuỗi mã hóa ký tự. Như vậy, trường hợp lý tưởng nhất là thông điệp cần mã hóa có độ dài là bội của . Tuy nhiên, trong môi trường thực tế chúng ta khó kiểm soát được độ dài đó. Bởi vậy, cần có một công việc giúp "sửa đổi" độ dài thông điệp thành bội của - Kỹ thuật padding.
Khi thông điệp cần mã hóa không thể chia đều thành các chuỗi plaintext có độ dài , chúng ta có thể sử dụng kỹ thuật padding nhằm bổ sung các bytes vào block cuối cùng để cho đủ độ dài (như hình vẽ).
2. Một số kiểu padding
Kỹ thuật padding cần đảm bảo tính an toàn của mật mã và tính khả thi cho quá trình giải mã. Bởi vậy, giá trị các bit bổ sung cũng như số lượng của chúng cần tuân theo một quy tắc đặt trước. Dựa vào đó chúng ta có thể chia kỹ thuật padding thành một số kiểu khác nhau: ZeroPadding, ISO/IEC 7816-4, PKCS7, ...
2.1. Zeropadding
Phương thức bổ sung của zeropadding đã được gợi qua cái tên của nó: chúng ta sẽ bổ sung các bit cho tới khi đủ độ dài block.
# Thêm zero-padding để có độ dài là bội số của block_size
data += b'\x00' * (block_size - len(data) % block_size)
2.2. ISO/IEC 7816-4
ISO/IEC 7816-4 giống với Zeropadding, chỉ khác nhau ở bytes đầu tiên thêm vào sẽ là 80
ở dạng Hex. Ví dụ cài đặt trong Python sử dụng thư viện Crypto.Util.Padding
# Perform ANSI X.923 padding
padded_data = pad(data, 16, style='iso7816')
print(padded_data)
2.3. PKCS7
PKCS7 khá đặc biệt, nếu số lượng bytes cần bổ sung là thì tất cả các bytes bổ sung có giá trị AB
. Chẳng hạn, nếu cần padded bytes thì chuỗi bytes padding sẽ là 05 05 05 05 05
ở dạng Hex. Ví dụ chương trình cài đặt kỹ thuật padding PKCS7 trong Python:
data = b'This is a test mess.'
padded_data = pad(data, 16)
print(padded_data)
3. Padding oracle attack
Challenge: ECB ORACLE
Source code:
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()}
Khác với các challenge trước, ở đây chúng ta chỉ được cung cấp một route mã hóa duy nhất /ecb_oracle/encrypt/<plaintext>/
. Chú ý cách khai báo padding padded = pad(plaintext + FLAG.encode(), 16)
. Các bytes padding được tạo bởi flag và các ký tự khác từ hàm pad()
.
Route /ecb_oracle/encrypt/<plaintext>/
nhận đầu vào là chuỗi plaintext (ở dạng hex), thực hiện padding (bao gồm flag) rồi trả về chuỗi ciphertext (ở dạng hex). Đặc biệt mã hóa AES sử dụng mode ECB (Electronic Codebook), tức mã hóa riêng biệt mỗi block gồm ký tự. Một cách rõ hơn, mỗi block gồm bytes hex của plaintext và trả về bytes hex của ciphertext.
Để có thể hiểu rõ hơn cách thức làm việc của hàm encrypt()
này, bạn đọc có thể hình dung sau khi thực hiện padding, chuỗi plaintext của chúng ta sẽ có dạng A+FLAG+B, trong đó chuỗi A có thể được thay đổi tùy ý về độ dài cũng như giá trị. Kết hợp với chế độ ECB, nên mặc dù chuỗi B là ẩn (không xác định được giá trị) cũng có thể "gạt bỏ" không cần quan tâm.
Vẫn chưa hiểu lắm sao? Thử dành sự chú ý vào duy nhất block đầu tiên. Nếu input có độ dài ký tự, thì plaintext của block đầu tiên sẽ có dạng với là ký tự đầu tiên của flag, và chúng ta thu được ciphertext của block này ( ký tự đầu tiên của toàn bộ ciphertext). Lúc này ý tưởng đã rõ ràng hơn rồi đúng không, hoàn toàn có thể thực hiện vét cạn trường hợp có thể xảy ra của ký tự này. Bằng cách đưa vào input , , , ... và thu được các ciphertext của block đầu tiên lần lượt là , , , ... (là ký tự đầu của toàn bộ ciphertext). Cho tới khi thì chúng ta cũng tìm ra - ký tự đầu tiên của flag. Chú ý rằng trong quá trình này chuỗi ký tự phải chọn cố định!
Giống như các challenge trước, chúng ta cần một hàm encrypt_aes_pad()
lấy ciphertext từ route website về:
def encrypt_aes_pad(plaintext, url, path):
plaintext = plaintext.encode().hex()
r = requests.get(url = url + path % plaintext)
response = r.text.strip()
data = json.loads(response)
return data['ciphertext']
Với biến key
có độ dài ký tự, chúng ta lấy về block đầu tiên của ciphertext check = encrypt_aes_pad(key, BASE_URL, encrypt_path)[0:32]
. Rồi thử từng trường hợp của char
: part = encrypt_aes_pad(temp + char, BASE_URL, encrypt_path)[0:32]
. Kiểm tra liên tục cho tới khi điều kiện check == part
đúng tức là có được ký tự đầu tiên của flag.
Để tìm các ký tự tiếp theo, chúng ta chỉ cần giảm dần ký tự của key
. Bạn đọc có thể tham khảm chương trình như sau:
key = '0' * 15
while len(key) > 0:
check = encrypt_aes_pad(key, BASE_URL, encrypt_path)[0:32]
for char in wordlist:
temp = key + flag1
part = encrypt_aes_pad(temp + char, BASE_URL, encrypt_path)[0:32]
if check == part:
flag1 = flag1 + char
print(flag1)
key = key[1:]
break
Hiện tại chúng ta chỉ lấy được ký tự của flag crypto{p3n6u1n5. Đối với phần sau, có thể tận dụng block thứ hai của ciphertext. Chẳng hạn, với input có ký tự bất kỳ thì chuỗi plaintext thứ hai là crypto{p3n6u1n5x (x là ký tự tiếp theo), lúc này chỉ cẩn tiếp tục brute force ký tự x này như trên. Chương trình tham khảo:
flag1 = 'crypto{p3n6u1n5'
flag2 = ''
key = '0' * 16
while len(key) > 0:
check = encrypt_aes_pad(key, BASE_URL, encrypt_path)[32:64]
for char in wordlist:
temp = key + flag1
temp = temp + flag2
part = encrypt_aes_pad(temp + char, BASE_URL, encrypt_path)[32:64]
if check == part:
flag2 = flag2 + char
print(flag2)
key = key[1:]
break
VI. Mode CBC (Cipher Block Chaining) trong Block cipher và AES
1. CBC cải tiến gì so với ECB?
Cùng nhìn lại một chút kiến thức, chế độ mã hóa ECB (Electronic Codebook) sử dụng một secret key duy nhất nhằm mã hóa cho tất cả khối dữ liệu. Điều này dẫn đến một vấn đề: Các khối dữ liệu giống nhau được mã hóa cho ra các khối ciphertext giống nhau, làm tăng nguy cơ bị tấn công bởi attacker khi bị nghe lén thông tin. Để khắc phục điều đó, chế độ CBC mang mỗi block plaintext XOR với block ciphertext ngay trước nó trước khi thực hiện mã hóa, hành động này giúp làm tăng tính ngẫu nhiên của dữ liệu. Đối với block đầu tiên, nhằm đảm bảo tính đồng bộ, nó sẽ được XOR với một vector khởi đầu IV (initialization vector) sinh ngẫu nhiên.
Như vậy, các block plaintext trong CBC có liên hệ chặt chẽ với nhau, đồng thời với mỗi lần mã hóa, do vector khởi đầu IV khác nhau (sinh ngẫu nhiên) nên các ciphertext cũng luôn thay đổi - tính bảo mật đã được cải thiện. Tuy nhiên, nguy cơ dẫn đến sai lệch trong mã hóa bị tăng lên, ví dụ: một khối dữ liệu bị hỏng trong ECB không ảnh hưởng đến các dữ liệu khác (các block độc lập với nhau), trong CBC khối dữ liệu thứ bị hỏng sẽ ảnh hưởng đến tất cả khối dữ liệu , , ... (block liên quan trực tiếp với block ).
Về ý tưởng giải mã AES-128 CBC, chúng ta thực hiện tương tự với ECB: chia ciphertext thành từng block và đưa vào hàm giải mã, khi đó các chuỗi output nhận được chưa thực sự là ciphertext. Cụ thể, chuỗi output ở block sẽ là kết quả của chuỗi plaintext XOR với chuỗi ciphertext . Nên chỉ cần thêm một bước XOR giữa output với chuỗi ciphertext sẽ thu được chuỗi plaintext . Với chuỗi ciphertext đầu tiên chúng ta sẽ XOR với vector khởi đầu IV.
Tài liệu tham khảo
All rights reserved