CTF-writeups

Some CTF writeups


Project maintained by Qyn-CTF

Secure Bank - Hard

Description:

The SP bAnK E2 introduced a new protocol to secure its services. But can it hold up to its promises?

Note: Do not try a online brute force attack against this service. Your chance of guessing the correct pins is negligibly small and it will just add unnecessary load to the service.

First looks

We’re given the source code of a client and server. In the server.py, we can find the following function:

def do_protocol(pin, user_id, user_pub, message):

    ### Protocol step 1 / identifier and DH parameter from client
    email_hash = SHA256.new(user_id.encode()).digest()
    email_num = int.from_bytes(email_hash, "big")

    if not (0 < user_pub < PRIME):
        # Invalid DH parameter
        sys.exit(1)

    ### Protocol step 2 / generate DH parameter
    id_a = dh_genpub(email_num)
    mask_client = dh_exchange(id_a, -pin)
    t_a = (user_pub * mask_client) % PRIME

    r_b = rng.randint(1, PRIME-1)
    t_b = dh_genpub(r_b)
    mask_server = dh_exchange(ID_SERVER, pin)
    server_pub = (t_b * mask_server) % PRIME

    ### Send public parameter
    print("Server public parameter:", server_pub)

    ### Calculate shared secret
    z = dh_exchange(t_a, r_b)
    key = SHA256.new(long_to_bytes(id_a) + long_to_bytes(ID_SERVER) + long_to_bytes(user_pub) + long_to_bytes(server_pub) + long_to_bytes(pin) + long_to_bytes(z)).digest()

    ### Send encrypted message

    aes = AES.new(key, AES.MODE_ECB)
    enc = aes.encrypt(Padding.pad(message.encode(), 16))

    return enc

Where we as a client supply the user_id and user_pub, however, the user_id is pretty much random because of the sha256. So what is going on here?
Let’s rename a few variables:

email_num = r_a
mask_client = m_a
mask_server = m_b
clientPin = p_a
serverPin = p_b
user_pub = A
server_pub = B

All operations are done \(\mod p\)
On the client:
\(\begin{align*} & t_a = g^{pr_a} \\ & id_a = g^{pb_a} \\ & m_a = id_a^{p_a} = g^{pb_a \times p_a} \\ & A = t_a \times m_a = g^{pr_a} \times g^{pb_a \times p_a} = g^{pr_a + pb_a \times p_a} \\ \end{align*}\)

We send \(A\) and \(pb_a\) to the server:
\(\begin{align*} & id_a = g^{pb_a} \\ & m_a = id_a^{-p_b} = g^{-p_b \times pb_a} \\ & A_s = A \times m_a = g^{pr_a + pb_a \times p_a} \times g^{-p_b \times pb_a} = g^{pr_a + pb_a \times p_a - p_b \times pb_a} \\ & t_b = g^{pr_b} \\ & id_b = g^{pb_b} \\ & m_b = id_b^{p_b} = g^{pb_b \times p_b} \\ & B = t_b \times m_b = g^{pr_b} \times g^{pb_b \times p_b} = g^{pr_b + pb_b \times p_b} \\ & z_b = A_s^{pr_b} = g^{(pr_a + pb_a \times p_a - p_b \times pb_a) \times pr_b} \end{align*}\)

We send \(B\) to the client:
\(\begin{align*} & id_b = g^{pb_b} \\ & m_b = id_b^{-p_a} = g^{-p_a \times pb_b} \\ & B_s = B \times m_b = g^{pr_b + pb_b \times p_b} \times g^{-p_a \times pb_b} = g^{pr_b + pb_b \times p_b - p_a \times pb_b} \\ & z_a = B_s^{pr_a} = g^{(pr_b + pb_b \times p_b - p_a \times pb_b) \times pr_a} \end{align*}\)

Solving

So our goal is to get the value of \(z_b\) while knowing \(B\). From the source code we know that \(p_b\) is in range(0, 10e3). So using some arithmetic tricks, we can get our value of \(z_b\)
\(\begin{align*} & B = t_b \times m_b = g^{pr_b} \times g^{pb_b \times p_b} = g^{pr_b + pb_b \times p_b} \\ & B_1 = B \times g^{-pb_b \times p_b} = g^{pr_b} \\ & B_2 = B_1^{pr_a} = g^{pr_a \times pr_b} \\ & B_3 = B_1^{pb_a \times p_a} = g^{pb_a \times p_a \times pr_b} \\ & B_4 = B_1^{-p_b \times pb_a} = g^{-p_b \times pb_a \times pr_b} \\ & z_b = B_2 \times B_3 \times B_4 = g^{pr_a \times pr_b} \times g^{pb_a \times p_a \times pr_b} \times g^{-p_b \times pb_a \times pr_b} \\ & = g^{pr_a \times pr_b + pb_a \times p_a \times pr_b - p_b \times pb_a \times pr_b} = g^{(pr_a + pb_a \times p_a - p_b \times pb_a) \times pr_b} \end{align*}\)

Here we can improve speed of the bruteforce by simply setting \(p_a = 0\) and \(pr_a = 1\). So that \(B_3\) is not required anymore
We can implement this in python:

from Crypto.Hash import SHA256
from Crypto.Cipher import AES
from Crypto.Util import Padding 
from Crypto.Util.number import long_to_bytes


email = "qyn"

p_a = 0
g = 2
p = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF
pr_a = 1
pb_a = int.from_bytes(SHA256.new(bytes("qyn", 'utf-8')).digest(), byteorder='big')
pb_b = int.from_bytes(b'server', 'big')


t_a = pow(g, pr_a, p) #should just be 2?
id_a = pow(g, pb_a, p)
id_b = pow(g, pb_b, p)

m_a = pow(id_a, p_a, p)

A = t_a * m_a
A %= p #Should be 2

print(f"Email: {email}")
print(f"A: {A}")

B = int(input("Enter the value of B: "))
ct = bytes.fromhex(input("Ciphertext: "))


for p_b in range(0, 9999+1):
    print(f"Pin: {p_b}")

    B_1 = B * pow(g, -pb_b * p_b, p)
    B_1 %= p
    B_2 = pow(B_1, pr_a, p)
    B_3 = pow(B_1, pb_a * p_a, p) #Should be 1
    B_4 = pow(B_1, -p_b * pb_a, p)

    z_b = B_2 * B_3 * B_4
    z_b %= p

    k = SHA256.new(long_to_bytes(id_a) + long_to_bytes(id_b) + long_to_bytes(A) + long_to_bytes(B) + long_to_bytes(p_b) + long_to_bytes(z_b)).digest()
    aes = AES.new(k, AES.MODE_ECB)
    try:
        pt = str(Padding.unpad(aes.decrypt(ct), 16), 'utf-8')
        if "CSCG" in pt or "Challenge" in pt:
            print(pt)
            break
    except:
        pass

Running this after passing the challenge round will give us the flag:
CSCG{i_hope_you_bank_has_better_security}

Home