---
# System prepended metadata

title: KMACTF_2025

---


## KMACTF - Crypto

### 1. RAS

```py
from Crypto.Util.number import *
from math import prod
import random

class RAS(object):
    def __init__(self, arr):
        self.n = prod(arr)*prod(arr)
        
    def generate_e(self):
        e = random.getrandbits(4048)
        return e
    
    def encrypt(self, pt):
        e = self.generate_e()
        assert self.n.bit_length() == 4048
        c = [pow(m, e, self.n) for m in pt]
        return  c
        
flag = b'KMACTF{???????????????????????????????}'
flag1, flag2, flag3 = bytes_to_long(flag[:len(flag)//3]), bytes_to_long(flag[len(flag)//3:2*len(flag)//3]), bytes_to_long(flag[2*len(flag)//3:])

# shuffle it
m1 = flag3
m2 = 65537*flag1**2 + flag3*flag2**2 

menu = '''
Welcome to my RAS stolen from tvd2004
1. Send primes
2. Get encrypted flag
'''

while True : 
    print(menu)
    choose = input('> ')
    if choose == '1':
        
        try:
            count = int(input(f"Not 1, not 2, you can choose number prime in RSA system : "))
        except :
            exit()
            
        arr = []
        for i in range(count) :
            p = int(input(f"Prime {i} : "))
            arr.append(p)
        
        ras =  RAS(arr)
        
    elif choose == '2':
        
        print("Here is ciphertext :")
        print(ras.encrypt([m1,m2]))
        
    else:
        raise Exception("Invalid choice!!!")
```

Mỗi lần chọn 2 server sinh ngẫu nhiên e (4048-bit, không tiết lộ) và trả về [m1^e mod n, m2^e mod n]. Với m1, m2 được tính như sau:

+ n = $(\prod{p_i})^2$ (bình phương tích primes do người dùng nhập).
+ $m1 = flag3$

+ $m2 = 65537*flag1^2 + flag3*flag2^2$

![image](https://hackmd.io/_uploads/rkHzdG33lg.png)


với 2 hint như trên thì ta có thể dễ thấy việc ta cần làm bây giờ là tạo ra một số có giá trị Carmichael lamda nhỏ nhất có thể khi đó ta có thể brute giá trị `d` trong khoảng [1, $\lambda$] để tìm lại flag3 ban đầu.

Khi đó để làm cho giá trị hàm Carmichael $\lambda(n)$ nhỏ so với $n$, ý tưởng cơ bản là chọn các thừa số nguyên tố $p$ sao cho $p-1$  (và với lũy thừa $p^e$ thì là $p^{\,e-1}(p-1)$) chia hết cho một số nhỏ đã biết $L$.  

Khi đó:

$$
\lambda(n) = \operatorname{lcm}\!\bigl(\lambda(p_1^{e_1}), \lambda(p_2^{e_2}), \dots, \lambda(p_k^{e_k})\bigr)
$$

sẽ chia $L$ (hoặc một ước của $L$).  
Điều này có nghĩa là $\lambda(n)$ được chặn trên bởi $L$,  
dù cho $n$ có thể rất lớn (là tích của nhiều số nguyên tố nhỏ hơn $L+1$).

Nên ta có thể tạo `n` theo cách như sau:

+ Duyệt qua các cặp số mũ $(a,b)$.  

+ Tạo số: $p = 2^a \cdot 3^b + 1.$ Nếu $p$ là nguyên tố thì giữ lại.  

+ Lấy tích tất cả các số nguyên tố vừa được tính để tạo thành $n$.  

Nên $n$ rất lớn, nhưng $\lambda(n) \;=\; 2^{\max a}\cdot 3^{\max b},$ nhỏ hơn rất nhiều so với $n$.

Khi đó từ `c_0` ta có thể tính được `flag_3` và ta chỉ cần phải tìm lại flag_1 và flag_2 từ $c_2 = m_2 ^ e = (65537*flag1^2 + flag3*flag2^2) ^ e \pmod{n}$

do c2 và c1 được tính từ cùng 1 số `e` và ta đã có được flag_3 từ trên nên khi đó ta hướng tới như sau:

+ gửi một số $n = c^k$ với hệ số c nhỏ
+ Khi đó ta có $c_0 = flag_3 ^ e \pmod{c^k}$ như vậy ta có thể dễ dàng tìm lại được e bằng việc sử dụng `hensel_lift` để tính.
+ Vì đã có e nên ta có thể tính được m2 từ c2, mà do $c^k$ rất lớn (khoảng gần 2024 bit) so với $m2 = (65537*flag1^2 + flag3*flag2^2)$ (khoảng tối đa 600 bit) nên ta có thể coi như tìm được $m2 = (65537*flag1^2 + flag3*flag2^2)$ thay vì $m2 = (65537*flag1^2 + flag3*flag2^2) \pmod{c^k}$ 

từ $m2 = (65537*flag1^2 + flag3*flag2^2)$ ta có thể tìm lại flag bằng cách sau:
+ Dễ thấy flag có dạng flag = b'KMACTF{???????????????????????????????}', nên khi đó flag1 = b'KMACTF{' + 15 bytes = b'KMACTF{' + f1. Khi đó có thể thấy `f1` nhỏ hơn flag3. Thực hiện mod 2 vế cho flag3 ta được $m2 = 65537*({b'KMACTF\{'} + f_1)^2 \pmod{flag_3}$ khi này có thể sử tìm nghiệm phương trình này bằng có sẵn của sage là ta dễ dàng tìm lại được flag_1
+ Có được flag_1 ta chỉ cần thay vào ngược lại phương trình ban đầu dể lấy flag_2 và hoàn thành challenge.

Có 1 lưu ý nhỏ là `e` được chọn ngẫu nhiên nên ta sẽ gặp phải nhiều trường hợp lỗi không thể nghịch đảo nên phải chạy brute tới khi nào tìm được `e` đủ tốt.

Ngoài ra để giải phương trình $m2 = (65537*flag1^2 + flag3*flag2^2)$ mình có nghĩ ra một vài cách khác như sau:
+ đưa về dạng dạng Diophantine bậc hai $a * x^2 + b * y^2 = c$.
+ đưa về sử dụng dạng phổ thông hơn là copper smith 2 ẩn bậc 2, cũng như coron method.

Tuy nhiên do flag ở đây khá lớn nên giải theo 2 cách này có vẻ không khả thi.

```py
from Crypto.Util.number import *
from string import ascii_letters, digits, punctuation
from tqdm import *
import os
import random

os.environ["TERM"] = "linux"

from pwn import process, connect

def reconnect():
	HOST, PORT = "165.22.55.200", 30001
	s = connect(HOST, PORT)

	# s = process(['python3', 'chall.py'])
	return s

def send_prime(p):
	s.sendline(b'1')
	s.sendline(b'1')
	s.recvuntil(b'Prime 0 : ')
	s.sendline(str(p).encode())

def get_cipher():
	s.sendline(b'2')
	s.recvuntil(b'Here is ciphertext :\n')
	c = eval(s.recvline().decode())
	return c

def d_log(k, base, p, r): 

	# solve k = base ^ x (mod p ^ r)
	R = Zp(p, prec = r)
	return (R(k).log() / R(base).log()).lift()

s = reconnect()

i = 16
j = 5

primes = []

for _ in range(1, i):
	for __ in range(1, j):
		p = 2**_ * 3**__ + 1
		if isPrime(int(p)):
			primes.append(p)
			
p = prod(primes)
padding_prime_bit = 2026 - p.bit_length()


k_bits = int(log(1 << padding_prime_bit, 5))
q = 5 ** k_bits
n = p * q

assert (n ** 2).bit_length() == 4048

p_order = carmichael_lambda(p)
alphabet = ascii_letters + digits + "{}_" + punctuation + " "

send_prime(n)
c = get_cipher()

for d in trange(1, p_order):
	tmp = pow(c[0], d, p)

	flag3 = long_to_bytes(tmp)
	if all([chr(x) in alphabet for x in flag3]):
		print(f"{flag3}")

		while 1:
			try:

				s = reconnect()
				base = 103
				k_bits = int(log(1 << 2024, base))
				q = base ** k_bits
				padding_bit = 2026 - q.bit_length()
				n = q
				while (n ** 2).bit_length() != 4048:
					p = random.getrandbits(padding_bit)
					n = q * p

				send_prime(n)
				c = get_cipher()
				s.close()
				e = d_log(c[0], tmp, base,  k_bits)
				d = pow(e, -1, euler_phi(q))
				ff = (int(pow(int(c[1]), (d), q)))
				print(ff)
				if ff.bit_length() < 600:
					break
			except:
				pass
		from gmpy2 import iroot
		F.<x> = PolynomialRing(Zmod(tmp))
		f = 65537*(bytes_to_long(b"KMACTF{") * (256 ** 15) + x)**2  - ff
		for i in (f.roots(multiplicities=False)):
			
			flag1 = long_to_bytes(int(i))
			flag2 = long_to_bytes(iroot(int(ff - 65537*(bytes_to_long(b"KMACTF{") * (256 ** 15) + int(i))**2) // tmp, 2)[0])
			print(b"KMACTF{" + flag1 + flag2 + flag3)
		break
```

### 2. Chatgpt

```py
from Crypto.Cipher import AES
from Crypto.Util import Counter
import secrets, sys
import random
from hashlib import sha256 


BLOCK = 16  
R_POLY = 0xE1000000000000000000000000000000 
KEY_SIZE_BITS = 256
MAX_INT = 1 << KEY_SIZE_BITS
MOD = MAX_INT - 189  
SEED = MAX_INT // 6

def xor_bytes(a: bytes, b: bytes) -> bytes:
    assert len(a) == len(b)
    return bytes(x ^ y for x, y in zip(a, b))

def pad16(b: bytes) -> bytes:
    if len(b) % BLOCK == 0:
        return b
    return b + b"\x00" * (BLOCK - (len(b) % BLOCK))

def int_from_bytes(b: bytes) -> int:
    return int.from_bytes(b, "big")

def int_to_bytes(x: int) -> bytes:
    return x.to_bytes(16, "big")

def gf_mul(X: int, Y: int) -> int:
    Z = 0
    V = X
    for i in range(128):
        if (Y >> (127 - i)) & 1:
            Z ^= V
        lsb = V & 1
        V >>= 1
        if lsb:
            V ^= R_POLY
    return Z & ((1 << 128) - 1)

def ghash(H: bytes, data: bytes) -> bytes:
    assert len(H) == 16
    H_int = int_from_bytes(H)
    data = pad16(data)
    y = 0
    for i in range(0, len(data), BLOCK):
        Xi = int_from_bytes(data[i:i+BLOCK])
        y = gf_mul(y ^ Xi, H_int)
    return int_to_bytes(y)

class GHASH:
    def __init__(self, aes_key_for_H: bytes):
        cipher = AES.new(aes_key_for_H, AES.MODE_ECB)
        self.H = cipher.encrypt(b"\x00"*16) 
    def h(self, X: bytes, T: bytes) -> bytes:
        return ghash(self.H, pad16(X) + pad16(T))

class AESBlock:
    def __init__(self, key: bytes):
        self.key = key
    def enc(self, block: bytes) -> bytes:
        cipher = AES.new(self.key, AES.MODE_ECB)
        return cipher.encrypt(block)
    def dec(self, block: bytes) -> bytes:
        cipher = AES.new(self.key, AES.MODE_ECB)
        return cipher.decrypt(block)

class StreamAESCTR:
    def __init__(self, key: bytes):
        self.key = key
    def keystream(self, S: bytes, length: int) -> bytes:
        init_val = int.from_bytes(S, "big")
        ctr = Counter.new(128, initial_value=init_val)
        cipher = AES.new(self.key, AES.MODE_CTR, counter=ctr)
        return cipher.encrypt(b"\x00" * length)

class Encryptor:
    def __init__(self, key_e: bytes, key_d: bytes, key_c: bytes, key_for_H: bytes):
        self.e = AESBlock(key_e)
        self.d = AESBlock(key_d)
        self.h1 = GHASH(key_for_H)
        self.h2 = GHASH(key_for_H)
        self.c = StreamAESCTR(key_c)
        
    def encrypt(self, T: bytes, plaintext: bytes) -> bytes:
        assert len(T) == BLOCK and len(plaintext) >= BLOCK
        A = plaintext[:BLOCK]; B = plaintext[BLOCK:]
        U = self.e.enc(A)
        S = xor_bytes(U, self.h1.h(B, T))
        keystream = self.c.keystream(S, len(B))
        E = xor_bytes(B, keystream) if len(B) > 0 else b""
        V = xor_bytes(S, self.h2.h(E, T))
        G = self.d.enc(V)
        return G + E
    
    def decrypt(self, T: bytes, ciphertext: bytes) -> bytes:
        assert len(T) == BLOCK and len(ciphertext) >= BLOCK
        G = ciphertext[:BLOCK]; E = ciphertext[BLOCK:]
        V = self.d.dec(G)
        S = xor_bytes(V, self.h2.h(E, T))
        keystream = self.c.keystream(S, len(E))
        B = xor_bytes(E, keystream) if len(E) > 0 else b""
        U = xor_bytes(S, self.h1.h(B, T))
        A = self.e.dec(U)
        return A + B
    
class Newbie_Stager:
    def __init__(self, alice_private_key: int, bob_key_private: int):
        self.alice_private_key = alice_private_key
        self.bob_key_private = bob_key_private
        
    def key_exchange(self, seed, exponents):
        result = seed
        exp = 1
        while exponents > 0:
            if exponents % 2 == 1:
                mult = 1
                for i in range(exp):
                    result = (3 * result * mult) % MOD
                    mult <<= 1
            exponents >>= 1
            exp += 1
        return result
    
    def get_shared_key(self): 
        A = self.key_exchange(SEED, self.alice_private_key)
        B = self.key_exchange(SEED, self.bob_key_private)
        shared_key_A = self.key_exchange(B, self.alice_private_key)
        share_key_B = self.key_exchange(A, self.bob_key_private)
        assert shared_key_A == share_key_B
        return shared_key_A , A, B


# Setup
m_blocks = 12
key_e = secrets.token_bytes(16)
key_d = secrets.token_bytes(16)
key_c = secrets.token_bytes(16)
key_for_H = secrets.token_bytes(16)
encryptor = Encryptor(key_e, key_d, key_c, key_for_H)

# Encrypt 
total_len = m_blocks * BLOCK
plaintext = secrets.token_bytes(total_len)
T = secrets.token_bytes(BLOCK)
ciphertext = encryptor.encrypt(T, plaintext)

# Encrypt but for newbie
alice_private = random.getrandbits(256)
bob_private = random.getrandbits(256)
shared_key, A, B = Newbie_Stager(alice_private, bob_private).get_shared_key()
shared_key = sha256(str(shared_key).encode()).digest()[:16]
newbie_aes = AESBlock(shared_key) 
cipher_enc = newbie_aes.enc(ciphertext)

print("Newbie cant see my ciphertext !!!")
print("Here is your ciphertext:", cipher_enc.hex())
print("Here is your T (hex):", newbie_aes.enc(T).hex())
print("Here is your A:", A) 
print("Here is your B:", B)

coint = 10
menu = """
1) Decrypt
2) Encrypt
3) Get Flag
"""
while coint > 0: 
    
    print(menu)
    print(f"You have {coint} coins left")
    choice = input("Your choice: ").strip()
    
    if choice == "1": 
        try:
            hex_ct = input("Ciphertext (hex): ").strip()
            T = bytes.fromhex(input("T (hex): ").strip())
            
            if len(T) != BLOCK:
                print("Invalid T length")
                continue
            
            ciphertext_ = bytes.fromhex(hex_ct)
            if len(ciphertext_) < BLOCK:
                print("Ciphertext too short")
                continue
            
            control = 0
            for i in range(0, len(ciphertext_), BLOCK):
                block = ciphertext_[i:i+BLOCK]
                if block in ciphertext :
                    control +=1 
            
            if control > 2 :
                print("You are not allowed to repeat blocks more than 2 times")
                exit()
            
            pt = encryptor.decrypt(T, ciphertext_)
            print("Plaintext (hex):", pt.hex())
            coint -= 3
        except Exception as e:
            print("Error:", e)
            continue
                
    if choice == "2":
        try:
            hex_pt = input("Plaintext (hex): ").strip()
            T = bytes.fromhex(input("T (hex): ").strip())
            if len(T) != BLOCK:
                print("Invalid T length")
                continue
            plaintext_ = bytes.fromhex(hex_pt)
            
            if len(plaintext_) < BLOCK:
                print("Plaintext too short")
                continue
            
            control = 0
            for i in range(0, len(plaintext_), BLOCK):
                block = plaintext_[i:i+BLOCK]
                if block in ciphertext :
                    control +=1 
            
            if control > 2 :
                print("You are not allowed to repeat blocks more than 2 times")
                exit()
                
            ct = encryptor.encrypt(T, plaintext_)
            print("Ciphertext (hex):", ct.hex())
            coint -= 4
        except Exception as e:
            print("Error:", e)
            continue
        
    if choice == "3":
        
        plaintext_ = bytes.fromhex(input("Plaintext (hex): ").strip())
        if plaintext_ == plaintext[BLOCK:]:
            print("Here is your flag: KMACTF{????????????????????????????????}")
            exit()
        else:
            print("Nope")
            print(plaintext[BLOCK:].hex().encode())
            print(T)
            coint -= 5
    
```

`Newbie_Stager.key_exchange(seed, exponents)` là hàm nhân phối theo dạng key_exchange(seed, e) = seed * F(e) mod MOD (với F(e) chỉ phụ thuộc vào exponents), do đó

`A = SEED * F(a)`

`B = SEED * F(b)`

⇒ shared = SEED * F(a) * F(b).

Từ đó shared = A * B * inv(SEED) (mod MOD) — đó là lý do phép nhân đơn giản A * B * inv(SEED) cho ra đúng shared_int.

Khi này ta đã có T và ciphertext nên việc còn lại ta cần làm là tìm lại M với chức năng encrypt và decrypt của chương trình (Nhưng ta payload ta gửi không được trùng quá 2 block so với ciphertext).


Với hint như sau:
![image](https://hackmd.io/_uploads/H1Y-Of22ee.png)


Mình có tìm kiếm các kiểu operation của AES, thì có thể đây là mã hóa dạng XCB block. Khi tìm kiếm XCB attack thì ta có thể thấy được 2 doc sau đây:
+ https://eprint.iacr.org/2024/1527.pdf
+ https://eprint.iacr.org/2024/1554.pdf

Trong doc `Breaking the IEEE Encryption Standard – XCB-AES in Two Queries`, có thể thấy ta có thể sử dụng được cách đã được miêu tả trong đây để giải quyết bài này.


**Một vài định nghĩa**

- $C = C_L \parallel C_R$ (ciphertext): trong code `C = G || E`, $C_L = C[:16]$, $C_R = C[16:]$.

- $M = M_L \parallel M_R$ (plaintext): trong code `M = A || B`, $M_L = A$, $M_R = B$.

- $E_{K_e}(\cdot), E_{K_d}(\cdot)$ là AES-ECB với hai key khác nhau (các hàm `self.e.enc`, `self.d.enc/dec`).

- $G_{K_c}(\cdot)$ là hàm sinh keystream CTR (trong code: `self.c.keystream(S, len(B))`).

- $H_1(K_h,T,\cdot), H_2(K_h,T,\cdot), H_{\text{sum}}(\cdot)$ là các hàm GHASH-derived  
  (trong code: `self.h1.h(...,T)`, `self.h2.h(...,T)`).  
  Tất cả đều tuyến tính theo XOR trên các khối dữ liệu đầu vào.

Trong doc ta có được giới thiệu tính chất cơ bản của Hash trong XCB như sau:

$$
\forall X,Y:\quad \mathrm{GHASH}(X \oplus Y) = \mathrm{GHASH}(X) \oplus \mathrm{GHASH}(Y).
$$

Do đó các hàm $H_1,H_2,H_{\text{sum}}$ thoả quan hệ tuyến tính qua XOR khi input bị XOR với $\Delta$.

Lưu ý: $G_{K_c}(\cdot)$ không cần tuyến tính cho tấn công — ta chỉ cần ép đối số của $G_K$ bằng nhau, thì output cũng bằng nhau.

---

Gọi $T$ là tweak cố định. Ta có:
Ta có bốn quan hệ:

$$
C_L \;=\; G_{K_c,m-n}\!\bigl(E_{K_e}(M_R) \oplus H_1(K_h,T,M_L)\bigr) \;\oplus\; M_L, \tag{1}
$$

$$
C_R \;=\; E_{K_d}^{-1}\!\Bigl(E_{K_e}(M_R) \oplus H_1(K_h,T,M_L) \oplus H_2(K_h,T,C_L)\Bigr), \tag{2}
$$

$$
M_L \;=\; G_{K_c,m-n}\!\bigl(E_{K_d}(C_R) \oplus H_2(K_h,T,C_L)\bigr) \;\oplus\; C_L, \tag{3}
$$

$$
M_R \;=\; E_{K_e}^{-1}\!\Bigl(E_{K_d}(C_R) \oplus H_2(K_h,T,C_L) \oplus H_1(K_h,T,M_L)\Bigr). \tag{4}
$$


$$
M_L \oplus C_L = G_{K_c}\bigl(E_{K_d}(C_R) \oplus H_2(K_h,T,C_L)\bigr) 
$$

và quan hệ phụ:

$$
E_{K_e}(M_R) \oplus E_{K_d}(C_R) = H_{\text{sum}}(K_h,T,M_L,C_L) = H_1(K_h,T,M_L) \oplus H_2(K_h,T,C_L)\Bigr.
$$

$$
\begin{aligned}
&\text{Gửi } (C_L \oplus \Delta)\parallel C_R \;\text{cho server giải mã ta được } M'_L \parallel M'_R, \\
& \\
&\text{Gửi } (M'_L \oplus \Delta)\parallel M'_R \;\text{cho server mã hóa ta có có được } C'_L \parallel C'_R, \\
\end{aligned}
$$

Khi đó ta có:

$$
\begin{aligned}
M_L \oplus C_L 
   &= G_{K_c}\!\left(E_{K_d}(C_R) \oplus H_2(K_h,T,C_L)\right)
\end{aligned}
$$

$$
\begin{aligned}
E_{K_e}(M_R) \oplus E_{K_d}(C_R) 
   &= H_{\mathrm{sum}}(K_h,T,M_L,C_L)
\end{aligned}
$$

$$
\begin{aligned}
(C_L \oplus \Delta)\parallel C_R 
   &\xrightarrow{\;\text{giải mã}\;} 
   M' = M'_L \parallel M'_R
\end{aligned}
$$

$$
\begin{aligned}
M'_L \oplus (C_L \oplus \Delta) 
   &= G_{K_c}\!\left(E_{K_d}(C_R) \oplus H_2(K_h,T,C_L \oplus \Delta)\right)
\end{aligned}
$$

$$
\begin{aligned}
E_{K_e}(M'_R) \oplus E_{K_d}(C_R) 
   &= H_{\mathrm{sum}}(K_h,T,M'_L,C_L \oplus \Delta)
\end{aligned}
$$

$$
\begin{aligned}
(M'_L \oplus \Delta)\parallel M'_R 
   &\xrightarrow{\;\text{mã hoá}\;} 
   C' = C'_L \parallel C'_R
\end{aligned}
$$

$$
\begin{aligned}
(M'_L \oplus \Delta) \oplus C'_L 
   &= G_{K_c}\!\left(E_{K_e}(M'_R) \oplus H_1(K_h,T,M'_L \oplus \Delta)\right)
\end{aligned}
$$



Từ đó ta có:

$$
\begin{aligned}
& (M'_L \oplus \Delta) \oplus C'_L \oplus M_L \oplus C_L \oplus GK_{c,m-n}\bigl(EK_d(C_R) \oplus H_2(K_h,T,C_L)\bigr)\\
&\quad= GK_{c,m-n}\bigl(EK_e(M'_R) \oplus H_1(K_h,T,M'_L \oplus \Delta)\bigr)\\
&\quad= GK_{c,m-n}\bigl(EK_e(M'_R) \oplus H_2(K_h,T,C_L) \oplus H_{\mathrm{sum}}(K_h,T,M'_L \oplus \Delta, C_L)\bigr)\\
&\quad= GK_{c,m-n}\bigl(EK_e(M'_R) \oplus H_2(K_h,T,C_L) \oplus H_{\mathrm{sum}}(K_h,T,M'_L, C_L \oplus \Delta)\bigr)\\
&\quad= GK_{c,m-n}\bigl(EK_d(C_R) \oplus H_2(K_h,T,C_L)\bigr).
\end{aligned}
$$

Vậy $M_L \; = \; (C_L \oplus M'_L \oplus C'_L \oplus \Delta)$


Khi đó ta có thể dễ dàng tìm được M chỉ với 2 query, và dễ dàng kiếm được flag.


```py


from pwn import *
from Crypto.Cipher import AES
from hashlib import sha256
import re
import sys
import secrets
context.log_level = 'info'
BLOCK = 16
KEY_SIZE_BITS = 256
MAX_INT = 1 << KEY_SIZE_BITS
MOD = MAX_INT - 189
SEED = MAX_INT // 6


def xor_bytes(a: bytes, b: bytes) -> bytes:
    assert len(a) == len(b)
    return bytes(x ^ y for x, y in zip(a, b))

def pad16(b: bytes) -> bytes:
    if len(b) % BLOCK == 0:
        return b
    return b + b"\x00" * (BLOCK - (len(b) % BLOCK))

def int_from_bytes(b: bytes) -> int:
    return int.from_bytes(b, "big")

def int_to_bytes(x: int) -> bytes:
    return x.to_bytes(16, "big")

def gf_mul(X: int, Y: int) -> int:
    Z = 0
    V = X
    for i in range(128):
        if (Y >> (127 - i)) & 1:
            Z ^= V
        lsb = V & 1
        V >>= 1
        if lsb:
            V ^= R_POLY
    return Z & ((1 << 128) - 1)

def ghash(H: bytes, data: bytes) -> bytes:
    assert len(H) == 16
    H_int = int_from_bytes(H)
    data = pad16(data)
    y = 0
    for i in range(0, len(data), BLOCK):
        Xi = int_from_bytes(data[i:i+BLOCK])
        y = gf_mul(y ^ Xi, H_int)
    return int_to_bytes(y)

class GHASH:
    def __init__(self, aes_key_for_H: bytes):
        cipher = AES.new(aes_key_for_H, AES.MODE_ECB)
        self.H = cipher.encrypt(b"\x00"*16) 
    def h(self, X: bytes, T: bytes) -> bytes:
        return ghash(self.H, pad16(X) + pad16(T))

class AESBlock:
    def __init__(self, key: bytes):
        self.key = key
    def enc(self, block: bytes) -> bytes:
        cipher = AES.new(self.key, AES.MODE_ECB)
        return cipher.encrypt(block)
    def dec(self, block: bytes) -> bytes:
        cipher = AES.new(self.key, AES.MODE_ECB)
        return cipher.decrypt(block)

class StreamAESCTR:
    def __init__(self, key: bytes):
        self.key = key
    def keystream(self, S: bytes, length: int) -> bytes:
        init_val = int.from_bytes(S, "big")
        ctr = Counter.new(128, initial_value=init_val)
        cipher = AES.new(self.key, AES.MODE_CTR, counter=ctr)
        return cipher.encrypt(b"\x00" * length)

class Encryptor:
    def __init__(self, key_e: bytes, key_d: bytes, key_c: bytes, key_for_H: bytes):
        self.e = AESBlock(key_e)
        self.d = AESBlock(key_d)
        self.h1 = GHASH(key_for_H)
        self.h2 = GHASH(key_for_H)
        self.c = StreamAESCTR(key_c)
        
    def encrypt(self, T: bytes, plaintext: bytes) -> bytes:
        assert len(T) == BLOCK and len(plaintext) >= BLOCK
        A = plaintext[:BLOCK]; B = plaintext[BLOCK:]
        U = self.e.enc(A)
        S = xor_bytes(U, self.h1.h(B, T))
        keystream = self.c.keystream(S, len(B))
        E = xor_bytes(B, keystream) if len(B) > 0 else b""
        V = xor_bytes(S, self.h2.h(E, T))
        G = self.d.enc(V)
        return G + E
    
    def decrypt(self, T: bytes, ciphertext: bytes) -> bytes:
        assert len(T) == BLOCK and len(ciphertext) >= BLOCK
        G = ciphertext[:BLOCK]; E = ciphertext[BLOCK:]
        V = self.d.dec(G)
        S = xor_bytes(V, self.h2.h(E, T))
        keystream = self.c.keystream(S, len(E))
        B = xor_bytes(E, keystream) if len(E) > 0 else b""
        U = xor_bytes(S, self.h1.h(B, T))
        A = self.e.dec(U)
        return A + B
    
class Newbie_Stager:
    def __init__(self, alice_private_key: int, bob_key_private: int):
        self.alice_private_key = alice_private_key
        self.bob_key_private = bob_key_private
        
    def key_exchange(self, seed, exponents):
        result = seed
        exp = 1
        while exponents > 0:
            if exponents % 2 == 1:
                mult = 1
                for i in range(exp):
                    result = (3 * result * mult) % MOD
                    mult <<= 1
            exponents >>= 1
            exp += 1
        return result
    
    def get_shared_key(self): 
        A = self.key_exchange(SEED, self.alice_private_key)
        B = self.key_exchange(SEED, self.bob_key_private)
        shared_key_A = self.key_exchange(B, self.alice_private_key)
        share_key_B = self.key_exchange(A, self.bob_key_private)
        assert shared_key_A == share_key_B
        return shared_key_A , A, B


def aes_ecb_decrypt(key16: bytes, data: bytes) -> bytes:
    cipher = AES.new(key16, AES.MODE_ECB)
    return cipher.decrypt(data)

def derive_shared_key_from_AB(A: int, B: int):
    # shared = A * B * inv(SEED) mod MOD
    inv_seed = pow(SEED, -1, MOD)
    shared_int = (A * B * inv_seed) % MOD
    shared_key = sha256(str(shared_int).encode()).digest()[:16]
    return shared_key, shared_int

def parse_initial_output(data: str):
    # parse hex ciphertext (the newbie-encrypted ciphertext)
    re_ct = re.search(r"Here is your ciphertext: *([0-9a-fA-F]+)", data)
    re_t = re.search(r"Here is your T \(hex\): *([0-9a-fA-F]+)", data)
    re_A = re.search(r"Here is your A: *([0-9]+)", data)
    re_B = re.search(r"Here is your B: *([0-9]+)", data)
    if not re_ct or not re_t or not re_A or not re_B:
        raise ValueError("Failed to parse service initial output")
    ct_enc_hex = re_ct.group(1).strip()
    t_enc_hex = re_t.group(1).strip()
    A = int(re_A.group(1).strip())
    B = int(re_B.group(1).strip())
    return bytes.fromhex(ct_enc_hex), bytes.fromhex(t_enc_hex), A, B

def interact_and_get_plaintext(io, ciphertext_hex: str, T_hex: str):
    # choose menu option 1 (Decrypt)
    io.recvuntil(b"Your choice: ")
    io.sendline(b"1")
    io.recvuntil(b"Ciphertext (hex): ")
    io.sendline(ciphertext_hex.encode())
    io.recvuntil(b"T (hex): ")
    io.sendline(T_hex.encode())
    # read response lines until Plaintext (hex):
    data = io.recvuntil(b"\n", timeout=2)  # maybe the server prints error or plaintext
    # We're going to read until we see "Plaintext (hex):"
    full = data
    try:
        more = io.recvrepeat(timeout=0.2)
        full += more
    except Exception:
        pass
    s = full.decode(errors='ignore')
    m = re.search(r"Plaintext \(hex\): *([0-9a-fA-F]+)", s)
    if not m:
        raise ValueError("Did not obtain plaintext from decrypt response; got:\n" + s)
    return m.group(1).strip()

def get_flag_by_submit(io, tail_hex: str):
    # menu again
    io.recvuntil(b"Your choice: ")
    io.sendline(b"3")

    io.recvuntil(b"Plaintext (hex): ")
    io.sendline(tail_hex.encode())
    res = io.recvline(timeout=2)
    if res is None:
        res = io.recvrepeat(timeout=1)
    return res.decode(errors='ignore')

def main():

    io = remote("165.22.55.200", 30002)
    # io = process(["python3", "chall.py"])  # adjust if needed to spawn local process
    initial = io.recvuntil(b"Here is your B:", timeout=5)
    initial += io.recvuntil(b"\n", timeout=1)
    try:
        initial += io.recvuntil(b"Your choice:", timeout=2)
    except Exception:
        # maybe menu printed later, but we already have the key lines
        pass
    initial_str = initial.decode(errors='ignore')

    ct_enc, t_enc, A, B = parse_initial_output(initial_str)

    shared_key, shared_int = derive_shared_key_from_AB(A, B)
    newbie_aes = AESBlock(shared_key) 
    T = newbie_aes.dec(t_enc)
    ciphertext = newbie_aes.dec(ct_enc)
    C = ciphertext
    CL, CR = C[:16], C[16:]
    delta = secrets.token_bytes(len(C) - len(CL))

    query1 = CL + xor(CR, delta)
    io.sendline(b"1")
    io.recvuntil(b"Ciphertext (hex): ")
    io.sendline((query1).hex().encode())
    io.recvuntil(b"T (hex): ")
    io.sendline(T.hex().encode())
    io.recvuntil(b"Plaintext (hex): ")
    A1 = io.recvline().strip().decode()
    M1 = bytes.fromhex(A1)
    M1L, M1R = M1[:16], M1[16:]


    query2 = M1L + xor(M1R, delta)
    io.sendline(b"2")
    io.recvuntil(b"Plaintext (hex): ")
    io.sendline((query2).hex().encode())
    io.recvuntil(b"T (hex): ")
    io.sendline(T.hex().encode())
    io.recvuntil(b"Ciphertext (hex):")
    A2 = io.recvline().strip().decode()
    C1 = bytes.fromhex(A2)
    C1L, C1R = C1[:16], C1[16:]

    M = xor(CR, M1R, C1R, delta)

    io.sendline(b"3")
    io.sendline(M.hex().encode())
    io.interactive()
if __name__ == "__main__":
    main()
```