# Symmetric Cipher - CryptoHack
[TOC]
# How AES Work
Để xử lí các bài tập trong set đầu tiên này thì cần trang bị một chút kiến thức về AES tại đây: https://www.youtube.com/watch?v=O4xNJsjtN6E&t=102s
## Keyed Permutations

one-to-one là chỉ ánh xạ song ánh.
`crypto{bijection}`
## Resisting Bruteforce

https://en.wikipedia.org/wiki/Biclique_attack
`crypto{biclique}`
## Structure of AES
Trong suốt quá trình mã hóa bằng thuật toán mã hóa AES ta sẽ làm việc trên các ma trận 4x4. Ta chuyển dữ liệu cần mã hóa thành một ma trận 4x4 gọi là ma trận trạng thái (state matrix) sau đó sẽ thực hiện các phép biến đổi để output ra ciphertext.
Ở bài này thì yêu cầu ta chuyển các giá trị ASCII trong matrix về xâu kí tự.
Code để giải:
```python=
from Cryptodome.Util.number import *
def bytes2matrix(text):
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
def matrix2bytes(matrix):
text=''
for i in range(len(matrix)):
for j in range(4):
text+=chr(matrix[i][j])
return text
matrix = [
[99, 114, 121, 112],
[116, 111, 123, 105],
[110, 109, 97, 116],
[114, 105, 120, 125],
]
print(matrix2bytes(matrix))
```
`crypto{inmatrix}`
## Round Keys

Đây là bước đầu tiên trong mã hóa AES : XOR các phần tử trong ma trận `state` với ma trận `round_key`
Code giải:
```python=
from Cryptodome.Util.number import *
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):
result=''
for i in range(4):
for j in range(4):
result+=chr(s[i][j]^k[i][j])
return result
print(add_round_key(state, round_key))
```
`crypto{r0undk3y}`
## Confusion through Substitution
```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 sub_bytes(s, sbox=s_box):
???
print(sub_bytes(state, sbox=inv_s_box))
```
Đây là bước SubBytes, ta cần thay từng bytes trong ma trận trạng thái thành một bytes khác ở vị trí tương ứng trong Sbox. Để đảo ngược lại quá trình này thì ta có một bảng khác để đối chiếu gọi là Inverse Sbox

```python=
def sub_bytes(s, box):
result=''
for i in range(4):
for j in range(4):
result+=chr(box[s[i][j]])
return result
print(sub_bytes(state, inv_s_box))
```
```crypto{l1n34rly}```
## Diffusion through Permutation
Chức năng ShiftRows thực hiện quay trái từng hàng của ma trận trạng thái, ngõ ra của SubBytes, theo byte với hệ số quay tăng dần từ 0 đến 3. Hàng đầu tiên có hệ số quay là 0 thì các byte được giữ nguyên vị trí. Hàng thứ hai có hệ số quay là 1 thì các byte được quay một byte. Hàng thứ ba quay hai byte và hàng thứ tư quay ba byte.

Sau bước ShiftRows thì sẽ là MixColumns. Chức năng MixColumns thực hiện nhân từng cột của ma trận trạng thái, ngõ ra của ShiftRows, với một ma trận chuyển đổi quy định bởi chuẩn AES.

Code giải mã :
```python=
from Cryptodome.Util.number import *
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],
]
inv_mix_columns(state)
inv_shift_rows(state)
print("".join([chr(j) for i in state for j in i]))
```
```crypto{d1ffUs3R}```
## Bringing It All Together
```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'
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
# Initial add round key step
for i in range(N_ROUNDS - 1, 0, -1):
pass # Do round
# Run final round (skips the InvMixColumns step)
# Convert state matrix to plaintext
return plaintext
# print(decrypt(key, ciphertext))
```
Ở đây ta có thêm một bước đó là KeyExpand. Độ dài khóa càng tăng thì ta lặp qua càng nhiều vòng AES. Ta viết hàm decrypt theo từng bước như các bài ở trên.
```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 add_round_key(s, k):
for i in range(4):
for j in range(4):
s[i][j]^=k[i][j]
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. """
res = ""
for i in range(4):
for j in range(4):
res += chr(matrix[i][j])
return res
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]
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 inv_sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = inv_s_box[s[i][j]]
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
text = bytes2matrix(ciphertext)
# Initial add round key step
add_round_key(text, round_keys[10])
for i in range(N_ROUNDS - 1, 0, -1):
inv_shift_rows(text)
inv_sub_bytes(text)
add_round_key(text, round_keys[i])
inv_mix_columns(text)
# Run final round (skips the InvMixColumns step)
inv_shift_rows(text)
inv_sub_bytes(text)
add_round_key(text, round_keys[0])
# Convert state matrix to plaintext
plaintext = matrix2bytes(text)
return plaintext
print(decrypt(key, ciphertext))
```
```crypto{MYAES128}```
# Symmetric Starter
## Modes of Operation Starter
Trong thực tế, không phải bản rõ nào cũng có độ dài cố định mà chúng có thể có những độ dài khác nhau. Vì vậy ta cần thiết kế nhiều modes khác nhau cho block cipher để xử lí các plaintext như vậy.
Source code của bài như sau:
```python=
from Crypto.Cipher import AES
KEY = ?
FLAG = ?
@chal.route('/block_cipher_starter/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('/block_cipher_starter/encrypt_flag/')
def encrypt_flag():
cipher = AES.new(KEY, AES.MODE_ECB)
encrypted = cipher.encrypt(FLAG.encode())
return {"ciphertext": encrypted.hex()}
```
Nhập URL sau vào trình duyệt https://aes.cryptohack.org/block_cipher_starter/decrypt/a3c22d1bfcccd351b995f478a962da376a2cb1620cfda8c02aa6befcda831698/ sau đó nó sẽ trả về plaintext ở dạng hex
```{"plaintext":"63727970746f7b626c30636b5f633170683372355f3472335f663435375f217d"}```
Việc tiếp theo cần làm là decode mã hex để ra flag
```python=
plaintext="63727970746f7b626c30636b5f633170683372355f3472335f663435375f217d"
flag=bytes.fromhex(plaintext).decode()
print(flag)
```
```crypto{bl0ck_c1ph3r5_4r3_f457_!}```
## Passwords as Keys
Các keys nên được sinh ra một cách ngẫu nhiên và không dễ đoán. Thông thường người ta sử dụng một bộ sinh số giả ngẫu nhiên (cryptographically-secure pseudorandom number generator (CSPRNG)) để tạo khóa.
Source code của bài:
```python=
from Crypto.Cipher import AES
import hashlib
import random
# /usr/share/dict/words from
# https://gist.githubusercontent.com/wchargin/8927565/raw/d9783627c731268fb2935a731a618aa8e95cf465/words
with open("/usr/share/dict/words") as f:
words = [w.strip() for w in f.readlines()]
keyword = random.choice(words)
KEY = hashlib.md5(keyword.encode()).digest()
FLAG = ?
@chal.route('/passwords_as_keys/decrypt/<ciphertext>/<password_hash>/')
def decrypt(ciphertext, password_hash):
ciphertext = bytes.fromhex(ciphertext)
key = bytes.fromhex(password_hash)
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('/passwords_as_keys/encrypt_flag/')
def encrypt_flag():
cipher = AES.new(KEY, AES.MODE_ECB)
encrypted = cipher.encrypt(FLAG.encode())
return {"ciphertext": encrypted.hex()}
```
Ta để ý dòng này:
```python=
keyword = random.choice(words)
KEY = hashlib.md5(keyword.encode()).digest()
```
KEY được sinh ra bằng cách chọn ngẫu nhiên một từ trong đường link ở trên rồi lấy hash md5 của nó.
Vậy thì ta chỉ còn cách là brute force đống words ở trên cho tới khi tìm ra key ban đầu :)
```python=
import requests
from Cryptodome.Cipher import AES
import hashlib
WORDS_URL = "https://gist.githubusercontent.com/wchargin/8927565/raw/d9783627c731268fb2935a731a618aa8e95cf465/words"
response = requests.get(WORDS_URL)
words = response.text.splitlines()
ciphertext_hex = "c92b7734070205bdf6c0087a751466ec13ae15e6f1bcdd3f3a535ec0f4bbae66"
ciphertext = bytes.fromhex(ciphertext_hex)
for word in words:
key = hashlib.md5(word.encode()).digest()
cipher = AES.new(key, AES.MODE_ECB)
plaintext = cipher.decrypt(ciphertext)
if b"crypto" in plaintext.lower():
print(f"key: {word}")
print(f"flag: {plaintext.decode(errors='ignore')}")
break
```
```crypto{k3y5__r__n07__p455w0rdz?}```
# Block Ciphers 1
Có một link git rất hay giải thích về block cipher : https://github.com/ashutosh1206/Crypton/tree/master/Block-Cipher
## ECB CBC WTF
Source code của bài
```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}
```
Đầu tiên ta cần hiểu rõ về hai modes CBC và ECB
CBC là viết tắt của Cipher Block Chaning. Minh họa:


Quan sát sơ đồ thì ta thấy: Plaintext được chia thành các block khác nhau. Ở block đầu tiên ta sẽ XOR nó với IV. Mỗi block plaintext sau đó ta sẽ XOR nó với block ciphertext trước nó. Điều này làm cho các cipher block phía sau dựa vào cipher block phía trước
ECB là viết tắt của Electronic codebook


