- Published on
BroncoCTF 2025 - Crypto challenges
Introduction
We solved 4 out of 5 tasks. More info about this CTF is here

Table of contents
Across the Tracks

It's Rail Fence (Zig-Zag) Cipher. Full text:
Some·ciphers·are·easier·to·solve.·Some·ciphers·are·harder·to·solve.·You·definitely·could·brute·force·this·one·if·you·did·it·by·hand.·I·had·to·do·that·recently·on·an·exam.·It·was·not·as·fun·as·I·had·hoped.·But·that·is·okay.·I·hope·you·didn't·do·this·by·hand.·Here·is·the·flag·tho:·bronco{r@1l_f3nc3_cip3rs_r_cool}
Flag: bronco{r@1l_f3nc3_cip3rs_r_cool}
Rahhh-SA

import math
e = 65537
phi_n = 3424680
n = 3429719
ciphertext = [-53102, -3390264, -2864697, -3111409, -2002688, -2864697, -1695722, -1957072, -1821648, -1268305, -3362005, -712024, -1957072, -1821648, -1268305, -732380, -2002688, -967579, -271768, -3390264, -712024, -1821648, -3069724, -732380, -892709, -271768, -732380, -2062187, -271768, -292609, -1599740, -732380, -1268305, -712024, -271768, -1957072, -1821648, -3418677, -732380, -2002688, -1821648, -3069724, -271768, -3390264, -1847282, -2267004, -3362005, -1764589, -293906, -1607693]
def extended_euclidean(e, phi_n):
A1, A2, A3 = 1, 0, phi_n
B1, B2, B3 = 0, 1, e
while True:
if B3 == 0:
return -1
if B3 == 1:
return B2 % phi_n
Q = A3 // B3
T1, T2, T3 = A1 - (Q * B1), A2 - (Q * B2), A3 - (Q * B3)
A1, A2, A3 = B1, B2, B3
B1, B2, B3 = T1, T2, T3
def decrypt(ciphertext, d, n):
return [pow(c, d, n) for c in ciphertext]
d = extended_euclidean(e, phi_n)
decrypted_numbers = decrypt(ciphertext, d, n)
print("".join(chr(i) for i in decrypted_numbers))
Flag:bronco{m4th3m4t1c5_r34l1y_1s_qu1t3_m4g1c4l_raAhH!}
Mid PRNG

- flag prefix is known
- generator can only have states in
range(0, 256)
, otherwisebytes()
would fail
We download encrypted flag 1000 times
for run in {1..1000}; do
nc bad-prng.nc.broncoctf.xyz 8000 >>history.txt
echo >>history.txt
done
We use known flag prefix to recover pairs of consecutive states
def xor(b1, b2):
return bytes(e1 ^ e2 for e1, e2 in zip(b1, b2))
with open("history.txt") as f:
data = [bytes.fromhex(e) for e in f.readlines()]
prefix = b"bronco{"
dct = {}
for row in data:
lst = list(xor(prefix, row))
for a, b in zip(lst, lst[1:]):
assert dct.setdefault(a, b) == b
for row in data:
x = row[0] ^ prefix[0]
key = bytearray([x])
for i in range(len(row) - 1):
try:
key.append(dct[key[-1]])
except KeyError:
break
else:
flag = xor(key, row)
print(f"{flag = }")
break
Flag: bronco{0k_1ts_n0t_gr34t}
Homie Owes Me

According to intel from Luigi:
1. Really likes being called a homie. Specifically, yoshiethehomie.
2. Likes to talk in "partial leetspeak."
3. Is a fan of "one, only one!" special character replacement.
4. Appends his 4-digit PIN to all his secure logins.
5. Loves BroncoCTF so much that he includes the flag format in his credentials.
I created a custom mask for hashcat:
hashcat -m 1400 -a 3 -O -1 o0 -2 s5$ -3 i1! -4 e3 d:\hash.txt bronco{y?1?2h?3?4th?4h?1m?3?4?d?d?d?d}
Flag: bronco{y0sh1eth3hom!e8778}