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 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']
Ở dạng hex thì ciphertext dài bytes, chứng tỏ bao gồm 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
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]
Dựa vào phương thức giải mã đã được nhắc tới ở phần , 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 block vì block đầu tiên là vector khởi tạo, nên flag cũng chỉ bao gồm 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)
VII. Mode CTR (Counter) trong Block cipher và AES
1. Cơ chế hoạt động
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 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)
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")
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.
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 đó key
và iv
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:
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ó:
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 blocki
trong fileflag.txt.enc
P_flag[i]
là plaintext blocki
trong fileflag.txt
C_mal[i]
là ciphertext blocki
trong filemalware.py.enc
P_mal[i]
là plaintext blocki
trong filemalware.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 bytes, chứng tỏ chiếm block ()
Bởi vậy, theo tính chất 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]
và 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ó file được mã hóa nên .
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 .
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ả:
Tài liệu tham khảo
- https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf
- https://cs.ru.nl/~joan/papers/JDA_VRI_Rijndael_2002.pdf
- https://pycryptodome.readthedocs.io/en/latest/src/cipher/aes.html
- https://pycryptodome.readthedocs.io/en/latest/src/util/util.html
- https://pycryptodome.readthedocs.io/en/latest/src/cipher/classic.html#ctr-mode
- https://docs.python.org/3/library/os.html#os.listdir
All rights reserved