Ta sẽ mã hóa từng khối riêng lẻ, sau đó nối chúng lại với nhau.
Nhược điểm của ECB là khi plaintext giống nhau thì nó sẽ ra kết quả giống nhau.
Quay trở lại bài tập. Đọc sơ qua source code thì ta thấy có dòng sau:
```python=
ciphertext = iv.hex() + encrypted.hex()
```
Ý tưởng của ta là như sau: Đầu tiên ta lấy ciphertext từ API và chia nó thành các block với độ dài là 16 bytes. Trong đoạn code ở trên thì 16 bytes đầu tiên chính là iv. 2 block kế tiếp chính là ciphertext. Như vậy, ta sẽ gửi lần lượt 2 block ở trên tới server để decrypt bằng mode ECB. Mode ECB như ta đã biết thì cùng plaintext sẽ cho ta ciphertext giống nhau.
\begin{gather*}
C1=AES( P1\oplus IV)\\
C2=AES( P2\oplus C1)\\
C3=AES( P3\oplus C2)
\end{gather*}
Sau đó ghép 3 ciphertext lại. Ta chỉ cần làm ngược lại quá trình trên là được.
Code giải:
```python=
import requests
import json
url="https://aes.cryptohack.org/ecbcbcwtf"
response=requests.get(f"{url}/encrypt_flag/")
ciphertext=bytes.fromhex(response.json()["ciphertext"])
iv=ciphertext[:16]
c1=ciphertext[16:32]
c2=ciphertext[32:48]
def decrypt_block(cipher):
response=requests.get(f"{url}/decrypt/{cipher.hex()}")
return bytes.fromhex(response.json()["plaintext"])
def xor(a,b):
return bytes([x^y for x,y in zip(a,b)])
p1_ecb=decrypt_block(c1)
p2_ecb=decrypt_block(c2)
# Ở đây ta decrypt bằng mode ECB nhưng plaintext ban đầu vẫn được XOR theo mode CBC nên ta cần XOR thêm 1 lần nữa
p1=xor(p1_ecb,iv)
p2=xor(p2_ecb,c1)
flag=p1+p2
print(flag)
```
```crypto{3cb_5uck5_4v01d_17_!!!!!}```
## ECB Oracle
Source code:
```python=
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
KEY = ?
FLAG = ?
@chal.route('/ecb_oracle/encrypt/<plaintext>/')
def encrypt(plaintext):
plaintext = bytes.fromhex(plaintext)
padded = pad(plaintext + FLAG.encode(), 16)
cipher = AES.new(KEY, AES.MODE_ECB)
try:
encrypted = cipher.encrypt(padded)
except ValueError as e:
return {"error": str(e)}
return {"ciphertext": encrypted.hex()}
```
Bài này yêu cầu ta tương tác với ECB Oracle. Đọc sơ qua source code thì ta thấy chương trình không có hàm để decrypt. Vậy ta phải làm thế nào để lấy được flag? Thực ra có một attack gọi là Byte at a Time Attack. Cụ thể như sau:

ECB Mode mã hóa độc lập từng block. Như vậy, mỗi lần ta sẽ gửi lên server một xâu có độ dài là 15 bytes, chẳng hạn AAAAAAAAAAAAAAA. Sau đó server pad thêm vào và chia plaintext thành các block. Block đầu tiên của ta lúc này sẽ có dạng AAAAAAAAAAAAAA(flag[0]). Như vậy sau khi có được ciphertext thì block đầu tiên của ta chính là encrypt của block AAAAAAAAAAAAAA(flag[0]). Việc cần làm lúc này là đi tìm kí tự flag[0] bằng việc thử gửi liên tiếp các block AAAAAAAAAAAAAAA,AAAAAAAAAAAAAAB,..... để xem cái nào khớp với AAAAAAAAAAAAAA(flag[0]). Đây chính là ý tưởng chính của bài. Tối ưu hơn là ta đã biết các kí tự đầu của flag là "crypto{"
Code giải:
```python=
import requests
import json
import binascii
import string
url="https://aes.cryptohack.org/ecb_oracle/encrypt/"
flag="crypto{"
def encrypt(pt):
p=binascii.hexlify(pt).decode()
r=requests.get(url+p)
ct=(json.loads(r.text))["ciphertext"]
return ct
def find_flag():
global flag
while True:
payload=b'1'*(31-len(flag))
ref=encrypt(payload)
for i in string.printable:
enc=encrypt(payload+flag.encode()+i.encode())
if ref[32:64]==enc[32:64]:
flag+=i
print("flag letter: ",i)
break
if flag[-1]=='}':
break
print(flag)
find_flag()
```
## Flipping Cookie
Source code:
```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}
```
Phân tích source code 1 chút: Bài này ta quay trở lại với mode CBC. Đầu tiên server yêu cầu nhập vào 2 giá trị cookie và iv
```python=
def check_admin(cookie, iv):
cookie = bytes.fromhex(cookie)
iv = bytes.fromhex(iv)
```
Và bằng cách nào đó ta thay admin=False thành admin=True trong plaintext ban đầu thì server sẽ trả về flag. Attack này được gọi là CBC Bit Flipping Attack.
Giải thích Attack: Trong CBC mode thì block đầu tiên được XOR với iv, nếu ta chỉnh sửa được iv thì ta có thể chỉnh sửa được đầu ra.
Tham khảo thêm tại:
https://masterpessimistaa.wordpress.com/2017/05/03/cbc-bit-flipping-attack/
Ý tưởng giải bài này sẽ là: Đầu tiên ta request tới server để lấy ciphertext gồm cookie+iv ban đầu. Sau đó ta sẽ chỉnh iv lại theo ý ta muốn. IV của ta khi chỉnh lại sẽ là $\displaystyle IV'=IV\oplus false\oplus true$.
Code giải :
```python=
import requests
import binascii
url="https://aes.cryptohack.org/flipping_cookie/"
get_cookie_url = url + "get_cookie/"
check_admin_url = url + "check_admin/"
def get_cookie():
r=requests.get(get_cookie_url)
return r.json()["cookie"]
original=b"False"
flipped=b"True;"
def flip_iv(iv):
check_iv=bytes.fromhex(iv)
new_iv=bytearray(check_iv)
for i in range(len(original)):
new_iv[i + 6] ^= original[i] ^ flipped[i]
return binascii.hexlify(new_iv).decode()
def check_admin(cookie_hex,iv_hex):
response=requests.get(f"{check_admin_url}/{cookie_hex}/{iv_hex}").json()
return response
if __name__ == "__main__":
cookie=get_cookie()
cookie_hex=cookie[32:]
iv_hex=cookie[:32]
iv_flipped=flip_iv(iv_hex)
respone=check_admin(cookie_hex,iv_flipped)
print(respone)
```
```crypto{4u7h3n71c4710n_15_3553n714l}```
## Lazy CBC
Source code của bài :
```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"}
```
Phân tích một chút: Để lấy được flag thì ta cần nhập đúng key:
```python=
def get_flag(key):
key = bytes.fromhex(key)
if key == KEY:
return {"plaintext": FLAG.encode().hex()}
else:
return {"error": "invalid key"}
```
Nhưng làm sao để tìm được key. Ta để ý khi gọi lên CBC Mode thì bài đã cho key và iv trùng nhau :
```python=
cipher = AES.new(KEY, AES.MODE_CBC, KEY)
```
Cho nên ta có thể dùng Key Recovery Attack CBC: https://bernardoamc.com/ecb-iv-as-key/
Các bước Attack có đề cập ở trong bài viết ở trên:


Từ phép XOR như dưới đây thì ta có thể tìm lại key : bằng cách cho C2=000..00
$\displaystyle \begin{array}{{>{\displaystyle}l}}
P1=DecryptAES( C1) \oplus IV\\
P3=DecryptAES( C3) \oplus C2=DecryptAES( C3)\\
\Longrightarrow P1\oplus P3=IV
\end{array}$
Code giải:
```python=
import requests
url="https://aes.cryptohack.org/lazy_cbc/"
def xor(a,b):
return bytes(x^y for x,y in zip(a,b))
def encrypt(plaintext):
r=requests.get(f"{url}/encrypt/{plaintext}").json()["ciphertext"]
return bytes.fromhex(r)
def get_flag(key):
response=requests.get(f"{url}/get_flag/{key}").json()
return response
def receive(ciphertext):
response=requests.get(f"{url}/receive/{ciphertext}").json()
return response
plaintext_hex="1122334455667788112233445566778811223344556677881122334455667788" # chia làm 3 block
ciphertext=encrypt(plaintext_hex)
ciphertext1=ciphertext[:16]
ciphertext2=ciphertext[16:32]
ciphertext3=ciphertext[32:]
new_ciphertext=ciphertext1+(b"\00"*16)+ciphertext1
new_ciphertext_hex=new_ciphertext.hex()
response=receive(new_ciphertext_hex)
if "error" in response and "Invalid plaintext" in response["error"]:
invalid_plaintext_hex=response["error"].split(": ")[1]
invalid_plaintext_hex_bytes=bytes.fromhex(invalid_plaintext_hex)
plaintext1,plaintext3=invalid_plaintext_hex_bytes[:16],invalid_plaintext_hex_bytes[32:48]
key=xor(plaintext1,plaintext3).hex()
flag=get_flag(key)
if "plaintext" in flag:
flag=bytes.fromhex(flag["plaintext"]).decode()
print(flag)
```
```crypto{50m3_p30pl3_d0n7_7h1nk_IV_15_1mp0r74n7_?}```
## Triple DES
Source code của bài:
```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())
```
Đầu tiên ta nói một chút về thuật mã hóa DES và 3DES
Tham khảo thêm tại link sau:
https://www.youtube.com/watch?v=j53iXhTSi_s&list=PLBlnK6fEyqRiOCCDSdi6Ok_8PU2f_nkuf&index=2
Thuật toán DES được sử dụng để mã hóa và giải mã các block (khối) dữ liệu 64 bit dựa trên một key (khóa mã) 64 bit. Khóa dùng trong DES có độ dài toàn bộ là 64 bit. Tuy nhiên chỉ có 56 bit thực sự được sử dụng; 8 bit còn lại chỉ dùng cho việc kiểm tra. Vì thế, độ dài thực tế của khóa chỉ là 56 bit. Chú ý, các block được đánh số thứ tự bit từ trái sang phải và bắt đầu từ 1, bit đầu tiên bên trái là bit số 1 và bit cuối cùng bên phải là bit số 64. Quá trình giải mã và mã hóa sử dụng cùng một key nhưng thứ tự phân phối các giá trị các bit key của quá trình giải mã ngược với quá trình mã hóa.

