+3

Symmetric ciphers - Mật mã đối xứng AES (phần 7)

VI. Mode CBC (Cipher Block Chaining) trong Block cipher và AES (tiếp)

2. Challenge CTF

Challenge ECB CBC WTF là một ví dụ tốt và đơn giản để luyện tập về quá trình giải mã AES-128 với mode CBC. Mã nguồn đề bài đưa ra như sau:

from Crypto.Cipher import AES

KEY = ?
FLAG = ?

@chal.route('/ecbcbcwtf/decrypt/<ciphertext>/')
def decrypt(ciphertext):
    ciphertext = bytes.fromhex(ciphertext)

    cipher = AES.new(KEY, AES.MODE_ECB)
    try:
        decrypted = cipher.decrypt(ciphertext)
    except ValueError as e:
        return {"error": str(e)}

    return {"plaintext": decrypted.hex()}


@chal.route('/ecbcbcwtf/encrypt_flag/')
def encrypt_flag():
    iv = os.urandom(16)

    cipher = AES.new(KEY, AES.MODE_CBC, iv)
    encrypted = cipher.encrypt(FLAG.encode())
    ciphertext = iv.hex() + encrypted.hex()

    return {"ciphertext": ciphertext}

Trước hết, quan sát hàm mã hóa encrypt_flag() tại route /ecbcbcwtf/encrypt_flag/. Chương trình sử dụng chế độ CBC trong AES thực hiện mã hóa thông điệp, với vector khởi tạo iv được tạo ngẫu nhiên dài 1616 byte. Đặc biệt, chú ý bản mã ciphertext = iv.hex() + encrypted.hex() trả về dưới dạng hex bao gồm cả vector khởi tạo iv.hex() ở đầu. Như vậy chúng ta được cung cấp giá trị iv và ciphertext.

Với hàm giải mã decrypt() tại route /ecbcbcwtf/decrypt/<ciphertext>/, chương trình cố tình sử dụng chế độ ECB trong AES - giúp chúng ta thực hiện bước đầu khi giải mã.

Tiếp theo, tạo một hàm lấy cipher trong challenge bằng Python qua route /ecbcbcwtf/encrypt_flag/

def get_ciphertext(url, path):
    r = requests.get(url = url + path)
    response = r.text.strip()
    data = json.loads(response)
    return data['ciphertext']

image.png

Ở dạng hex thì ciphertext dài 9696 bytes, chứng tỏ bao gồm 33 block, trong đó block đầu là vector khởi tạo iv. Để dễ dàng làm việc với từng block riêng biệt, có thể tạo một hàm split() thực hiện chia chuỗi thành các phần (block) có độ dài bằng nhau:

def split(string, num_parts):
    part_length = len(string) // num_parts
    parts = [string[i:i+part_length] for i in range(0, len(string), part_length)]
    return parts

image.png

Với từng block, sử dụng route giải mã theo mode ECB lấy về các phần plaintext (đã được XOR theo chế độ CBC):

def decrypt(ciphertext, url, path):
    ciphertexts = split(ciphertext, 3)
    plaintexts = []
    for ciphertext in ciphertexts:
        r = requests.get(url = url + path % ciphertext)
        response = r.text.strip()
        data = json.loads(response)
        plaintexts.append(data['plaintext'])
    return plaintexts[0], plaintexts[1], plaintexts[2]

image.png

Dựa vào phương thức giải mã đã được nhắc tới ở phần 11, chúng ta chỉ còn bước cuối cùng là mang các phần plaintext này XOR với các ciphertext ngay trước nó sẽ thu được flag. Lưu ý thực tế ciphertext chỉ gồm 22 block vì block đầu tiên là vector khởi tạo, nên flag cũng chỉ bao gồm 6464 bytes ở dạng hex, nên có thể bỏ qua block plaintext đầu tiên. Sửa lại một chút ở hàm decrypt():

def decrypt(ciphertext, url, path):
    ciphertexts = split(ciphertext, 3)
    plaintexts = []
    for ciphertext in ciphertexts:
        r = requests.get(url = url + path % ciphertext)
        response = r.text.strip()
        data = json.loads(response)
        plaintexts.append(data['plaintext'])
    pt2 = int(plaintexts[2], 16) ^ int(ciphertexts[1], 16)
    pt1 = int(plaintexts[1], 16) ^ int(ciphertexts[0], 16)
    flag = ''
    flag += bytes.fromhex(hex(pt1)[2:]).decode()
    flag += bytes.fromhex(hex(pt2)[2:]).decode()
    print(flag)

image.png

VII. Mode CTR (Counter) trong Block cipher và AES

1. Cơ chế hoạt động

image.png

Chế độ mã hóa CTR (Counter) được xây dựng dựa trên nền tảng chế độ ECB (Electronic Code Book), phát sinh thêm giá trị nonce mang tính ngẫu nhiên và một biến đếm counter, sẽ tăng thêm 11 mỗi lần thực hiện mã hóa một block thông điệp mới. Kết quả thu được sau quá trình block cipher encryption sẽ thực hiện XOR với plaintext và trả về ciphertext. Do các block không liên quan đến nhau nên Counter mode có thể thực hiện song song.

Ví dụ chương trình mã hóa trong Python:

import json
from base64 import b64encode
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes

data = b"viblo security"
key = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_CTR)
ct_bytes = cipher.encrypt(data)
nonce = b64encode(cipher.nonce).decode('utf-8')
ct = b64encode(ct_bytes).decode('utf-8')
result = json.dumps({'nonce':nonce, 'ciphertext':ct})
print(result)

image.png

Ví dụ chương trình giải mã:

import json
from base64 import b64decode
from Crypto.Cipher import AES

