# SQUARE ATTACK AES Square Attack là một kĩ thuật tấn công đối với Block Cipher khi khai thác vào các tính chất không đổi của các round encryption trong cipher đó. Kĩ thuật này được phát hiện lần đầu tiên đối với Square Cipher. Trong bài viết này, mình sẽ tập trung vào việc khai thác AES 4 round và giải một số bài liên quan đến kĩ thuật này. Ngoài ra, ta cũng sẽ thảo luận về áp dụng Square Attack cho 5 round AES. ### Vòng 3 anh cho 2 đánh Giả xử, ta có 256 bản rõ như sau, giờ ta sẽ gọi nó là $\varLambda$**-set** (Thực ra chỉ cần các plaintext giống nhau, chỉ khác nhau ở 1 vị trí và rải từ 0-255 là được) ![image](https://hackmd.io/_uploads/Sy6Wmqlm0.png) Giờ ta sẽ lấy tất cả các giá trị của $\varLambda$**-set** đi qua AES. Đầu tiên sẽ là XOR chúng với first_round_key (các key tạo từ expand_key). ![image](https://hackmd.io/_uploads/B126Q9xmC.png) Từ đây ta vẫn có thể thấy được $\varLambda$**-set** không bị ảnh hưởng lắm, vẫn có thể tìm lại được. Và giờ ta sẽ bắt đầu vòng đầu tiên. Phép biến đổi đầu tiên là **SubBytes**. ![image](https://hackmd.io/_uploads/HJL8V5eXA.png) Dựa vào hình trên, ta có được kết quả của $\varLambda$**-set** như sau: ![image](https://hackmd.io/_uploads/HJlu4ceQ0.png) Từ đây, ta thấy giá trị $\varLambda$**-set** không bị ảnh hưởng. Tiếp theo, sẽ là phép **ShiftRows** có thuật toán như sau: ![image](https://hackmd.io/_uploads/By8pNqeXC.png) Từ đó, ta có được đầu ra của $\varLambda$**-set** ![image](https://hackmd.io/_uploads/rkQJSqgQ0.png) Tiếp tục với **MixColumns**, cái này thì phức tạp hơn một chút. Vì cột mới được tạo từ phép nhân tuyến tính nên nó sẽ ảnh hưởng đến tất cả các phần tử của cột đầu ra. Và ta sẽ thu được như sau: ![image](https://hackmd.io/_uploads/HJDERqxQR.png) Nhớ lại các hoạt động của phép biến đổi này, ta có công thức như sau ![image](https://hackmd.io/_uploads/SJr_AcxmA.png) Lưu ý, ma trận cố định kia có thể khác tùy từng bài. Ta có được rằng cách tính cột sẽ như này $$ \begin{bmatrix} 2a_0 + 3a_1 + 1a_2 + 1a_3\\ 1a_0 + 2a_1 + 3a_2 + 1a_3\\ 1a_0 + 1a_1 + 2a_2 + 3a_3\\ 3a_0 + 1a_1 + 1a_2 + 2a_3 \end{bmatrix} $$ Từ đó giá trị đầu tiên của cột sẽ có công thức như sau: $$\underbrace{2a_0}_\text{active} + \underbrace{3a_1 + 1a_2 + 1a_3}_\text{constant}$$ Bây giờ, $\varLambda$**-set** đã có 4 **active indexes**. Và sau khi chạy hết round 3 thì ta có được như sau ![image](https://hackmd.io/_uploads/BySsW2emR.png) Sau round 3, ta không còn thu được $\varLambda$**-set** nữa, nhưng mà ngay sau Add_round_key, ta lấy byte đầu của các ciphertext xor lại với nhau ta được $$b_1 \oplus\\ b_2 \oplus\\ \cdots\\ b_{256}$$ Từ đó, ta có được rằng $$ \begin{gather*} 2 a_{1, 0} \oplus 3 a_{1, 1} \oplus 1 a_{1, 2} \oplus 1 a_{1, 3}\\ \oplus \\ 2 a_{2, 0} \oplus 3 a_{2, 1} \oplus 1 a_{2, 2} \oplus 1 a_{2, 3}\\ \oplus \\ \cdots\\ \oplus\\ 2 a_{256, 0} \oplus 3 a_{256, 1} \oplus 1 a_{256, 2} \oplus 1 a_{256, 3}\\ \end{gather*} $$ Và biến đổi ta sẽ được như sau $$ \begin{gather*} 2 (a_{0, 0} \oplus \cdots \oplus a_{256, 0})\\ \oplus\\ 3 (a_{0, 1} \oplus \cdots \oplus a_{256, 1})\\ \oplus\\ 1 (a_{0, 2} \oplus \cdots \oplus a_{256, 2})\\ \oplus\\ 1(a_{0, 3}\oplus \cdots \oplus a_{256, 3}) \end{gather*} = 2 \times 0 \oplus 3 \times 0 \oplus 1 \times 0 \oplus 1 \times 0 = 0 $$ Lưu ý là các byte có cùng thứ tự đều có khả năng như thế nha. Giờ ta có thể check var coi có đúng không bằng code này nha. ```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, ) def sub_bytes(s): for i in range(4): for j in range(4): s[i][j] = s_box[s[i][j]] 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 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] def add_round_key(s, k): for i in range(4): for j in range(4): s[i][j] ^= k[i][j] # learned from https://web.archive.org/web/20100626212235/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) 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, ) 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, [])) def xor_bytes(a, b): """ Returns a new byte array with the elements xor'ed. """ return bytes(i^j for i, j in zip(a, b)) def inc_bytes(a): """ Returns a new byte array with the value increment by 1 """ out = list(a) for i in reversed(range(len(out))): if out[i] == 0xFF: out[i] = 0 else: out[i] += 1 break return bytes(out) def pad(plaintext): """ Pads the given plaintext with PKCS#7 padding to a multiple of 16 bytes. Note that if the plaintext size is a multiple of 16, a whole block will be added. """ padding_len = 16 - (len(plaintext) % 16) padding = bytes([padding_len] * padding_len) return plaintext + padding def unpad(plaintext): """ Removes a PKCS#7 padding, returning the unpadded text and ensuring the padding was correct. """ padding_len = plaintext[-1] assert padding_len > 0 message, padding = plaintext[:-padding_len], plaintext[-padding_len:] assert all(p == padding_len for p in padding) return message def split_blocks(message, block_size=16, require_padding=True): assert len(message) % block_size == 0 or not require_padding return [message[i:i+16] for i in range(0, len(message), block_size)] class AES: """ Class for AES-128 encryption with CBC mode and PKCS#7. This is a raw implementation of AES, without key stretching or IV management. Unless you need that, please use `encrypt` and `decrypt`. """ rounds_by_key_size = {16: 4} def __init__(self, master_key): """ Initializes the object with a given key. """ assert len(master_key) in AES.rounds_by_key_size self.n_rounds = AES.rounds_by_key_size[len(master_key)] self._key_matrices = self._expand_key(master_key) def _expand_key(self, master_key): """ Expands and returns a list of key matrices for the given master_key. """ # Initialize round keys with raw key material. key_columns = bytes2matrix(master_key) iteration_size = len(master_key) // 4 i = 1 while len(key_columns) < (self.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 = xor_bytes(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 encrypt_block(self, plaintext): """ Encrypts a single block of 16 byte long plaintext. """ assert len(plaintext) == 16 plain_state = bytes2matrix(plaintext) add_round_key(plain_state, self._key_matrices[0]) for i in range(1, 4): # self.n_rounds sub_bytes(plain_state) shift_rows(plain_state) mix_columns(plain_state) add_round_key(plain_state, self._key_matrices[i]) # sub_bytes(plain_state) # shift_rows(plain_state) # add_round_key(plain_state, self._key_matrices[-1]) return matrix2bytes(plain_state) import os lst = [] cipher = AES(b'a'*16) index = 0 x = os.urandom(16) for i in range(256): temp = i.to_bytes(1,byteorder='big') a = bytearray(x) a[index] = i ciphertext = cipher.encrypt_block(a) lst.append(ciphertext) from pwn import* result = b'' for i in lst: result = xor(result,i) print(result) ``` Từ đó ta có tính chất như thế nhaaa. Giờ mình sang vòng 4 nhéeee ### Vòng 4 Vì chỉ có 4 vòng, thế nên vòng cuối, sẽ không có Mixcolumns, thế nên, sơ đồ sẽ như thế này ![image](https://hackmd.io/_uploads/ryjkvCxXR.png) Giờ thì phải làm sao đây, ở cuối vòng 3 mình chỉ có tính chất là tất cả xor đều bằng 0, phải làm sheo phải làm sheo ??? Các bước sẽ đoán roundKey[4] như sau: - Giờ mình sẽ tạo 1 $\varLambda$**-set**, sau đó ta sẽ encrypt hết 256 phần tử này và gọi là **enc-**$\varLambda$**-set** - Ví dụ muốn tìm lại byte đầu tiên của roundKey[4] đi, gọi là roundKey[4][0] đi, thì ta sẽ bruteforce. - Lấy 1 biến $\text{guess} \in [0,255]$. Ta sẽ thay đổi các ciphertext trong **enc-**$\varLambda$**-set** theo công thức này $\text{ciphertext[0] = ciphertext[0] ^ guess}$. - Sau đó các ciphertext này sẽ đi qua lại $\text{InvShiftRows }$ và $\text{InvSubBytes}$. Các ciphertext mới này, nếu thỏa mãn điều kiện ở vòng 3 anh cho 2 đánh thì là giá trị guess này chính xác. - Nhưng nhỡ có trường hợp fake fake thì sao nhỉ, ta sẽ gen lại $\varLambda$**-set** sao cho chỉ tìm được 1 giá trị duy nhất thoaiii. Ví dụ tiếp bài code ở trên nhá, ta phải thêm round 4 vào nhéee ``new.py`` ```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, ] def sub_bytes(s): for i in range(4): for j in range(4): s[i][j] = s_box[s[i][j]] 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 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] def add_round_key(s, k): for i in range(4): for j in range(4): s[i][j] ^= k[i][j] # learned from https://web.archive.org/web/20100626212235/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) 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, ) 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, [])) def xor_bytes(a, b): """ Returns a new byte array with the elements xor'ed. """ return bytes(i^j for i, j in zip(a, b)) def inc_bytes(a): """ Returns a new byte array with the value increment by 1 """ out = list(a) for i in reversed(range(len(out))): if out[i] == 0xFF: out[i] = 0 else: out[i] += 1 break return bytes(out) def pad(plaintext): """ Pads the given plaintext with PKCS#7 padding to a multiple of 16 bytes. Note that if the plaintext size is a multiple of 16, a whole block will be added. """ padding_len = 16 - (len(plaintext) % 16) padding = bytes([padding_len] * padding_len) return plaintext + padding def unpad(plaintext): """ Removes a PKCS#7 padding, returning the unpadded text and ensuring the padding was correct. """ padding_len = plaintext[-1] assert padding_len > 0 message, padding = plaintext[:-padding_len], plaintext[-padding_len:] assert all(p == padding_len for p in padding) return message def split_blocks(message, block_size=16, require_padding=True): assert len(message) % block_size == 0 or not require_padding return [message[i:i+16] for i in range(0, len(message), block_size)] class AES: """ Class for AES-128 encryption with CBC mode and PKCS#7. This is a raw implementation of AES, without key stretching or IV management. Unless you need that, please use `encrypt` and `decrypt`. """ rounds_by_key_size = {16: 4} def __init__(self, master_key): """ Initializes the object with a given key. """ assert len(master_key) in AES.rounds_by_key_size self.n_rounds = AES.rounds_by_key_size[len(master_key)] self._key_matrices = self._expand_key(master_key) def _expand_key(self, master_key): """ Expands and returns a list of key matrices for the given master_key. """ # Initialize round keys with raw key material. key_columns = bytes2matrix(master_key) iteration_size = len(master_key) // 4 i = 1 while len(key_columns) < (self.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 = xor_bytes(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 encrypt_block(self, plaintext): """ Encrypts a single block of 16 byte long plaintext. """ assert len(plaintext) == 16 plain_state = bytes2matrix(plaintext) add_round_key(plain_state, self._key_matrices[0]) for i in range(1, self.n_rounds): sub_bytes(plain_state) shift_rows(plain_state) mix_columns(plain_state) add_round_key(plain_state, self._key_matrices[i]) sub_bytes(plain_state) shift_rows(plain_state) add_round_key(plain_state, self._key_matrices[-1]) return matrix2bytes(plain_state) ``` ``x.py`` ```python from new import* cipher = AES(b'a'*16) while True: plain = input('> ') ciphertext = cipher.encrypt_block(bytes.fromhex(plain)).hex() print(ciphertext) ``` Ta dùng code sau để có thể reverse_state và sau đó guess_key ```python from pwn import * from tqdm import * import os from aeskeyschedule import * from new import* from typing import Callable, Iterable, List, Tuple key_pos = [0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, 1, 6, 11] # get from https://hackmd.io/@Giapppp/square_attack#AES-4-Round io = process(["python3", "x.py"]) def encrypt(pt:bytes): io.sendlineafter(b'> ', pt.hex().encode()) ct = io.recvuntil(b'\n',drop=True).decode() return bytes.fromhex(ct) def get_ciphertexts(index): origin = os.urandom(16) A_set = [] for i in range(256): temp = bytearray(origin) temp[index] = i A_set.append(encrypt(temp)) return A_set def reverse_state(guess, state, index): state = bytearray(state) state[index] = state[index] ^ guess state = list(state) state = bytes2matrix(bytes(state)) inv_shift_rows(state) inv_sub_bytes(state) return matrix2bytes(state) def guess_key_byte(index): while True: A_set = get_ciphertexts(index) answer = [] for i in range(256): target = 0 for state in A_set: state = reverse_state(i,state,index) target = target ^ state[key_pos[index]] if target == 0: answer.append(i) if len(answer) == 1: return answer[0] key = [] for i in tqdm(range(16)): ans = guess_key_byte(i) key.append(ans) print((key)) ``` Sau khi đoán được round_key[4] rồi, ta có thể sử dụng thêm tool đảo ngược lại [**key**](https://github.com/fanosta/aeskeyschedule) Thêm dòng code này nữa thôi. ```python hexkey = reverse_key_schedule(bytes(key), 4).hex() print(hexkey) ``` Từ đó ta có thể tìm lại được key của bài. ### Vòng 5 Giờ mình mới có thời gian để tìm hiểu thêm về cái này, thực ra cũng không có gì nghiêm trọng lắm, cùng nhìn vào cái hình sau nhé ![image](https://hackmd.io/_uploads/SJfqjXc7C.png) Giờ ta có 5 rounds, để quay lại cách tấn công như phần trước, thì trước hết ta phải brute 4 bytes ở ``AddRoundKey`` round 5, sau đó sẽ đảo ngược về trạng thái đầu round 5, sau đó sẽ phải bruteforce tiếp 4 bytes ở ``AddRoundKey`` round 4 nữa, thế thì sẽ phải bruteforce tổng cộng 8 bytes chỉ để tìm được lại 1 bytes, quá tốn thời gian và tài nguyên đúng không nào. Thế nhưng, ta có được tính chất như này: $$ \begin{align*} &\text{MixColumns}(\text{state)} \oplus \text{RoundKey[4]} \\ = &\text{MixColumns}(\text{state}) \oplus \text{MixColumns}(\text{MixColumnsInv}(\text{RoundKey[4]})) \\ = &\text{MixColumns}(\text{state } \oplus \text{MixColumnsInv}(\text{RoundKey[4]})) \end{align*} $$ Thế nên, ta chỉ cần bruteforce 4 byte ở Roundkey thứ 5, sau đó brute 1 byte ở Roundkey thứ 4, ta sẽ tìm được 1 byte của key. Có giảm xuống còn $256^5$ thay vì $256^8$ nhưng mà không đáng kể. ![image](https://hackmd.io/_uploads/rkLWxNc7R.png) Mình chưa có gặp bào nào 5 round nên mình cũng không biết lấy ví dụ kiểu gì vì nếu làm như các ví dụ trước thì rất tốn tài nguyên. Có lẽ mình sẽ bổ sung ví dụ vào sau để các đọc giả dễ hiểu hơn. Cách tấn công này còn có thể tấn công ở round thứ 6 nữa, nhưng mà chắc cũng khó có trong CTF vì nó còn mất nhiều thời gian hơn round 5 rất nhiều. Nếu bạn muốn tìm hiểu thêm thì có thể đọc 2 đường link mình có gán ở trên nhé.