Sơ đồ giải thuật cho DES:

Ta sẽ tóm tắt sơ qua từng bước từ trên xuống:
Đầu tiên là bước hoán vị khởi tạo - IP. Hoán vị ở đây là làm thay đổi vị trí các bit trong một chuỗi giá trị nhưng không làm thay đổi giá trị của các bit này. Các bit này sẽ được hoán vị theo sơ đồ như sau:

Tức là ta sẽ viết plaintext thành 64 bit lần lượt từ trái sang phải. Sau đó ta sẽ lấy tương ứng từng vị trí của các bit. Ví dụ bit thứ 58 trong plaintext ban đầu thì ta sẽ chuyển thành bit đầu tiên. Và cứ như vậy làm tương tự với các bit khác. Sau hoán vị, chuỗi bit mới được phân ra làm hai đoạn, mỗi đoạn 32 bit để bắt đầu vào quy trình tính toán mã hóa với key. Đoạn bên trái ký hiệu là L, đoạn bên phải ký hiệu là R. Đoạn L gồm các bit từ bit số 1 đến bit số 32, đoạn R gồm các bit từ bit số 33 đến bit số 64.
Quan sát trên sơ đồ thì cặp L,R đầu tiên được đánh số là L0,R0. Đoạn L của vòng sau sẽ là đoạn R của vòng trước đó. Còn đoạn R của vòng sau sẽ chính là đoạn L của vòng trước đó XOR với đoạn R trước đó sau khi thực hiện tính toán. Cụ thể thì
\begin{gather*}
L_{n+1} =R_{n}\\
R_{n+1} =L_{n} \oplus f( R_{n} ,K_{n+1})
\end{gather*}
Tiếp theo là hàm mã hóa $f(R,K)$

Đầu tiên, 32 bit của đoạn R được đánh số từ 1 đến 32 theo thứ tự từ trái qua phải. Giá trị này sẽ được chuyển đổi thông qua bảng tra E để tạo thành một giá trị 48 bit. Bit đầu tiên trong chuỗi giá trị 48 bit là bit số 32 của R, bit thứ 2 là bit số 1 của R, bit thứ 3 là bit số 2 của R và bit cuối cùng là bit số 1 của R.
.....
Sơ đồ thuật toán 3DES

3DES đơn giản là chế độ mã hóa DES mã hóa plaintext 3 lần khác nhau với 3 khóa riêng biệt. Đầu tiên là mã hóa với khóa K1, sau đó giải mã với khóa K2 và cuối cùng là mã hóa lại với khóa K3.
Sau một hồi mò tài liệu về DES và 3DES thì mình biết được một khái niệm trong DES gọi là Weak keys. Thì đại loại weak keys là các key có cấu trúc đặc biệt trong DES mà sau khi mã hóa 2 lần thì quay trở lại plaintext ban đầu. Lý do có những khóa như vậy mình sẽ giải thích ở bước tạo khóa vòng(cập nhật sau)
Ví dụ một khóa yếu sẽ là 0101010101010101.
Tham khảo thêm : https://en.wikipedia.org/wiki/Weak_key#:~:text=DES%20weak%20keys%20produce%20sixteen,%2B%20'E'%20(0xFEFEFEFEFEFEFEFE)
Giải: Chọn weak key 0000000000000000FFFFFFFFFFFFFFFF

Sau đó encrypt 1 lần nữa cùng với key đó:

Được plaintext là : 63727970746f7b6e30745f346c6c5f6b3379735f3472335f673030645f6b3379737d060606060606
Cuối cùng là flag:
```python=
flag="63727970746f7b6e30745f346c6c5f6b3379735f3472335f673030645f6b3379737d060606060606"
print(bytes.fromhex(flag))
```
```crypto{n0t_4ll_k3ys_4r3_g00d_k3ys}```
# Stream Ciphers
Sơ đồ giải thích đơn giản về stream ciphers

Đầu vào là plaintext, khóa dòng được tạo ra từ keystream generator bằng một secret key. Sau đó plaintext sẽ XOR với khóa này và đầu ra là ciphertext.
Điểm khác biệt giữa Stream Ciphers và Block Ciphers đó là thay vì xử lí từng khối như Block Ciphers thì Stream Ciphers mã hóa trên từng bit hoặc từng bytes một cách liên tục.
Có một số modes AES giúp ta chuyển từ block ciphers về stream ciphers ví dụ như OFB, CTR, CFB. Còn cụ thể sao thì mình sẽ giải thích trong phần bài tập:
## Symmetry
Source code của bài:
```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}
```
Một số hàm trong source:
```python=
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}
```
Mã hóa theo mode OFB. Sơ đồ mã hóa OFB như dưới đây:

Đầu vào của block 0 là IV và sau đó được mã hóa AES với một key. Đầu ra sau đó được XOR với plaintext. Tiếp đó ở block tiếp theo thì đầu vào sẽ là block trước đó.
Điểm khác biệt giữa mode OFB và mode CFB nằm ở đầu vào khi tạo Block Cipher

Ở mode CFB thì đầu vào của block đầu tiên vẫn là IV sau đó được mã hóa AES hệt như OFB. Nhưng kể từ các block kế tiếp thì đầu vào của block sẽ là ciphertext trước đó.
Ngoài ra còn một điểm đặc biệt của OFB nữa đó chính là
hàm giải mã cũng đồng thời là hàm mã hóa.

Ở bài tập trên thì ta để ý dòng:
```python=
ciphertext = iv.hex() + encrypted.hex()
```
Ta lấy được iv và enc(flag) từ server. Output của block cipher ở đây sẽ đóng vai trò là keystream và sẽ được XOR với plaintext. Vậy các bước để giải bài tập trên sẽ là: Đầu tiên requests tới server để lấy iv và flag. Sau khi có iv và enc(flag) ta lấy iv tìm được gửi cùng với plaintext dạng 000..00 để lấy keystream. Sau khi có được keystream thì ta xor lại với enc(flag) để ra flag ban đầu
Code giải:
```python=
import requests
import json
from binascii import unhexlify,hexlify
def xor_bytes(a,b):
return bytes(x^y for x,y in zip(a,b))
url = "https://aes.cryptohack.org/symmetry"
def get_iv_flag():
response = requests.get(f"{url}/encrypt_flag/")
r=json.loads(response.text)["ciphertext"]
iv_hex=r[:32]
enc_flag=unhexlify(r[32:])
return iv_hex,enc_flag
def get_key_stream(iv_hex,flag_len):
plaintext_hex="00" * flag_len
response=requests.get(f"{url}/encrypt/{plaintext_hex}/{iv_hex}")
encrypt=unhexlify(json.loads(response.text)["ciphertext"])
return encrypt
def main():
iv_hex,enc_flag=get_iv_flag()
key_stream=get_key_stream(iv_hex,len(enc_flag))
flag=xor_bytes(enc_flag,key_stream)
sol_flag=flag.decode()
print(sol_flag)
if __name__ == "__main__":
main()
```
```crypto{0fb_15_5ymm37r1c4l_!!!11!}```
## Bean Counter
Source code của bài:
```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)}
```
Nôm na thì bài mình hiểu là ta sẽ mã hóa một file ảnh PNG chứa flag bằng ECB mode cùng với thiết lập StepUpCounter.
Code giải:
```python=
import requests
import json
from binascii import unhexlify
def xor_bytes(a, b):
return bytes(x ^ y for x, y in zip(a, b))
url = "https://aes.cryptohack.org/bean_counter"
def get_encrypted_file():
return unhexlify(json.loads(requests.get(f"{url}/encrypt/").text)["encrypted"])
def decrypt_image():
encrypted_data = get_encrypted_file()
keystream = xor_bytes(encrypted_data[:16], bytes.fromhex("89504E470D0A1A0A0000000D49484452"))
decrypted_data = b''.join(xor_bytes(encrypted_data[i:i+16], keystream) for i in range(0, len(encrypted_data), 16))
with open("decrypted_bean_flag.png", "wb") as f:
f.write(decrypted_data)
print("Done")
if __name__ == "__main__":
decrypt_image()
```
Sau đó mình xài lệnh ```eog decrypted_bean_flag.png``` xem flag:

```crypto{hex_bytes_beans}```
## CTRIME
Source code của bài:
```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()}
```
Nói một chút về CTR Mode:
CTR Mode là viết tắt của Counter Mode. CTR là chế độ mã hóa sử dụng một tập các khối ngõ vào, gọi là các counter, để sinh ra một tập các giá trị ngõ ra thông qua một thuật toán mã hóa. Sau đó, giá trị ngõ ra sẽ được XOR với plaintext để tạo ra ciphertext trong quá trình mã hóa, hoặc XOR với ciphertext để tạo ra plaintext trong quá trình giải mã.
Lưu đồ minh họa: https://xilinx.github.io/Vitis_Libraries/security/2020.1/guide_L1/internals/chacha20.html

Tiếp tục với bài tập ở trên: Ta chú ý đến dòng
```python=
encrypted = cipher.encrypt(zlib.compress(plaintext + FLAG.encode()))
```
Về thuật toán nén của zlib thì mọi người có thể tham khảo tại đây: https://www.euccas.me/zlib/
Khi gửi plaintext đến server thì server sẽ trả về ciphertext được encrypt bằng block cipher. Nhưng trước khi encrypt thì dữ liệu đã được nén bằng zlib. zlib có một tính chất đặc biệt đó chính là nó sẽ nén một chuỗi lại bằng cách loại bỏ đi các chuỗi con giống nhau. Bây giờ ta sẽ bruteforce một chút, bằng cách gửi tới server encrypt một plaintext rỗng. Sau đó ta sẽ thử lại bằng cách gửi lần lượt từng chữ cái trong bảng chữ cái và so sánh độ dài với len(encrypted) khi gửi plaintext rỗng. Ở đây thay vì gửi plaintext rỗng thì ta biết được từ đề bài thì flag có format crypto{... nên có thể rút ngắn quá trình bruteforce.
Code cụ thể
```python=
import requests
import string
url = "https://aes.cryptohack.org/ctrime/encrypt/"
candidates = string.ascii_letters + string.digits + "{}_!?"
flag = "crypto{"
while True:
min_len = float("inf")
best_char = ""
for c in candidates:
test_plaintext = (flag + c).encode().hex()
r = requests.get(url + test_plaintext + "/").json()
cipher_len = len(r["ciphertext"])
if cipher_len < min_len:
min_len = cipher_len
best_char = c
flag += best_char
print(f"FLAG progress: {flag}")
if best_char == "}":
break
```
Bug :v
Oke lúc đầu mình nghỉ v là oke nhưng sau khi chạy được 1 lúc lâu thì nó ra như này:

Có lẽ là mình đã hiểu sai về zlib? Sau đó mình thử lại bằng cách thay Flag từ "crypto{", bổ sung thêm thành "crypto{CRIM" coi sao.

Kết quả vẫn vậy.
Mình có chạy thử đoạn code này để thử kiểm tra lại tính chất của zlib.compress()
```python=
import zlib
known_flag="crypto{CRIM"
known_flag=known_flag.encode()
print(len(zlib.compress(known_flag)))
print(len(zlib.compress(known_flag+b'crypto{CRIM')))
print(len(zlib.compress(known_flag+b'crypto{CRIN')))
print(len(zlib.compress(known_flag+b'crypto{CRIK')))
```

Hướng tiếp cận của mình ban đầu là sử dụng tính chất của zlib.compress(). Hàm này có một đặc điểm là nó sẽ rút gọn bớt đi các ký tự trùng lặp, sau đó sẽ nén lại. Như ví dụ ở trên thì nếu ta điền đúng kí tự M thì nó sẽ ngắn hơn nếu ta dùng sai các kí tự như N,K.
Ý tưởng có vẻ khá giống bài sau :
https://github.com/apoirrier/CTFs-writeups/blob/master/PicoCTF/Crypto/CompressAndAttack.md
Mình thử code giống ý tưởng trên và ra flag :)) (mặc dù chạy hơi lâu)
```python=
import requests
url = "https://aes.cryptohack.org/ctrime/encrypt/"
alphabet = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!{}_?"
def response(plaintext):
return bytes.fromhex(requests.get(url + plaintext.hex() + "/").json()["ciphertext"])
flag = b"crypto{CRIME"
length = len(response(flag))
while not flag.endswith(b"}"):
for c in alphabet:
print(chr(c))
if len(response(flag + bytes([c]))) == length:
flag += bytes([c])
print(flag.decode())
break
print(f"FLAG FOUND: {flag.decode()}")
```
Lúc đầu mình để flag là crypto{CRIM nhưng nó brute force k ra được kí tự tiếp theo. Sau đó mình để luôn crypto{CRIME vào rồi brute force (mình tìm hiểu thì bài này khá giống với compression side-channel attack và có một attack gọi là SSL CRIME attacks).
Cách khác (chatGPT :) mình vẫn chưa hiểu sao nó chạy được, đại loại thì việc nhân lên giúp tăng hiệu ứng nén của zilb.compress()):
```python=
import requests
alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ_{}0123456789abcdefghijklmnopqrstuvwxyz"
def encrypt(plaintext):
url = f"https://aes.cryptohack.org/ctrime/encrypt/{plaintext.hex()}/"
return requests.get(url).json()["ciphertext"]
flag = b"crypto{"
last_chr = b""
while last_chr != b"}":
base_cipher_len = len(encrypt((flag + b"*") * 5))
for c in alpha:
if len(encrypt((flag + c.encode()) * 5)) < base_cipher_len:
flag += c.encode()
last_chr = c.encode()
print(flag.decode())
break
print(f"FLAG FOUND: {flag.decode()}")
```
```crypto{CRIME_571ll_p4y5}```
## Logon Zero
Source code của bài:
```python=
#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.number import bytes_to_long
from os import urandom
from utils import listener
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.'}
import builtins; builtins.Challenge = Challenge # hack to enable challenge to be run locally, see https://cryptohack.org/faq/#listener
listener.start_server(port=13399)
```
Server: ```socket.cryptohack.org 13399```
Phân tích: Bài này source code khá dài
Trước hết thì qua source trên ta biết mode encrypt là CFB8. Còn cụ thể về mode này thì mọi người có thể xem qua ở đây https://crypto.stackexchange.com/questions/79654/what-is-the-difference-between-cfb-and-cfb1-and-cfb8-like-openssls-aria-128. Đại loại thì mode CFB8 mỗi lần chỉ xử lý 1 byte (8 bit) thay vì cả một block 16 byte của AES.
Tiếp theo là class challenge:
```python=
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))
```
Thì để lấy được flag ta cần nhập đúng password:
```python=
if your_password.encode() == self.password:
self.exit = True
return {'msg': 'Welcome admin, flag: ' + FLAG}
else:
return {'msg': 'Wrong password.'}
```
Nhưng password được khởi tạo random nên việc bruteforce gần như bất khả thi :v
Ta đọc tiếp để xem có cách nào exploit không
```python=
if your_input['option'] == 'reset_connection':
self.cipher = CFB8(urandom(16))
return {'msg': 'Connection has been reset.'}
```
Ở option này khi ta chọn reset_connection thì server sẽ tạo lại block cipher CFB8 mới với một khóa khác.
```python=
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.'}
```
Đây có lẽ là phần quan trọng nhất của bài. Nếu ta chọn option reset_password thì server yêu cầu ta gửi tới thêm một token. Ta cần nhập một token có độ dài lớn hơn 28 và sau đó server sẽ giải mã và trích xuất một mật khẩu mới từ nó. Ok vậy thì mục tiêu của ta là tìm một token, để sau khi server giải mã nó thì ta có thể đoán được pass. Để ý dòng :
```python=
self.password_length = bytes_to_long(token[-4:])
```
Server sẽ lấy 4 bytes cuối token sau giải mã làm độ dài của mật khẩu. Nếu ta bằng cách nào đó làm cho 4 bytes cuối là ```b'\x00\x00\x00\x00'``` thì password mới sẽ là ```''``` rỗng.
...
Thử research một chút:

https://viblo.asia/p/tim-hieu-ve-zerologon-WAyK8rk9lxX
Đề bài có gợi ý cho ta về CVE Zerologon.
Ý tưởng chính giải bài như sau:
Ta nói rõ về mode CFB : CFB là chế độ mã hóa mà ciphertext của lần mã hóa hiện tại sẽ được phản hồi (feedback) đến đầu vào của lần mã hóa tiếp theo. Nghĩa là, ciphertext của lần mã hóa hiện tại sẽ được sử dụng để tính toán ciphertext của lần mã hóa kế tiếp. Mô tả có vẻ giống CBC nhưng quá trình trực hiện lại khác.

Ở lần đầu tiên, một IV được khởi tạo ngẫu nhiên, sau đó qua chế độ AES và đầu ra sẽ được XOR với Plaintext. Điểm khác biệt của CFB so với các chế độ khác đó chính là khi vector khởi tạo IV đầu tiên được mã hóa AES để tạo ra một khối dữ liệu b bit, thì ta chỉ lấy s bit trong số b bit đó để XOR với plaintext chứ không lấy toàn bộ. Lưu ý rằng ta sẽ lấy ra s bit MSB của kết quả encrypt so với s bit của plaintext. Lưu ý $\displaystyle 1\leqslant s\leqslant b$ và tùy thuộc vào độ dài của $s$ ta sẽ có các mode CFB khác nhau. Hơi khó hiểu thì mọi người có thể tham khảo tại đây: https://www.youtube.com/watch?v=c_9MABuOVJI
Nói cách khác $s$ chính là độ dài của plaintext và ciphertext cho một lần mã hóa/giải mã.
Với CFB - 8 thì chỉ đơn giản là ta lấy $s=8$. Như hình dưới đây:

Bây giờ mấu chốt của bài nằm ở đây:

Ta gửi payload ```token = b'\x00' * 28``` tới server. Dựa vào ý tưởng ở trên, lúc này sẽ có 1/256 xác suất rằng key mới sẽ khiến ```AES.encrypt(state)[0] = 0x00```, từ đó cho phép ta reset mật khẩu về rỗng.
Code exploit:
```python=
import json
from pwn import *
address=("socket.cryptohack.org",13399)
token=b"\x00" * 28
while True:
try:
conn = remote(address[0],address[1])
print(conn.recvline().decode())
resetPassword=json.dumps({"option": "reset_password","token":token.hex()}).encode()
authenticate=json.dumps({"option":"authenticate","password": ""}).encode()
resetconnect=json.dumps({"option":"reset_connection"}).encode()
while True:
conn.sendline(resetPassword)
conn.recvline(timeout=5)
conn.sendline(authenticate)
flag=conn.recvline(timeout=5).decode()
if "crypto{" in flag:
print(flag)
conn.close()
exit(0)
conn.sendline(resetconnect)
conn.recvline(timeout=5)
except Exception as e:
print(error)
```
```crypto{Zerologon_Windows_CVE-2020-1472}```
## Stream of Consciousness
Source code của bài:
```python=
from Crypto.Cipher import AES
from Crypto.Util import Counter
import random
KEY = ?
TEXT = ['???', '???', ..., FLAG]
@chal.route('/stream_consciousness/encrypt/')
def encrypt():
random_line = random.choice(TEXT)
cipher = AES.new(KEY, AES.MODE_CTR, counter=Counter.new(128))
encrypted = cipher.encrypt(random_line.encode())
return {"ciphertext": encrypted.hex()}
```
## Dancing Queen
Source code của bài(dài vl):
```python=
#!/usr/bin/env python3
from os import urandom
FLAG = b'crypto{?????????????????????????????}'
def bytes_to_words(b):
return [int.from_bytes(b[i:i+4], 'little') for i in range(0, len(b), 4)]
def rotate(x, n):
return ((x << n) & 0xffffffff) | ((x >> (32 - n)) & 0xffffffff)
def word(x):
return x % (2 ** 32)
def words_to_bytes(w):
return b''.join([i.to_bytes(4, 'little') for i in w])
def xor(a, b):
return b''.join([bytes([x ^ y]) for x, y in zip(a, b)])
class ChaCha20:
def __init__(self):
self._state = []
def _inner_block(self, state):
self._quarter_round(state, 0, 4, 8, 12)
self._quarter_round(state, 1, 5, 9, 13)
self._quarter_round(state, 2, 6, 10, 14)
self._quarter_round(state, 3, 7, 11, 15)
self._quarter_round(state, 0, 5, 10, 15)
self._quarter_round(state, 1, 6, 11, 12)
self._quarter_round(state, 2, 7, 8, 13)
self._quarter_round(state, 3, 4, 9, 14)
def _quarter_round(self, x, a, b, c, d):
x[a] = word(x[a] + x[b]); x[d] ^= x[a]; x[d] = rotate(x[d], 16)
x[c] = word(x[c] + x[d]); x[b] ^= x[c]; x[b] = rotate(x[b], 12)
x[a] = word(x[a] + x[b]); x[d] ^= x[a]; x[d] = rotate(x[d], 8)
x[c] = word(x[c] + x[d]); x[b] ^= x[c]; x[b] = rotate(x[b], 7)
def _setup_state(self, key, iv):
self._state = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]
self._state.extend(bytes_to_words(key))
self._state.append(self._counter)
self._state.extend(bytes_to_words(iv))
def decrypt(self, c, key, iv):
return self.encrypt(c, key, iv)
def encrypt(self, m, key, iv):
c = b''
self._counter = 1
for i in range(0, len(m), 64):
self._setup_state(key, iv)
for j in range(10):
self._inner_block(self._state)
c += xor(m[i:i+64], words_to_bytes(self._state))
self._counter += 1
return c
if __name__ == '__main__':
msg = b'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula.'
key = urandom(32)
iv1 = urandom(12)
iv2 = urandom(12)
c = ChaCha20()
msg_enc = c.encrypt(msg, key, iv1)
flag_enc = c.encrypt(FLAG, key, iv2)
print(f"iv1 = '{iv1.hex()}'")
print(f"iv2 = '{iv2.hex()}'")
print(f"msg_enc = '{msg_enc.hex()}'")
print(f"flag_enc = '{flag_enc.hex()}'")
```
Và file output.txt
```
iv1 = 'e42758d6d218013ea63e3c49'
iv2 = 'a99f9a7d097daabd2aa2a235'
msg_enc = 'f3afbada8237af6e94c7d2065ee0e221a1748b8c7b11105a8cc8a1c74253611c94fe7ea6fa8a9133505772ef619f04b05d2e2b0732cc483df72ccebb09a92c211ef5a52628094f09a30fc692cb25647f'
flag_enc = 'b6327e9a2253034096344ad5694a2040b114753e24ea9c1af17c10263281fb0fe622b32732'
```
## Oh SNAP
Source code của bài:
```python=
from Crypto.Cipher import ARC4
FLAG = ?
@chal.route('/oh_snap/send_cmd/<ciphertext>/<nonce>/')
def send_cmd(ciphertext, nonce):
if not ciphertext:
return {"error": "You must specify a ciphertext"}
if not nonce:
return {"error": "You must specify a nonce"}
ciphertext = bytes.fromhex(ciphertext)
nonce = bytes.fromhex(nonce)
cipher = ARC4.new(nonce + FLAG.encode())
cmd = cipher.decrypt(ciphertext)
if cmd == b"ping":
return {"msg": "Pong!"}
else:
return {"error": f"Unknown command: {cmd.hex()}"}
```
# Padding Attacks
Solutions cho phần này: https://dvck13.notion.site/Padding-Attacks-23a2a63906a280ce9b3eedc69016608a
# Authenticated Encryption
## Paper Plane
Source code:
```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."}
```
Phân tích: Đầu tiên thì ta sẽ nói về mode Infinite Garble Extension. Link paper https://www.links.org/files/openssl-ige.pdf
IGE dựa trên công thức sau đây:
$$ c_i = f_K(m_i \oplus c_{i-1}) \oplus m_{i-1} $$
trong đó $\displaystyle f_{K}$ là hàm mã hóa (ví dụ AES Encryption) với khóa $\displaystyle K$ và $\displaystyle i$ chạy từ 1 tới $\displaystyle n$ là số lượng các plaintext blocks.
Sơ đồ:

Về cách khởi tạo các giá trị $c_{0},m_{0}$ mọi người có thể tham khảo ở paper trên.
Server trả về các giá trị của $m_{0},c_{0}$ và ciphertext. Đồng thời ta cũng được gửi tới server các bộ tương tự để server decrpyt.
Để giải được bài này thì ta cần hiểu về Padding Oracle Attack: https://robertheaton.com/2013/07/29/padding-oracle-attack/
Nhắc lại về mode CBC:

Để decrypt ngược lại trong mode CBC thì ta sẽ XOR ciphertext $C1$ trước đó với block ciphertext $C2$ tiếp theo sau khi được xử lí qua block cipher. Ta gọi $I2$ là đầu ra của Block Cipher Decryption với $C2$ thì ta có $I2 \oplus C1 = P2$ và $I2=P2\oplus C1$. Ý tưởng của attack đơn giản sẽ là như sau: Ta đã biết ciphertext $C1$, cho nên nếu như ta tính toán được $I2$ thì ta có thể khôi phục được plaintext $P2$. Server cho ta gửi lên ciphertext để decrypt và nó sẽ thông báo cho ta biết là plaintext có được padding một cách hợp lệ theo PKCS#7 hay không (đối với CBC Mode thì sẽ được pad theo PKCS#7)
Nói sơ qua về thuật padding PKCS#7
Demo:
```python=
from Cryptodome.Util.Padding import pad,unpad
plain=b"Hello,World"
pad_add=pad(plain,16)
print(pad_add)
```
Output sẽ là:

Đầu tiên ta sẽ đếm số bytes còn thiếu trong data gốc. Sau đó sẽ đệm thêm vào sau số lượng bytes còn thiếu, trong đó các bytes được đệm thêm sẽ chính là số bytes còn thiếu đó. Ví dụ như Hello,World có 11 bytes, thì 1 block size tiêu chuẩn của ta sẽ cần có 16 bytes, tức là còn thiếu 5 bytes nữa. Vậy khi ta gọi hàm pad, nó sẽ pad theo chuẩn PKCS#7 và pad vào đúng 5 bytes nữa : `b'\x05\x05\x05\x05\x05'`
Kể cả khi block đủ 16bytes thì hàm pad vẫn sẽ đệm thêm dữ liệu vào
```python=
from Cryptodome.Util.Padding import pad,unpad
plain=b"Hello,Worldheheh"
pad_add=pad(plain,16)
print(pad_add)
```
Output: `b'Hello,Worldheheh\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'`
Vậy bây giờ ta sẽ đi khôi phục lại $I2$ bằng ý tưởng này.
Chú ý dòng này:
```python=
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 được quyền gửi lên server các ciphertext và server sẽ kiểm tra xem padding có hợp lệ hay không.

Padding như trên thì sẽ được coi là hợp lệ. Nếu như chỉ có 6 bytes `b'\x07'` ở cuối thì nó sẽ là không hợp lệ.
Giả sử ciphertext được chia làm 2 block là $C1,C2$. Ta gửi lên server $C1'+C2$, trong đó $C1'$ là block đã được ta chỉnh sửa. Ta lấy bytes cuối của $C1'$ là `00` sau đó gửi lên server ciphertext tạo bởi $C1'+C2$ và nhận được plaintext đã giải mã là $P2'$. Nếu như server trả về kết quả padding là hợp lệ thì ta biết được bytes cuối của $P2'$ là `b'\0x01'` và ta sẽ dùng giá trị này XOR lại với `C1'[16]` để có được `I2[16]`

Minh họa:

Ví dụ như này thì padding không hợp lệ, ta sẽ thử từ 00,01,... cho tới hết để xem thử có hợp lệ không


Và cho các bytes là random

Ở đây mục tiêu của ta là tính $I2$ cho nên ta lấy 18 XOR 01 = 19 sẽ chính là bytest cuối của I2 mà ta cần tìm.
Toàn bộ quá trình diễn ra như sau:
Đầu tiên ta cho `C1'[16]=00`,sau đó sẽ bruteforce cho tới khi valid padding , thì bytes cuối của `P2'` sẽ là `\0x01` và ta sẽ XOR với `C1'[16]` để tính `I2[16]`. Tương tự để tính I2[15] ta cũng làm như vậy, ta điều chỉnh giá trị của C1'[16] XOR I2[16] = 02 rồi sau đó bruteforce C1'[15] cho tới khi P2'[15] cũng bằng 02

Bruteforce 00,01,...

Được 05 là giá trị hợp lí.
Mode IGE thì khác biệt 1 chút, mình sẽ dùng m0,c0 thay cho IV và ciphertext để giải block đầu

Code Exploit:
```python=
import json
import requests
from pwn import xor
from Cryptodome.Util.strxor import strxor
import logging
def encrypt_flag():
url="https://aes.cryptohack.org/paper_plane/encrypt_flag/"
response=requests.get(url)
response=json.loads(response.text)
return (
bytes.fromhex(response["ciphertext"]),bytes.fromhex(response["c0"]),
bytes.fromhex(response["m0"])
)
def send_msg(ciphertext,m0,c0):
url="https://aes.cryptohack.org/paper_plane/send_msg/"
url+=ciphertext.hex()+"/"+m0.hex()+"/"+c0.hex()+"/"
response=requests.get(url)
response=json.loads(response.text)
return "error" not in response
def attack_block(send_msg,p0,c0,c):
logging.info(f"Attacking block {c.hex()}")
r=bytes()
for i in reversed(range(16)):
s=bytes([16-i]*(16-i))
for b in range(256):
c0_=bytes(i)+strxor(s,bytes([b])+r)
if send_msg(c0_,p0,c):
r=bytes([b])+r
break
else:
logging.error("Failed to find byte")
return strxor(c0,r)
def attack(send_msg,p0,c0,c):
p=attack_block(send_msg,p0,c0,c[0:16])
for i in range(16,len(c),16):
p+=attack_block(send_msg,p[i-16:i],c[i-16:i],c[i:i+16])
return p
ciphertext,c0,m0=encrypt_flag()
c1=ciphertext[:16]
c2=ciphertext[16:]
p1=attack(send_msg,m0,c0,c1)
p2=attack(send_msg,c1,c0,c2)
print(p1+p2)
```
Tham khảo tại : https://github.com/jvdsn/crypto-attacks/blob/master/attacks/ige/padding_oracle.py
## Forbidden Fruit
Source code:
```python=
from Crypto.Cipher import AES
import os
IV = ?
KEY = ?
FLAG = ?
@chal.route('/forbidden_fruit/decrypt/<nonce>/<ciphertext>/<tag>/<associated_data>/')
def decrypt(nonce, ciphertext, tag, associated_data):
ciphertext = bytes.fromhex(ciphertext)
tag = bytes.fromhex(tag)
header = bytes.fromhex(associated_data)
nonce = bytes.fromhex(nonce)
if header != b'CryptoHack':
return {"error": "Don't understand this message type"}
cipher = AES.new(KEY, AES.MODE_GCM, nonce=nonce)
encrypted = cipher.update(header)
try:
decrypted = cipher.decrypt_and_verify(ciphertext, tag)
except ValueError as e:
return {"error": "Invalid authentication tag"}
if b'give me the flag' in decrypted:
return {"plaintext": FLAG.encode().hex()}
return {"plaintext": decrypted.hex()}
@chal.route('/forbidden_fruit/encrypt/<plaintext>/')
def encrypt(plaintext):
plaintext = bytes.fromhex(plaintext)
header = b"CryptoHack"
cipher = AES.new(KEY, AES.MODE_GCM, nonce=IV)
encrypted = cipher.update(header)
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
if b'flag' in plaintext:
return {
"error": "Invalid plaintext, not authenticating",
"ciphertext": ciphertext.hex(),
}
return {
"nonce": IV.hex(),
"ciphertext": ciphertext.hex(),
"tag": tag.hex(),
"associated_data": header.hex(),
}
```
Bài này sử dụng AES - Galois Counter Mode. Trước hết ta cần hiểu mode này hoạt động như thế nào:
Tham khảo doc tại [đây](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf) và tại [đây](https://csrc.nist.rip/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-spec.pdf):
Và một vid khá hay của Computerphile giải thích về cái này https://www.youtube.com/watch?v=-fpVv_T4xwA&pp=ygUMYWVzIGdjbSBtb2Rl và một vid khác của David Wong: https://www.youtube.com/watch?v=g_eY7JXOc8U
### Thuật toán
Về cơ bản GCM giống mode CTR nhưng các phép tính toán được thực hiện trên trường Galois và được Hash thêm để đảm bảo tính toàn vẹn của thông tin (Authenticated Encryption)
GCM gồm 2 thuật mã hóa và giải mã. Thuật mã hóa yêu cầu 4 input bao gồm:
- Một khóa bí mật $K$ với kích thước tùy chỉnh dựa vào hiệu suất tính toán
- Nonce $n$ có 96 bit, $\displaystyle n\in \{0,1\}^{96}$, kí hiệu $IV$.
- Plaintext $P$ với số lượng bit trong khoảng $\displaystyle 0$ đến $\displaystyle 2^{39} -256$
- Additional authenticated data $A$ có số lượng bit từ $0$ đến $2^{64}$
Đầu ra sẽ bao gồm
- Một ciphertext $C$ với kích thước bằng với plaintext $P$.
- Authentication Tag T dùng để xác thực dữ liệu
Ngược lại với thuật mã hóa thì thuật giải mã sẽ nhận đầu vào là $C$,$T$,$A$,$K$,$IV$.
Ta sẽ làm việc trên trường Galois $\displaystyle GF\left( 2^{128}\right)$. Phép nhân hai phần tử $\displaystyle X,Y\in GF\left( 2^{128}\right)$ kí hiệu là $\displaystyle X\cdot Y$ và phép cộng sẽ được kí hiệu là $\displaystyle X\oplus Y$. Hàm mã hóa Block Cipher, mã hóa bản rõ $\displaystyle X$ với khóa $\displaystyle K$ sẽ được kí hiệu là $\displaystyle E( K,X)$. Ngoài ra ta còn có các kí hiệu khác : hàm len() trả về độ dài của bit-string, $\displaystyle 0^{l}$ để chỉ các bit-strings có độ dài là $\displaystyle l$ và chỉ chứa các bit 0, $\displaystyle A||B$ chỉ phép nối giữa hai xâu $\displaystyle A$ và B. $\displaystyle MSB_{t}( S)$ là hàm có output là $\displaystyle t$ bit đầu tiên tính từ bên trái của $\displaystyle S$ và $\displaystyle \{\}$ dùng để chỉ các xâu rỗng.
Thuật mã hóa sẽ hoạt động như sau:

Đầu tiên ta có plaintext sẽ có tổng cộng $\displaystyle ( n-1) 128+u$ bits, trong đó $\displaystyle n,u$ nguyên dương. Plaintext $\displaystyle P$ sẽ được chia thành các bit strings, trong đó $\displaystyle n-1$ bit strings đầu tiên sẽ có độ dài là 128 bits và bit string cuối cùng sẽ có độ dài là $\displaystyle u$, trong đó $\displaystyle 1\leqslant u\leqslant 128$. Ta gọi dãy các bit strings như trên là $\displaystyle P_{1} ,...,P_{n-1} ,P_{n}^{*}$ và tương tự các Ciphertext cũng được kí hiệu lần lượt là $\displaystyle C_{1} ,C_{2} ,...,C_{n-1} ,C_{n}^{*}$. Các Additional authenticated data được kí hiệu là $\displaystyle A_{1} ,A_{2} ,...,A_{m-1} ,A_{m}^{*}$, bit string cuối cùng là $\displaystyle A_{m}^{*}$ sẽ có độ dài là $\displaystyle v$ với $\displaystyle 1\leqslant v\leqslant 128$ và tổng độ dài của Additional authenticated data $\displaystyle A$ là $\displaystyle ( m-1) 128+v$, $\displaystyle A$ cũng tương tự như $\displaystyle P$ sẽ được chia thành các strings trong đó các strings đầu thì có độ dài 128 bits, trong khi đó bit strings cuối cùng độ dài sẽ giao động từ 1 đến 128 bits.
Tiếp theo là bước mã hóa.
- Đầu tiên ta tạo khóa $\displaystyle H$ cho hàm GHASH, bằng cách lấy $\displaystyle H=E\left( K,0^{128}\right)$
- Ta có $\displaystyle Y_{0} =\begin{cases}
IV||0^{31} 1 & \text{nếu như len(IV)=96 (len(Y0)=128)}\\
GHASH( H,\{\} ,IV) & \text{trường hợp còn lại}
\end{cases}$
- $\displaystyle Y_{i} =incr( Y_{i-1})$ với $\displaystyle i=1,2,...,n$
- $\displaystyle C_{i} =P_{i} \oplus E( K,Y_{i})$ với $\displaystyle i=1,...,n-1$
- $\displaystyle C_{n}^{*} =P_{n}^{*} \oplus MSB_{u}( E( K,Y_{n}))$
- $\displaystyle T=MSB_{t}( GHASH( H,A,C) \oplus E( K,Y_{0}))$
Ta xét hàm incr có tính chất: $\displaystyle incr( F||I) =F||\left( I+1\bmod 2^{32}\right)$. Để dễ hình dung thì ta giả sử IV ban đầu là `0x1234567890abcdef12345678` (96 bits) thì khi đó $\displaystyle Y_{0} =IV||000....1$, tiếp theo ta sẽ lấy $\displaystyle Y_{1} =IV||000....2$ và cứ tiếp tục như vậy.
Hàm $\displaystyle GHASH( H,A,C) =X_{m+n+1}$ trong đó $\displaystyle A,C$ đã được định nghĩa như trên và $\displaystyle X_{i} ,i=0,1,...,m+n+1$ được tính như sau:
\begin{equation*}
X_{i} =\begin{cases}
0 & ,i=0\\
( X_{i-1} \oplus A_{i}) \cdot H & ,i=1,...,m-1\\
\left( X_{m-1} \oplus \left( A_{m}^{*} ||0^{128-v}\right)\right) \cdot H & ,i=m\\
( X_{i-1} \oplus C_{i}) \cdot H & ,i=m+1,...,m+n-1\\
\left( X_{m+n-1} \oplus \left( C_{m}^{*} ||0^{128-u}\right)\right) \cdot H & ,i=m+n\\
( X_{m+n} \oplus ( len( A) ||len( C))) \cdot H & ,i=m+n+1
\end{cases}
\end{equation*}
Ta có sơ đồ như dưới đây: [Nguồn](https://soatok.blog/2020/05/13/why-aes-gcm-sucks/)

Doc của [Nist](https://nvlpubs.nist.gov/nistpubs/legacy/sp/nistspecialpublication800-38d.pdf):

Vì mỗi chỗ định nghĩa cách tính Counter 0 mỗi kiểu nên sau một hồi loay hoay mình có hỏi a Giáp thì cách trên của Nist là chuẩn nhất.
Tiếp theo ta sẽ nói qua một chút về phép nhân và cộng trên trường hữu hạn $\displaystyle GF\left( 2^{128}\right)$.
Mọi người có thể đọc qua bài viết này trên [CryptoPal](https://cryptopals.com/sets/8/challenges/63.txt)
### Forgery Attack
Tham khảo repo sau: https://github.com/tl2cents/AEAD-Nonce-Reuse-Attacks/tree/main/aes-gcm
Attack này dùng trong trường hợp AES-GCM sử dụng cùng một key và nonce khi mã hóa.
### Giải bài
Muốn lấy được flag thì ta phải gửi vào hàm decrypt đủ các tham số `decrypt(nonce, ciphertext, tag, associated_data)` và server sẽ check
```python=
if b'give me the flag' in decrypted:
return {"plaintext": FLAG.encode().hex()}
```
Như vậy ta sẽ dùng attack ở trên để Forge tag của plaintext `b'give me the flag'` và gửi lại tới server. Ta forge được vì ở bài này key và nonce được khởi tạo cố định (reuse).
Code exploit:
```python=
import requests
import json
from sage.all import *
# from https://github.com/jvdsn/crypto-attacks/blob/master/attacks/gcm/forbidden_attack.py
x = GF(2)["x"].gen()
gf2e = GF(2 ** 128, name="y", modulus=x ** 128 + x ** 7 + x ** 2 + x + 1)
# Converts an integer to a gf2e element, little endian.
def _to_gf2e(n):
return gf2e([(n >> i) & 1 for i in range(127, -1, -1)])
# Converts a gf2e element to an integer, little endian.
def _from_gf2e(p):
n = p.integer_representation()
ans = 0
for i in range(128):
ans <<= 1
ans |= ((n >> i) & 1)
return ans
c = b'test'
assert _from_gf2e(_to_gf2e(int.from_bytes(c,'big'))) == int.from_bytes(c,'big')
def encrypt(plaintext):
url = 'http://aes.cryptohack.org/forbidden_fruit/encrypt/'
url += plaintext.hex()
r = requests.get(url).json()
if "error" in r:
return None, bytes.fromhex(r["ciphertext"])
return bytes.fromhex(r["nonce"]), bytes.fromhex(r["ciphertext"]), bytes.fromhex(r["tag"]), bytes.fromhex(r["associated_data"])
def decrypt(nonce,ciphertext,tag,associated_data):
url = 'http://aes.cryptohack.org/forbidden_fruit/decrypt/'
url += nonce.hex() + '/' + ciphertext.hex() + '/' + tag + '/' + associated_data.hex()
r = requests.get(url).json()
return bytes.fromhex(r["plaintext"])
# Gửi 2 msgs tới để lấy về tag1,tag2 và ct1,ct2
msg1 = b'\x00' * 16
msg2 = b'\x01' * 16
r1 = encrypt(msg1)
r2 = encrypt(msg2)
A = r1[3]
nonce = r1[0]
c1, T1 = r1[1],r1[2]
c2, T2 = r2[1],r2[2]
c1 = _to_gf2e(int.from_bytes(c1,'big'))
c2 = _to_gf2e(int.from_bytes(c2,'big'))
T1 = _to_gf2e(int.from_bytes(T1,'big'))
T2 = _to_gf2e(int.from_bytes(T2,'big'))
H_2 = (T1-T2)/(c1-c2)
assert T1-c1*H_2 == T2-c2*H_2
X = T1-c1*H_2
msg3 = b'give me the flag'
c3 = int.from_bytes(encrypt(msg3)[1],'big')
T3 = X +_to_gf2e(c3)*H_2
tag3 = _from_gf2e(T3)
ct3 = encrypt(msg3)[1]
print(decrypt(nonce,ct3,hex(tag3)[2:],A).decode())
```
# AES Implementation
Cài đặt lại AES trên trường hữu hạn:
- https://www.youtube.com/watch?v=x1v2tX4_dkQ
- http://blog.simulacrum.me/2019/01/aes-galois/
# Some CTF Challenges
## Chall 1
Source code:
```python=
#!/usr/bin/env python3
from hashlib import sha256
from random import choices
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
FLAG = b'W1{???????????????????????}'
chars = b'AoThuatGiaDP'
L = 3
w, x, y, z = (
bytes(choices(chars, k=L)),
bytes(choices(chars, k=L)),
bytes(choices(chars, k=L)),
bytes(choices(chars, k=L)),
)
k1 = sha256(w).digest()
k2 = sha256(x).digest()
k3 = sha256(y).digest()
k4 = sha256(z).digest()
pt = b'AES_AES_AES_AES!'
ct = AES.new(k4, AES.MODE_ECB).encrypt(
AES.new(k3, AES.MODE_ECB).encrypt(
AES.new(k2, AES.MODE_ECB).encrypt(
AES.new(k1, AES.MODE_ECB).encrypt(
pt
)
)
)
)
key = sha256(w + x + y + z).digest()
enc_flag = AES.new(key, AES.MODE_ECB).encrypt(pad(FLAG, AES.block_size))
with open('output.txt', 'w') as f:
f.write(f'pt = {pt.hex()}\nct = {ct.hex()}\nenc_flag = {enc_flag.hex()}')
```
```
pt = 4145535f4145535f4145535f41455321
ct = a5d45cdb322abe38b9da6df19f997696
enc_flag = ecd0486b5c1c5a2b9af4e42abc8891445a97337cf1857e366bff6063bdbeaa7f
```
Dùng kĩ thuật MITM để Attack: Mục tiêu của ta là tìm lại 4 khóa $\displaystyle k_{1} ,k_{2} ,k_{3} ,k_{4}$. Để tìm lại 4 khóa này thì ta cần tìm lại 4 kí tự $\displaystyle w,x,y,z$ được tạo ngẫu nhiên bằng cách lấy 3 kí tự trong chars. Ta đã biết cả pt và ct thì việc cần làm là ta sẽ đi encrypt pt bằng 2 khóa bất kì rồi lưu lại trong bảng, tiếp đó ta sẽ decrypt ct bằng 2 khóa bất kì rồi sau đó sẽ tìm collision.
Ta dùng ý tưởng MITM.
Code giải:
```python=
#!/usr/bin/env python3
from hashlib import sha256
from itertools import product
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import unpad
pt = bytes.fromhex("4145535f4145535f4145535f41455321")
ct = bytes.fromhex("a5d45cdb322abe38b9da6df19f997696")
enc_flag = bytes.fromhex("ecd0486b5c1c5a2b9af4e42abc8891445a97337cf1857e366bff6063bdbeaa7f")
L = 3
chars = b'AoThuatGiaDP'
char_keys = list(map(bytes, product(chars, repeat=L)))
def encrypt_aes(key, plaintext):
return AES.new(key, AES.MODE_ECB).encrypt(plaintext)
def decrypt_aes(key, ciphertext):
return AES.new(key, AES.MODE_ECB).decrypt(ciphertext)
def mitm():
middle = {}
for w in char_keys:
k1 = sha256(w).digest()
for x in char_keys:
k2 = sha256(x).digest()
enc = encrypt_aes(k2, encrypt_aes(k1, pt))
middle[enc] = (w, x)
for z in char_keys:
k4 = sha256(z).digest()
for y in char_keys:
k3 = sha256(y).digest()
dec = decrypt_aes(k3, decrypt_aes(k4, ct))
if dec in middle:
return middle[dec] + (y, z)
wxyz = mitm()
assert wxyz is not None, 'failed'
w, x, y, z = wxyz
k1 = sha256(w).digest()
k2 = sha256(x).digest()
k3 = sha256(y).digest()
k4 = sha256(z).digest()
assert ct == encrypt_aes(k4, encrypt_aes(k3, encrypt_aes(k2, encrypt_aes(k1, pt)))), 'Wrong keys found'
key = sha256(b''.join(wxyz)).digest()
FLAG = unpad(decrypt_aes(key, enc_flag), AES.block_size)
print(FLAG.decode())
```
## Chall 2: Absolutely Encrypted Shenanigans (KashiCTF 2025)
Source code:
```python=
def xor(b1, b2):
if len(b1)!=len(b2):
raise ValueError("Lengths of byte strings are not equal")
return bytes([b1[i]^b2[i] for i in range(len(b1))])
def bytes2matrix(text):
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
def matrix2bytes(matrix):
s = b""
for l in matrix:
s += bytes(l)
return s
def shift_rows(s):
s[2][2], s[2][1], s[0][3], s[2][0], s[3][3], s[2][3], s[3][1], s[1][3], s[0][2], s[1][0], s[0][1], s[0][0], s[1][1], s[3][0], s[3][2], s[1][2] = s[2][2], s[3][3], s[0][0], s[1][1], s[2][1], s[1][2], s[3][0], s[2][3], s[0][3], s[0][2], s[3][2], s[0][1], s[3][1], s[1][0], s[2][0], s[1][3]
return s
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
def mix_single_column(a):
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)
return a
def mix_columns(s):
for i in range(4):
s[i] = mix_single_column(s[i])
return s
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,
)
def add_round_key(s, k):
ns = []
for i in range(4):
ns.append([])
for j in range(4):
ns[i].append(s[i][j]^k[j][i])
return ns
def sub_bytes(s, sbox=s_box):
resmatrix = []
for i in range(4):
resmatrix.append([])
for j in range(4):
hexval=hex(s[i][j])[2:]
if len(hexval)==1:
a,b = 0,int(hexval,16)
else:
a,b = int(hexval[0],16), int(hexval[1],16)
resmatrix[i].append(sbox[a*16+b])
return resmatrix
N_ROUNDS = 10
def expand_key(master_key):
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,
)
key_columns = bytes2matrix(master_key)
iteration_size = len(master_key) // 4
i = 1
while len(key_columns) < (N_ROUNDS + 1) * 4:
word = list(key_columns[-1])
if len(key_columns) % iteration_size == 0:
word.append(word.pop(0))
word = [s_box[b] for b in word]
word[0] ^= r_con[i]
i += 1
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
word = [s_box[b] for b in word]
word = bytes(i^j for i, j in zip(word, key_columns[-iteration_size]))
key_columns.append(word)
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
def encrypt_block(key, pt_block):
round_keys = expand_key(key)
state = bytes2matrix(pt_block)
state = add_round_key(state, round_keys[0])
state = sub_bytes(state)
state = shift_rows(state)
for i in range(1,N_ROUNDS):
state = mix_columns(state)
state = add_round_key(state, round_keys[i])
state = sub_bytes(state)
state = shift_rows(state)
state = add_round_key(state, round_keys[N_ROUNDS])
ct_block = matrix2bytes(state)
return ct_block
def encrypt(key, plaintext, mode="ECB", iv=None):
if len(plaintext)%16 != 0:
raise ValueError("Invalid Plaintext")
elif len(key)!=16:
raise ValueError("Invalid Key")
ciphertext = b""
if mode=="ECB":
for i in range(0, len(plaintext), 16):
ciphertext += encrypt_block(key, plaintext[i: i+16])
elif mode=="CBC":
if (iv==None or len(iv)!=16):
raise ValueError("Invalid IV")
ciphertext += iv
for i in range(0, len(plaintext), 16):
ciphertext += encrypt_block(key, xor(ciphertext[i: i+16], plaintext[i: i+16]))
return ciphertext[16:]
def pad(text, blocksize):
padding_len = blocksize - (len(text)%blocksize)
padding = bytes([padding_len])*padding_len
return text+padding
```
Và `server.py`
```python=
from AES import encrypt, pad
from redacted import secret, flag, EXIT
import json
import os
plaintext = pad(flag, 16)
for _ in range(10):
iv = os.urandom(8)*2
key = os.urandom(16)
try:
ciphertext = encrypt(key, plaintext, mode="CBC", iv=iv)
except:
EXIT()
print(json.dumps({
'key': key.hex(),
'ciphertext': ciphertext.hex()
}))
inp = input("Enter iv: ")
if (iv.hex() != inp):
EXIT()
print()
plaintext = pad(secret, 16)
iv = os.urandom(8)*2
key = os.urandom(16)
try:
ciphertext = encrypt(key, plaintext, mode="CBC", iv=iv)
except:
EXIT()
print(json.dumps({
'key': key.hex(),
'ciphertext': ciphertext.hex()
}))
```
Xem WU tại đây: https://github.com/Cryptonite-MIT/Write-ups/tree/master/KashiCTF-2025/crypto/Absolutely%20Encrypted%20Shenanigans
## Chall 3: ACECTF 2025
Source code:
```python=
from Crypto.Cipher import AES
from Crypto.Util import Counter
import os
k = os.urandom(16) # Is it too short?
def encrypt(plaintext):
cipher = AES.new(k, AES.MODE_CTR, counter=Counter.new(128)) # I was told, CTR can't be broken!
ciphertext = cipher.encrypt(plaintext)
return ciphertext.hex()
msg = b'This is just a test message and can totally be ignored.' # Just checking functionality
encrypted_msg = encrypt(msg)
with open('flag.txt', 'r') as f:
flag = f.readline().strip().encode()
encrypted_flag = encrypt(flag)
with open('msg.txt', 'w+') as o:
o.write(f"{encrypted_msg}\n")
o.write(f"{encrypted_flag}")
```
Và file output.txt
```
d71f4a2fd1f9362c21ad33c7735251d0a671185a1b90ecba27713d350611eb8179ec67ca7052aa8bad60466b83041e6c02dbfee738c2a3
c234661fa5d63e627bef28823d052e95f65d59491580edfa1927364a5017be9445fa39986859a3
```
Ta đã biết về mode CTR, và trong bài này keystream trong mode CTR đều được khởi tạo bởi cùng một key $k$. Khi đó
$$\displaystyle C1=P1\oplus K$$
$$\displaystyle C2=P2\oplus K$$
Ta muốn tìm lại flag là $\displaystyle P2$ thì có thể tính $\displaystyle C1\oplus C2=P1\oplus P2$ và $\displaystyle C1\oplus C2\oplus P1=P2$ vì ta đã biết Messages, Enc(Messages) và Enc(Flag) nên có thể tính được flag ban đầu.
```python=
from pwn import xor
msg = b'This is just a test message and can totally be ignored.'
enc_flag="c234661fa5d63e627bef28823d052e95f65d59491580edfa1927364a5017be9445fa39986859a3"
enc_msg="d71f4a2fd1f9362c21ad33c7735251d0a671185a1b90ecba27713d350611eb8179ec67ca7052aa8bad60466b83041e6c02dbfee738c2a3"
enc_msg_bytes=bytes.fromhex(enc_msg)
enc_flag_bytes=bytes.fromhex(enc_flag)
print(xor(xor(enc_msg_bytes,enc_flag_bytes),msg))
```