json_input = '{"nonce": "0mwhu1+dd+0=", "ciphertext": "feLs7PP3g4Cw2CmDEko="}'

# We assume that the key was securely shared beforehand
key = b'\x9f\x12\xc5\xf8\x88\x91\x16j\x8b\xfbiD\x9447\x04'
try:
    b64 = json.loads(json_input)
    nonce = b64decode(b64['nonce'])
    ct = b64decode(b64['ciphertext'])
    cipher = AES.new(key, AES.MODE_CTR, nonce=nonce)
    pt = cipher.decrypt(ct)
    print("The message was: ", pt)
except (ValueError, KeyError):
    print("Incorrect decryption")

image.png

2. Known-plaintext attack

Known-plaintext attack là một loại tấn công mật mã mà kẻ tấn công đã biết hoặc có thể thu thập được một phần plaintext và key được sử dụng để mã hóa thông điệp. Dạng tấn công này đặc biệt nguy hiểm khi kẻ tấn công có thể thu thập nhiều cặp plaintext-ciphertext từ quá trình truyền dữ liệu hoặc thông qua các phương tiện khác.

Về phương thức tấn công này chúng ta sẽ phân tích kỹ hơn thông qua challenge CTF malware trong giải Umass CTF 2021. Các file cung cấp bạn đọc có thể tải về tại link.

image.png

Challenge mô phỏng một folder bị mã hóa bởi mã độc, các file bị mã hóa có extension .enc. Và cung cấp mã nguồn chương trình thực hiện mã hóa malware.py.

from Crypto.Cipher import AES
from Crypto.Util import Counter
import binascii
import os

key = os.urandom(16)
iv = int(binascii.hexlify(os.urandom(16)), 16)

for file_name in os.listdir():
    data = open(file_name, 'rb').read()

    cipher = AES.new(key, AES.MODE_CTR, counter = Counter.new(128, initial_value=iv))
    
    enc = open(file_name + '.enc', 'wb')
    enc.write(cipher.encrypt(data))

    iv += 1

Chương trình mã độc khá ngắn gọn, sử dụng thuật toán AES mode CTR thực hiện mã hóa từng file trong folder. Trong đó keyiv mang giá trị ngẫu nhiên và đặc biệt, sau mỗi file được mã hóa, giá trị iv tăng lên một.

Lỗ hổng của chương trình mã hóa xảy ra khi sử dụng hàm os.listdir() đồng thời sẽ mã hóa cả file chương trình malware.py thành malware.py.enc, tức là chúng ta có được một số cặp plaintext - ciphertext.

Nhìn lại sơ đồ mã hóa của chế độ CTR:

image.png

Gọi AES(ctr) đại diện cho chuỗi ngay sau khi thực hiện bước "block cipher encryption" với đầu vào iv có giá trị ctr, thì có:

P  AES(ctr)=CP\ \oplus \ AES(ctr) = C

Do hàm os.listdir() sẽ liệt kê file theo thứ tự tùy ý (theo https://docs.python.org/3/library/os.html#os.listdir) nên chúng ta có thể giả sử file flag.txt được mã hóa sau k file so với malware.py. Đồng thời, gọi:

  • C_flag[i] là ciphertext block i trong file flag.txt.enc
  • P_flag[i] là plaintext block i trong file flag.txt
  • C_mal[i] là ciphertext block i trong file malware.py.enc
  • P_mal[i] là plaintext block i trong file malware.py

Khi đó, nếu iv có giá trị là ctr khi thực hiện mã hóa file malware.py, thì khi bắt đầu mã hóa file flag.txt, nó nhận giá trị ctr+k. File flag.txt.enc có độ lớn 3838 bytes, chứng tỏ chiếm 33 block (16×2<38<16×316\times 2 < 38 < 16\times 3)

image.png

Bởi vậy, theo tính chất P  AES(ctr)=CP\ \oplus \ AES(ctr) = C ta có:

P_flag[i] ^ AES(ctr + k + i) = C_flag[i]
P_mal[i] ^ AES(ctr + i) = C_mal[i]

Thay i bằng k + i có:

P_mal[k + i] ^ AES(ctr + k + i) = C_mal[k + i]

Biến đổi một chút:

P_flag[i] = C_flag[i] ^ AES(ctr + k + i)
          = C_flag[i] ^ P_mal[k + i] ^ C_mal[k + i]

C_flag[i], P_mal[k + i]C_mal[k + i] là các giá trị chúng ta đã biết, nên chỉ cần thử các trường hợp của giá trị k là có thể tìm được flag thông qua thực hiện phép XOR C_flag[i] ^ P_mal[k + i] ^ C_mal[k + i]. Do chỉ có 44 file được mã hóa nên k{1;2;3}k\in \{1;2;3\}.

Chúng ta cần một hàm get_block() chuyển dữ liệu từ file thành các block có độ dài 1616.

def get_block(filename):
    with open(filename, 'rb') as f:
        data = f.read()
    res = []
    for i in range(0, len(data), 16):
        res.append(data[i:i+16])
    return res

Và một hàm XOR hai block đơn giản:

def xor_blocks(data1, data2):
    res = []
    for i in range(len(data1)):
        res.append(data1[i] ^ data2[i])
    return res

Cuối cùng chỉ cần thực hiện phép XOR trong mỗi trường hợp của k:

for k in range(1, 4):
    result = ''
    for index in range(3):
        xored_malware_block = xor_blocks(data_pt_malware[index + k], data_ct_malware[index + k])
        flag_plaintext_block = xor_blocks(data_ct_flag[index], xored_malware_block)
        result += ''.join(map(chr, flag_plaintext_block))
    print('Result for k=%d is %s' % (k, result))

Kết quả:

image.png

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í