> Em tên là Thái Vĩnh Đạt (tên discord là tvdat20004), hôm 22/7 vừa rồi em có tham gia miniCTF do clb Wanna.W1n tổ chức và em được hạng 3. Em làm được 2 bài thuộc mảng Crypto và sau đây em xin trình bày write-up cho 2 bài đó ạ.
# flipping login

- [Attachment](https://cnsc.uit.edu.vn/ctf/files/39d2533105fc81e83ec4ddb273f18655/public.zip?token=eyJ1c2VyX2lkIjo2MDYsInRlYW1faWQiOm51bGwsImZpbGVfaWQiOjEwOH0.ZLyC1w.AmNd3c4YyyzDnZF2J6vxES08L_Q)
- Server.py:
```python=
#!/usr/bin/env python3
import json
import os
import signal
from base64 import b64decode, b64encode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from secret import flag
key = os.urandom(32)
def menu_choice() -> str:
print(
"""Valid choices:
1. Login
2. Register
3. Quit"""
)
return input(">> ").strip()
def handler(_signum, _frame):
print("Time out!")
exit(0)
def encrypt(msg):
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
return iv+cipher.encrypt(pad(msg, 16))
def decrypt(iv, msg):
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
return unpad(cipher.decrypt(msg), 16)
def login():
enc_token = b64decode(input("Login token: ").strip())
token = decrypt(enc_token[:16], enc_token[16:])
token = json.loads(token)
return token['name'], token['admin']
def register():
name = input("Nickname: ").strip()
token = {
"admin": False,
"name": name
}
token = json.dumps(token).encode()
enc_token = b64encode(encrypt(token)).decode()
print("Here is your login token: " + enc_token)
def main():
signal.signal(signal.SIGALRM, handler)
signal.alarm(60)
name = 'N.A'
is_admin = False
print("Welcome to my under-development program. Who are you?")
while True:
choice = menu_choice()
match choice:
case '1':
name, is_admin = login()
break
case '2':
register()
print('')
case '3':
print("Bye bye!")
break
case _:
print("??????????????")
raise RuntimeError("I found a hacker")
if is_admin:
print(f"Hello {name}! Here is your secret: " + flag)
else:
print(f"Hello {name}! Sorry, only admin can read my secret!")
try:
main()
except Exception as E:
print(str(E))
```
- Tóm tắt đoạn code:
- Server sẽ cho ta 3 lựa chọn: Login(1), Register(2), Quit(3).
- Khi chọn Register, server cho phép ta nhập `name`, sau đó nó sẽ tạo một `login_token`. `login_token` chính là base64 của kết quả sau khi mã hóa AES_CBC của một json:`{
"admin": False,
"name": name
}`
- Tùy chọn login cho phép ta nhập vào một `login_token`. Server sẽ encode base64, sau đó decrypt AES_CBC và loads json `token`. Để nhận được flag thì `token` phải có key `admin` ứng với giá trị True.
- Vậy điểu mình cần làm để lấy được flag là phải gởi token như yêu cầu ở trên. Với tên đề bài là `Flipping login` cộng với việc đã làm bài flipping cookie trên Cryptohack thì em nghĩ ngay đến kĩ thuật bit flipping attack. Trong quá trình làm bài em có tham khảo ở [link này](https://crypto.stackexchange.com/questions/66085/bit-flipping-attack-on-cbc-mode)
- Nhìn chung thì kĩ thuật này cho phép ta modify một vài byte trên chuỗi ciphertext (thực ra là iv + ciphertext) để thu được ciphertext mới và sau khi decrypt ta thu được plaintext như mong muốn. Ở đây mình cần modify sao cho từ plaintext là `{
"admin": false,
"name": name
}` thành plaintext `{
"admin": true,
"name": name
}`, tức là chuyển chuỗi `false` thành `true`. Mình sẽ tiến hành flip từng byte như sau:
- Ví dụ để chuyển 'f' thành 't', em sẽ xor bit thứ 10 (vị trí của 'f' trong plaintext) với `(ord(f)^ord(t))`.
- Lặp lại quá trình đồi với các bit tiếp theo cho đến khi flip hết chuỗi 'false' thành 'true '.
- Python implementation:
```python=
import base64
import json
from pwn import *
r = remote('45.122.249.68', 20022)
def login(token : str):
data = r.recvuntil(b'>> ')
r.sendline(b'1')
data = r.recvuntil(b'Login token: ')
r.sendline(token.encode())
flag = r.recvline()
print(flag)
def register() -> str:
data = r.recvuntil(b'>> ')
r.sendline(b'2')
data = r.recvuntil(b'Nickname: ')
r.sendline(b'tvdat20004')
data = r.recvuntil(b'token: ')
token = r.recvuntil(b'\n').decode().strip()
return token
def flipping_bits(ciphertext, pos, plaintext : bytes, new_plaintext : bytes):
assert len(plaintext) == len(new_plaintext)
ciphertext = list(ciphertext)
for i in range(pos, pos + len(plaintext)):
ciphertext[i] = ciphertext[i] ^ (plaintext[i-pos] ^ new_plaintext[i-pos])
iv = ciphertext[:16]
ct = ciphertext[16:]
return base64.b64encode(bytes(iv)+bytes(ct))
token = base64.b64decode(register())
ans = flipping_bits(token,10,b" true",b"false")
login(ans.decode())
r.interactive()
```

- Flag: `W1{CBC_bit_flipping_attack_can_flip_your_system_https://tsublogs.wordpress.com/2015/07/18/bit-flipping-attack-on-cbc-mode}`
# Weird Primes

- [chall.py](https://cnsc.uit.edu.vn/ctf/files/98af14b1a283d388b47f7e5f9eee2bf9/chall.py?token=eyJ1c2VyX2lkIjo2MDYsInRlYW1faWQiOm51bGwsImZpbGVfaWQiOjEwOX0.ZLyaIQ.HLODE-JrfZuY7cm45CJ-uHXOpWo),[ output.txt](https://cnsc.uit.edu.vn/ctf/files/eb0605b499533756b713a7c5c6197679/output.txt?token=eyJ1c2VyX2lkIjo2MDYsInRlYW1faWQiOm51bGwsImZpbGVfaWQiOjExMH0.ZLyaIQ.KkSAy8ATjTZI6y7j6UP4ebB3yLY)
- Hint: 'Bruteforcing each digit since number of possible bytes for each prime is small'.
- Sau khi đọc xong hint và một hồi suy nghĩ, em cảm thấy nó "khá giống" với parital bit leak dạng "random known bit of p and q" mà em đã làm trước đây (chi tiết về nó em để ở [đây](https://hal.science/hal-03045663/document), 4.3.1, trang 21). Tuy thoạt nhìn thì thấy 2 bài có vẻ rất khác, nhưng ý tưởng brute-force lại hoàn toàn giống nhau, đều sử dụng "branch and prune algorithm".
- Cụ thể, em sẽ brute-force từng byte(do p và q được tạo từ chuỗi byte gồm 128 số) của p và q theo thứ tự từ LSB đến MSB.
- Mô tả thuật toán:
- Xét cặp LSB của p và q, có tất cả 10x10 cặp có thể có. Bruteforce hết không gian mẫu đó cho đến khi gặp `p.q mod 256 = n mod 256` (p,q đang chỉ có 1 byte LSB)
- Xét tiếp byte sau đó, gọi là dp và dq, cũng có 10x10 cặp có thể có. Bruteforce hết không gian mẫu đó cho đến khi gặp `p.q mod (256**2)= n mod (256**2)`(khi này p,q có 2 byte LSB)
- Lặp lại quá trình cho đến khi tìm được 128 byte của p và q.
- Python implementation:
``` python=
n = 1193924133214231889546351753346545092386694958282484590742357919398336185127955297065849705214714735049853117873865465248652047796382636343398101815207127117475834303652582611557916482763041203527984476566898846821511189884575355212170099724076171155888215024629846800791116844007098877699954543318380357268011069937195402719153407503698044969734971221799699731859308452701292314029495280470102667890355711974564903038932606840517852702768264359240118381178825407856623508271302891317946429909210917646516863004718071228523457611307993744722272634542518670009533519107837470331125580871788354030347986972475782995433
e = 65537
enc_flag = '0255ad928673b69d2c1998cafaadda7028d339dd82483c9d01363231a01def067bcea9d2cc378e2b9054feadf31412b1f52e9b9061fc1ba8863cfffff840979e3e44743409e3d5a19ab2bcb2f570776b028ea5f5422962d103e2ea2e59c9c479e5a46c514e2196dd7d3f1c30d588879ef029e0fd41d278c6715998ed746817a29f9d09df775368c038cd616b1d17b0bb318fa4501a6375a98497ceda9a827e0f25a55790bb0a259305dc696e140a6f14a901148113ca5ae3a78195d2d06005a036a19478e7cf1d30d38791479169092745bbfc325d73c162e602e4918dab99221f2f64a50c4b349124d7ca8fb56d8358614f20dd2d445debfe711a6d3b441bb5'
from Crypto.Util.number import *
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
p = ''
q = ''
for i in range(128):
sign = False
for dp in '0123456789':
for dq in '0123456789':
tmp_p = dp + p
tmp_q = dq + q
if bytes_to_long(tmp_p.encode()) * bytes_to_long(tmp_q.encode()) % pow(256,i+1) == n % pow(256,i+1):
p = tmp_p
q = tmp_q
sign = True
break
if sign:
break
print(p)
print(q)
p = bytes_to_long(p.encode())
q = bytes_to_long(q.encode())
assert isPrime(p) and isPrime(q) and p*q == n
d = pow(e,-1,(p-1)*(q-1))
key = RSA.construct((n, e, d, p, q))
cipher = PKCS1_OAEP.new(key)
flag = cipher.decrypt(bytes.fromhex(enc_flag))
print(flag)
```

- Flag: `W1{branch_and_prune_is_sometime_very_useful!!!!}`