---
# System prepended metadata

title: Writeup minictf 22/7/2022
tags: [writeup]

---

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!!!!}
