# Lý thuyết
## AES
- [**AES**](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) là viết tắt của **Advanced Encryption Standard** (tiêu chuẩn mã hóa nâng cao), AES sử dụng phương thức **mã hóa đối xứng** tức là nó sử dụng chung một **key** cho quá trình mã hóa và giải mã. Mục đích là để gửi tin nhắn dài một cách an toàn và hiệu quả.
- AES được đặc trưng bằng việc chia văn bản gốc thành nhiều **khối** có kích thước cố định sau đó gửi những khối đó qua một hệ thống mã hóa gồm những thuật toán mã hóa phức tạp cùng với một **key bí mật** mà ta chọn.
- Cơ chế mã hóa như sau:
- Nếu chọn mã hóa AES-128 thì độ dài văn bản của ta phải là bội của **128 bits = 16 bytes** và key của ta phải dài **128 bits**.
- Đầu tiên AES sẽ chia văn bản (giả sử dài 16 bytes) thành một khối có kích thước **4X4** với mỗi cột là **4 bytes** từ văn bản gốc:

- Sau khi chia thành khối thì khối đó sẽ được biến đổi qua các bước sau:
- **KeyExpansion** hay **Key Schedule**: từ `key 16 bytes` được ta chọn ban đầu, $10$ `key 16 bytes` khác sẽ được tạo ra từ nó, mục đích là để sử dụng trong bước `AddRoundKey`.
- **Initial key addition**: Đây sẽ là bước đầu tiên của quá trình mã hóa, key đầu tiên mà ta chọn sẽ được $\oplus$ với khối văn bản của ta.
- **Round**: quá trình này sẽ lặp tổng cộng $10$ round với $9$ round chính và $1$ round cuối. Mỗi round chính gồm $4$ bước là: **SubBytes**, **ShiftRows**, **MixColumns**, **AddRoundKey**. Round cuối sẽ không có bước MixColumns.

### Add Round Key
- Khối văn bản gốc và khối key sẽ được $\bigoplus$ với nhau:

