+1

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 1616 ký tự và đầu ra cũng là một chuỗi mã hóa 1616 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 1616. 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 1616 - Kỹ thuật padding.

image.png

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 1616, 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 00 cho tới khi đủ độ dài block.

image.png

# 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)

image.png

2.3. PKCS7

image.png

PKCS7 khá đặc biệt, nếu số lượng bytes cần bổ sung là 0xAB0xAB thì tất cả các bytes bổ sung có giá trị AB. Chẳng hạn, nếu cần padded 55 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)

image.png

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 1616 ký tự. Một cách rõ hơn, mỗi block gồm 3232 bytes hex của plaintext và trả về 3232 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 1515 ký tự, thì plaintext của block đầu tiên sẽ có dạng AxAx với xx là ký tự đầu tiên của flag, và chúng ta thu được ciphertext CC của block này (3232 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ự xx này. Bằng cách đưa vào input AaAa, AbAb, AcAc, ... và thu được các ciphertext của block đầu tiên lần lượt là C1C_1, C2C_2, C3C_3, ... (là 3232 ký tự đầu của toàn bộ ciphertext). Cho tới khi C=CiC = C_i thì chúng ta cũng tìm ra xx - ký tự đầu tiên của flag. Chú ý rằng trong quá trình này chuỗi ký tự AA 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 1515 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

image.png

Hiện tại chúng ta chỉ lấy được 1515 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ó 1616 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

image.png

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.

image.png

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ứ ii bị hỏng sẽ ảnh hưởng đến tất cả khối dữ liệu i+1i+1, i+2i+2, ... (block ii liên quan trực tiếp với block i1i-1).

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 ii sẽ là kết quả của chuỗi plaintext ii XOR với chuỗi ciphertext i1i-1. Nên chỉ cần thêm một bước XOR giữa output ii với chuỗi ciphertext i1i-1 sẽ thu được chuỗi plaintext ii. 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí