Blunt Writeup - Cyber Apocalypse 2024
→ 1 Introduction
This writeup covers the Blunt Crypto challenge from the Hack The Box Cyber Apocalypse 2024 CTF, which was rated as having an ‘easy’ difficulty. The challenge involved brute forcing a private key used in a Diffie-Hellman key exchange.
The description of the challenge is shown below.
→ 2 Key Techniques
The key techniques employed in this writeup are:
- Manual Python source code review
- Brute forcing a private key used in a Diffie-Hellman key exchange.
→ 3 Artifacts Summary
The downloaded artifact had the following hash:
$ shasum -a256 crypto_blunt.zip
ff554950fd03d7f739042c113e2622b6138c9c3489b5b71eb51e3cb7619244ba crypto_blunt.zip
The zip file contained a single Python source code file,
source.py
, and output.txt
:
$ unzip crypto_blunt.zip
Archive: crypto_blunt.zip
creating: challenge/
inflating: challenge/source.py
inflating: challenge/output.txt
$ shasum -a256 challenge/*
88cfc1b6ad50a9df20efdf8b4dae459088ba8719d4ca1225cc93fb32165ee79f challenge/output.txt
5e027a2850eb0e0d2e644636538063d65bf4080cadbae3369855f0ed07683a1d challenge/source.py
→ 4 Static Analysis
→ 4.1 output.txt
output.txt
contains ciphertext and 4 variables which
look suspiciously like components used in Diffie-Hellman key agreement
protocols:
-
p
, the prime number of the underlying group -
g
, the base number -
A
, the public key for one party of the exchange -
B
, the public key for the other party of the exchange
p = 0xdd6cc28d
g = 0x83e21c05
A = 0xcfabb6dd
B = 0xc4a21ba9
ciphertext = b'\x94\x99\x01\xd1\xad\x95\xe0\x13\xb3\xacZj{\x97|z\x1a(&\xe8\x01\xe4Y\x08\xc4\xbeN\xcd\xb2*\xe6{'
→ 4.2 source.py
source.py
contains Python code that appears to implement
a Diffie-Hellman key agreement protocol to compute private keys
a
and b
on lines 17-18 and their corresponding
public keys A
and B
on line 20. A shared key
is created on lines 25-26, which can be independently computed by either
party of the exchange using a party’s secret key and the other party’s
public key. Lines 28-36 use the key to encrypt the flag using the AES
encryption algorithm.
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.number import getPrime, long_to_bytes
from hashlib import sha256
from secret import FLAG
import random
p = getPrime(32)
print(f'p = 0x{p:x}')
g = random.randint(1, p-1)
print(f'g = 0x{g:x}')
a = random.randint(1, p-1)
b = random.randint(1, p-1)
A, B = pow(g, a, p), pow(g, b, p)
print(f'A = 0x{A:x}')
print(f'B = 0x{B:x}')
C = pow(A, b, p)
assert C == pow(B, a, p)
# now use it as shared secret
hash = sha256()
hash.update(long_to_bytes(C))
key = hash.digest()[:16]
iv = b'\xc1V2\xe7\xed\xc7@8\xf9\\\xef\x80\xd7\x80L*'
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(pad(FLAG, 16))
print(f'ciphertext = {encrypted}')
→ 5 Vulnerability analysis
In the calculation of the shared key, only b
and
a
are unknown:
However, both a
and b
are bounded by the
range [1, p)
on lines 17-18 and p
has a
maximum bit strength of 32 bits, which is very weak for Diffie-Hellman.
Therefore, the value of a
or b
can be brute
forced until the value generates a C
that can decrypt the
ciphertext. Additionally, given all flags for the challenges start with
HTB{
, a distinct terminating condition exists.
→ 6 Implementing the decryptor
decrypt.py
was implemented as below:
-
Lines 8-12: contents of
output.txt
. -
Line 15: iterate through all integers in the range
[1, p)
- Line 16-17: every 1 million iterations, print a status message.
- Line 19: calculate a candidate shared secret.
- Line 21-29: use the candidate shared secret to decrypt the ciphertext.
-
Line 30-32: if the decrypted text contains
HTB{
, print it out and break the loop.
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.number import getPrime, long_to_bytes
from hashlib import sha256
import random
p = 0xdd6cc28d
g = 0x83e21c05
A = 0xcfabb6dd
B = 0xc4a21ba9
ciphertext = b'\x94\x99\x01\xd1\xad\x95\xe0\x13\xb3\xacZj{\x97|z\x1a(&\xe8\x01\xe4Y\x08\xc4\xbeN\xcd\xb2*\xe6{'
print(f"p: {p}")
for b in range(1, p):
if b % 1000000 == 0:
print(f"b: {b}")
C = pow(A, b, p)
# now use it as shared secret
hash = sha256()
hash.update(long_to_bytes(C))
key = hash.digest()[:16]
iv = b'\xc1V2\xe7\xed\xc7@8\xf9\\\xef\x80\xd7\x80L*'
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(ciphertext)
if decrypted.find(b"HTB{") > -1:
print(f'decrypted = {decrypted}')
break
→ 7 Obtaining the flag
The script was run and the flag was obtained:
$ python3 -mvenv venv
$ . ./venv/bin/activate
$ pip install pycryptodome
$ python3 decrypt.py
p: 3714892429
b: 1000000
b: 2000000
b: 3000000
b: 4000000
b: 5000000
b: 6000000
b: 7000000
b: 8000000
decrypted = b'HTB{y0u_n3ed_a_b1gGeR_w3ap0n!!}\x01'
→ 8 Conclusion
The flag was submitted and the challenge was marked as pwned