# SYMMETRIC CIPHER ADVENTURE!!!
Đây là những bài tập liên quan đến phần symmetric cipher.
## 1. HOW AES WORKS
### Keyed Permutations

Flag: crypto{bijection}
### Resisting Bruteforce

Flag: crypto{biclique}
### Structure of AES

Challenge description: "Included is a **bytes2matrix** function for converting our initial plaintext block into a state matrix. Write a **matrix2bytes** function to turn that matrix back into bytes, and submit the resulting plaintext as the flag."
```python=
# matrix.py
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. """
????
matrix = [
[99, 114, 121, 112],
[116, 111, 123, 105],
[110, 109, 97, 116],
[114, 105, 120, 125],
]
print(matrix2bytes(matrix))
```
Ở đây, ta chỉ cần chuyển ma trận 4x4 thành mảng 16 bytes để nhận được flag ban đầu. Sử dụng hàm sum để chuyển ma trận 4x4 thành 1 danh sách đơn.
```python=
# solution.py
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. """
return bytes(sum(matrix, []))
matrix = [
[99, 114, 121, 112],
[116, 111, 123, 105],
[110, 109, 97, 116],
[114, 105, 120, 125],
]
print(matrix2bytes(matrix))
```
Flag: crypto{inmatrix}
### Round Keys

```python=
# add_round_key.py
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):
???
print(add_round_key(state, round_key))
```
Ta sẽ lấy từng phần tử ở `state` để XOR với phần tử tương ứng của **round_key** để nhận được `AddRoundKey` sau đó dùng `matrix2bytes` để nhận được flag
```python=
# solution.py
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):
return [[s[i][j] ^ k[i][j] for j in range(4)] for i in range(4)]
def matrix2bytes(matrix):
return bytes(sum(matrix, []))
print(matrix2bytes(add_round_key(state, round_key)))
```
Flag: crypto{r0undk3y}
### Confusion through Substitution


```python=
# sbox.py
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))
```
Để lấy flag, ta phải đưa ma trận trạng thái qua hàm `inv_s_box` rồi chuyển đổi về bytes.
```python=
# solution.py
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=s_box):
return [[sbox[i] for i in k] for k in s]
print(matrix2bytes(sub_bytes(state, sbox=inv_s_box)))
```
Flag: crypto{l1n34rly}
### Diffusion through Permutation


```python=
# diffusion.py
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):
???
# 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],
]
```
Có thể thấy rằng, `ShiftRows` và `MixColumns` được kết hợp với nhau để làm cho mỗi byte của `state` ảnh hưởng tới mọi byte khác. Do đó, ta phải dùng hàm đảo của hai hàm trên để đưa `state` về lại trạng thái ban đầu và lấy flag.
```python=
# solution.py
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):
return bytes(sum(matrix, []))
inv_mix_columns(state)
inv_shift_rows(state)
print(matrix2bytes(state))
```
### Bringing It All Together

```python=
#aes_decrypt
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))
```
Ở thử thách này, ta dùng lại các hàm đã sử dụng ở các thử thách trước như `bytes2matrix, matrix2bytes, inv_shift_rows,...` Sau đó, ta viết lại hàm `decrypt` như cấu trúc để đưa `ciphertext` về `plaintext` rồi sử dụng để tìm ra flag.
```python=
# solution.py
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 bytes2matrix(text):
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
def matrix2bytes(matrix):
return bytes(sum(matrix, []))
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]
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 add_round_key(s, k):
return [[s[i][j] ^ k[i][j] for j in range(4)] for i in range(4)]
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 sub_bytes(s, sbox=s_box):
return [[sbox[i] for i in k] for k in s]
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
plaintext = bytes2matrix(ciphertext)
# Initial add round key step
plaintext = add_round_key(plaintext, round_keys[N_ROUNDS])
# Perform InvMixColumns step (not shown in the example)
for i in range(N_ROUNDS - 1, 0, -1):
inv_shift_rows(plaintext)
plaintext = sub_bytes(plaintext, sbox=inv_s_box)
plaintext = add_round_key(plaintext, round_keys[i])
inv_mix_columns(plaintext)
# Run final round (skips the InvMixColumns step)
inv_shift_rows(plaintext)
plaintext = sub_bytes(plaintext, sbox=inv_s_box)
plaintext = add_round_key(plaintext, round_keys[0])
# Convert state matrix to plaintext
plaintext = matrix2bytes(plaintext)
return plaintext
print(decrypt(key, ciphertext))
```
Flag: crypto{MYAES128}
## 2. BLOCK CIPHERS 1
### ECB CBC WTF

```python=
# source
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}
```
Qua đoạn source trên, ta chú ý:
```python!
ciphertext = iv.hex() + encrypted.hex()
```
Ciphertext chứa 16 bytes đầu là iv dạng hex.

Decrypt dạng ECB giúp ta đưa ciphertext về trước khi qua block cipher, xem xét cấu trúc mã hóa của AES mode CBC:

Dựa vào cấu trúc trên thì ta rút ra được đẳng thức sau: $Ciphertext(IV) \oplus Plaintext = Decrypt_{ECB} \Rightarrow Plaintext = Decrypt_{ECB} \oplus Ciphertext(IV)$
```python=
# solution.py
import requests
def response(input):
url = "http://aes.cryptohack.org/ecbcbcwtf/decrypt/" + input.hex() + "/"
r = requests.get(url)
js = r.json()
return bytes.fromhex(js["plaintext"])
def encrypt_flag():
url = "http://aes.cryptohack.org/ecbcbcwtf/encrypt_flag/"
r = requests.get(url)
js = r.json()
return bytes.fromhex(js["ciphertext"])
def XOR(a, b):
return bytes(a ^ b for a, b in zip(a, b))
encrypt = encrypt_flag()
iv = encrypt[:16]
ciphertext1 = encrypt[16:32]
ciphertext2 = encrypt[32:]
decrypt1 = XOR(response(ciphertext1), iv)
decrypt2 = XOR(response(ciphertext2), ciphertext1)
print(decrypt1 + decrypt2)
```
Flag: crypto{3cb_5uck5_4v01d_17_!!!!!}
### ECB Oracle

```python=
# source
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()}
```
Sau khi thử thì ta biết được rằng khi đưa plaintext có 6 bytes thì ciphertext có 32 bytes, còn khi đưa plaintext có 7 bytes thì ciphertext có 48 bytes. Vì sau khi padding mới làm cho ciphertext tăng giá trị nên ta biết được rằng flag có 32 - 7 = 25 bytes do:
```python!
padded = pad(plaintext + FLAG.encode(), 16)
```
Vì flag có định dạng là crypto{.. ..} nên ta có thể bruteforce 1 khối ban đầu có dạng
`b’\x00\x00...\x00’`(24 bytes) + `b'crypto{'`(7 bytes) + 1 byte(bruteforce) và so sánh với khối `b’\x00\x00...\x00’`(24 bytes) được đưa vào challenge để tìm ra byte cuối. Tương tự như vậy cho đến khi thu được flag hoàn chỉnh.
```python=
# solution.py
import requests
def response(input):
url = "http://aes.cryptohack.org//ecb_oracle/encrypt/" + input.hex() + "/"
r = requests.get(url)
js = r.json()
return bytes.fromhex(js["ciphertext"])
flag = b'crypto{'
for i in range(7, 25):
input = b"\x00" * (31 - i)
pattern = response(input)[:32]
input += flag
for j in range(33, 127):
input = input[:31]
input += bytes([j])
later = response(input)[:32]
if pattern == later:
flag += bytes([j])
break
print(flag.decode())
```
Flag: crypto{p3n6u1n5_h473_3cb}
### Flipping Cookie

```python=
# Source
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}
```
Chú ý:
```python
if b"admin=True" in unpadded.split(b";"):
return {"flag": FLAG}
```
Nhận thấy để lấy được flag thì sau khi decrypt phải có cụm b"admin=True" khi tách dấu b";". Phân tích hàm get_cookie thì ta sẽ có được iv và cookie với b"admin=False" trong đó.

Ở hàm check_admin thì ta có thể thay đổi iv để đưa cookie trước và sau khi biến đổi có dạng như sau:
```python
initial = b"admin=False;expi"
expected = b"admin=True;"
```
bằng cách cho $iv_{later}=iv\oplus initial \oplus expected$ vì khi decrypt khối mã hóa đầu thì nó sẽ trở thành $iv \oplus initial$ nên khi đó đoạn plaintext sẽ trở thành: $iv\oplus initial \oplus expected\oplus iv \oplus initial = expected$. Vì có lệnh unpad nữa nên ta sẽ phải pad khối expected để đưa về dạng ban đầu như ý muốn.
```python=
# solution.py
import requests
from Crypto.Util.Padding import pad
def response(cookie, iv):
url = "https://aes.cryptohack.org/flipping_cookie/check_admin/" + cookie.hex() + "/" + iv.hex() + "/"
r = requests.get(url)
js = r.json()
print(js)
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 XOR(a, b):
return bytes([x ^ y for x, y in zip(a, b)])
get = get_cookie()
iv = get[:16]
cipher1 = get[16:32]
cipher2 = get[32:]
initial = b"admin=False;expi"
expected = b"admin=True;"
padded = pad(expected, 16)
iv_later = XOR(XOR(initial, padded), iv)
response(cipher1, iv_later)
```
Flag: crypto{4u7h3n71c4710n_15_3553n714l}
### Lazy CBC

```python=
# Source
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"}
```
Chú ý: iv được cho bằng KEY
```python!
cipher = AES.new(KEY, AES.MODE_CBC, KEY)
```
Qua đoạn source, ta thấy muốn tìm được flag thì phải có key đúng nhập vào hàm get_flag.
Để có key thì ta có thể thông qua cấu trúc decrypt sau và tìm khối plaintext đầu cũng như khối decrypt đầu ngay trước khi XOR để XOR 2 khối này lại và có kết quả là key.

Do đó, tôi có ý tưởng sử dụng khối ciphertext 32 bytes: b"\x00\x00...\x00" để đưa vào hàm decrypt này
```python!
send = b"\x00" * 32
```
Ta được hai khối 16 bytes lần lượt là khối plaintext đầu và khối decrypt đầu luôn vì khối thứ hai được XOR với ciphertext đầu là b"\x00\x00...\x00"(16 bytes) thì vẫn giữ nguyên.
```python!
plaintext = receive(send)
plain1 = plaintext[:16] #plaintext[0]
plain2 = plaintext[16:] #decrypt[0] before XOR
```
Khi đó: $plain1 \oplus plain2 = key$ vì $plain2 \oplus key = plain1$
```python=
# solution.py
import requests
def get_flag(key):
url = "https://aes.cryptohack.org/lazy_cbc/get_flag/" + key.hex() + "/"
r = requests.get(url)
js = r.json()
print(bytes.fromhex(js["plaintext"]).decode())
def receive(ciphertext):
url = "https://aes.cryptohack.org/lazy_cbc/receive/" + ciphertext.hex() + "/"
r = requests.get(url)
js = r.json()
return bytes.fromhex(js["error"][len("Invalid plaintext: "):])
def XOR(a, b):
return bytes([a ^ b for a, b in zip(a, b)])
send = b"\x00" * 32
plaintext = receive(send)
plain1 = plaintext[:16]
plain2 = plaintext[16:]
key = XOR(plain1, plain2)
get_flag(key)
```
Flag: crypto{50m3_p30pl3_d0n7_7h1nk_IV_15_1mp0r74n7_?}
### Triple DES

```python=
# Source
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())
```
Ở thử thách này, ta khai thác 1 lỗ hổng của DES3 là các cặp khóa yếu. Khi sử dụng các cặp khóa yếu này để mã hóa 2 lần thì ta sẽ có được plaintext ban đầu.
```python!
weak_key = b"\x01" * 8 + b"\xfe" * 8
```
```python=
# solution.py
import requests
def encrypt(key, plaintext):
url = "https://aes.cryptohack.org/triple_des/encrypt/" + key.hex() + "/" + plaintext.hex() + "/"
r = requests.get(url)
js = r.json()
return bytes.fromhex(js["ciphertext"])
def encrypt_flag(key):
url = "https://aes.cryptohack.org/triple_des/encrypt_flag/" + key.hex() + "/"
r = requests.get(url)
js = r.json()
return bytes.fromhex(js["ciphertext"])
weak_key = b"\x01" * 8 + b"\xfe" * 8
cipher = encrypt_flag(weak_key)
print(encrypt(weak_key, cipher))
```
Flag: crypto{n0t_4ll_k3ys_4r3_g00d_k3ys}
## 3. SYMMETRIC STARTER
### Modes of Operation Starter

```python=
# Source
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()}
```
Ở thử thách này, chỉ đơn giản là mình gửi lệnh nhận ciphertext và decrypt nó để nhận flag.
```python=
# solution.py
import requests
def encrypt_flag():
url = "https://aes.cryptohack.org/block_cipher_starter/encrypt_flag/"
r = requests.get(url)
js = r.json()
return bytes.fromhex(js["ciphertext"])
def decrypt(ciphertext):
url = "https://aes.cryptohack.org/block_cipher_starter/decrypt/" + ciphertext.hex() + "/"
r = requests.get(url)
js = r.json()
return bytes.fromhex(js["plaintext"])
ciphertext = encrypt_flag()
flag = decrypt(ciphertext)
print(flag)
```
Flag: crypto{bl0ck_c1ph3r5_4r3_f457_!}
### Passwords as Keys

```python=
# Source
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()}
```
Với key được tạo từ 1 từ trong danh sách giới hạn đã cho, ta có thể bruteforce từng từ trong đó để tạo key và decrypt cho đến khi tìm được flag.
```python=
# solution.py
from Crypto.Cipher import AES
import hashlib
# /usr/share/dict/words from
# https://gist.githubusercontent.com/wchargin/8927565/raw/d9783627c731268fb2935a731a618aa8e95cf465/words
with open("words.txt") as f:
words = [w.strip() for w in f.readlines()]
ciphertext = "c92b7734070205bdf6c0087a751466ec13ae15e6f1bcdd3f3a535ec0f4bbae66" # from encrypt_flag()
ciphertext = bytes.fromhex(ciphertext)
for i in words:
KEY = hashlib.md5(i.encode()).digest()
cipher = AES.new(KEY, AES.MODE_ECB)
flag = cipher.decrypt(ciphertext)
if b"crypto{" in flag:
print(flag)
break
```
Flag: crypto{k3y5__r__n07__p455w0rdz?}
## 4. STREAM CIPHERS
### Symmetry

```python=
# Source
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}
```
Qua hàm encrypt_flag, ta có iv và đoạn ciphertext:
```python
ciphertext = iv.hex() + encrypted.hex()
```

Từ cấu trúc của OFB mode, ta có thể thấy rằng với cùng 1 iv mà ta cho 1 plaintext được encrypted 1 lần ra ciphertext rồi encrypt tiếp đoạn ciphertext này thì sẽ quay lại plaintext ban đầu vì $X \oplus plaintext = ciphertext \Rightarrow X\oplus ciphertext = plaintext$
```python=
# solution.py
import requests
def encrypt_flag():
url = "https://aes.cryptohack.org/symmetry/encrypt_flag/"
r = requests.get(url)
js = r.json()
return bytes.fromhex(js["ciphertext"])
def encrypt(plaintext, iv):
url = "https://aes.cryptohack.org/symmetry/encrypt/" + plaintext.hex() + "/" + iv.hex() + "/"
r = requests.get(url)
js = r.json()
return bytes.fromhex(js["ciphertext"])
ciphertext = encrypt_flag()
iv = ciphertext[:16]
decrypted = ciphertext[16:]
flag = encrypt(decrypted, iv)
print(flag)
```
Flag: crypto{0fb_15_5ymm37r1c4l_!!!11!}
### Bean Counter

```python=
#Source
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)}
```
Ta thấy step_up mặc định là False nên self.stup = step_up = 0(False) do đó hàm increment sẽ không tăng mà giảm 1 lượng self.stup = 0, nghĩa là giữ nguyên. Do đó keystream sẽ giống nhau với mỗi cụm độ dài 16 bytes.
```python!
self.newIV = hex(int(self.value, 16) - self.stup)
```
Thông thường, mỗi file PNG sẽ có 16 bytes header đầu giống nhau gồm 8 bytes đầu cố định là phần chữ ký của file PNG, 4 byte tiếp theo là kích thước của chunk dữ liệu và 4 byte cuối là loại chunk (thường là Image Header).


Lúc này, ta có được 16 bytes đầu của encrypted và của PNG header thì ta có thể xor chúng với nhau để lấy được key vì:
```python!
xored = [a^b for a, b in zip(block, keystream)] # block = f.read(16)
```
Có được key, ta xor các phần còn lại của encrypted để lấy các bytes của ảnh.
```python=
# solution.py
import requests
def encrypt():
url = "https://aes.cryptohack.org/bean_counter/encrypt"
r = requests.get(url)
js = r.json()
return bytes.fromhex(js["encrypted"])
def XOR(a, b):
return bytes([x ^ y for x, y in zip(a, b)])
png_header = bytes.fromhex("89504e470d0a1a0a0000000d49484452")
encrypt_flag = encrypt()
key = XOR(encrypt_flag[:16],png_header)
lst = []
for i in range(0, len(encrypt_flag), 16):
lst.append(XOR(encrypt_flag[i:i+16], key))
flag = b''
for i in lst:
flag += i
with open("Bean_Counter.png", "wb") as file:
file.write(flag)
```

