Description
A message was encrypted using a shared secret... but it looks like one side of the exchange leaked something. Can you piece together the secret and get the flag? Download the message.txt and source encryption.py .
Setup
Download message.txt and encryption.py.
Read encryption.py to understand the Diffie-Hellman key exchange scheme used.
cat message.txtcat encryption.pySolution
Walk me through it- Step 1Analyse the encryption schemeThe source shows a Diffie-Hellman exchange with g=2 and a large prime p. Both parties publish their public key (g^priv mod p). The shared secret is A^b mod p where A is one party's public key and b is the other's leaked private key.
Learn more
Diffie-Hellman key exchange (1976) was the first published public-key cryptographic protocol, enabling two parties to establish a shared secret over an insecure channel without having met before. Each party generates a private key (a random integer) and computes a public key as
g^private mod p. They exchange public keys, and each can compute the same shared secret:shared = other_public^my_private mod p. An eavesdropper who sees both public keys cannot compute the shared secret without solving the discrete logarithm problem - findingxsuch thatg^x = A mod p.The security of DH depends entirely on the private keys remaining secret. If either party leaks their private key, the shared secret is immediately compromised: anyone can compute
A^b mod pfrom the public keyAand the leaked privateb. This is a fundamental lesson in cryptographic key management - even a mathematically secure protocol is broken by implementation failures like logging private keys or storing them insecurely.In modern systems, DH is used in TLS (as DHE or ECDHE cipher suites), Signal protocol, and WireGuard VPN. The "E" in DHE stands for "Ephemeral" - a new key pair is generated for every session, providing forward secrecy: compromising the server's long-term key does not expose past session keys. ECDH uses elliptic curve groups instead of multiplicative groups, providing equivalent security with smaller key sizes.
- Step 2Compute the shared secretPull p, A, b, and the ciphertext (verify the encoding - hex, base64, or raw - from encryption.py rather than guessing). Validate 1 < A,B < p as a sanity check, compute shared = pow(A, b, p), then try the obvious key derivations until one yields a printable picoCTF{...} prefix.python
python3 << 'EOF' import hashlib # Confirm the ciphertext encoding by reading encryption.py: # - hex string -> bytes.fromhex(...) # - base64 -> base64.b64decode(...) # - raw bytes -> open(..., "rb").read() p = YOUR_PRIME A = YOUR_PUBLIC_KEY # g^a mod p b = YOUR_LEAKED_PRIV # leaked private key # Sanity check on the public values assert 1 < A < p, "A must satisfy 1 < A < p" shared = pow(A, b, p) ct = bytes.fromhex("YOUR_CIPHERTEXT_HEX") # Try the obvious key derivations in order: candidates = [ ("low byte", bytes([shared & 0xFF])), ("byte 1", bytes([(shared >> 8) & 0xFF])), ("sha256[:1]", hashlib.sha256(str(shared).encode()).digest()[:1]), ("sha256[:16]", hashlib.sha256(str(shared).encode()).digest()[:16]), ] for name, key in candidates: pt = bytes(c ^ key[i % len(key)] for i, c in enumerate(ct)) if pt.startswith(b"picoCTF{"): print(name, "->", pt.decode(errors="replace")) break EOFLearn more
What pow(A, b, p) actually does. Python's three-argument
powruns square-and-multiply modular exponentiation: scan the bits of the exponentbfrom MSB to LSB, square the running result modpon every step, and multiply byAmodpwhen the current bit is 1. That is O(log b) multiplications, each of which is at most a 1024x1024-bit modular multiply for this challenge. A 1024-bit b means roughly 1500 mod-mul operations. Done in microseconds. Naively computingA**bfirst would produce a number with ~10^308 digits and never finish.Validate inputs. A real DH stack rejects values where
A <= 1,A == p-1, orA >= p; a leaked private key paired with a degenerate public value can collapse the shared secret into a small set. Check1 < A < pand1 < B < pbefore doing anything else.Entropy collapse. A 1024-bit prime
pmeans the shared secret has roughly 1024 bits of entropy. Reducing toshared % 256throws away the other 1016 bits and gives you 256 candidate keys total - already brute-forceable without DH. A real KDF (HKDF) would absorb the whole shared secret and expand into a full-size key; this protocol throws all that work away in one line.If shared % 256 doesn't produce a flag, read encryption.py more carefully and try the next-most-likely derivations: high byte (
shared >> 8 & 0xFF), the SHA-256 ofstr(shared)truncated to 1 or 16 bytes, or the raw shared bytes viashared.to_bytes((shared.bit_length()+7)//8, 'big'). The brute snippet above tries the common ones in order.See RSA attacks for CTF for the broader public-key attack catalogue and AES for CTF for symmetric KDF practice.
Diffie-Hellman walkthrough (toy parameters): Public params: p = 23, g = 5 Alice picks a = 6 (private) Computes A = g^a mod p = 5^6 mod 23 = 15625 mod 23 = 8 Bob picks b = 15 (private) Computes B = g^b mod p = 5^15 mod 23 = 30517578125 mod 23 = 19 Public exchange: Alice sends A=8, Bob sends B=19. Alice computes shared = B^a mod p = 19^6 mod 23 = 47045881 mod 23 = 2 Bob computes shared = A^b mod p = 8^15 mod 23 = 35184372088832 mod 23 = 2 Both arrive at the same shared = 2 without exchanging it. Now suppose Bob's private key b = 15 LEAKS. An eavesdropper sees A = 8 (from the public network) and learns b = 15 from the leak. They compute: shared = A^b mod p = 8^15 mod 23 = 2 The shared secret is recovered without ever solving the discrete log problem. In this challenge, p is a 1024-bit prime, but the principle is identical: pow(A, b, p) recovers shared in microseconds. Then key = shared % 256 reduces to one byte. XOR each ciphertext byte with this byte: ct[i] XOR key --> pt[i] For the toy: if ct = [0x70, 0x69, 0x63, 0x6f] XOR 0x02 = [0x72, 0x6b, 0x61, 0x6d], then "rkam" is the wrong recovery (toy params lack proper byte semantics); in the real challenge the recovered byte XOR produces "pico..." and the flag emerges.
Alternate Solution
Once you compute the single-byte key (shared_secret % 256), use the XOR Cipher tool on this site to decrypt the ciphertext - paste the hex bytes, enter the single-byte key, and the flag appears without writing any code. The tool also supports single-byte brute-force if you prefer to skip the DH computation.
Flag
picoCTF{dh_sh4r3d_s3cr3t_...}
Diffie-Hellman with a leaked private key b. Compute shared = A^b mod p, then key = shared % 256. XOR each ciphertext byte with key to decrypt the flag.