Unbreakable
This is a writeup for Unbreakable, a challenge that was part of ASCWG 2025
Before we start, this challenge was one of the first attempts at creating a more 'realistic' cryptography challenge, which I respect, I will provide my feedback later.
Handouts
We're given a .pcap file containing a tls key exchange, and a params.txt containing DH parameters
-----BEGIN DH PARAMETERS-----
MIGHAoGBAQxn6qbQaxmBfw2xnnZangJ79Exx1gozR1dRf2NMytxLwq1+Qonj0E/D
wtyKvZubz5w7gVIjvvFc/fUEsr5wvGE/GYFVlhk1w3uoDDhQHdoWGRSKNgxMW/UZ
Yi0fIA5Lxf3jpYKqItSIJyYdaMtTimY5NF7mD7sav+GEAtqOoE8DAgEC
-----END DH PARAMETERS-----

Analysis & Solver
So, inspecting the packets in the pcap file we can extract the public diffie hellman parameters used in the key exchange, A
, B
, p
, g
, and the client random and server random, and lastly, the cipher suite used in encryption.


So the cipher suite is TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
.


Okay so we can just copy value for all these parameters, I noticed the p in the .pcap is not equal to the p in the params.txt. And I also noticed that p in the pcap is composite, so out of curiosity I did tried to gcd between them and I found that:
And that q = p_pcap // p_params is also a large prime, so from now on I'll rename p_pcap to N for context.
Now, we try factorizing p-1 (the order of p) and we find that its a bunch of 45 bit primes, very smooth lol.
So I decide to do pohlig-hellman to retrieve alpha.
from sage.all import *
# from pcap
n = 0x026da5e48506222782ad4f571311db0dc3ddd606ad525dfdf6ecad87ab54e01e8bd7e7729654db92e7de6ee642ed7857eb01b5c5cae303c42db95f9e5be0f08fd18a603c899f07081cad66ac0c2498d0ae66aa64b4998c821139b9fa57c5b479134aff75865e878ee0c184bd3d39f3d173131a605464b8805c32763228fdc265e61a45cb12b91ddeb5579e02ee14bc56928152f47509bec85ea007bf791894915b0567201921e4b48b9276ad644a675ce78882132c5939dad91f4fac65718ff13640f78f8ce660a9e858150eac23b0a0bf62fb15ae70b469f6bba0439ec901188f51a99bb4eade7608cfdca7acf8123cb52c442ecc46941cfcec97550ebdecbc31
A = 0x0e9e94f03d6dd901e44b4c04ce876e202a8d43373c7ffee374b6d833d806c03764919f2b1615b4f664cf2d6e70e0a4af23bd7c8088d3139b529a3919639a736478d71b91063143ce332fbbdba841b16e5bbe1c8300e953c0914e262b29f69d1d04462809605e8ad3f359dca22e0c7b7a73139e649af721ec2ae4ce76a0bef245dce9248435592faba79793eb439a6e2a0798c5ed7dea2945f3adeadcf9e980fc8a4597f35305d9d7328752dd94691d8289828af000884f6308385cd7b6b643e58b34268e2e4ac151feefed048c92f673dad5869cf26dc6ec1cfaa1949d3b8095962b4cfef71827249063f099acc8b2c43926af644f69663591619c4e31e519f9
B = 0x0221afb11938cb1fc0335894210daeacfd8ce929cff9a433b607a59c452bb2fff45c0a5ca857a42c6ad267e9e4d97200ef3ae61be453fe6e7761a6b93f41520f7b6566c731c1b661d07167990f431aeb0becc6dd7d7414ff8477c1d18f1c6b3c55faf6a20dc0976dc30b396d4499fdf27a2178c910a7f08452658c58689cbef548da5f12ed11474b5aa91ee033f6412ae424944afcfb0fb87f2727e8272da4ac5f8fbf30f9f3b7715d2877192133c4e342b55152d2da5ebbb99a3dad0f33a40f5f863c31772847d2a76e37a88a3096442e8fe6fed20e64cefe66cb23d2e28e22109bde899398a93ab4a7c1d33ee877084f96f8ce0b9b56eacee01e36915addd26a
# from params.txt
p = 188481049757722254887446294741765543978345244373357803285748557139380955689864149390822640119996411454499583101834756749814893915942331906825914752722686129146728313414926325181335481676891411570489611040565048816424707189697438089020659203823234671280879018477037306908989340237413694212744228716516386819843
g = 2
A_p = []
Ms = []
for p_test in ecm.factor(p-1)[1:]: # removing the first entry (2)
try:
F = GF(p)
k = (p - 1) // p_test
gp = pow(g, k, p)
Ap = pow(A, k, p)
Ap = F(Ap)
log_A = Ap.log(gp)
A_p.append(log_A)
Ms.append(p_test)
print(f"q: {p_test}, A_q: {log_A}")
except Exception as e:
print(f"Error with q={p_test}: {e}")
# you dont have to do all factors, just enough to recover alpha which is like 5-6 factors
alpha = crt(A_p, Ms)
assert pow(g, alpha, n) == A
s = pow(B, alpha, n)
print(f's = {s}') # s = 60688211686490993502225397016951709378899105728454300621619790878716459365293819429817061593788805624842645819708800542592578704113204283061530414854707762945163802740718097156641401944546594691622913268230011444106678784616711602156362897566986213535065055451307793398003279782892828100336922229947584416675331163165739089947208503530135765115141601317672241950654800909952170324515842678984776597868753762398260037024185244250838607852700041572953484387182066249251052243759375179760754772216756408532649414178383647906265224980773038167857850629631159784148136939564262138414791017733630907516898870886172325347692
This is stage 1, stage 2 of out attack is to build the client and server write keys + IVs using HMAC-SHA256 (inspired by the suite in the traffic)
import hashlib
import hmac
from Crypto.Cipher import AES
def prf_sha256(secret, label, seed, length):
def p_hash(secret, seed, length):
result = b''
a = seed
while len(result) < length:
a = hmac.new(secret, a, hashlib.sha256).digest()
result += hmac.new(secret, a + seed, hashlib.sha256).digest()
return result[:length]
return p_hash(secret, label + seed, length)
def derive_keys(master_secret, client_random, server_random):
seed = server_random + client_random
key_block = prf_sha256(master_secret, b"key expansion", seed, 128)
keys = {}
offset = 0
keys['client_write_MAC_key'] = key_block[offset:offset+32]
offset += 32
keys['server_write_MAC_key'] = key_block[offset:offset+32]
offset += 32
keys['client_write_key'] = key_block[offset:offset+16]
offset += 16
keys['server_write_key'] = key_block[offset:offset+16]
offset += 16
keys['client_write_IV'] = key_block[offset:offset+16]
offset += 16
keys['server_write_IV'] = key_block[offset:offset+16]
return keys
def decrypt_aes_cbc_with_mac(ciphertext, key, iv, mac_key):
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(ciphertext)
padding_length = decrypted[-1]
plaintext_with_mac = decrypted[:-padding_length]
mac_length = 32
plaintext = plaintext_with_mac[:-mac_length]
received_mac = plaintext_with_mac[-mac_length:]
return plaintext, received_mac
def main():
# from previous script
s = 60688211686490993502225397016951709378899105728454300621619790878716459365293819429817061593788805624842645819708800542592578704113204283061530414854707762945163802740718097156641401944546594691622913268230011444106678784616711602156362897566986213535065055451307793398003279782892828100336922229947584416675331163165739089947208503530135765115141601317672241950654800909952170324515842678984776597868753762398260037024185244250838607852700041572953484387182066249251052243759375179760754772216756408532649414178383647906265224980773038167857850629631159784148136939564262138414791017733630907516898870886172325347692
# from .pcap
client_random = 0x19a4d59aa8039682faf6bb1ea9ec9542977e23941b1ea966e5d0cff8f2e389d8
server_random = 0x8518043440128863a52c1ead2fae81916b62ef2bc99f188fdb699a177727fabf
client_random_bytes = client_random.to_bytes(32, 'big')
server_random_bytes = server_random.to_bytes(32, 'big')
pre_master_secret = s.to_bytes((s.bit_length() + 7) // 8, 'big')
seed = client_random_bytes + server_random_bytes
master_secret = prf_sha256(pre_master_secret, b"master secret", seed, 48)
print(f"Master secret: {master_secret.hex()}")
keys = derive_keys(master_secret, client_random_bytes, server_random_bytes)
print(f"Client write key: {keys['client_write_key']}")
print(f"Server write key: {keys['server_write_key']}")
print(f"Client write IV: {keys['client_write_IV']}")
print(f"Server write IV: {keys['server_write_IV']}")
if __name__ == "__main__":
main()
output is:
Master secret: 8524c5204e35e69ba92afd5ac57315f8a4065cf7b2e36ee8a0ea915dc5a3a17992e18e1db5412a3faa9fa40775e4675f
Client write key: b'\x0b4\xfa|T\xef\xcf\xceOV=^I#Fr'
Server write key: b'z9\xd8e\xf3\x11\xd4q\xdf\xb0\xbf&\x84C\xec\x16'
Client write IV: b'\xcb\xcdg\xb4\xff$;\xa7~\x86\xcb\xfa(\x8d-\xbf'
Server write IV: b'Z]\xc8\t\xb9\x95\xb7C\x19VySE\x96\xc77'
Finally we take the application ciphertexts we got from the pcap file and decrypt using the keys
from Crypto.Cipher import AES
Client_write_key = b'\x0b4\xfa|T\xef\xcf\xceOV=^I#Fr'
Server_write_key= b'z9\xd8e\xf3\x11\xd4q\xdf\xb0\xbf&\x84C\xec\x16'
Server_write_key = b'\xeb\x07\xf5R\x06\xc5\xbc]\xe1\xe8\x11\xcb>\x14\xdbb'
Client_write_IV= b'\xcb\xcdg\xb4\xff$;\xa7~\x86\xcb\xfa(\x8d-\xbf'
Server_write_IV= b'Z]\xc8\t\xb9\x95\xb7C\x19VySE\x96\xc77'
def decrypt_aes_cbc(ciphertext, key, iv):
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(ciphertext)
return decrypted
ct1 = 0x53f1d16dce50f0347820e66be29c50247cda52aba5224c65de0bdacc68fb8dc0be02cf1ddad15caa3a195da55f0e0cffc14eef56f550a4247f852b47e792916bce9ef32c9bb3dfc911a7191e3b1f27e717a4eff0c07a9db40d76c880b7461b968f502e58d2387e972663d36559864284a7df20d091da3fac0787462dff63af718b72c9cc2fea73235b4921de5fee7a00
ct2 = 0xa65e934f770b2129fc32fe0caeae8d5559e04b108da69e4b1b645681a9b41078c7cc863672d94f22429b6f37df06ddbe67fdf413ff78fd0cdbfe12a14d39c5d6f85f93d2e86ddfdcf79659ef3ccd2110
plaintext = decrypt_aes_cbc(
ciphertext=ct1.to_bytes((ct1.bit_length() + 7) // 8, 'big'),
key=Client_write_key,
iv=Client_write_IV
)
plaintext2 = decrypt_aes_cbc(
ciphertext=ct2.to_bytes((ct2.bit_length() + 7) // 8, 'big'),
key=Server_write_key,
iv=Server_write_IV
)
print("Decrypted plaintext:", plaintext.decode('utf-8', errors='ignore'))
print("Decrypted plaintext2:", plaintext2.decode('utf-8', errors='ignore'))
which gives us :
Decrypted plaintext: .M>ͤv*cϖHello, old friend!
I can see that, how cool it is.
ASCWG{S0m3_0ld_c00l_DH_b4ckd00rs}
And that's it
Feedback
The only not so nice part about the challenge is that dh params contained a different public modulus to the one in the communication which caused some confusion, I think it's a little guessy.
But overall, nice chall, except for this minor detail, excited to see more 'realistic' challenges, but let's try to maintain a level of fun-to-solve experience while doing it, kudos to the writer :)).
Last updated