Cable Temporal loop
Cable’s Temporal Loop - Crypto Writeup#
Challenge Info#
- Name: Cable’s Temporal Loop
- Category: Cryptography
- Given:
cable-challenge.py, remote servicenc chals2.apoorvctf.xyz 13424 - Flag format:
apoorvctf{...}
TL;DR#
The service combines:
- A linear congruential update check over integers modulo a secret 32-bit prime
p. - AES-CBC decryption with a padding oracle (
padding_okvspadding_error).
Even though decryption is gated behind an algebraic condition, that condition is only on the full ciphertext interpreted as an integer modulo p. We can always satisfy it by solving for a custom IV. That gives unlimited padding-oracle queries and allows full CBC plaintext recovery of the encrypted flag.
Recovered flag:
apoorvctf{T1m3_trAv3l_w1ll_n0t_h3lp_w1th_st4t3_crypt0}
Source Analysis#
From cable-challenge.py:
- Flag is encrypted once per connection:
ct = _ec(k, _Q)pythonwhere _ec is AES-CBC with random IV and PKCS#7 padding.
- Exposed endpoints:
1) math_test#
result = (a*d + b) % ppythonIt leaks outputs of an affine function modulo unknown prime p.
2) decrypt#
q = (a*s + b) % p
if int(ct_input) % p != q: fail
s = q
return padding_ok / padding_errorpythonSo decryption oracle exists, but only if submitted ciphertext integer satisfies a modulus constraint.
Weakness #1: Recovering p and b#
From math_test(0):
[
y_0 = (a*0 + b) \bmod p = b
]
Because b is chosen as randint(1, 0xFFFF) and p is a 32-bit prime, we get exactly b.
For arbitrary d, let:
[
y = (a d + b) \bmod p
]
Then:
[
t = a d + b - y = k p
]
so each non-zero t is a multiple of p.
Taking gcd over several samples recovers p with overwhelming probability:
[
p = \gcd(t_1, t_2, \dots)
]
This is exactly what the solver does in recover_modulus().
Weakness #2: Bypassing the algebraic gate for decrypt#
decrypt requires:
[
\text{int}(C) \equiv q \pmod p
]
where q = (a*s+b) mod p is known to us once a,s,b,p are known.
Suppose we want to query oracle on some chosen tail bytes T (at least 2 blocks, block-aligned), and let total ciphertext be:
[
C = IV || T
]
Interpret as integer:
[
\text{int}(C) = IV \cdot 256^{|T|} + \text{int}(T)
]
Need:
[
IV \cdot 256^{|T|} + \text{int}(T) \equiv q \pmod p
]
Since p is prime and not divisible by 2, 256^{|T|} is invertible modulo p. Therefore:
[
IV \equiv (q - \text{int}(T)) \cdot (256^{|T|})^{-1} \pmod p
]
This gives a valid 16-byte IV every time (p < 2^32, so the residue is tiny and fits in 16 bytes).
So the gate does not protect anything: we can always build valid ciphertexts for chosen-oracle queries.
Turning It Into a CBC Padding Oracle Attack#
Once decrypt accepts our ciphertext, response tells us if PKCS#7 padding is valid.
For a target block pair (C_{i-1}, C_i) from the original flag ciphertext:
- Keep
C_ifixed. - Replace previous block with controlled
X. - Query oracle on
X || C_i(with dynamically solved IV prepended to satisfy modulus check). - Use standard byte-wise PKCS#7 logic from last byte to first byte to recover intermediate bytes
I_i = D_k(C_i). - Recover plaintext block:
[
P_i = I_i \oplus C_{i-1}
]
Repeat for all blocks.
The script includes an extra disambiguation check to avoid false positives on padding matches.
Exploit Script#
Implemented in:
solve_cable.py
Key components:
recover_modulus()- Gets
bviamath_test(0) - Computes gcd of multiple
a*d + b - yvalues to recoverp
- Gets
decrypt_oracle(tail)- Computes required
q - Solves for a valid IV modulo
p - Sends
decryptrequest and returns boolean padding status
- Computes required
recover_block(...)- Standard CBC padding-oracle byte recovery
Reproduction#
From challenge directory:
python3 solve_cable.pybashOptional verbose logs:
python3 solve_cable.py --debugbashObserved output:
recovered block 1/4: b'apoorvctf{T1m3_t'
recovered block 2/4: b'rAv3l_w1ll_n0t_h'
recovered block 3/4: b'3lp_w1th_st4t3_c'
recovered block 4/4: b'rypt0}\n\n\n\n\n\n\n\n\n\n'
FLAG: apoorvctf{T1m3_trAv3l_w1ll_n0t_h3lp_w1th_st4t3_crypt0}textWhy This Works (Core Reasoning in 5 Bullets)#
math_testleaks affine congruence outputs, enough to recover hidden moduluspby gcd.bis directly leaked by queryingd=0.decryptgate checks onlyint(ciphertext) mod p, not semantic structure.- We can always choose an IV that forces any chosen ciphertext tail to satisfy the modulus gate.
- This exposes a full AES-CBC PKCS#7 padding oracle, which decrypts the flag block-by-block.
Final Flag#
apoorvctf{T1m3_trAv3l_w1ll_n0t_h3lp_w1th_st4t3_crypt0}