Flag: crypto{hex_bytes_beans}
### Dancing Queen

```python=
# chacha20.py
#!/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()}'")
```
```java=
// output.txt
iv1 = 'e42758d6d218013ea63e3c49'
iv2 = 'a99f9a7d097daabd2aa2a235'
msg_enc = 'f3afbada8237af6e94c7d2065ee0e221a1748b8c7b11105a8cc8a1c74253611c94fe7ea6fa8a9133505772ef619f04b05d2e2b0732cc483df72ccebb09a92c211ef5a52628094f09a30fc692cb25647f'
flag_enc = 'b6327e9a2253034096344ad5694a2040b114753e24ea9c1af17c10263281fb0fe622b32732'
```
#### Phân tích
Trong lớp ChaCha20 về hàm `encrypt`, đầu tiên nó tạo 1 biến `counter` và tách m ra thành các khối 64 bytes để mã hóa rồi tăng `counter` lên 1 rồi làm tiếp với những khối sau.
Trong mỗi khối 64 bytes này, hàm tạo 1 `state` ban đầu (16-word) gồm 128 bit hằng số (4-word), 256 bit `key` (8-word), 32 bit tham số bộ đếm `Counter` (1-word), 96 bit `iv` (3-word). Rồi thực hiện 10 vòng lặp luân phiên thực thi các biến đổi dịch vòng cột và dịch vòng chéo.
Cuối cùng, xor từng khối của m với state và ra được ciphertext.
```python!
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
```
#### Giải:
Thử thách cho ta `msg` có độ dài 80 bytes và cipher cùng iv của nó. Như vậy, ta có thể tách khối 64 bytes đầu ra để tìm `state` của nó sau 10 vòng biển đổi và truy ngược lại để tìm `state` ban đầu và tìm được `key` vì đã biết được cấu trúc `state`.
- Quá trình truy ngược diễn ra như sau:
Đầu tiên, ta xor khối 64 bytes đầu của `msg` và `msg_enc` lấy được state sau 10 vòng.
```python!
self._state = xor(m[:64], m_enc[:64])
```
Sau đó, với mỗi vòng ta thực hiện dịch ngược vòng chéo rồi tới vòng cột. Để dịch ngược, thì ta có hàm `word`, phép xor và hàm `rotate`.
Với hàm `word` là phép đồng dư với $2^{32}$ thì có thể biển đổi tương đương với phép trừ.
Phép xor thì có tính chất $A \oplus A = 0$ nên ta có thể lặp lại để ra giá trị ban đầu.
Hàm `rotate(x, n)` có chức năng chuyển khối n bit đầu của x ra sau. Như vậy, để truy ngược thì ta chỉ cần chuyền khối 32 - n bit đầu ra sau là được `rotate(x, 32 - n)`
Sau đây, là script cho thử thách:
```python=
#solution.py
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 reverse_rotate(x, n):
rotate(x, 32 - n)
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 _reverse_quarter_round(self, x, a, b, c, d):
x[b] = reverse_rotate(x[b], 7); x[b] ^= x[c]; x[c] = word(x[c] - x[d])
x[d] = reverse_rotate(x[d], 8); x[d] ^= x[a]; x[a] = word(x[a] - x[b])
x[b] = reverse_rotate(x[b], 12); x[b] ^= x[c]; x[c] = word(x[c] - x[d])
x[d] = reverse_rotate(x[d], 16); x[d] ^= x[a]; x[a] = word(x[a] - x[b])
def _reverse_inner_block(self, state):
self._reverse_quarter_round(state, 0, 5, 10, 15)
self._reverse_quarter_round(state, 1, 6, 11, 12)
self._reverse_quarter_round(state, 2, 7, 8, 13)
self._reverse_quarter_round(state, 3, 4, 9, 14)
self._reverse_quarter_round(state, 0, 4, 8, 12)
self._reverse_quarter_round(state, 1, 5, 9, 13)
self._reverse_quarter_round(state, 2, 6, 10, 14)
self._reverse_quarter_round(state, 3, 7, 11, 15)
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
def key(self, m, m_enc, iv):
self._state = xor(m[:64], m_enc[:64])
self._state = bytes_to_words(self._state)
for j in range(10):
self._reverse_inner_block(self._state)
key = self._state[4:12]
return words_to_bytes(key)
with open("output.txt", 'r') as file:
data = {}
for line in file:
key, value = line.strip().split(' = ')
data[key] = value.strip("'")
iv1 = data['iv1']
iv2 = data['iv2']
msg_enc = data['msg_enc']
flag_enc = data['flag_enc']
msg = b'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula.'
iv1 = bytes.fromhex(iv1)
iv2 = bytes.fromhex(iv2)
msg_enc = bytes.fromhex(msg_enc)
flag_enc = bytes.fromhex(flag_enc)
c = ChaCha20()
key = c.key(msg, msg_enc, iv1)
flag = c.decrypt(flag_enc, key, iv2)
print(flag)
```
Flag: crypto{M1x1n6_r0und5_4r3_1nv3r71bl3!}
## 5.HackTheBox: The Last Dance
### CHALLENGE DESCRIPTION
To be accepted into the upper class of the Berford Empire, you had to attend the annual Cha-Cha Ball at the High Court. Little did you know that among the many aristocrats invited, you would find a burned enemy spy. Your goal quickly became to capture him, which you succeeded in doing after putting something in his drink. Many hours passed in your agency's interrogation room, and you eventually learned important information about the enemy agency's secret communications. Can you use what you learned to decrypt the rest of the messages?
```python=
# source.py
from Crypto.Cipher import ChaCha20
from secret import FLAG
import os
def encryptMessage(message, key, nonce):
cipher = ChaCha20.new(key=key, nonce=iv)
ciphertext = cipher.encrypt(message)
return ciphertext
def writeData(data):
with open("out.txt", "w") as f:
f.write(data)
if __name__ == "__main__":
message = b"Our counter agencies have intercepted your messages and a lot "
message += b"of your agent's identities have been exposed. In a matter of "
message += b"days all of them will be captured"
key, iv = os.urandom(32), os.urandom(12)
encrypted_message = encryptMessage(message, key, iv)
encrypted_flag = encryptMessage(FLAG, key, iv)
data = iv.hex() + "\n" + encrypted_message.hex() + "\n" + encrypted_flag.hex()
writeData(data)
```
```python=
# out.txt
c4a66edfe80227b4fa24d431
7aa34395a258f5893e3db1822139b8c1f04cfab9d757b9b9cca57e1df33d093f07c7f06e06bb6293676f9060a838ea138b6bc9f20b08afeb73120506e2ce7b9b9dcd9e4a421584cfaba2481132dfbdf4216e98e3facec9ba199ca3a97641e9ca9782868d0222a1d7c0d3119b867edaf2e72e2a6f7d344df39a14edc39cb6f960944ddac2aaef324827c36cba67dcb76b22119b43881a3f1262752990
7d8273ceb459e4d4386df4e32e1aecc1aa7aaafda50cb982f6c62623cf6b29693d86b15457aa76ac7e2eef6cf814ae3a8d39c7
```
### Solution:
Chacha20 chủ yếu dùng để mã hóa với cốt lõi là một chuỗi được tạo ngẫu nhiên. Ciphertext nhận được từ phép XOR giữa plaintext với chuỗi này:
$ciphertext = plaintext \oplus stream \Rightarrow ciphertext \oplus plaintext = stream$
Thử thách cho ta 1 ciphertext mẫu và plaintext tương ứng được sử dụng cùng key và iv với ciphertext của flag mà ta cần tìm. Nếu vậy, 2 chuỗi được tạo bởi chacha20 để mã hóa là tương tự nhau. Mặt khác, độ dài ciphertext mẫu lớn hơn độ dài của ciphertext của flag nên việc chắc chắn ta lấy được stream và từ đó tìm được flag
```python=
# solution.py
from pwn import xor
message = b"Our counter agencies have intercepted your messages and a lot "
message += b"of your agent's identities have been exposed. In a matter of "
message += b"days all of them will be captured"
with open("out.txt", "r") as f:
data = f.read()
lines = data.split("\n")
iv = bytes.fromhex(lines[0])
encrypted_message = bytes.fromhex(lines[1])
encrypted_flag = bytes.fromhex(lines[2])
flag = xor(
xor(message[: len(encrypted_flag)], encrypted_message[: len(encrypted_flag)]),
encrypted_flag,
)
print(flag)
```
Flag : HTB{und3r57AnD1n9_57R3aM_C1PH3R5_15_51mPl3_a5_7Ha7}