- Ví dụ:
$$\underset{Plain}{\underbrace{\begin{bmatrix}
04 & e0 & 48 & 28 \\
66 & cb & f8 & 06 \\
81 & 19 & d3 & 26 \\
e5 & 9a & 7a & 4c \\
\end{bmatrix}}}
\bigoplus
\underset{KEY}{\underbrace{\begin{bmatrix}
a0 & 88 & 23 & 2a \\
fa & 54 & a3 & 6c \\
fe & 2c & 39 & 76 \\
17 & b1 & 39 & 05 \\
\end{bmatrix}}}
=
\begin{bmatrix}
a4 & 68 & 6b & 02 \\
9c & 9f & 5b & 6a \\
7f & 35 & ea & 50 \\
f2 & 2b & 43 & 49 \\
\end{bmatrix} $$
- Bước Add Round Key sẽ được thực hiện tổng cộng $11$ lần với $1$ lần ở **Initial key addition** và $10$ lần ở **Round**.
### SubBytes
- Bước SubBytes sẽ thay thế một byte trong khối của chúng ta với byte trong bảng [**Rijndael S-box**](https://en.wikipedia.org/wiki/Rijndael_S-box).

- Giờ ta sẽ thử `SubByte` khối $a$ sau:
$$\begin{bmatrix}
19 & a0 & 9a & e9 \\
3d & f4 & c6 & f8 \\
e3 & e2 & 8d & 48 \\
be & 2b & 2a & 08 \\
\end{bmatrix}$$
- Byte $a_{00}$ là $19$ khi đối chiếu qua bảng **S-box** sẽ là $d4$.

- Ta đối chiếu tương tự với các phần tử còn lại sẽ được một khối mới như sau:
$$\begin{bmatrix}
d4 & e0 & b8 & 1e \\
27 & bf & b4 & 41 \\
11 & 98 & 5d & 52 \\
ae & f1 & e5 & 30 \\
\end{bmatrix}$$
### Shift Rows
- Bước Shift Rows sẽ tác động các **hàng** của khối. Nó sẽ thực hiện dịch byte qua bên **trái** theo quy luật:

- Đối với AES thì hàng đầu tiên sẽ không đổi, ở hàng thứ hai thì mỗi byte sẽ được dịch sang trái **một** ô và tương tự ở hàng ba và tư thì mỗi byte sẽ dịch tương ứng **hai** và **ba** ô.
- Tầm quan trọng của bước này là tránh việc các cột được mã hóa độc lập với nhau.
- Ví dụ ta sẽ thử thực hiện `Shift Rows` ma trận sau:
$$\begin{bmatrix}
d4 & e0 & b8 & 1e \\
27 & bf & b4 & 41 \\
11 & 98 & 5d & 52 \\
ae & f1 & e5 & 30 \\
\end{bmatrix}$$
- Mỗi byte ở hàng hai được dịch sang trái một ô như sau: $$\begin{bmatrix}
27 & bf & b4 & 41 \\
\end{bmatrix}
\rightarrow
\begin{bmatrix}
bf & b4 & 41 & 27 \\
\end{bmatrix}$$
- Tương tự thì hàng ba và bốn sẽ dịch tương ứng hai và ba byte sang trái:
$$\begin{bmatrix}
11 & 98 & 5d & 52 \\
ae & f1 & e5 & 30 \\
\end{bmatrix}
\to
\begin{bmatrix}
5d & 52 & 11 & 98 \\
30 & ae & f1 & e5 \\
\end{bmatrix} $$
- Sau bước `Shift Rows` thì ma trận của ta trở thành:
$$
\begin{bmatrix}
d4 & e0 & b8 & 1e \\
bf & b4 & 41 & 27 \\
5d & 52 & 11 & 98 \\
30 & ae & f1 & e5 \\
\end{bmatrix}$$
### The MixColumns
- Bước này, ta sẽ nhân từng **cột** của khối với khối $c(\times)$ cụ thể để ra một khối mới.

- Khối [**c(X)**](https://en.wikipedia.org/wiki/Rijndael_MixColumns) sẽ như sau:
$$\begin{bmatrix}
02 & 03 & 01 & 01 \\
01 & 02 & 03 & 01 \\
01 & 01 & 02 & 03 \\
03 & 01 & 01 & 02 \\
\end{bmatrix}$$
- Ta sẽ thử `MixColumn` khối sau:
$$\begin{bmatrix}
d4 & e0 & b8 & 1e \\
bf & b4 & 41 & 27 \\
5d & 52 & 11 & 98 \\
30 & ae & f1 & e5 \\
\end{bmatrix}$$
- Giờ ta sẽ **nhân** lần lượt các **cột** với khối **c(X)**:
$$\begin{bmatrix}
02 & 03 & 01 & 01 \\
01 & 02 & 03 & 01 \\
01 & 01 & 02 & 03 \\
03 & 01 & 01 & 02 \\
\end{bmatrix}
\times
\underset{Column\hspace{1mm}1}{\underbrace{\begin{bmatrix}
d4 \\
bf \\
5d \\
30 \\
\end{bmatrix}}}
=
\begin{bmatrix}
04 \\
66 \\
81 \\
e5 \\
\end{bmatrix}$$
$$\begin{bmatrix}
02 & 03 & 01 & 01 \\
01 & 02 & 03 & 01 \\
01 & 01 & 02 & 03 \\
03 & 01 & 01 & 02 \\
\end{bmatrix}
\times
\underset{Column\hspace{1mm}2}{\underbrace{\begin{bmatrix}
e0 \\
b4 \\
52 \\
ae \\
\end{bmatrix}}}
=
\begin{bmatrix}
e0 \\
cb \\
19 \\
9a \\
\end{bmatrix}$$
$$\begin{bmatrix}
02 & 03 & 01 & 01 \\
01 & 02 & 03 & 01 \\
01 & 01 & 02 & 03 \\
03 & 01 & 01 & 02 \\
\end{bmatrix}
\times
\underset{Column\hspace{1mm}3}{\underbrace{\begin{bmatrix}
b8 \\
41 \\
11 \\
f1 \\
\end{bmatrix}}}
=
\begin{bmatrix}
48 \\
f8 \\
d3 \\
7a \\
\end{bmatrix}$$
$$\begin{bmatrix}
02 & 03 & 01 & 01 \\
01 & 02 & 03 & 01 \\
01 & 01 & 02 & 03 \\
03 & 01 & 01 & 02 \\
\end{bmatrix}
\times
\underset{Column\hspace{1mm}4}{\underbrace{\begin{bmatrix}
1e \\
27 \\
98 \\
e5 \\
\end{bmatrix}}}
=
\begin{bmatrix}
28 \\
06 \\
26 \\
4c \\
\end{bmatrix}$$
- Vậy sau khi `MixColumn` thì ta được khối mới như sau:
$$\begin{bmatrix}
04 & e0 & 48 & 28 \\
66 & cb & f8 & 06 \\
81 & 19 & d3 & 26 \\
e5 & 9a & 7a & 4c \\
\end{bmatrix}$$
### KeyExpansion
- Vì bước **AddRoundKey** được thực hiện đến $11$ lần nên từ **key gốc** ta phải [**tạo ra**](https://en.wikipedia.org/wiki/AES_key_schedule) thêm $10$ key **16 bytes** khác nữa.
- Để tạo key mới thì ta cần đến một bảng tên là **Rcon**.
|Round|$$1$$|$$2$$|$$3$$|$$4$$|$$5$$|$$6$$|$$7$$|$$8$$|$$9$$|$$10$$|
|-|-|-|-|-|-|-|-|-|-|-|
|$Rcon$|$$ \begin{bmatrix}1 \\0 \\0 \\0 \\\end{bmatrix}$$|$$ \begin{bmatrix}2 \\0 \\0 \\0 \\\end{bmatrix}$$|$$ \begin{bmatrix}4 \\0 \\0 \\0 \\\end{bmatrix}$$|$$ \begin{bmatrix}8 \\0 \\0 \\0 \\\end{bmatrix}$$|$$ \begin{bmatrix}10 \\0 \\0 \\0 \\\end{bmatrix}$$|$$ \begin{bmatrix}20 \\0 \\0 \\0 \\\end{bmatrix}$$|$$ \begin{bmatrix}40 \\0 \\0 \\0 \\\end{bmatrix}$$|$$ \begin{bmatrix}80 \\0 \\0 \\0 \\\end{bmatrix}$$|$$ \begin{bmatrix}1b \\0 \\0 \\0 \\\end{bmatrix}$$|$$ \begin{bmatrix}36 \\0 \\0 \\0 \\\end{bmatrix}$$|
- Quá trình tạo **key** sẽ như sau:

- Ta sẽ lấy một **key** có giá trị là `2b7e151628aed2a6abf7158809cf4f3c`.
- Biến đổi **key** thành khối như sau:
$$\overset{W_{0}\hspace{6mm}W_{1}\hspace{6mm}W_{2}\hspace{6mm}W_{3}}{\overline{\begin{bmatrix}
2b & 28 & ab & 09 \\
7e & ae & f7 & cf \\
15 & d2 & 15 & 4f \\
16 & a6 & 88 & 3c \\
\end{bmatrix}}}$$
- Gọi mỗi **cột** của từng khối key là $W_i$ , ta sẽ tính hết toàn bộ $W_i$ của $10$ khối key mới. Ở đây ta sẽ tính $W_{i}$ của khối key thứ nhất là $W_{4}$ , $W_{5}$ , $W_{6}$ , $W_{7}$ :
- **Lưu ý:** Cột $W_i$ thứ nhất của mỗi khối key sẽ được tính khác với các cột $W_i$ khác trong khối đó.
- Đầu tiên để tính cột đầu tiên là $W_{4}$ ta sẽ RotWord $W_{i-1}$ là $W_{3}$ theo quy luật: dịch **một byte** của $W_{i-1}$ lên phía trên:
$$
\uparrow
\begin{bmatrix}
09 \\
cf \\
4f \\
3c \\
\end{bmatrix}
\to
\begin{bmatrix}
cf\\
4f\\
3c\\
09\\
\end{bmatrix}$$
- Sau khi RotWord thì ta sẽ thực hiện **SubWord** bằng bảng [**S-box**](https://en.wikipedia.org/wiki/Rijndael_S-box):
$$\begin{bmatrix}
cf \\
4f \\
3c \\
09 \\
\end{bmatrix}
\overset{S-box}{\rightarrow}
\begin{bmatrix}
8a\\
84\\
eb\\
01\\
\end{bmatrix}$$
- Bước cuối cùng sẽ sử dụng đến bảng **Rcon** ở trên. Thực hiện $\oplus$ kết quả ở trên với $W_{i-4}$ và một **cột Rcon**, ta đang tính toán khối key thứ nhất nên ta sẽ dùng cột Rcon round $1$, ở các khối sau thì ta chỉ cần tăng round của Rcon lên:
$$\begin{bmatrix}
8a\\
84\\
eb\\
01\\
\end{bmatrix}
\bigoplus
\underset{W_{0}}{\underbrace{\begin{bmatrix}
2b\\
7e\\
15\\
16\\
\end{bmatrix}}}
\bigoplus
\underset{Rcon-1}{\underbrace{\begin{bmatrix}
01\\
00\\
00\\
00\\
\end{bmatrix}}}
=
\underset{W_4}{\underbrace{\begin{bmatrix}
a0 \\
fa \\
fe \\
17 \\
\end{bmatrix}}}$$
- Như đã đề cập ở trên thì chỉ có cột $W_i$ đầu tiên là tính khác với các cột còn lại nên để tính các cột $W_i$ còn lại của khối thì ta đơn giản chỉ cần lấy $W_{i-1}$ $\oplus$ $W_{i-4}$. Giờ ta sẽ đi tính $W_5$ và làm tương tự với $W_6$ và $W_7$ :
$$W_5 = W_{4} \oplus W_{1}$$
$$
\underset{W_4}{\underbrace{\begin{bmatrix}
a0\\
fa\\
fe\\
17\\
\end{bmatrix}}}
\bigoplus
\underset{W_1}{\underbrace{\begin{bmatrix}
28\\
ae\\
d2\\
a6\\
\end{bmatrix}}}
=
\underset{W_5}{\underbrace{\begin{bmatrix}
88 \\
54 \\
2c \\
b1 \\
\end{bmatrix}}}$$
- Làm tương tự như $W_5$ thì ta có $W_6$ và $W_7$ như sau:
$$\underset{W_6}{\underbrace{\begin{bmatrix}
23 \\
a3 \\
39 \\
39 \\
\end{bmatrix}}}
\underset{W_7}{\underbrace{\begin{bmatrix}
2a \\
6c \\
76 \\
05 \\
\end{bmatrix}}}$$
- Qua các bước trên thì ta đã tính được $KEY_1$ là:
$$\begin{bmatrix}
a0 & 88 & 23 & 2a \\
fa & 54 & a3 & 6c \\
fe & 2c & 39 & 76 \\
17 & b1 & 39 & 05 \\
\end{bmatrix}$$
- Giờ ta chỉ cần dùng những công thức như trên để tính $9$ key còn lại.
## Block cipher modes of operation
### Electronic Code Book (ECB)
- Kiểu mã hóa đơn giản nhất gọi tên electronic codebook (ECB). Văn bản gốc được chia thành các khối có kích thước cố định giống nhau và được đưa qua thuật toán mã hóa AES cùng với key của ta:

- Để giải mã thì ta cũng chia văn bản mã hóa thành các khối và giải mã với key thôi:

- ECB không được khuyến khích sử dụng trong các giao thức mã hóa vì nó đơn giản. Vì đơn giản nên sinh ra rất nhiều nhược điểm. Đó là thiếu đi tính [**diffusion**](https://en.wikipedia.org/wiki/Confusion_and_diffusion), có thể hiểu là nó thiếu đi tính phức tạp của Ciphertext.
- Nó không thể giấu đi những dữ liệu giống nhau vì khi văn bản gốc được mã hóa thì hai văn bản mã hóa sẽ giống hệt nhau. Hacker có thể nhận ra và tấn công ngay.
- Bức hình phía dưới là kết quả của mã hóa ECB, nhìn qua thì ta cũng có thể mường tượng được tấm hình gốc như thế nào.

- Để code kiểu mã hóa ECB trong python thì ta có câu lệnh [**sau**](https://pycryptodome.readthedocs.io/en/latest/src/cipher/classic.html#ecb-mode):

- Đầu tiên cần khai báo các hàm cần thiết sau: **AES**, **get_random_bytes** và **pad**, **unpad**. Hàm `AES` sẽ hỗ trợ ta mã hóa các kiểu mã hóa AES, `get_random_bytes` giúp ta tạo ra một chuỗi byte ngẫu nhiên mà ta sẽ sử dụng để làm key. Hàm `Pad` sẽ giúp ta thêm byte để kích thước của văn bản phù hợp để mã hóa AES, còn `unpad` sẽ làm ngược lại, loại bỏ đi những byte thừa mà hàm Pad thêm vào.
```python
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad,unpad
```
- Ta sẽ tạo key ngẫu nhiên bằng câu lệnh: `key = get_random_bytes(16)`.
- Để mã hóa ECB thì ta có câu lệnh sau:
```python
data = "something went wrong"
cipher = AES.new(key, AES.MODE_ECB)
en_data = cipher.encrypt(pad(data.encode(), 16))
```
- Sau khi có en_data thì ta sẽ giải mã bằng cách thay `.encrypt` bằng `.decrypt`.
```python
en_data = b'\xa0{\x08\xc6\xbc\xd2i\xb9\xae\x10PA\xf2\xc0jj\xa7Cu_A\xdb\xfe\xff\x9e\x11\xd9\xbc\xcb\x02\x10\x9f'
de_data = unpad(cipher.decrypt(en_data), 16)
```
- Đoạn code hoàn chỉnh như sau:
```python=
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad,unpad
key = get_random_bytes(16)
print("key:", key)
data = "something went wrong"
cipher = AES.new(key, AES.MODE_ECB)
en_data = cipher.encrypt(pad(data.encode(), 16))
de_data = unpad(cipher.decrypt(en_data), 16)
print("encrypted_data:",en_data)
print("decrypted_data:", de_data.decode())
```
### Cipher block chaining (CBC)
- Kiểu mã hóa tiếp theo là Cipher block chaining (CBC). Ở kiểu mã hóa này thì văn bản gốc của ta vẫn sẽ được chia thành những khối kích thước giống nhau nhưng trước khi được đưa qua thuật toán mã hóa AES thì những khối văn bản đó sẽ được $\bigoplus$ với **khối văn bản mã hóa** phía trước nó.
- Để làm cho mỗi khối văn bản được độc nhất thì khối đầu tiên sẽ được $\bigoplus$ với một [**initialization vector**](https://en.wikipedia.org/wiki/Initialization_vector):

- Để giải mã CBC thì ta chỉ cần làm ngược lại như sau:

- Khi ta giải mã CBC mà ta dùng **IV sai** thì chỉ có khối Plaintext đầu tiên bị sai chứ những khối sau vẫn đúng. Lý do là vì những khối sau được $\oplus$ với khối Ciphertext phía trước chứ không phải khối Plaintext phía trước :+1:
- Để code kiểu CBC trong python thì ta có câu lệnh sau:

- Kiểu CBC sẽ cần IV nhưng nếu ta không thêm thì thư viện sẽ tự tạo IV ngẫu nhiên cho ta.
- Code mẫu như sau:
```python=
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
key = get_random_bytes(16)
print("key:", key)
iv = get_random_bytes(16)
print("iv:", iv)
plaintext = b'something went wrong'
def encrypt_CBC(key, iv, plaintext):
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = pad(plaintext,16)
ciphertext = cipher.encrypt(plaintext)
return ciphertext
def decrypt_CBC(key, iv, ciphertext):
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = unpad(cipher.decrypt(ciphertext), 16)
return plaintext
# Mã hóa
ciphertext = encrypt_CBC(key, iv, plaintext)
print('Ciphertext:', ciphertext)
# Giải mã
decrypted_text = decrypt_CBC(key, iv, ciphertext)
print('Plaintext:', decrypted_text)
```
### Cipher Feedback (CFB)
- Cipher Feedback (CFB) sẽ có khác biệt so với hai kiểu mã hóa trên. Ở hai kiểu mã hóa trên thì input cho thuật toán mã hóa AES là Plaintext hoặc Plaintext $\oplus$ IV. Còn ở CFB thì input cho thuật toán mã hóa AES sẽ là IV. Bởi vì CFB hoạt động giống như mã hóa dòng (stream cipher).
- IV sau khi được mã hóa AES với key thì cho ra output gọi là [**Keystream**](https://en.wikipedia.org/wiki/Keystream). Keystream sẽ được $\oplus$ với khối Plaintext để cho ra Ciphertext. Ciphertext sẽ được dùng làm input cho thuật toán mã hóa AES tiếp theo:

- Để giải mã thì ta chỉ cần làm ngược lại:

- Nếu bạn thắc mắc Keystream ở đâu thì nó sẽ nằm ở đây:

- CFB, OFB và CTR có chung ưu điểm so với chế độ CBC là khối Plaintext không cần phải được đệm (Pad) để phù hợp với kích thước khối của thuật toán mã hóa AES. Bởi vì Plaintext của ta chỉ được xor với keystream mà thôi, chứ không được đưa qua thuật toán AES.
- CFB phụ thuộc vào cả Plaintext và Ciphertext trước đó. Do đó, bất kỳ lỗi nào trong quá trình mã hóa sẽ lan truyền qua các khối mã hóa sau đó. Điều này khiến CFB khó đồng bộ hóa lại nếu có lỗi hoặc mất dữ liệu, vì mỗi Ciphertext phụ thuộc vào Ciphertext trước đó. Điều đó cũng giúp CFB tạo ra độ phức tạp hơn đối với mỗi khối dữ liệu sau đó.
- Câu lệnh để code kiểu CFB như sau:

- Ta có thể thấy rằng có một tham số là **segment_size**, nó sẽ là kích thước mà Plaintext được chia nhỏ ra (ví dụ 1 bit, 8 bit...).
### Output feedback (OFB)
- The output feedback (OFB) giống như CFB, nó biến mã hóa khối thành mã hóa dòng (stream cipher).
- Ở OFB thì input cho thuật toán mã hóa AES sẽ là IV. Output của quá trình trên tương tự như CFB cũng là **Keystream**. Keystream sau đó được $\oplus$ với Plaintext để có được Ciphertext.
- Khác với CFB thì đầu vào của thuật toán mã hóa AES tiếp theo không phải là Ciphertext phía trước mà sẽ là Keystream phía trước.

- Vì thuật toán $\bigoplus$ có tính đối xứng nên quá trình mã hóa và giải mã sẽ khá giống nhau:

- OFB Chỉ phụ thuộc vào Keystream trước đó (không phụ thuộc vào Plaintext hay Ciphertext trước đó). Do đó, một lỗi trong quá trình truyền sẽ chỉ ảnh hưởng đến khối mã hóa hiện tại và không lan truyền cho các khối sau.
- Câu lệnh để code kiểu OFB:

### Counter (CTR)
- Giống như OFB hay CFB thì Counter (CTR) biến mã hóa khối thành mã hóa dòng (stream cipher). Nó tạo ra Keystream bằng cách mã hóa liên tục các giá trị của **"counter"**.
- Khác với OFB và CFB thì CTR **không cần** sử dụng lại dữ liệu của các khối trước đó để mã hóa mà sẽ tự tạo cho mình Keystream từ **counter**. Điều đó giúp CTR có thể mã hóa các khối mã một cách song song, vì giá trị bộ đếm (counter) có thể được tính toán trước mà không cần phải đợi kết quả từ khối trước đó.
- Một nonce (Number used once) và một giá trị bộ đếm (counter) ban đầu được chọn. Nonce thường là ngẫu nhiên (ví dụ 8 bytes) và bộ đếm có thể bắt đầu từ 0.
- Input của thuật toán mã hóa AES sẽ là: **nonce || counter**. Tức là một chuỗi được nối từ nonce và counter. Giả sử ta sử dụng AES-128 thì **nonce |$\hspace{0mm}$| counter** sẽ dài 128 bit, tức là sự kết hợp của nonce 64 bit và counter 64 bit, nối lại thành một chuỗi 128 bit. Output của ta sẽ là Keystream dài 128 bit.

- Quá trình giải mã ta chỉ cần lấy keystream xor với ciphertext:

- Trong chế độ CTR, đầu vào của thuật toán mã hóa AES là sự kết hợp của nonce và counter. Nonce đảm bảo tính duy nhất cho mỗi phiên mã hóa, trong khi counter đảm bảo tính duy nhất cho mỗi khối dữ liệu trong một phiên mã hóa. Kết quả mã hóa của AES trên giá trị nonce và counter tạo ra khối keystream, sau đó được $\oplus$ với văn bản gốc để tạo ra văn bản mã hóa.
- Code mẫu:
```python
from Crypto.Cipher import AES
from Crypto.Util import Counter
from Crypto.Random import get_random_bytes
key = get_random_bytes(16)
nonce = get_random_bytes(8) # vì counter dài 64 bit nên nonce cũng chỉ dài 64 bit
message = b"https://youtu.be/IzSYlr3VI1A"
counter = Counter.new(nbits = 64, prefix = nonce, initial_value = 0)
cipher = AES.new(mode = AES.MODE_CTR, key = key, counter=counter)
ciphertext = cipher.encrypt(message)
print(f"{ciphertext = }")
cipher = AES.new(mode = AES.MODE_CTR, key = key, counter=counter)
plaintext = cipher.decrypt(ciphertext)
print(f"{plaintext = }")
#ciphertext = b'\xca2\xf4\xd4\x95\xb9\x04vm\x84\xb2G\x15h@m\xe2\xb23"\xf2\x1e\x8b\xe6\x0b+nr'
#plaintext = b'https://youtu.be/IzSYlr3VI1A'
```
## DES 2DES 3DES
### DES
- [**Data Encryption Standard**](https://en.wikipedia.org/wiki/Data_Encryption_Standard) (DES) là một thuật toán mã hóa đối xứng, nó có ảnh hưởng lớn đến sự phát triển của ngành mật mã học, ảnh hưởng quan trọng đến sự phát triển của AES và các thuật toán mã hóa hiện đại khác.
- DES sử dụng một khóa 56 bit để mã hóa và giải mã. Thực tế, khóa đầu vào là 64 bit, nhưng 8 bit được sử dụng cho kiểm tra lỗi, nên chỉ có 56 bit thực sự được sử dụng cho quá trình mã hóa.
- Kích thước khóa 56 bit của DES hiện tại được xem là quá ngắn, dễ bị tấn công bằng phương pháp brute-force. Do đó, DES không còn được coi là đủ an toàn cho các ứng dụng hiện đại.
- Giống như AES thì DES sẽ chia văn bản gốc của ta thành nhiều khối rồi đưa khối đó qua những thuật toán mã hóa để mã hóa chúng. Nhưng AES thì mạnh và phức tạp hơn DES nên được sử dụng cho mã hóa hiện đại trong khi DES đã trở nên lỗi thời do các hạn chế về bảo mật.
- Với Plaintext $x$ và khóa $K$, quá trình mã hóa diễn ra như sau
- Ban đầu, dùng một phép hoán vị $IP$, từ $x$ với 64 bit sẽ biến thành $IP(x)$, sau đó $IP(x)$ được chia làm hai nửa là $L_0$ và $R_0$ dài 32 bit.
- Từ cặp $(L_0$ và $R_0)$ sẽ dùng $16$ lần những phép toán giống nhau cùng với $K$ để có được $(L_1$ và $R_1)$,...$(L_{16}$ và $R_{16})$, sau đó dùng phép hoán vị nghịch đảo là $FP$ cho $R_{16}L_{16}$ để có được Ciphertext $y$ tương ứng.

- Từ sơ đồ trên ta sẽ đào sâu về **IP**, **FP**, **hàm F**, **thuật toán sinh khóa** và cách code DES trong python.
#### IP và FP
- $IP$ & $FP$ (Initial and final permutation) là hai phép hoán vị, chúng có tính chất đối nhau (Trong quá trình mã hóa thì IP trước FP, khi giải mã thì ngược lại). Cụ thể thì $IP$ sẽ hoán vị như [**sau**](https://en.wikipedia.org/wiki/DES_supplementary_material):

- Tức là từ 64 bit của Plaintext $x$, $IP$ sẽ ánh xạ chúng thành $IP(x)$ như trên.
- Có thể biểu diễn bằng bảng dưới đây để dễ hình dung hơn.

- $FP$ hay $IP^{-1}$ thì cũng tương tự nhưng sẽ là nghịch đảo của $IP$:

- Bảng $FP$ như sau:

#### Hàm F (Mảng Feistel)
- Hàm $F$ lấy đầu vào là: $R$ có 32 bit và $K$ có 48 bit, và có kết quả ở đầu ra là $F(R, K)$ dài 32 bit theo sơ đồ sau:

- Hàm $F$, như được miêu tả ở trên, hoạt động trên khối 32 bit và bao gồm bốn giai đoạn:
- **Mở rộng**: 32 bit đầu vào được mở rộng thành 48 bit sử dụng thuật toán hoán vị mở rộng (expansion permutation) với việc nhân đôi một số bit. Giai đoạn này được ký hiệu là E trong sơ đồ.
- **Trộn khóa**: 48 bit thu được sau quá trình mở rộng được $\oplus$ với khóa con. Mười sáu khóa con 48 bit được tạo ra từ khóa chính 56 bit theo một chu trình tạo khóa con (key schedule) miêu tả ở phần sau.
- **Thay thế**: 48 bit sau khi trộn được chia làm 8 khối con 6 bit và được xử lý qua hộp thay thế S-box. Đầu ra của mỗi khối 6 bit là một khối 4 bit theo một chuyển đổi phi tuyến tính.
- **Hoán vị**: Cuối cùng, 32 bit thu được sau S-box sẽ được sắp xếp lại theo một thứ tự cho trước (còn gọi là P-box).
- Ở hàm $F$ trên thì ta sẽ gặp thêm các khái niệm như $E$ (expansion permutation), **S-box** và **P-box**. Mình sẽ giới thiệu sau đây.
- $E$ (expansion permutation) là một phép hoán vị, nó biến khối $R$ dài 32 bit của ta thành 48 bit thông qua các phép hoán vị và nhân bản:

- Biểu diễn bằng bảng sau:

- [**S-box**](https://en.wikipedia.org/wiki/DES_supplementary_material#Substitution_boxes_(S-boxes)) (Substitution boxes) gồm $8$ bảng. Như đã nói ở trên thì kết quả của $E(R) \oplus K$ sẽ được chia làm 8 phần, mỗi phần sẽ dài 6 bit. Mỗi bảng của S-box có tác dụng biến đổi 6 bit này thành 4 bit thay thế theo quy luật dưới đây:
- Cho đầu vào 6 bit (gọi là xyyyyx), đầu ra 4 bit được xác định như sau: dùng 2 bit ngoài cùng là xx để xác định hàng và 4 bit bên trong là yyyy để xác định cột. Có hàng và cột thì ta đối chiếu sang bảng S-box tương ứng.
- ++Ví dụ++: cho đầu vào là: **0**1101**1**, hai bit ngoài cùng ta ghép lại được `01` đổi sang decimal là $1$, nó sẽ là hàng thứ 2 (có tổng cộng 4 hàng tương ứng với 4 giá trị của xx là 0, 1, 2, 3). 4 bit ở trong là `1101`, đổi sang decimal là $13$, sẽ là cột thứ 14 (tổng cộng 16 cột tương ứng 16 giá trị của yyyy). Sau đó đối chiếu qua bảng S-box (ví dụ bảng S-box thứ 5) thì ta sẽ được kết quả là `1001`, tương ứng với $9$. Vậy là ta đã chuyển đổi từ `011011` sang `1001` rồi.

- Dưới đây là danh sách 8 bảng S-box:


- *Fun fact*: Các S-box là phần quan trọng nhất trong việc đảm bảo tính bí mật của hệ mã DES.
- **P-box** hay $P$ (Permutation) đơn giản là phép hoán vị các bit theo quy luật dưới đây:

- Biểu diễn bằng bảng sau:

#### Thuật toán sinh khóa
- Từ 64 bit ban đầu của khóa, 56 bit được chọn (Permuted Choice 1, hay $PC1$); 8 bit còn lại bị loại bỏ. 56 bit thu được được chia làm hai phần bằng nhau, mỗi phần được xử lý độc lập.
- Sau mỗi chu trình, mỗi phần được dịch đi 1 hoặc 2 bit (tùy thuộc từng chu trình, nêu đó là chu trình 1,2,9,16 thì đó là dịch 1 bit, còn lại thì sẽ được dich 2 bit).
- Các khóa con 48 bit được tạo thành bởi $PC2$ (Permuted Choice 2) gồm 24 bit từ mỗi phần. Quá trình dịch bit (được ký hiệu là "<<<" trong sơ đồ) khiến cho các khóa con sử dụng các bit khác nhau của khóa chính; mỗi bit được sử dụng trung bình ở 14 trong tổng số 16 khóa con.

- $PC1$ (Permuted Choice 1) sẽ hoán vị các bit như sau:

- Biểu diễn bằng bảng sau:

- Lưu ý rằng chỉ 56 bit trong số 64 bit của đầu vào được chọn; tám còn lại (8, 16, 24, 32, 40, 48, 56, 64) được chỉ định để sử dụng làm bit chẵn lẻ.
- $PC2$ (Permuted choice 2) sẽ thực hiện hoán vị như sau:

- Biểu diễn bằng bảng sau:

- $PC-2$ sẽ bỏ qua các bit là 9, 18, 22, 25, 35, 38, 43, 54.
#### Code DES
- Để code DES thì ta vẫn làm tương tự như AES, thay vì khai báo hàm AES từ thư viện pycryptodome thì ta sẽ khai báo DES.
- Đoạn code mẫu như sau:
```python=
from Crypto.Cipher import DES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
# Khóa DES phải dài 8 byte
key = get_random_bytes(8)
print("key:", key)
data = b'Hello, World!'
cipher = DES.new(key, DES.MODE_ECB)
ciphertext = cipher.encrypt(pad(data, DES.block_size))
print(f'Ciphertext: {ciphertext}')
decrypted_data = unpad(cipher.decrypt(ciphertext), DES.block_size).decode()
print(f'Decrypted Data: {decrypted_data}')
```
### 2DES
- Double DES (Data Encryption Standard) là một phương pháp mã hóa được phát triển để tăng cường bảo mật so với DES thông thường.
- DES là một thuật toán mã hóa khối sử dụng một khóa 56-bit để mã hóa và giải mã dữ liệu. Tuy nhiên, với sự phát triển của công nghệ và khả năng tính toán, việc phá vỡ mã DES bằng phương pháp brute-force đã trở nên khả thi.
- Double DES ra đời nhằm mục đích khắc phục hạn chế này bằng cách sử dụng hai lần mã hóa DES, nhưng không thực sự cải thiện đáng kể về độ bảo mật.
#### Cách hoạt động của Double DES
- Double DES sử dụng hai khóa khác nhau, mỗi khóa dài 56-bit, gọi là $K_1$ và $K_2$. Quá trình mã hóa và giải mã diễn ra như sau:

1. Mã hóa:
- Đầu tiên Plaintext được mã hóa DES bằng khóa $K_1$ để tạo ra Ciphertext trung gian $X$.
- $X$ sau đó được mã hóa lần nữa bằng khóa thứ hai $K_2$ để tạo ra bản mã cuối cùng.
$$C = E_{K_2}(E_{K_1}(P))$$
2. Giải mã:
- Ciphertext C được giải mã bằng khóa thứ hai $K_2$ để tạo ra Ciphertext trung gian $X$.
- Ciphertext trung gian sau đó được giải mã một lần nữa bằng khóa $K_1$ để tạo ra Plaintext ban đầu.
$$P = D_{K_1}(D_{K_2}(C))$$
- 2DES nhìn có vẻ là an toàn vì nó dùng đến hai khóa $K$ khiến thời gian brute-force trở nên lâu hơn. Nhưng nó lại tạo cơ hội cho kiểu tấn công [**meet-in-the-middle**](https://en.wikipedia.org/wiki/Meet-in-the-middle_attack) sẽ được đề cập ở phần Attack.
### 3DES
- Triple DES được xây dựng dựa trên thuật toán DES (Data Encryption Standard) tương tự như Double DES. 3DES sử dụng 3 lần mã hóa DES để tăng độ bảo mật. Nhìn thì nó có vẻ giống 2DES thêm một lần mã hóa DES nhưng không chỉ dừng ở việc thêm một lần mã hóa DES, mà còn khác ở cách thức các bước mã hóa và giải mã được thực hiện.
- 3DES mã hóa dữ liệu ba lần liên tiếp, sử dụng ba khóa khác nhau hoặc hai khóa lặp lại theo một quy trình cụ thể. Có hai chế độ phổ biến của 3DES: **EDE** (Encrypt-Decrypt-Encrypt) và **EEE** (Encrypt-Encrypt-Encrypt). EDE thường được sử dụng hơn vì lý do tương thích ngược với DES.

- Quy trình mã hóa **3DES-EDE** như sau:
1. $M$ được mã hóa lần thứ nhất với khóa đầu tiên $K_1$.
2. Giải mã kết quả từ bước 1 bằng khóa thứ hai $K_2$.
3. Mã hóa lần thứ hai kết quả từ bước 2 bằng khóa thứ ba $K_3$ ta được Ciphertext $C$.
$$C = E_{K_3}(D_{K_2}(E_{K_1}(M)))$$
- Để giải mã thì ta chỉ cần làm ngược lại
$$M = D_{K_1}(E_{K_2}(D_{K_3}(C)))$$
#### Các chế độ khóa trong 3DES
- 3DES với ba khóa độc lập (3DES-EEE3 hoặc 3DES-EDE3): Sử dụng ba khóa khác nhau $K_1$, $K_2$ và $K_3$. Đây là phương pháp an toàn nhất với $3\times56=168$ key bit độc lập. Nó vẫn có khả năng bị tấn công **meet-in-the-middle attack** nhưng mà cần phải brute-force tới $2^{2\times56}$ lần nên có thể yên tâm được.
- 3DES với hai khóa (3DES-EEE2 hoặc 3DES-EDE2): Sử dụng hai khóa, với khóa đầu tiên $K_1$ và khóa thứ ba $K_3$ là giống nhau. Đây là một cải tiến so với 2DES cái mà cần $2^{56}$ bước để tấn công. Đáng buồn là nó đã bị NIST bỏ vào năm 2015.
- 3DES với một khóa (1DES): Thực tế chỉ là DES với khóa $K_3$ sử dụng cho cả ba bước. Đây không phải là 3DES và nó không hề an toàn tí nào.
# Attack
## Meet in the middle
- [**Meet in the middle**](https://en.wikipedia.org/wiki/Meet-in-the-middle_attack) là một kiểu tấn công hiệu quả đối với các hệ thống mã hóa sử dụng nhiều lớp khóa, chẳng hạn như trong thuật toán DES (Data Encryption Standard). Kiểu tấn công này thường được áp dụng đối với các hệ thống mã hóa sử dụng 2DES (Double DES), 3DES (Triple DES) hoặc kể cả AES nếu nó sử dụng **chẵn** lần mã hóa.
- Meet in the middle có thể tấn công cả 2DES và 3DES nhưng 3DES sử dụng ba khóa và mã hóa ba lần, làm cho việc áp dụng tấn công MITM phức tạp hơn và ít hiệu quả hơn so với 2DES, do đó 3DES được coi là an toàn hơn trước các tấn công này. Sau đây chúng ta sẽ nghiên cứu về cách tấn công Meet in the middle đối với 2DES.
- Công thức mã hóa $C$ và giải mã $P$ của 2DES như sau:
$$C = E_{K_2}(E_{K_1}(P))$$
$$P = D_{K_1}(D_{K_2}(C))$$
- Để tấn công thì đầu tiên giải mã Ciphertext $C$ với $K_2$ như sau:
$$\underline{D_{K_2}(C)} = D_{K_2}(E_{K_2}(E_{K_1}(P))) = \underline{E_{K_1}(P)}$$
- Như bạn thấy ở vế phải thì $E_{K_1}(P)$ chính là mã hóa plaintext $P$ với khóa $K_1$, vì vậy ta có được phương trình:
$$D_{K_2}(C) = E_{K_1}(P)$$
- Từ phương trình trên ta sẽ brute-force $K_1$ và thêm toàn bộ giá trị của $E_{K_1}(P)$ vào một list, sau đó ta sẽ đi brute force $K_2$, nếu có một giá trị nào của $D_{K_2}(C)$ nằm trong list thì ta đã tìm được $K_2$ rồi.
- Để brute-force $K_1$ sẽ cần $2^{56}$ bước, để brute-force $K_2$ cũng cần $2^{56}$ bước. Vậy ta cần phải brute-force tổng cộng $2^{56}+2^{56} = 2 \times 2^{56} = 2^{57}$.
- Ban đầu để phá được 2DES thì cần $2^{112}$ bước nhưng sau khi dùng Meet in the middle thì số bước đã giảm xuống còn $2^{57}$ bước, máy tính ta có thể hoàn toàn tính toán được.
- **Bài tập:**
```python
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from hashlib import md5
from os import urandom
FLAG = b"KCSC{???????????????????????????}"
assert len(FLAG) % 16 == 1 # hint
key1 = md5(urandom(3)).digest()
key2 = md5(urandom(3)).digest()
cipher1 = AES.new(key1, AES.MODE_ECB)
cipher2 = AES.new(key2, AES.MODE_ECB)
enc = cipher1.encrypt(pad(FLAG,16))
enc = cipher2.encrypt(enc)
print(enc.hex())
# 21477fac54cb5a246cb1434a1e39d7b34b91e5c135cd555d678f5c01b2357adc0c6205c3a4e3a8e6fb37c927de0eec95
```
## Padding Oracle
### Attack
- Đầu tiên xin cảm ơn anh **Việt** đã cung cấp video vô cùng trực quan: [Padding Oracle Attack Visualization](https://www.youtube.com/watch?v=uDHo-UAM6_4).
- Padding Oracle Attack là một kiểu tấn công nhắm vào đặc điểm của tiêu chuẩn padding **PKCS#7**. Kiểu tấn công này thường áp dụng với chế độ mã hóa **CBC** (Cipher Block Chaining), nơi mà thông điệp được chia thành các khối và mỗi khối được $\oplus$ với Ciphertext phía trước trước khi được mã hóa.
- Một hệ thống dễ bị tấn công Padding Oracle nếu nó trả về các thông báo khác nhau dựa trên việc padding có đúng hay không sau khi giải mã. Ví dụ, nếu hệ thống trả về thông báo lỗi "**Padding is invalid**" khi padding sai và một thông báo khác khi giải mã thành công như "**Padding is valid**", kẻ tấn công có thể sử dụng sự khác biệt này để xác định thông tin về plaintext.
- Giả sử ta đang tấn công vào một hệ thống mã hóa $CBC$ như hình dưới (có những dữ kiện là $IV$ và **Ciphertext**). Ta nhận ra có thông báo "**Padding is invalid**" tức là Plaintext của ta đã được Pad thêm các byte để đảm bảo độ dài là bội của $16$. Từ giờ ta sẽ lợi dụng cái thông báo đó để tấn công hệ thống :smiling_imp: .

- Ta sẽ biến đổi $IV$ để khi $IV$ $\oplus$ với **Decrypted Text** sẽ ra thứ mình cần. Gọi $IV$ mới là $IV_2$.

- Ở hình trên thì $IV_2$ hiện tại đang rỗng [`00000000000000000000000000000000`] nên hệ thống trả về thông báo là "**Padding is Invalid**" tức Plaintext chưa hề có byte pad hoặc là được pad sai.
- Ta sẽ bắt đầu từ byte cuối của Plaintext hay `Plaintext[15]`. Đầu tiên brute-force byte **cuối cùng** của $IV_2$ , đến khi ta được một byte nào đó mà $\oplus$ với byte cuối cùng của Decrypted text ra `01` thì dừng lại.

- Ta đã tìm được byte đó là `c7`, từ byte `c7` tìm được ta có thể tính được byte cuối cùng của Decrypted Text bằng cách lấy `01` $\oplus$ `c7` thôi, ta có được `Decrypted Text [15]` là `c6`.
- Từ byte `c6` tìm được ở trên, ta sẽ tính được byte cuối cùng của Plaintext bằng cách lấy `c6` $\oplus$ với byte cuối cùng của $IV$ ban đầu là `c1`, ta được byte cuối cùng của Plaintext là `07`.

> Vậy để tính byte kế tiếp của Plaintext thì làm sao?
> $\rightarrow$ Ta sẽ lợi dụng thông báo "Padding is valid" để cho Plaintext[14:16] là \x02\x02 rồi từ đó tính được Plaintext[14].
- Nhưng để `Plaintext[15]` là `02` thì đầu tiên phải thay đổi $IV_2[15]$. Vì khi này nó là `c7`, mà `c7` khi đem $\oplus$ với `Decrypted Text [15]` thì ra `01` chứ không thể cho ra `02` được.
- Để tính $IV_2[15]$, ta sẽ chuyển byte cuối của Plaintext thành `02` rồi lấy `02` $\oplus$ với `Decrypted Text [15]` là `c6`, ta sẽ được byte cuối cùng của $IV_2$ là `c4`. Bây giờ ta sẽ tìm byte kế tiếp của $IV_2$ là $IV_2[14]$ bằng cách brute-force như lúc nãy.

- Ta tìm được $IV_2[14]$ là `4a`, từ `4a` ta sẽ tính được `Decrypted text [14]` bằng cách lấy $4a \oplus 02 = 48$, `Plaintext[14]` là $4f \oplus 48 = 07$.

- Để tính `Plaintext[13]` thì ta phải gửi $IV_2$ sao cho khi $\oplus$ với Decrypted text sẽ ra $3$ byte cuối là `\x03\x03\x03`, rồi từ đó tính được `Plaintext[13]` như trên. Có thể được biểu diễn bằng công thức như sau:
$$Pt[13] = IV[13] \bigoplus Dt[13]$$
$$ \Leftrightarrow Pt[13] = IV[13] \bigoplus \underset{brute-force}{\underbrace{IV_2[13]}} \bigoplus 03$$
- Như vậy ta sẽ làm tương tự với các byte còn lại đến khi hoàn thiện Plaintext :smile: Kết quả như sau:

> Lí thuyết có vẻ khá dễ hiểu nhưng để code được chương trình thực hiện Oracle Padding Attack thì không hề dễ. Mình đã mất *rất nhiều* thời gian cho việc code.
### Paper Plane (120 pts)


- Challenge này sử dụng kiểu mã hóa khá là giống $CBC$, chỉ khác là Output của thuật toán mã hóa AES sẽ được $\oplus$ với Plaintext, kết quả sẽ được dùng làm Input cho thuật toán AES tiếp theo. Vì vậy Challenge này sẽ cần đến hai $IV$ là `c0` và `m0`.
- Ta có source code như sau:
```python=
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os
KEY = ?
FLAG = ?
class AesIge:
def __init__(self, key):
self.cipher = AES.new(key, AES.MODE_ECB)
def encrypt(self, data, m0=os.urandom(16), c0=os.urandom(16)):
data = pad(data, 16, 'pkcs7')
last_block_plaintext = m0
last_block_ciphertext = c0
result = b''
for i in range(0, len(data), 16):
block = data[i: i + 16]
x = AesIge._xor_blocks(block, last_block_ciphertext)
x = self.cipher.encrypt(x)
x = AesIge._xor_blocks(x, last_block_plaintext)
result += x
last_block_plaintext = block
last_block_ciphertext = x
return result, m0, c0
def decrypt(self, data, m0, c0):
last_block_plaintext = m0
last_block_ciphertext = c0
result = b''
for i in range(0, len(data), 16):
block = data[i: i + 16]
x = AesIge._xor_blocks(block, last_block_plaintext)
x = self.cipher.decrypt(x)
x = AesIge._xor_blocks(x, last_block_ciphertext)
result += x
last_block_ciphertext = block
last_block_plaintext = x
if AesIge._is_pkcs7_padded(result):
return unpad(result, 16, 'pkcs7')
else:
return None
@staticmethod
def _is_pkcs7_padded(message):
padding = message[-message[-1]:]
return all(padding[i] == len(padding) for i in range(0, len(padding)))
@staticmethod
def _xor_blocks(a, b):
return bytes([x ^ y for x, y in zip(a, b)])
@chal.route('/paper_plane/encrypt_flag/')
def encrypt_flag():
ciphertext, m0, c0 = AesIge(KEY).encrypt(FLAG.encode())
return {"ciphertext": ciphertext.hex(), "m0": m0.hex(), "c0": c0.hex()}
@chal.route('/paper_plane/send_msg/<ciphertext>/<m0>/<c0>/')
def send_msg(ciphertext, m0, c0):
ciphertext = bytes.fromhex(ciphertext)
m0 = bytes.fromhex(m0)
c0 = bytes.fromhex(c0)
if len(ciphertext) % 16 != 0:
return {"error": "Data length must be a multiple of the blocksize!"}
if len(c0) != 16 or len(m0) != 16:
return {"error": "m0 and c0 must be 16 bytes long!"}
plaintext = AesIge(KEY).decrypt(ciphertext, m0, c0)
if plaintext is not None:
return {"msg": "Message received"}
else:
return {"error": "Can't decrypt the message."}
```
- Ta chú ý vào class `AesIge`, nó có hai câu lệnh sau:
```python
def _is_pkcs7_padded(message):
padding = message[-message[-1]:]
return all(padding[i] == len(padding) for i in range(0, len(padding)))
```
```python
if AesIge._is_pkcs7_padded(result):
return unpad(result, 16, 'pkcs7')
else:
return None
```
> Hàm `_is_pkcs7_padded` sẽ kiểm tra xem tin nhắn của ta có được **Pad** đúng hay không, đúng thì sẽ trả về giá trị **Unpad** cho tin nhắn còn không thì trả về `None`.
- Ta sẽ gửi dữ liệu cho Server bằng hàm `send_msg(ciphertext, m0, c0)`, nếu Ciphertext sau khi được giải mã mà không trả về giá trị `None` thì Server sẽ trả về thông báo **{"msg": "Message received"}**.
```python=
plaintext = AesIge(KEY).decrypt(ciphertext, m0, c0)
if plaintext is not None:
return {"msg": "Message received"}
```
- Thông báo `{"msg": "Message received"}` được trả về giống như thông báo `Padding is valid` ở ví dụ trên vậy, ta sẽ lợi dụng nó để tấn công **Oracle Padding**.
- Đoạn code tấn công như sau:
```python=
from Crypto.Util.number import long_to_bytes
from requests import get
from pwn import xor
url = "https://aes.cryptohack.org/paper_plane/"
def send_server(ciphertext, c0, m0):
feedback = get(url + "send_msg/" + ciphertext + "/" + m0 + "/" + c0).json()
return "error" not in feedback
def oracle_padding(ciphertext, c0, m0):
pt = b""
dt = b""
for i in range(1,17):
tmp = (b"\x00" * 16)[:16 - i]
for j in range(256, -1, -1):
print(f"{j = }")
if len(pt) == 0:
new_c0 = (tmp + long_to_bytes(j)).hex()
elif len(pt) > 0:
new_c0 = (tmp + long_to_bytes(j) + xor(i, dt)).hex()
if send_server(ciphertext, new_c0, m0):
dt = xor(i, j) + dt
pt = xor(bytes.fromhex(c0)[16-i], xor(i, j)) + pt #xor(i, j) là dt á
print(f"{pt = }")
break
return pt
data = get(url + "encrypt_flag/").json()
ciphertext = data["ciphertext"]
c0 = data["c0"]
m0 = data["m0"]
ciphertext1 = ciphertext[:32]
block1 = oracle_padding(ciphertext1, c0, m0)
print(block1)
ciphertext2= ciphertext[32:64]
block2 = oracle_padding(ciphertext2, ciphertext1, block1.hex())
print(block2)
```
- Flag của challenge này: **crypto{h3ll0_t3l3gr4m}**.
## Key-Recovery Attacks on GCM with Repeated Nonces
### The Galois/Counter Mode of Operation
- Tham khảo đường Link này: https://toadstyle.org/cryptopals/63.txt
- Mình sẽ giải thích cách nó hoạt động sau vì đến giới hạn của HackMD rồi, đại khái thì khi mình truyền vào $C_1, T_1,C_2, T_2, C$ thì nó sẽ trả về **Tag** của $C$.
```python
from sage.all import * # import sage library
from Crypto.Util.number import long_to_bytes as lb
from Crypto.Util.number import bytes_to_long as bl
import struct
def bytes_to_polynomial(block, a):
poly = 0
bin_block = bin(bl(block))[2:].zfill(128)
for i in range(len(bin_block)):
poly += a**i * int(bin_block[i])
return poly
def polynomial_to_bytes(poly):
return lb(int(bin(poly.integer_representation())[2:].zfill(128)[::-1], 2))
def convert_to_blocks(ciphertext):
return [ciphertext[i:i + 16] for i in range(0 , len(ciphertext), 16)]
F, a = GF(2**128, name="a", modulus=x**128 + x**7 + x**2 + x + 1).objgen()
R, x = PolynomialRing(F, name="x").objgen()
# C1, C2 là ciphertext mình nhận sau khi nhập hai input, T1, T2 là hai tag, L là len, C là ciphertext của admin
C1 = convert_to_blocks(bytes.fromhex("f0147e9adad82549a9855ba8d14b4b832313c61ff0714f9a2eb4fab37607eb27"))
T1 = bytes.fromhex("5a970753cca67e87aa8fa65d8ff23123")
C2 = convert_to_blocks(bytes.fromhex("f3177d99d9db264aaa8658abd24848802010c51cf3724c992db7f9b07504e824"))
T2 = bytes.fromhex("0253324b1d00b7f491b799a4744253ac")
C = convert_to_blocks(bytes.fromhex("f814729adfd42d46a18557a8d447438c2b13ca1ff57d479526b4f6b3730be328"))
L = struct.pack(">QQ", 0 * 8, len(C1) * 8)
# p là polynomal á
C1_p = [bytes_to_polynomial(C1[i], a) for i in range(len(C1))]
C2_p = [bytes_to_polynomial(C2[i], a) for i in range(len(C2))]
C_p = [bytes_to_polynomial(C[i], a) for i in range(len(C))]
T1_p = bytes_to_polynomial(T1, a)
T2_p = bytes_to_polynomial(T2, a)
L_p = bytes_to_polynomial(L, a)
G_1 = (C1_p[0] * x**3) + (C1_p[1] * x**2) + (L_p * x) + T1_p
G_2 = (C2_p[0] * x**3) + (C2_p[1] * x**2) + (L_p * x) + T2_p
G_3 = (C_p[0] * x**3) + (C_p[1] * x**2) + (L_p * x)
# xor G1 với G2 thì tụi nó sẽ triệt tiêu s, sau đó dùng .roots() để tính h
# có h thì thế vào G1 để tìm s, có s thì kết thúc bài toán
P = G_1 + G_2
for H, _ in P.roots():
s = G_1(H)
T3 = G_3(H) + s
print("Tag: " + str(polynomial_to_bytes(T3).hex()))
```
### Forbiden Fruit (150 pts)
# Cryptohack SYMMETRIC CIPHERS
## HOW AES WORKS
### Keyed Permutations

- AES, cũng như những thuật toán mã hóa khối khác, nó thực hiện "**hoán vị có khóa**", tức nó ánh xạ mỗi khối đầu vào có thể có vào một khối đầu ra độc nhất, với một khóa xác định hoán vị nào sẽ thực hiện.
- Sử dụng cùng một khóa, hoán vị có thể được thực hiện ngược lại, ánh xạ khối đầu ra trở lại khối đầu vào ban đầu. Điều quan trọng là phải có sự **tương ứng một-một** giữa các khối đầu vào và đầu ra, nếu không chúng ta sẽ không thể dựa vào Ciphertext để giải mã trở lại cùng Plaintext mà chúng ta đã bắt đầu.
- Challenge yêu cầu ta trả lời câu hỏi sau: Thuật ngữ toán học cho sự **tương ứng một-một** là gì?
$\Rightarrow$ Đáp án là [**song ánh**](https://vi.wikipedia.org/wiki/Song_ánh), tiếng anh là **Bijection**.
- Vậy Flag của challenge này là: **crypto{bijection}**.
### Resisting Bruteforce

- Việc brute-force một key dài 128 bit khó như thế nào?
- Một key dài 128 bit có $2^{128}$ khả năng. Con số này tương đương với khoảng $3.4\times10^{38}$ khả năng.
- Giả sử chúng ta có một siêu máy tính có thể kiểm tra một tỷ ($10^{9}$) key mỗi giây. Số này đã là một con số cực kỳ lạc quan và vượt xa khả năng của hầu hết các máy tính hiện tại :smile:
- Thì tổng số thời gian cần thiết để thử hết tất cả các keys sẽ là: $\frac{2^{128}}{10^{9}}$ (giây).
$$\frac{2^{128}}{10^{9}}\sim \frac{3.4\times10^{38}}{10^{9}} = 3.4\times10^{29}\hspace{1mm}(s)$$
- $3.4\times10^{29}$ (giây) tương đương với khoảng $1.08\times 10^{22}$ (năm).
- So sánh với tuổi của vũ trụ, ước tính khoảng 13.8 tỷ năm ($13.8 \times 10^{10}$) thì ta phải mất gấp trăm lần tuổi của vũ trụ để bẻ khóa.
- Vậy brute-force một key dài 128 bit là một nhiệm vụ bất khả thi trong thực tế với công nghệ hiện tại. Điều này là lý do tại sao các thuật toán mã hóa sử dụng key 128 bit được coi là rất an toàn và hầu như không thể bị phá vỡ bằng phương pháp brute-force.
- Hóa ra có một cách tấn công vào AES tốt hơn bruteforce, nhưng chỉ **một chút** thôi :smiley: – nó hạ mức độ bảo mật của AES-128 xuống còn 126,1 bit.
- Challenge yêu cầu ta trả lời câu hỏi sau: tên của kiểu tấn công tốt nhất đối với AES là gì?
$\Rightarrow$ Đáp án là **Biclique attack**.
- Vậy Flag của challenge này là: **crypto{biclique}**.
### Structure of AES


- Để cho Ciphertext không thể bị đảo ngược nếu không có khóa thì khối đầu vào của ta phải được thực hiện rất nhiều bước hoán vị và thay thế. Điều này khác hoàn toàn với RSA, một thuật toán mã hóa mà chỉ dựa vào trên một vấn đề toán học riêng lẻ (**Modular arithmetic**). AES thì không giống vậy, nó không sử dụng một đề tài toán học nào cho quá trình mã hóa, nhưng mà nó nhanh :smile:
- AES-128 bắt đầu với "key schedule" hay thuật toán sinh khóa rồi sau đó thực hiện $10$ vòng lặp mã hóa. Trong suốt $10$ vòng, Plaintext của ta được biến đổi nhiều lần bởi một số phép biến đổi không thể bị đảo ngược.
- Sau đây là tổng quát về mã hóa RSA.
- **KeyExpansion** hay **Key Schedule**: từ key 16 bytes được ta chọn ban đầu, $10$ key 16 bytes khác sẽ được tạo ra từ nó, mục đích là để sử dụng trong bước AddRoundKey.
- **Initial key addition**: Đây sẽ là bước đầu tiên của quá trình mã hóa, key đầu tiên mà ta chọn sẽ được $\oplus$ với khối văn bản của ta.
- **Round**: quá trình này sẽ lặp tổng cộng $10$ round với $9$ round chính và $1$ round cuối. Mỗi round chính gồm $4$ bước là: **SubBytes**, **ShiftRows**, **MixColumns**, **AddRoundKey**. Round cuối sẽ không có bước MixColumns.
1. SubBytes: Mỗi byte của khối văn bản gốc được thay thế với một byte khác ở bảng $S-box$.
2. ShiftRows: $3$ dòng cuối của khối Plaintext sẽ được chuyển đổi, các byte sẽ bị dịch một, hai hoặc ba byte qua bên trái.
3. MixColumns: Mỗi cột của khối Plaintext sẽ được $\oplus$ với một khối $4\times 4$ cụ thể, tên nó là $C(X)$.
4. AddRoundKey: Khối văn bản của ta sẽ được $\oplus$ với key. Tổng cộng sẽ có $11$ bước AddRoundKey xuyên suốt quá trình Initial key addition và $10$ round mã hóa :smiley:

- Quay lại với Challenge thì đề cho ta một file là `matrix.py` bao gồm hàm `bytes2matrix` để chuyển đổi Plaintext của chúng ta thành một ma trận. Yêu cầu chúng ta viết hàm `Matrix2bytes` để biến ma trận đó trở lại thành byte và gửi văn bản gốc làm $Flag$.
- Đoạn code như sau:
```python=
def bytes2matrix(text):
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
lst = []
def matrix2bytes(matrix):
for i in matrix:
for o in i:
lst.append(chr(o))
matrix = [
[99, 114, 121, 112],
[116, 111, 123, 105],
[110, 109, 97, 116],
[114, 105, 120, 125],
]
matrix2bytes(matrix)
print(''.join(lst))
```
- Vậy Flag của challenge này là: **crypto{inmatrix}**.
### Round Keys


- Bước AddRoundKey rất đơn giản: nó $\oplus$ khối văn bản hiện tại với những khóa được sinh ra từ quá trình KeyExpansion.
- Ở mỗi round thì bước AddRoundKey được diễn ra ở cuối cùng. AddRoundKey là yếu tố khiến AES trở thành một "hoán vị có khóa" thay vì chỉ là một hoán vị. Đây là phần duy nhất của AES mà khóa được trộn vào khối văn bản.
- Như đã thấy trong các phần trước, **XOR** là một thao tác dễ dàng đảo ngược nếu bạn biết key nhưng khó hoàn tác nếu bạn không biết. Bây giờ hãy tưởng tượng bạn đang cố gắng khôi phục bản rõ đã được $\oplus$ với $11$ khóa khác nhau và bị xáo trộn rất nhiều :wink:
- Quay trở lại với challenge thì đề cho ta một file là `add_round_key.py` gồm hai ma trận $4 \times 4$ và yêu cầu ta viết hàm `add_round_key` để $\oplus$ hai ma trận đó rồi dùng hàm `matrix2bytes` từ challenge trước để nhận $Flag$.
- Đoạn code như sau:
```python=
state = [
[206, 243, 61, 34],
[171, 11, 93, 31],
[16, 200, 91, 108],
[150, 3, 194, 51],
]
round_key = [
[173, 129, 68, 82],
[223, 100, 38, 109],
[32, 189, 53, 8],
[253, 48, 187, 78],
]
def add_round_key(s, k):
for i in range(4):
for j in range(4):
s[i][j] = s[i][j] ^ k[i][j]
return s
def matrix2bytes(matrix):
s = ""
for i in matrix:
for j in i:
s = s + chr(j)
return s
print(matrix2bytes(add_round_key(state, round_key)))
```
- Vậy Flag của challenge này là: **crypto{r0undk3y}**.
### Confusion through Substitution


- Bước đầu tiên của mỗi round AES là SubBytes. Bước SubBytes sẽ lấy từng byte của khối văn bản của bạn và thay thế nó bằng một byte khác trong bảng "**[Substitution box](https://en.wikipedia.org/wiki/Rijndael_S-box)**" hay gọi ngắn gọn là "S-box".
- Năm 1945, nhà toán học người Mỹ Claude Shannon đã xuất bản một bài báo mang tính đột phá về "Lý thuyết thông tin". Bài báo đó xác định "[**Confusion**](https://en.wikipedia.org/wiki/Confusion_and_diffusion)" là một thuộc tính thiết yếu cho một hệ thống mật mã. "Confusion" có nghĩa là mối quan hệ giữa Ciphertext và Key phải phức tạp nhất có thể. Nếu chỉ được cung cấp một Ciphertext, sẽ không có cách nào để tìm ra bất cứ thông tin gì về Key.
- Nếu mật mã có Confusion thấp thì có thể được biểu diễn thành mối quan hệ giữa Ciphertext, Key và Plaintext dưới dạng **hàm tuyến tính**. Ví dụ, trong mã Caesar thì $Ciphertext = Plaintext + Key$. Đó là mối quan hệ dễ nhận ra, ta có thể dễ dàng đảo ngược được.
- Mục đích chính của S-box là biến đổi Input theo cách có khả năng chống lại sự gần đúng của các hàm tuyến tính. S-box đang hướng tới tính phi tuyến tính cao, mặc dù S-box của AES không hoàn hảo nhưng nó cũng khá gần :smile:
- Quay trở lại với bài toán thì challenge cho ta một file là `sbox.py` gồm hai bảng là `S-box`, `inv_s_box` và một ma trận trạng thái `state`. Challenge yêu cầu ta `sub_bytes` ma trận `state` bằng cách gửi ma trận `state` qua bảng `inv_s_box` rồi sau đó chuyển nó về byte để nhận $Flag$.
- Đoạn code như sau:
```python=
s_box = (
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)
inv_s_box = (
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)
state = [
[251, 64, 182, 81],
[146, 168, 33, 80],
[199, 159, 195, 24],
[64, 80, 182, 255],
]
def matrix2bytes(matrix):
return bytes(sum(matrix, []))
def sub_bytes(s, sbox):
for i in range(4):
for j in range(4):
s[i][j] = sbox[s[i][j]]
return s
print(matrix2bytes(sub_bytes(state, inv_s_box)))
```
- Vậy Flag của challenge này là: **crypto{l1n34rly}**.
### Diffusion through Permutation


- Chúng ta biết rằng bảng S-box sẽ tạo ra tính "Confusion" cho khối văn bản của ta. Còn một đặc điểm quan trọng khác tên là "[**diffusion**](https://en.wikipedia.org/wiki/Confusion_and_diffusion)". Hiểu theo tiếng việt là "sự khuếch tán", diffusion sẽ khiến cho byte của khối Plaintext được khuếch tán đi khắp các phần của Ciphertext.
- Nếu không có diffusion, cùng một byte ở cùng một vị trí sẽ nhận được các phép biến đổi giống nhau được áp dụng cho mỗi vòng. Điều này sẽ cho phép các nhà thám mã tấn công từng vị trí byte trong ma trận một cách riêng biệt.
- Các bước ShiftRows và MixColumns kết hợp lại với nhau để tạo ra tính diffusion. Chúng làm việc cùng nhau để đảm bảo mỗi byte đều sẽ ảnh hưởng đến mọi byte khác trong khối.
- ShiftRows là phép chuyển đổi đơn giản nhất trong AES. Nó giữ nguyên hàng đầu tiên. Ở hàng thứ hai thì mỗi byte sẽ được dịch sang trái một ô và tương tự ở hàng ba và tư thì mỗi byte sẽ dịch tương ứng hai và ba ô. Tầm quan trọng của bước ShiftRows là tránh việc các cột được mã hóa độc lập với nhau.
- MixColumns phức tạp hơn. Nó sẽ nhân từng cột của khối văn bản với ma trận $C(X)$ cho trước. Do đó, mỗi byte của mỗi cột sẽ ảnh hưởng đến tất cả các byte của cột kết quả.
- Quay lại với bài toán thì challenge cho ta một file là `diffusion.py` để thực hiện MixColumns và ShiftRows. Đề yêu cầu ta hoàn thiện hàm `inv_shift_rows` rồi thực hiện `inv_shift_rows` và `inv_mix_columns`, sau đó chuyển qua byte để được $Flag$.
- Đoạn code như sau:
```python=
def shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]
def inv_shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]
# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
def mix_single_column(a):
# see Sec 4.1.2 in The Design of Rijndael
t = a[0] ^ a[1] ^ a[2] ^ a[3]
u = a[0]
a[0] ^= t ^ xtime(a[0] ^ a[1])
a[1] ^= t ^ xtime(a[1] ^ a[2])
a[2] ^= t ^ xtime(a[2] ^ a[3])
a[3] ^= t ^ xtime(a[3] ^ u)
def mix_columns(s):
for i in range(4):
mix_single_column(s[i])
def inv_mix_columns(s):
# see Sec 4.1.3 in The Design of Rijndael
for i in range(4):
u = xtime(xtime(s[i][0] ^ s[i][2]))
v = xtime(xtime(s[i][1] ^ s[i][3]))
s[i][0] ^= u
s[i][1] ^= v
s[i][2] ^= u
s[i][3] ^= v
mix_columns(s)
state = [
[108, 106, 71, 86],
[96, 62, 38, 72],
[42, 184, 92, 209],
[94, 79, 8, 54],
]
def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
return ''.join([chr(_) for i in matrix for _ in i])
inv_mix_columns(state)
inv_shift_rows(state)
print(matrix2bytes(state))
```
- Vậy Flag của challenge này là: **crypto{d1ffUs3R}**.
### Bringing It All Together

- Mô tả: Challenge này sẽ kết hợp toàn bộ các challenge phía trước để ta có thể code được một chương trình giải mã AES (bạn có thể gian lận bằng cách sử dụng thư viện PyCryptodome :upside_down_face: nhưng mà nó không có ý nghĩa gì cả).
- Đề cho ta một file là `aes_decrypt.py` có sẵn các hàm và thông tin cần thiết, bây giờ ta cần hoàn thiện file trên để có thể giải mã AES để có $Flag$.
- Đoạn code hoàn chỉnh như sau:
```python=
N_ROUNDS = 10
key = b'\xc3,\\\xa6\xb5\x80^\x0c\xdb\x8d\xa5z*\xb6\xfe\\'
ciphertext = b'\xd1O\x14j\xa4+O\xb6\xa1\xc4\x08B)\x8f\x12\xdd'
s_box = (
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)
inv_s_box = (
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)
def bytes2matrix(text):
""" Converts a 16-byte array into a 4x4 matrix. """
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
plaintext = ''
for i in range(len(matrix)):
for j in range(4):
plaintext += chr(matrix[i][j])
return plaintext
def add_round_key(state, round_key):
for i in range(4):
for j in range(4):
state[i][j] ^= round_key[i][j]
return state
def sub_bytes(state, sbox=s_box):
for i in range(4):
for j in range(4):
state[i][j] = sbox[state[i][j]]
return state
# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
def mix_single_column(a):
# see Sec 4.1.2 in The Design of Rijndael
t = a[0] ^ a[1] ^ a[2] ^ a[3]
u = a[0]
a[0] ^= t ^ xtime(a[0] ^ a[1])
a[1] ^= t ^ xtime(a[1] ^ a[2])
a[2] ^= t ^ xtime(a[2] ^ a[3])
a[3] ^= t ^ xtime(a[3] ^ u)
def mix_columns(s):
for i in range(4):
mix_single_column(s[i])
def inv_mix_columns(s):
# see Sec 4.1.3 in The Design of Rijndael
for i in range(4):
u = xtime(xtime(s[i][0] ^ s[i][2]))
v = xtime(xtime(s[i][1] ^ s[i][3]))
s[i][0] ^= u
s[i][1] ^= v
s[i][2] ^= u
s[i][3] ^= v
mix_columns(s)
def shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]
def inv_shift_rows(s):
s[1][1], s[2][1], s[3][1], s[0][1]= s[0][1], s[1][1], s[2][1], s[3][1]
s[2][2], s[3][2], s[0][2], s[1][2]= s[0][2], s[1][2], s[2][2], s[3][2]
s[3][3], s[0][3], s[1][3], s[2][3]= s[0][3], s[1][3], s[2][3], s[3][3]
def expand_key(master_key):
"""
Expands and returns a list of key matrices for the given master_key.
"""
# Round constants https://en.wikipedia.org/wiki/AES_key_schedule#Round_constants
r_con = (
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
)
# Initialize round keys with raw key material.
key_columns = bytes2matrix(master_key)
iteration_size = len(master_key) // 4
# Each iteration has exactly as many columns as the key material.
i = 1
while len(key_columns) < (N_ROUNDS + 1) * 4:
# Copy previous word.
word = list(key_columns[-1])
# Perform schedule_core once every "row".
if len(key_columns) % iteration_size == 0:
# Circular shift.
word.append(word.pop(0))
# Map to S-BOX.
word = [s_box[b] for b in word]
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
word[0] ^= r_con[i]
i += 1
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
# Run word through S-box in the fourth iteration when using a
# 256-bit key.
word = [s_box[b] for b in word]
# XOR with equivalent word from previous iteration.
word = bytes(i^j for i, j in zip(word, key_columns[-iteration_size]))
key_columns.append(word)
# Group key words in 4x4 byte matrices.
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
def decrypt(key, ciphertext):
round_keys = expand_key(key) # Remember to start from the last round key and work backwards through them when decrypting
# Convert ciphertext to state matrix
state = bytes2matrix(ciphertext)
# Initial add round key step
state = add_round_key(state,round_keys[10])
for i in range(N_ROUNDS - 1, 0, -1):
pass # Do round
inv_shift_rows(state)
state = sub_bytes(state,inv_s_box)
state = add_round_key(state, round_keys[i])
inv_mix_columns(state)
# Run final round (skips the InvMixColumns step)
inv_shift_rows(state)
state = sub_bytes(state, inv_s_box)
state = add_round_key(state, round_keys[0])
# Convert state matrix to plaintext
plaintext = matrix2bytes(state)
return plaintext
print(decrypt(key, ciphertext))
```
- Vậy Flag của challenge này là: **crypto{MYAES128}**.
## SYMMETRIC STARTER
### Modes of Operation Starter

- Các challenge trước đã cho ta thấy cách AES thực hiện hoán vị Plaintext, trong thực tế thì ta cần phải mã hóa tin nhắn dài hơn nhiều chứ không đơn thuần là một khối $4 \times 4$. Nên là chế độ hoạt động (**mode of operation**) của AES ra đời nhằm mã hóa các văn bản dài một cách an toàn và bảo mật.
- Từ challenge này trở đi ta sẽ được làm quen với các chế độ hoạt động của AES, cách tấn công và khai thác chúng khi chúng bị mã hóa sai cách.
- Những challenge sau này yêu cầu ta phải tương tác với trang Web để có thể có $Flag$, chúng ta phải làm quen thư viện [**request**](https://requests.readthedocs.io/en/latest/) của python.
- Cài như sau: `pip install requests`
- Để sử dụng thì ta phải import thư viện: `import requests`.
- Để truy cập tới một đường Link nào đó thì ta có câu lệnh sau:
```python
import requests
URL = "https://aes.cryptohack.org/block_cipher_starter/"
r = requests.get(URL)
```
- Bây giờ thì ta đã có một đối tượng phản hồi tên là **r**, chúng ta sẽ dùng "r" để lấy những thông tin mà ta cần từ trang Web.
- Đoạn code như sau:
```python=
import requests
BASE_URL = "http://aes.cryptohack.org/block_cipher_starter"
r = requests.get(f"{BASE_URL}/encrypt_flag")
data = r.json()
ciphertext = data["ciphertext"]
print("ciphertext", ciphertext)
r = requests.get(f"{BASE_URL}/decrypt/{ciphertext}")
data = r.json()
plaintext = data["plaintext"]
print("plaintext", plaintext)
print("flag", bytes.fromhex(plaintext).decode())
#ciphertext ecc66eb0f6359eb85cda93a63ad91a7852f6002466e25c62eaa82f576aea95de
#plaintext 63727970746f7b626c30636b5f633170683372355f3472335f663435375f217d
#flag crypto{bl0ck_c1ph3r5_4r3_f457_!}
```
- Vậy Flag của challenge này là: **crypto{bl0ck_c1ph3r5_4r3_f457_!}**.
### Passwords as Keys

- Điều quan trọng là các Key trong thuật toán khóa đối xứng phải là các byte **ngẫu nhiên**, thay vì mật khẩu hoặc dữ liệu có thể dự đoán được khác. Các byte ngẫu nhiên phải được tạo bằng cách sử dụng trình tạo số giả ngẫu nhiên được bảo mật bằng mật mã (CSPRNG). Nếu các khóa có thể dự đoán được theo bất kỳ cách nào thì mức độ bảo mật của mật mã sẽ giảm và có thể bị tấn công bởi kẻ xấu.
- Challenge này sử dụng các từ ngẫu nhiên từ trang Web [**này**](https://gist.githubusercontent.com/wchargin/8927565/raw/d9783627c731268fb2935a731a618aa8e95cf465/words) để tạo thành Key bằng hàm băm md5. Và nhấn mạnh rằng Key không nên là Password vì Hacker có thể brute-force tất cả các từ trong từ điển để có được Password của bạn.
- Đoạn code hoàn chỉnh như sau:
```python=
from requests import get
from hashlib import md5
from Crypto.Cipher import AES
with open("wordlist.txt") as f:
words = [w.strip() for w in f.readlines()]
url = "https://aes.cryptohack.org/passwords_as_keys/"
ciphertext = bytes.fromhex(get(url + "encrypt_flag/").json()["ciphertext"])
for keyword in words:
key = md5(keyword.encode()).digest()
cipher = AES.new(key, AES.MODE_ECB)
flag = cipher.decrypt(ciphertext)
if b"crypto{" in flag:
print(f"{flag = }")
print(f"{keyword = }")
break
```
- Vậy Flag của challenge này là: **crypto{k3y5__r__n07__p455w0rdz?}**.
## BLOCK CIPHERS
### ECB CBC WTF

- Ta có source code như sau:
```python
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}
```
- Nhìn vào source ta thấy rằng $Flag$ được mã hóa bằng kiểu **CBC** nhưng Ciphertext lại được giải mã bằng **ECB**?
- Ciphertext của ta bao gồm $IV + Flag$ vì vậy ta có thể có được $IV$ bằng câu lệnh `Ciphertext[:32]`.
> Ta có những dữ kiện sau: $IV$; Ciphertext có được từ quá trình mã hóa CBC và Plaintext có được từ việc đưa Ciphertext trên qua ECB. Ta sẽ từ những dữ kiện trên để tìm ra $Flag$.
- Vì Ciphertext được mã hóa bằng CBC nên ta phải giải mã nó bằng CBC chứ không thể dùng ECB, nhưng đề chỉ cho hàm giải mã ECB, phải làm sao?
- ECB và CBC khác nhau thế nào? Ở ECB thì Ciphertext được đưa qua thuật toán mã hóa AES để ra thẳng Plaintext. Còn với CBC, để có Plaintext thì Decrypted_text phải được $\oplus$ với khối Ciphertext phía trước. Hay nói cách khác thì CBC có bước **XOR** còn ECB thì không.
> :bulb: Vì đã có được Plaintext từ mã hóa ECB nên việc cần làm còn lại là **XOR** thôi. Ta sẽ tìm $IV$ và khối Ciphertext phía trước để đem đi $\oplus$ với Plaintext.

- Đoạn code như sau:
```python=
from pwn import*
import requests
def encrypt_flag():
URL = "https://aes.cryptohack.org/ecbcbcwtf/encrypt_flag/"
r = requests.get(URL)
data = r.json()
return (data["ciphertext"])
def decrypt(ciphertext):
URL = "https://aes.cryptohack.org/ecbcbcwtf/decrypt/"
URL += ciphertext + "/"
r = requests.get(URL)
data = r.json()
return (data["plaintext"])
cipher = encrypt_flag()
plaintext = decrypt(cipher)
iv = bytes.fromhex(cipher[:32])
ciphertext = cipher[32:]
pt1 = bytes.fromhex(plaintext[32:64])
pt2 = bytes.fromhex(plaintext[64:])
print((xor(pt1, iv) + xor(pt2, bytes.fromhex(ciphertext[:32]))).decode())
```
- Vậy Flag của challenge này là: **crypto{3cb_5uck5_4v01d_17_!!!!!}**.
### ECB Oracle

- ECB là kiểu mã hóa đơn giản nhất với mỗi khối văn bản gốc được mã hóa độc lập với nhau. Ở challenge này thì $Input$ của chúng ta sẽ được thêm vào với $Flag$ rồi đem đi mã hóa, chỉ có thế thôi. Thậm chí source code còn không có hàm `Decrypt` :smile:
- Ý tưởng của chúng ta như sau:
- Giả sử $Flag$ là `crypto{OBF_newbie}`.
- 
- Gọi $5$ khối trên là Plaintext. Nó là kết quả của $Input + Flag$ + $Pad$ . Các dấu "$*$" ở cuối đại diện cho các byte được pad vào để đảm bảo kích thước của Plaintext. $Input$ là `24 byte "*" + b'crypto{' + ? + 24 byte "*"`.
- "$?$" là byte mà ta sẽ dùng để brute-force. Ta brute-force tới khi nào khối thứ **hai** bằng khối thứ **tư** thì ta sẽ thêm "$?$" vào `Crypto{` để có $Flag$.
- Sau khi tìm được một ký tự thì ta dịch hai khối đó sang bên trái như sau:

- Giờ thì $Input$ không còn là `24 byte "*" + b'crypto{' + ? 24 byte "*"` như trên nữa mà sẽ là `23 byte "*" + b'crypto{O' + ? + 23 byte "*"`. Mỗi khi tìm được một ký tự của $Flag$ thì ta sẽ phải thay đổi $Input$.
- Ta tiếp tục brute-force "$?$" đến khi nào nó bằng ký tự "B". Sau đó làm tương tự đến khi ký tự cuối cùng là "}" thì dừng lại.
- Đoạn code như sau:
```python=
import string
from requests import get
url = "https://aes.cryptohack.org/ecb_oracle/"
def send_server(plaintext):
ciphertext = get(url + "encrypt/" + plaintext).json()["ciphertext"]
return ciphertext
characters = string.ascii_letters + string.digits + string.punctuation
flag = b"crypto{"
while True:
for i in characters:
print(f"{i = }")
payload = (b"*" * (32 - len(flag) - 1) + flag + i.encode() + b"*" * (32 - len(flag) - 1)).hex()
ciphertext = send_server(payload)
if ciphertext[32:64] == ciphertext[96:128]:
flag += i.encode()
print(flag)
break
if flag[-1:] == b"}":
print(flag)
break
```

- Vậy Flag của challenge này là: **crypto{p3n6u1n5_h473_3cb}**.
### Flipping Cookie

- Đề cho ta source code như sau:
```python=
from Crypto.Cipher import AES
import os
from Crypto.Util.Padding import pad, unpad
from datetime import datetime, timedelta
KEY = ?
FLAG = ?
@chal.route('/flipping_cookie/check_admin/<cookie>/<iv>/')
def check_admin(cookie, iv):
cookie = bytes.fromhex(cookie)
iv = bytes.fromhex(iv)
try:
cipher = AES.new(KEY, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(cookie)
unpadded = unpad(decrypted, 16)
except ValueError as e:
return {"error": str(e)}
if b"admin=True" in unpadded.split(b";"):
return {"flag": FLAG}
else:
return {"error": "Only admin can read the flag"}
@chal.route('/flipping_cookie/get_cookie/')
def get_cookie():
expires_at = (datetime.today() + timedelta(days=1)).strftime("%s")
cookie = f"admin=False;expiry={expires_at}".encode()
iv = os.urandom(16)
padded = pad(cookie, 16)
cipher = AES.new(KEY, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(padded)
ciphertext = iv.hex() + encrypted.hex()
return {"cookie": ciphertext}
```
- Ở hàm `get_cookie()` ta sẽ nhận được một cookie có dạng như sau: `admin=False;expiry={expires_at}`. Cookie và IV của ta sẽ được dùng để làm input cho hàm `check_admin(cookie, iv)`.
- Để nhận được $Flag$ thì hàm `check_admin(cookie, iv)` phải decrypt cookie ra được đoạn sau: "**admin=True**".
- Nhưng nhìn vào cookie thì ta thấy nó có dạng "**admin=False**;expiry={...}". Giờ ta phải làm sao? làm sao để xuất hiện "admin=True"?
- Ta không thể thay đổi cookie được vì cookie được mã hóa từ Key của đề bài, chỉ còn lại IV. Ta có thể lấy được IV vì nó được thêm vào trong Ciphertext như sau: `ciphertext = iv.hex() + encrypted.hex()`.
- Đây là mã hóa CBC nên IV sẽ được $\oplus$ với Decrypted_text để có được Plaintext, ta sẽ lợi dụng điều đó, $\oplus$ một IV giả với Decrypted_text để có được "admin=True".
- Ta biết rằng Decrypted_text của challenge này sẽ là: IV $\oplus$ *admin=False;expiry={...}*.
- Để ra được Plaintext thì Decrypted_text cần phải $\oplus$ với IV một lần nữa, ta sẽ lợi dụng điều đó, thay đổi IV để thay đổi được Plaintext.

- Đặt $X$ là IV mới cần tìm, $Pt$ là "admin=True;", $Dt$ là IV $\oplus$ *admin=False;expiry={...}*, Ta có phương trình như sau:
$$IV \oplus Dt \oplus X = Pt$$
$$\rightarrow X = Pt \oplus Dt \oplus IV$$
- Vậy IV mới sẽ được tính như sau: **IV_new = admin=True $\oplus$ admin=False $\oplus$ IV**
- Đoạn code hoàn chỉnh như sau:
```python=
from Crypto.Util.Padding import pad
from pwn import xor
import requests
def get_cookie():
url = "https://aes.cryptohack.org/flipping_cookie/get_cookie/"
r = requests.get(url)
js = r.json()
return bytes.fromhex(js["cookie"])
def check_admin(cookie, iv):
url = "http://aes.cryptohack.org/flipping_cookie/check_admin/"
url += cookie.hex() + "/" + iv.hex() + "/"
r = requests.get(url)
js = r.json()
print("Flag:", js["flag"])
cookie = get_cookie()
print("cookie:", cookie.hex())
admin_false = b"admin=False;"
admin_true = b"admin=True;"
IV = cookie[:16]
block1 = cookie[16:]
IV_new = xor(xor(admin_false, admin_true), IV)
check_admin(block1, IV_new)
```
- Vậy Flag của challenge này là: **crypto{4u7h3n71c4710n_15_3553n714l}**.
### Lazy CBC

- Ta có source code như sau:
```python=
from Crypto.Cipher import AES
KEY = ?
FLAG = ?
@chal.route('/lazy_cbc/encrypt/<plaintext>/')
def encrypt(plaintext):
plaintext = bytes.fromhex(plaintext)
if len(plaintext) % 16 != 0:
return {"error": "Data length must be multiple of 16"}
cipher = AES.new(KEY, AES.MODE_CBC, KEY)
encrypted = cipher.encrypt(plaintext)
return {"ciphertext": encrypted.hex()}
@chal.route('/lazy_cbc/get_flag/<key>/')
def get_flag(key):
key = bytes.fromhex(key)
if key == KEY:
return {"plaintext": FLAG.encode().hex()}
else:
return {"error": "invalid key"}
@chal.route('/lazy_cbc/receive/<ciphertext>/')
def receive(ciphertext):
ciphertext = bytes.fromhex(ciphertext)
if len(ciphertext) % 16 != 0:
return {"error": "Data length must be multiple of 16"}
cipher = AES.new(KEY, AES.MODE_CBC, KEY)
decrypted = cipher.decrypt(ciphertext)
try:
decrypted.decode() # ensure plaintext is valid ascii
except UnicodeDecodeError:
return {"error": "Invalid plaintext: " + decrypted.hex()}
return {"success": "Your message has been received"}
```
- Theo như mô tả của đề và đọc source code thì ta có thông tin sau: Challenge này sử dụng Key như là IV luôn. Điều này nghe có vẻ không sao nhưng lại tạo ra một lỗ hổng cực kỳ lớn. Qua challenge này ta sẽ thấy được tầm quan trọng của IV.
- Ta có sơ đồ giải mã của CBC như sau:

$\rightarrow$ Ta tìm được IV thì sẽ có được Key.
- Ý tưởng giải challenge như sau:
- Ta sẽ gửi một Ciphertext là 32 byte `\x00` vào hàm `receive(ciphertext)`, mục đích là để tạo ra hai khối `Decrypted_text`($Dt$) giống nhau, để làm gì thì mình sẽ giải thích sau đây.
- Ta có công thức của hai khối Plaintext ($Pt$) khi được giải mã ra như sau:
$$Pt_1 = Dt \oplus Key$$
$$Pt_2 = Dt \oplus Ct_1$$
- Mà $Ct_1$ lại là 16 byte `\x00` $\Rightarrow Pt_2 = Dt \oplus 0 = Dt$.
- Ta đã có những dữ kiện sau:
$$\left\{\begin{matrix}
Pt_1 = Dt \oplus Key \\
Pt_2 = Dt \\
\end{matrix}\right.$$
$\rightarrow$ để có được $Key$ ta chỉ cần lấy $Pt_1 \oplus Pt_2$.
- Đoạn code hoàn chỉnh như sau:
```python=
from Crypto.Util.Padding import*
from pwn import xor
import requests
def get_flag(key):
url = "https://aes.cryptohack.org/lazy_cbc/get_flag/"
url += key.hex() + "/"
r = requests.get(url)
js = r.json()
print("Flag:", bytes.fromhex(js["plaintext"]).decode())
def receice(ciphertext):
url = "https://aes.cryptohack.org/lazy_cbc/receive/"
url += ciphertext.hex() + "/"
r = requests.get(url)
js = r.json()
return bytes.fromhex(js["error"][len("Invalid plaintext: "):])
ciphertext = b"\x00" * 32
plaintext = receice(ciphertext)
print(f"{plaintext = }")
get_flag(xor(plaintext[:16], plaintext[16:]))
```
- Vậy Flag của challenge này là: **crypto{50m3_p30pl3_d0n7_7h1nk_IV_15_1mp0r74n7_?}**.
### Triple DES

- Challenge này sử dụng 3DES để mã hóa, 3DES có khả năng bảo mật tốt nhưng nó có một điểm yếu là [**Weak key**](https://en.wikipedia.org/wiki/Weak_key).
- Một khóa yếu là một khóa $K$ sao cho $Des_{K_2}(Des_{K_1}(x))$ với mọi $x$.
- Ta có source code như sau:
```python=
from Crypto.Cipher import DES3
from Crypto.Util.Padding import pad
IV = os.urandom(8)
FLAG = ?
def xor(a, b):
# xor 2 bytestrings, repeating the 2nd one if necessary
return bytes(x ^ y for x,y in zip(a, b * (1 + len(a) // len(b))))
@chal.route('/triple_des/encrypt/<key>/<plaintext>/')
def encrypt(key, plaintext):
try:
key = bytes.fromhex(key)
plaintext = bytes.fromhex(plaintext)
plaintext = xor(plaintext, IV)
cipher = DES3.new(key, DES3.MODE_ECB)
ciphertext = cipher.encrypt(plaintext)
ciphertext = xor(ciphertext, IV)
return {"ciphertext": ciphertext.hex()}
except ValueError as e:
return {"error": str(e)}
@chal.route('/triple_des/encrypt_flag/<key>/')
def encrypt_flag(key):
return encrypt(key, pad(FLAG.encode(), 8).hex())
```
- Có thể thấy rằng source code không có hàm `decrypt` nên không thể giải mã trực tiếp ra $Flag$ được, ta sẽ dựa vào điểm yếu **Weak key** của DES là lấy được $Flag$.
- Một số khóa yếu tiêu biểu như sau:
- Xen kẽ $0$ và $1$: `0x0101010101010101`.
- Xen kẽ $F$ và $E$: `0xFEFEFEFEFEFEFEFE`.
- `0xE0E0E0E0F1F1F1F1`
- `0x1F1F1F1F0E0E0E0E`.
- Toàn $0$: `0x0000000000000000`.
- Toàn $F$: `0xFFFFFFFFFFFFFFFF`.
- `0xE1E1E1E1F0F0F0F0`.
- `0x1E1E1E1E0F0F0F0F`.
- Giờ ta sẽ thử ghép các khóa yếu ở trên để có một khóa yếu 16 byte để gửi tới server, mình sẽ sử dụng khóa sau: `0101010101010101FEFEFEFEFEFEFEFE`.
- Đoạn code như sau:
```python=
from Crypto.Util.Padding import*
import requests
def encrypt_flag(key):
url = "https://aes.cryptohack.org/triple_des/encrypt_flag/"
url += key
r = requests.get(url)
js = r.json()
return (js["ciphertext"])
def encrypt(key, plaintext):
url = "https://aes.cryptohack.org/triple_des/encrypt/"
url += key + "/" + plaintext + "/"
r = requests.get(url)
js = r.json()
print(bytes.fromhex(js["ciphertext"]))
key = "0101010101010101FEFEFEFEFEFEFEFE"
ciphertext = encrypt_flag(key)
flag = encrypt(key, ciphertext)
#b'crypto{n0t_4ll_k3ys_4r3_g00d_k3ys}\x06\x06\x06\x06\x06\x06'
```
- Vậy Flag của challenge này là: **crypto{n0t_4ll_k3ys_4r3_g00d_k3ys}**.
## Stream Ciphers
### Symmetry

- **Source Code:**
```python!
from Crypto.Cipher import AES
KEY = ?
FLAG = ?
@chal.route('/symmetry/encrypt/<plaintext>/<iv>/')
def encrypt(plaintext, iv):
plaintext = bytes.fromhex(plaintext)
iv = bytes.fromhex(iv)
if len(iv) != 16:
return {"error": "IV length must be 16"}
cipher = AES.new(KEY, AES.MODE_OFB, iv)
encrypted = cipher.encrypt(plaintext)
ciphertext = encrypted.hex()
return {"ciphertext": ciphertext}
@chal.route('/symmetry/encrypt_flag/')
def encrypt_flag():
iv = os.urandom(16)
cipher = AES.new(KEY, AES.MODE_OFB, iv)
encrypted = cipher.encrypt(FLAG.encode())
ciphertext = iv.hex() + encrypted.hex()
return {"ciphertext": ciphertext}
```
- **Script:**
```python
from requests import *
from pwn import xor
url = "https://aes.cryptohack.org/symmetry/"
data = get(url + "/encrypt_flag").json()["ciphertext"]
iv = data[:32]
ct = data[32:]
# chúng ta sẽ gửi plaintext là bytes \x00, nó sẽ xor với keystream và cho ta kết quả là keystream
pt = (b"\x00" * 48).hex() #ước chừng flag dài 3 khối
data = get(url + "encrypt/" + pt + "/" + iv).json()["ciphertext"]
# có keystream thì ta sẽ đem xor với ciphertext là ra flag
block1 = xor(bytes.fromhex(ct[:32]), bytes.fromhex(data[:32]))
block2 = xor(bytes.fromhex(ct[32:64]), bytes.fromhex(data[32:64]))
block3 = xor(bytes.fromhex(ct[64:96]), bytes.fromhex(data[64:96]))
print(block1 + block2 + block3)
```
- **Flag: crypto{0fb_15_5ymm37r1c4l_!!!11!}**
### Bean Counter

- **Source Code**:
```python!
from Crypto.Cipher import AES
KEY = ?
class StepUpCounter(object):
def __init__(self, step_up=False):
self.value = os.urandom(16).hex()
self.step = 1
self.stup = step_up
def increment(self):
if self.stup:
self.newIV = hex(int(self.value, 16) + self.step)
else:
self.newIV = hex(int(self.value, 16) - self.stup)
self.value = self.newIV[2:len(self.newIV)]
return bytes.fromhex(self.value.zfill(32))
def __repr__(self):
self.increment()
return self.value
@chal.route('/bean_counter/encrypt/')
def encrypt():
cipher = AES.new(KEY, AES.MODE_ECB)
ctr = StepUpCounter()
out = []
with open("challenge_files/bean_flag.png", 'rb') as f:
block = f.read(16)
while block:
keystream = cipher.encrypt(ctr.increment())
xored = [a^b for a, b in zip(block, keystream)]
out.append(bytes(xored).hex())
block = f.read(16)
return {"encrypted": ''.join(out)}
```
- **Mô tả:** Challenge này thực hiện mã hóa **CTR** trên một file **PNG** nhưng thuật toán bị thực hiện sai nên tạo ra một lỗ hổng vô cùng nghiêm trọng.

- **CTR** sử dụng đầu vào cho thuật toán AES là **nonce + Counter**, điều đó tạo nên sự riêng biệt cho từng khối. **Plaintext** của ta sẽ được $\oplus$ với **streamkey** (đầu ra của thuật toán AES) ở trên để có ciphertext.
- Chú ý hàm **increment()**:
```python!
def increment(self):
if self.stup:
self.newIV = hex(int(self.value, 16) + self.step)
else:
self.newIV = hex(int(self.value, 16) - self.stup)
self.value = self.newIV[2:len(self.newIV)]
return bytes.fromhex(self.value.zfill(32))
```
- Ta thấy rằng khi **self.stup** mà **False** thì nó sẽ thực hiện trừ `self.value` cho `self.stup`, vấn đề là `self.stup` mà `False` thì nó có giá trị là $0$ :smile:
- Mà giá trị khởi tạo của `self.stup` định sẵn là `False` nên giá trị của `self.value` luôn bị trừ cho $0$ mỗi lần hàm **increment()** bị gọi. Điều đó có nghĩa **nonce + Counter** không bao giờ thay đổi, khiến cho **keystream** qua mỗi block đều không đổi. Vậy nếu ta tìm được `keystream` của một block thì ta sẽ biết toàn bộ **plaintext** của các block còn lại.
> Nhưng tìm lại **keystream** như thế nào?
- Sau một hồi tìm tòi thì mình biết được rằng file **PNG** sẽ luôn bắt đầu bằng $16$ giá trị hex cố định, gọi là **PNG header**:

- Có được **Ciphertext**, có được **Plaintext** của khối đầu, ta chỉ cần $\oplus$ chúng lại là có **streamkey**, mà có streamkey thì ta thực hiện $\oplus$ nó với các khối còn lại là có được **Flag**.
- Đoạn code như sau:
```python!
from pwn import *
import requests
def encrypt_flag():
url = "http://aes.cryptohack.org/bean_counter/encrypt/"
r = requests.get(url)
js = r.json()
return (js["encrypted"])
a = bytes.fromhex(encrypt_flag())
#16 bytes đầu của một file PNG
png_hdr = bytes([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52])
#vì biết được 16 bytes đầu nên ta sẽ tính được stream key
key = xor(a[:16], png_hdr)
PNG = b''
for i in range(0,len(a),16):
PNG += xor(a[i:i+16], key)
with open('flag1.png', 'wb') as f:
f.write(PNG)
```
- **Flag**:

### CTRIME

- **Source code:**
```python
from Crypto.Cipher import AES
from Crypto.Util import Counter
import zlib
KEY = ?
FLAG = ?
@chal.route('/ctrime/encrypt/<plaintext>/')
def encrypt(plaintext):
plaintext = bytes.fromhex(plaintext)
iv = int.from_bytes(os.urandom(16), 'big')
cipher = AES.new(KEY, AES.MODE_CTR, counter=Counter.new(128, initial_value=iv))
encrypted = cipher.encrypt(zlib.compress(plaintext + FLAG.encode()))
return {"ciphertext": encrypted.hex()}
```
- Lần đầu nhìn vào source code thì mình thấy nó khá hoàn hảo vì không thấy chỗ nào có lỗ hổng cả, nhưng rõ ràng là mode **CTR** không thể bị crack được nếu như không biết được **KEY** và **IV** nên thứ duy nhất ta có thể khai thác là hàm **compress** của thư viện **zlib**.
- Sau một hồi tìm kiếm thì mình biết hàm `compress()` nén thông tin bằng thuật toán **deflate**, thuật toán này gồm hai bước là `LZ77 encoding` và `Huffman encoding`, `LZ77 encoding` sẽ nén dữ liệu lại bằng cách đếm số lần một từ lặp lại và rút ngắn những từ đó lại, ví dụ: `cryptocryptocrypto` là ba từ `crypto` liên tục, nó sẽ rút gọn thành `crypto3` chẳng hạn. Còn `Huffman encoding` là bước ánh xạ các từ được rút gọn qua cây Huffman.
- Theo mình tìm hiểu được thì cứ một từ lặp lại **ba** lần thì thuật toán sẽ tiến hành rút gọn, vì thế ý tưởng của ta là gửi một từ chắc chắn sai để kiếm tra độ dài của Flag sai, sau đó **brute force** từng chữ của bảng chữ cái, chữ nào làm cho ciphertext của ta ngắn hơn chuỗi Flag sai thì đó là ký tự của **Flag**.
- **Ví dụ:** Flag của ta là `crypto{CTR_MODE}`, ta gắn hai từ `crypto{/` vào phía trước Flag như sau: `crypto{/crypto{/crypto{CTR_MODE}`, khi này chiều dài của **compress** là 27, sau đó ta tạo một biến mới là `crypto{?crypto{?crypto{CTR_MODE}` sau đó tiến hành brute force bytes `?`, nếu mà `?` ra đúng ký tự của Flag thì len của compress sẽ bị rút ngắn xuống 26. **Ví dụ** ta brute force được chữ `C` thì khi này nó như thế này: `crypto{Ccrypto{Ccrypto{CTR_MODE}`, ta thấy ba từ `crypto{C` lặp lại nên nó sẽ bị rút gọn, chiều dài compress giảm xuống 26.
- **Script:**
```python!
from requests import get
from string import printable
alphabet = printable[:-5]
url = "https://aes.cryptohack.org/ctrime/"
flag = b"crypto{"
while not flag.endswith(b"}"):
tmp = get(url + "/encrypt/" + (flag + b"/").hex()*2).json()["ciphertext"] # gửi một byte sai để kiểm tra độ dài
print(f"{len(tmp) = }")
for char in alphabet:
payload = flag + char.encode()
plaintext = (payload.hex())*2
ciphertext = get(url + "/encrypt/" + plaintext).json()["ciphertext"]
print(char,"=",len(ciphertext))
if len(ciphertext) < len(tmp): #nếu ra đúng ký tự của Flag thì ciphertext sẽ ngắn hơn tmp vì khi này nó bị rút gọn
flag += char.encode()
print(f"found {char}!")
break
print(flag)
```

- **Flag: crypto{CRIME_571ll_p4y5}**
### Logon Zero

- **nc socket.cryptohack.org 13399**
- **Source code:**
```python
from Crypto.Cipher import AES
from Crypto.Util.number import bytes_to_long
from os import urandom
FLAG = "crypto{???????????????????????????????}"
class CFB8:
def __init__(self, key):
self.key = key
def encrypt(self, plaintext):
IV = urandom(16)
cipher = AES.new(self.key, AES.MODE_ECB)
ct = b''
state = IV
for i in range(len(plaintext)):
b = cipher.encrypt(state)[0]
c = b ^ plaintext[i]
ct += bytes([c])
state = state[1:] + bytes([c])
return IV + ct
def decrypt(self, ciphertext):
IV = ciphertext[:16]
ct = ciphertext[16:]
cipher = AES.new(self.key, AES.MODE_ECB)
pt = b''
state = IV
for i in range(len(ct)):
b = cipher.encrypt(state)[0]
c = b ^ ct[i]
pt += bytes([c])
state = state[1:] + bytes([ct[i]])
return pt
class Challenge():
def __init__(self):
self.before_input = "Please authenticate to this Domain Controller to proceed\n"
self.password = urandom(20)
self.password_length = len(self.password)
self.cipher = CFB8(urandom(16))
def challenge(self, your_input):
if your_input['option'] == 'authenticate':
if 'password' not in your_input:
return {'msg': 'No password provided.'}
your_password = your_input['password']
if your_password.encode() == self.password:
self.exit = True
return {'msg': 'Welcome admin, flag: ' + FLAG}
else:
return {'msg': 'Wrong password.'}
if your_input['option'] == 'reset_connection':
self.cipher = CFB8(urandom(16))
return {'msg': 'Connection has been reset.'}
if your_input['option'] == 'reset_password':
if 'token' not in your_input:
return {'msg': 'No token provided.'}
token_ct = bytes.fromhex(your_input['token'])
if len(token_ct) < 28:
return {'msg': 'New password should be at least 8-characters long.'}
token = self.cipher.decrypt(token_ct)
new_password = token[:-4]
self.password_length = bytes_to_long(token[-4:])
self.password = new_password[:self.password_length]
return {'msg': 'Password has been correctly reset.'}
```
- **Mô tả:** Challenge này mô phỏng lại lỗ hổng [**Zerologon**](https://viblo.asia/p/tim-hieu-ve-zerologon-WAyK8rk9lxX) (CVE-2020-1472), một lỗ hổng nghiêm trọng khiến cho kẻ tấn công có thể bỏ qua được bước xác thực admin và được cấp quyền admin chỉ trong vài giây.
- **Script:**
```python
from pwn import remote
from json import loads, dumps
from tqdm import trange
#nc socket.cryptohack.org 13399
io = remote("socket.cryptohack.org", 13399)
your_input = {"option":"reset_password", "token": b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".hex()}
your_input = dumps(your_input).encode()
io.recvuntil(b"Please authenticate to this Domain Controller to proceed\n")
attempt = 0
while True:
io.sendline(your_input)
io.recvuntil(b'{"msg": "Password has been correctly reset."}\n')
password = {"option":"authenticate", "password": ""}
password = dumps(password).encode()
io.sendline(password)
feedback = loads(io.recvline().decode())["msg"]
print(feedback)
if "Welcome admin, flag: " in feedback:
print(f"attempt {attempt}")
break
reset = {"option":"reset_connection"}
reset = dumps(reset).encode()
io.sendline(reset)
io.recvuntil(b'{"msg": "Connection has been reset."}\n')
attempt += 1
```

- **Flag: crypto{Zerologon_Windows_CVE-2020-1472}**
:::info
Mình sẽ update thêm Challenge nếu mình giải ra
:::