Mình xin trình bày một số bài mà mình làm được trong minictf tối 22/7 vừa qua
# Cryptography
## Flipping Login
`server.py`
```py
#!/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))
```
Trong bài này, một json token được mã hõa bằng phương thức AES-CBC với "admin": False. Ta cần phải tìm cách thay đổi sao cho "admin": True
Với tiêu đề bài đã gợi ý cho ta sử dụng Bits Flipping Attack trong AES-CBC, mình đã search trên google và tìm được một [tài liệu](https://tsublogs.wordpress.com/2015/07/18/bit-flipping-attack-on-cbc-mode/) liên quan đến nó. Chi tiết về cách tấn công thì các bạn có thể đọc trên link mình vừa gửi, nhưng mình có thể tóm gọn lại như sau:
Ta có công thức để mã hóa trong AES - CBC là:
$$\begin{aligned} C_0 &= E(P_0) \oplus IV \\ C_i &= E(P_i) \oplus C_{i-1} \ {với \ i > 0} \end{aligned}$$
Và công thức để giải mã là:
$$\begin{aligned} P_0 &= D(C_0) \oplus IV \\ P_i &= D(C_i) \oplus P_{i-1} \ {với \ i > 0} \end{aligned}$$
Như vậy, khi mã hóa cũng như giải mã, nó sẽ xor với ciphertext block / plaintext block trước đó (hoặc $IV$). Chính vì vậy, ta có thể thay đổi nội dung của block để có thể tạo ra nội dung theo ý muốn
Ở trong bài này, vị trí của '"admin": False, ' nằm đủ 1 block thứ hai nên ta sẽ chỉnh block thứ nhất là $IV$ để khi mà xor với block thứ hai sẽ có được '"admin": True , '. Các bạn có thể hiểu như thế này:
$$\begin{aligned}IV \oplus D(C_1) &= '"admin": False, ' \\ IV_{fake} \oplus D(C_1) &= '"admin": True , ' \\ IV_{fake} &= IV \oplus '"admin": False, ' \oplus '"admin": True , ' \end{aligned}$$
Sau khi có được $IV_{fake}$, ta chỉ cần gửi cho server $IV_{fake}+token[16:]$ (Token là thứ ta nhận được sau khi register, 16 bytes đầu của nó là IV) và sẽ có flag
Phần code chính:
`solve.py`
```py
from pwn import *
from base64 import b64decode, b64encode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Util.number import *
token = input() #Sau khi register ở server sẽ có một cái login token, bạn sẽ nhập nó ở đây
token = b64decode(token)
iv = token[:16]
s = token[16:]
plaintext = b'"admin": False, '
target = b'"admin": True,, '
new_iv = bytes([x ^ y ^ z for (x, y, z) in zip(iv, plaintext, target)])
print(b64encode(new_iv+s)) #Gửi cái này lại cho server là được
```
> **Flag:** W1{CBC_bit_flipping_attack_can_flip_your_system_https://tsublogs.wordpress.com/2015/07/18/bit-flipping-attack-on-cbc-mode}
Đúng cái mình search ban đầu luôn :)))
## Anti Pollard p-1
`chall.py`
```py
from math import prod
from Crypto.Util.number import *
from secret import flag, init
import os
assert isPrime(init) and 2**20 < init < 2**24
e = 0x10001
DEBUG = os.getenv("DEBUG", False)
def gen_smooth(init: int, bits: int, smoothness: int = 16) -> int:
q = 2*init
k = (bits - q.bit_length())//smoothness + 1
while True:
p = q * prod(getPrime(smoothness) for _ in range(k))
if GCD(e, p) == 1 and isPrime(p + 1):
return p + 1
p = gen_smooth(init, 768, 20)
q = gen_smooth(init, 768, 20)
N = p*q
pt = bytes_to_long(flag)
ct = pow(pt, e, N)
print(f"{e = }")
print(f"{N = }")
print(f"{ct = }")
if DEBUG:
print(f"{p = }")
print(f"{q = }")
```
Trong bài này, $p-1$ và $q-1$ đều là tích của các số nguyên tố nhỏ hơn 20 bits, vì vậy ta có thể nghĩ đến thuật toán [Pollard p-1](https://en.wikipedia.org/wiki/Pollard%27s_p_%E2%88%92_1_algorithm). Lưu ý rằng đối với bài này, ta sẽ không tính tích từ 1 đến $2^{20}$ mà sẽ shuffle lên rồi sau đó nhân lại dần.
Code để tìm $p$:
```py
import gmpy2
from tqdm import tqdm
import random
n = gmpy2.mpz(N)
ar = list(range(2, 2**20))
random.shuffle(ar)
a = gmpy2.mpz(2)
for p in tqdm(ar):
a = gmpy2.powmod(a, 4 * init * init * p, n)
p = gmpy2.gcd(a - 1, n)
if 1 < p < n:
print(p)
break
#p = 2782649911045466726583702468234636496093912422560611231304084465742455791597284396733219895856691138873818553244424989101233983426332838798108072186621785877449683943964193663359691028105593053522429899787764095292535702906703651599
```
Sau khi đã có $p$, mọi chuyện còn lại là đơn giản:
`solve.py`
```py
from Crypto.Util.number import *
from sage.all import *
e = 65537
N = ..
ct = ..
p = ..
q = N // p
phi = (p -1) * (q - 1)
d = pow(e, -1, phi)
m = pow(ct, d, N)
print(long_to_bytes(m))
```
>**flag:** W1{tricked_Pollard_p-1_attack_7270278554bb1eebf73197e4c88099d9f636e1626a6fa124f6ae411bf171dd76}
## Weird Primes
`chall.py`
```py
import os
from Crypto.PublicKey import RSA
from Crypto.Util.number import bytes_to_long, isPrime, inverse
from Crypto.Cipher import PKCS1_OAEP
from random import choices
from math import ceil
DEBUG = os.getenv("DEBUG", False)
digits = list("0123456789")
flag = b"W1{??????????????????????????????????}"
def getWeirdPrime(bit_length: int) -> int:
ndigits = ceil(bit_length/8)
while True:
p = ''.join(choices(digits, k=ndigits))
if isPrime(bytes_to_long(p.encode())):
return bytes_to_long(p.encode())
p = getWeirdPrime(1024)
q = getWeirdPrime(1024)
e = 0x10001
n = p*q
d = inverse(e, (p-1)*(q-1))
key = RSA.construct((n, e, d, p, q))
cipher = PKCS1_OAEP.new(key)
enc_flag = cipher.encrypt(flag).hex()
assert cipher.decrypt(bytes.fromhex(enc_flag)) == flag
print(f"{n = }")
print(f"{e = }")
print(f"{enc_flag = }")
if DEBUG:
print(f"{p = }")
print(f"{q = }")
```
Trong bài này, $p$ và $q$ là các số nguyên tố sao cho `long_to_bytes(p)` và `long_to_bytes(q)` đều chỉ có các kí tự số từ 0-9. Chính vì vậy, ta sẽ bruteforce dần các chữ số của `long_to_bytes(p)` và `long_to_bytes(q)` cho đến khi tìm được 2 số thỏa mãn, do chỉ có 128 bytes nên thời gian bruteforce sẽ rất nhanh:
`solve.py`
```py
n = ..
e = 65537
enc_flag = ..
from Crypto.PublicKey import RSA
from Crypto.Util.number import *
from Crypto.Cipher import PKCS1_OAEP
from random import choices
from math import ceil
digits = b'0123456789'
p = ''
q = ''
tempp = 0
tempq = 0
p = ''
q = ''
for step in range(128):
mod = 2**(8*step)
for dp in digits:
for dq in digits:
testp = bytes_to_long(p.encode()) + dp*mod
testq = bytes_to_long(q.encode()) + dq*mod
if (testp * testq)%(mod*256) == n%(mod*256):
p = long_to_bytes(dp).decode() + p
q = long_to_bytes(dq).decode() + q
else:
testp -= dp*mod
testq -= dq*mod
pp = bytes_to_long(p.encode())
qq = bytes_to_long(q.encode())
d = inverse(65537, (pp-1)*(qq-1))
key = RSA.construct((n, e, d, pp, qq))
cipher = PKCS1_OAEP.new(key)
#enc_flag = cipher.encrypt(flag).hex()
print(cipher.decrypt(bytes.fromhex(enc_flag)))
```
> **flag:** W1{branch_and_prune_is_sometime_very_useful!!!!}