## Block Cipher 1
### Description
- Challenge code:
```python=
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from hashlib import md5
from os import urandom
FLAG = b"KCSC{???????????????????????????}"
assert len(FLAG) % 16 == 1 # hint
key1 = md5(urandom(3)).digest()
key2 = md5(urandom(3)).digest()
cipher1 = AES.new(key1, AES.MODE_ECB)
cipher2 = AES.new(key2,AES.MODE_ECB)
enc = cipher1.encrypt(pad(FLAG,16))
enc = cipher2.encrypt(enc)
print(enc.hex())
# 21477fac54cb5a246cb1434a1e39d7b34b91e5c135cd555d678f5c01b2357adc0c6205c3a4e3a8e6fb37c927de0eec95
```
### Solution
- Phân tích một chút, bài cho `enc` được mã hóa hai lần qua AES với `key1` và `key2` 3 bytes random rồi được hash MD5. Ý tưởng xuất hiện đầu tiên của mình là brute force nhưng `key1` và `key2` đều là 3 bytes nên không gian sẽ là `256**3 * 256**3 = 256**6`. Đây là không gian khá lớn để brute force được.
- Tuy nhiên, mình nhớ lại về 2DES với cách tấn công `meet-in-the-middle attack`. Khi đó, không gian của ta sẽ nhỏ lại chỉ còn `256**3 + 256**3 = 2**24 + 2**24 = 2**25` nên có thể brute force được `key1`, `key2` dễ dàng.
- Ta có thuật toán mã hóa như sau:
```
E(flag, key1) = enc1
E(enc1, key2) = enc2 --> D(enc, key2) = enc1
```
- Thuật toán của `meet-in-the-middle attack` khá đơn giản, mình sẽ thực hiện `encrypt` với `256**3` giá trị `key1` và lưu vào bảng, sau đó thực hiện `decrypt` với `256**3` giá trị `key2`. Check với bảng vừa khởi tạo, chắc chắn ta luôn có được một cặp `key1`, `key2` thỏa mãn vì cùng tạo ra đoạn `enc1`.
- Mình đã có `enc2` để `decrypt`, vậy còn `flag` thì sao. Có hint `len(flag) % 16 == 1` nên `len(flag) = 16*k + 1`. Check `len(enc) = 48` nên `len(flag + pad) = 48`, suy ra `len(flag) = 33`. Ta có thể chia `flag` thành hai `block` 16 bytes. Có `block2 = b'}' + b'\x0f'*15` do PKCS#7. Vậy mình sẽ `encrypt` với `block2` này và thực hiện `meet-in-the-middle attack` với `block2` để tìm ra `key1`, `key2` thỏa mãn.
- Code:
```python=
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from tqdm import tqdm as t
from hashlib import md5
ct = '21477fac54cb5a246cb1434a1e39d7b34b91e5c135cd555d678f5c01b2357adc0c6205c3a4e3a8e6fb37c927de0eec95'
ct = bytes.fromhex(ct)
block2_check = ct[-16:]
block2 = b'}' + b'\x0f'*15
look_up = {}
key1, key2 = b'', b''
for temp in t(range(256**3), desc=':Collecting'):
temp = temp.to_bytes(3)
key = md5(temp).digest()
cipher = AES.new(key, AES.MODE_ECB)
enc = cipher.encrypt(block2)
look_up[enc] = key
for temp in t(range(256**3), desc='Checking'):
temp = temp.to_bytes(3)
key = md5(temp).digest()
cipher = AES.new(key, AES.MODE_ECB)
dec = cipher.decrypt(block2_check)
if dec in look_up:
key1 = look_up[dec]
key2 = key
print(key1)
print(key2)
break
cipher1 = AES.new(key1, AES.MODE_ECB)
cipher2 = AES.new(key2, AES.MODE_ECB)
flag = cipher2.decrypt(ct)
flag = cipher1.decrypt(flag)
flag = unpad(flag, 16).decode()
assert len(flag) % 16 == 1
print(flag)
```
### Flag
> ~~`KCSC{MeEt_In_tHe_mIdDLe_AttaCk__}`~~
### Note
```python=
key1 = b'/\xc5\xe63%\xac\x93\xc1\xaf\xd3\x94\xe5\n\xd3\xf3I'
key2 = b'\x8f\x06\x88\x17\x01\xd9\xd9j\xf5F\xe6\x08_z\xf4\xb1'
```
---
## Block Cipher 2: 2000_ECB_CBC
### Description
- Challenge code:
```python=
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from random import choice
from os import urandom
import socket
import threading
FLAG = b'KCSC{Bingo!_PKCS#7_padding}'
class ThreadedServer(object):
def __init__(self, host, port):
self.host = host
self.port = port
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind((self.host, self.port))
def listen(self):
self.sock.listen(5)
while True:
client, address = self.sock.accept()
client.settimeout(60)
threading.Thread(target = self.listenToClient,args = (client,address)).start()
def listenToClient(self, client, address):
size = 1024
for i in range(100):
x = choice(['ECB','CBC'])
if x == 'ECB':
cipher = AES.new(urandom(16), AES.MODE_ECB)
else:
cipher = AES.new(urandom(16), AES.MODE_CBC, urandom(16))
try:
msg = bytes.fromhex(client.recv(size).strip().decode())
assert len(msg) <= 16
client.send(cipher.encrypt(pad(msg,16)).hex().encode() + b'\n')
ans = client.recv(size).strip().decode()
assert ans == x
client.send(b'Correct!\n')
except:
client.send(b"Exiting...\n")
client.close()
return False
client.send(FLAG)
client.close()
return False
if __name__ == "__main__":
ThreadedServer('',2000).listen()
```
### Solution
- Phân tích một chút, mình cần gửi cho server một `msg` và server trả về một `ciphertext`. Mình cần trả lời xem `msg` đã được mã hóa theo mode `ECB` hay `CBC`. Trước tiên cần hiểu được sơ đồ của hai mode này:
- ECB:

- CBC:

- Nếu `block` không đủ 16 bytes thì sẽ được pad thêm theo PKCS#7. Do tất cả mọi thứ đều là random trừ `msg` mình có thể chọn nên mình sẽ hướng theo phân tích `msg` hay nói cách khác là các `block` của mình.
- Mình sẽ thử với `msg = b'\x00'*16` trên local:
```python=
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
key_ECB = b'\xee\xbc\x83\x91\xeaPT\xbe\x10>g\xe9\x9c\xfd\x9d\xee'
key_CBC = b'\xaf\x0f\xcej\x05>\x18\x81\xc4\xa9\xcf\x96\x1a\x04\xb7q'
iv_CBC = b'\xf9_I\x17\xcfs\x8c\xb7\x98\xf3\xa1\r\xd0\xcc69'
msg = b'\x00'*16
cipher1 = AES.new(key_ECB, AES.MODE_ECB)
cipher2 = AES.new(key_CBC, AES.MODE_CBC, iv_CBC)
print(pad(msg, 16))
ct_ECB = cipher1.encrypt(pad(msg, 16))
ct_CBC = cipher2.encrypt(pad(msg, 16))
```
- Một điều cần chú ý ở đây là `pad(msg, 16)` của mình trả về 32 bytes (được đệm thêm 16 bytes `b'\x10'`). Tuy nhiên, mình cũng biết được mode `ECB` mã hóa giống nhau cho các `block` giống nhau, mà `msg` ở dạng hex không được dài quá 16. Chính vì thế, mình sẽ gửi `msg = b'\x10' * 16`. Khi đó nếu `ct[:16] = ct[16:]` thì sure đó là mode `ECB`, nếu không thì là mode `CBC`.
- Code thử trên local:
```python=
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
key_ECB = b'\xee\xbc\x83\x91\xeaPT\xbe\x10>g\xe9\x9c\xfd\x9d\xee'
msg = b'\x10'*16
cipher = AES.new(key_ECB, AES.MODE_ECB)
print(pad(msg, 16))
ct_ECB = cipher.encrypt(pad(msg, 16))
block1 = ct_ECB[:16]
block2 = ct_ECB[16:]
if(block1 == block2):
print('ECB')
```
- Code solve:
```python=
from pwn import *
def send(msg):
return r.sendline(msg.encode())
r = remote('localhost', 2000)
msg = bytes.hex(b'\x10'*16)
for i in range(100):
send(msg)
get = bytes.fromhex(r.recv().decode())
block1 = get[:16]
block2 = get[16:]
send('ECB') if block1 == block2 else send('CBC')
print(r.recv().decode())
if i == 99:
flag = r.recv().decode()
print(flag)
```
### Flag
> ~~`KCSC{Bingo!_PKCS#7_padding}`~~
### Note
- Vì `len(msg) <= 16` và một `block` luôn có 16 bytes, bên cạnh đó PKCS#7 pad ngay cả khi `len(block) % 16 == 0` nên bắt buộc phải giải bài với `msg` như trên.
---
## Block Cipher 3: 2003_ECB
### Description
- Challenge code:
```python=
from Crypto.Cipher import AES
from os import urandom
from base64 import b64encode
import string
import socket
import threading
chars = string.ascii_lowercase + string.ascii_uppercase + '!_{}'
FLAG = b'KCSC{Chosen_Plaintext_Attack___ECB_ECB_ECB___you_made_it!}'
assert all(i in chars for i in FLAG.decode())
def pad(msg, block_size):
if len(msg) % block_size == 0:
return msg
return msg + bytes(block_size - len(msg) % block_size)
def chall(usrname):
key = urandom(16)
cipher = AES.new(key, AES.MODE_ECB)
msg = b'Hello ' + usrname + b', here is your flag: ' + FLAG + b'. Have a good day, we hope to see you soon.'
enc = cipher.encrypt(pad(msg,16))
return b64encode(enc)
class ThreadedServer(object):
def __init__(self, host, port):
self.host = host
self.port = port
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind((self.host, self.port))
def listen(self):
self.sock.listen(5)
while True:
client, address = self.sock.accept()
client.settimeout(60)
threading.Thread(target = self.listenToClient,args = (client,address)).start()
def listenToClient(self, client, address):
size = 1024
while True:
try:
usrname = client.recv(size).strip()
client.send(chall(usrname) + b'\n')
except:
client.close()
return False
if __name__ == "__main__":
ThreadedServer('',2003).listen()
```
### Solution
- Đối với bài, mình có một cách để cheat ra độ dài của flag với code:
```python=
from pwn import *
from json import *
from base64 import b64decode as D
def send(msg):
return r.sendline(msg)
r = remote('localhost', 2003)
for i in range(30):
usrname = b'\x00'*i
send(usrname)
ct = r.recvline().decode()
ct = D(ct)
print(i, len(ct))
```
- Sau khi thực thi code, mình nhận được cặp giá trị đặc biệt như sau:
```python=
...
16 144
17 160
...
```
- Giải thích một chút, khi mình gửi lên `usrname` có độ dài là `i`, server sẽ tự động pad thêm để `len(ct) % 16 == 0`
- Khi độ dài là 16, nhận được `len(ct) = 144`
- Khi độ dài là 17, nhận được `len(ct) = 160`
- Trong đó cơ chế PKCS#7 cũng sẽ pad ngay cả khi `len(ct) % 16 == 0`.
- Chính vì vậy, tại giá trị thay đổi kia ta tìm được độ dài của flag do `len(txt) + len(usrname) + len(flag) = 144`
- Biết được
```python=
txt = b'Hello , here is your flag: . Have a good day, we hope to see you soon' # len = 69
usrname = b'\x00' * 17 # len = 17
```
- Vì vậy, `len(flag) = 144 - 69 - 17 = 58`. Ta cần dùng số 58 này để assert cũng như thực hiện giải mã dễ dàng hơn.
- Ở [đây](https://exploit-notes.hdks.org/exploit/cryptography/algorithm/aes-ecb-padding-attack/) cũng có một thuật toán giúp tìm lại từng kí tự của flag bằng cách brute force, tuy nhiên chỉ áp dụng được khi `key` là cố định.
- Bài khá khoai khi cho `key` random nhưng mình có sơ đồ ECB như sau:

- Điều này chứng tỏ nếu `P1 = P2` thì `C1 = C2` và khắc phục được nhược điểm của thuật toán nói trên. Thay vì brute force lên server nhiều lần, mình sẽ chọn gửi `usrname` giống với phần liền kề ngay trước của `flag` rồi brute force bằng bytes cuối cùng của `usrname`.
- Ý tưởng (thử với chữ đầu của `flag` là `K`):
```python=
block2 = b' is your flag: ' + bruteforce()
block_real = b' is your flag: K'
Bằng cách nào đó để Enc(block2) = Enc(block_real) thì bruteforce() = K
```
- Mình đã phân tích như sau:
```python=
Hello ########## is your flag: K##########, here is your flag: K
block1 = b'Hello ##########'
block2 = b' is your flag: K'
block3 = b'##########, here'
block4 = b' is your flag: K'
```
- Điều này giúp cho `block2 = block4`, thực tế thì `block2 = b' is your flag: ' + char` và `usrname = ########## is your flag: K##########`
- Thử gửi xem sao:
```python=
from pwn import *
from json import *
from base64 import b64decode as D
def send(msg):
return r.sendline(msg)
r = remote('localhost', 2003)
LEN = 58
FLAG = ''
usrname = b'########## is your flag: K##########'
send(usrname)
test = D(r.recv().decode())
block2 = test[16:32]
block4 = test[48:64]
print(block2 == block4)
```
- Nhận được `True`, vậy là done :smile_cat:. Tuy nhiên nếu làm như trên thì chỉ thu được một phần `flag` nên mình sẽ thay đổi `usrname` đi sao cho `flag` được cover trong `block` nào đó.
- Code:
```python=
from pwn import *
from json import *
from base64 import b64decode as D
def send(msg):
return r.sendline(msg)
r = remote('localhost', 2003)
LEN = 58
FLAG = ''
temp = ''
chars = string.ascii_lowercase + string.ascii_uppercase + '!_{}'
prefix = b'#' * (16 - len(b'Hello '))
sub = b' is your flag: '
for i in range(58):
num = 58 - i
sub = sub[1:] + temp.encode()
suffix = b'#' * num
for char in chars:
usrname = prefix + sub + char.encode() + suffix
send(usrname)
get = D(r.recv().decode())
block2 = get[16:32]
block_real = get[96:112]
if block2 == block_real:
FLAG += char
temp = char
print(FLAG)
assert len(FLAG) == LEN
r.close()
print(FLAG)
```
- Giải thích code với trường hợp `i = 0`:
```python=
num = 58 - 0
prefix = b'Hello ' + b'#' * 10
sub = b' is your flag: K'
suffix = b'#' * num
test = prefix + sub + suffix + b', here is your flag: K. Have a good day, we hope to see you soon'
block2 = test[16:32]
block_real = test[96:112]
print(block2 == block_real)
```
### Flag
> ~~`KCSC{Chosen_Plaintext_Attack___ECB_ECB_ECB___you_made_it!}`~~
---
## Block Cipher 4: 2004_CBC
### Descrition
- Challenge code:
```python=
import socket
import threading
from Crypto.Cipher import AES
from os import urandom
import string
chars = string.ascii_lowercase + string.ascii_uppercase + string.digits + '_{}'
FLAG = b'KCSC{CBC_p4dd1ng_0racle_}'
assert all(i in chars for i in FLAG.decode())
def pad(msg, block_size):
pad_len = 16 - len(msg) % block_size
return msg + bytes([pad_len])*pad_len
def encrypt(key):
iv = urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
return (iv + cipher.encrypt(pad(FLAG,16)) ).hex().encode()
def decrypt(enc,key):
enc = bytes.fromhex(enc)
iv = enc[:16]
ciphertext = enc[16:]
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(ciphertext)
pad_len = decrypted[-1]
if all(i == pad_len for i in decrypted[-pad_len:]):
return b'Decrypted successfully.'
else:
return b'Incorrect padding.'
class ThreadedServer(object):
def __init__(self, host, port):
self.host = host
self.port = port
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind((self.host, self.port))
def listen(self):
self.sock.listen(5)
while True:
client, address = self.sock.accept()
client.settimeout(60)
threading.Thread(target = self.listenToClient,args = (client,address)).start()
def listenToClient(self, client, address):
size = 1024
key = urandom(16)
while True:
try:
choice = client.recv(size).strip()
if choice == b'encrypt':
client.send(encrypt(key) + b'\n')
elif choice == b'decrypt':
client.send(b'Ciphertext: ')
c = client.recv(size).strip().decode()
client.send(decrypt(c,key) + b'\n')
except:
client.close()
return False
if __name__ == "__main__":
ThreadedServer('',2004).listen()
```
### Solution
- Vì server có chức năng check padding PKCS#7 hợp lệ hay không nên mình sẽ sử dụng Padding Oracle Attack cho bài.
- Chi tiết thuật toán ở [đây](https://app.gitbook.com/o/ReP9yeC0bUTTA2QlLOpD/s/cdRw8tNOTeDBfoMGZBvR/padding-oracle-attack)
- Giải thích ngắn gọn:
```=
P[i] = D(C[i]) ^ C[i-1]
P'[i] = D(C[i]) ^ C'[i-1]
```
- Ta sẽ thay đổi `C[i-1]` thành `C'[i-1]` sao cho thỏa mãn PKCS#7 của server để tìm lại `D(C[i])` rồi xor với `C[i-1]` để tìm lại `P[i]`
- Code thử cho một byte cuối cùng:
```python=
from pwn import *
r = remote('localhost', 2004)
r.sendline(b'encrypt')
get = bytes.fromhex(r.recv().decode())
iv = get[:16]
block1 = get[16:32]
block2 = get[32:]
for i in range(256):
fake = block1[:-1] + bytes([i])
r.sendline(b'decrypt')
r.recvuntil(b'Ciphertext: ')
r.sendline(bytes.hex(iv + fake + block1).encode())
get = r.recvuntil(b'\n')
if b'Decrypted successfully.' in get:
last = xor(b'\x01', i)
print(xor(last, iv[-1:]))
break
```
- Giải thích dòng 14 tại sao lại là `iv + fake + block1`:
- Đơn giản vì `flag` được pad thành 32 bytes nên mình cần gửi 2 blocks.
- `+ block1` chứ không phải là `+ block2` vì server sẽ thực hiện decrypt `iv + fake + block1`, mình đang thay đổi `fake` để tìm lại `block1` của flag. Chính vì vậy nên nếu có chạy cả 1 tỉ lần code trên thì output vẫn sẽ là `b'g'` (code sai thì output mỗi lần khác nhau)
- Tuy nhiên, cần lưu ý sau khi tìm được byte cuối của `block` trước, ta cần tiếp tục thay đổi byte đó sao cho thỏa PKCS#7 padding cho byte tiếp theo (Ví dụ tìm được byte thỏa `xor` thành `b'\x01'` thì cần thay nó thành một byte khác `xor` thành `b'\x02'`).
- Code (đã đổi tên các biến):
```python=
from pwn import *
from Crypto.Util.Padding import unpad
r = remote('localhost', 2004)
r.sendline(b'encrypt')
get = bytes.fromhex(r.recv().decode())
iv, temp = get[:16], get[16:]
blocks = [temp[:16], temp[16:32]]
flag = b''
for block in blocks:
mod_pt = b''
mod_ct = b'\x00'*16
decrypted = b''
for i in range(1, 17):
for j in range(256):
mod_ct = mod_ct[:-i] + bytes([j]) + mod_pt
r.sendline(b'decrypt')
r.recvuntil(b'Ciphertext: ')
r.sendline(bytes.hex(iv + mod_ct + block).encode())
get = r.recvuntil(b'\n')
if b'Decrypted successfully.' in get:
decrypted = xor(i, j) + decrypted
mod_pt = xor(decrypted[-i:], bytes([i+1]*i))
break
flag += xor(decrypted, iv) if block == blocks[0] else xor(decrypted, blocks[0])
flag = unpad(flag, 16).decode()
print(flag)
```
### Flag
> ~~`KCSC{CBC_p4dd1ng_0racle_}`~~
### Note
- Phần tìm được 2 bytes cuối (trở đi) để ra `b'\x02\x02'` có thể khá khó hiểu nhưng [đoạn video này](https://youtu.be/8Tr2aj6JETg?si=qJwX08dcuD7oEuu_&t=180) giải thích bằng animation cực xịn :100:
- Giải thích dòng 26:
- Xét mode CBC:

- Decrypted đầu tiên sẽ được xor với `iv` nên mình chỉ cần tìm `P1 = xor(decrypted, iv)`. Đến decrypted tiếp theo thì được xor với `C1` nên `P2 = xor(decrypted, C1)`. Ghép hai `P1` và `P2` nhận được flag.