0xnhl

Cable Temporal loop

/ Update
3 min read

Cable’s Temporal Loop - Crypto Writeup#

Challenge Info#

  • Name: Cable’s Temporal Loop
  • Category: Cryptography
  • Given: cable-challenge.py, remote service nc chals2.apoorvctf.xyz 13424
  • Flag format: apoorvctf{...}

TL;DR#

The service combines:

  1. A linear congruential update check over integers modulo a secret 32-bit prime p.
  2. AES-CBC decryption with a padding oracle (padding_ok vs padding_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)
python

where _ec is AES-CBC with random IV and PKCS#7 padding.

  • Exposed endpoints:

1) math_test#

result = (a*d + b) % p
python

It 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_error
python

So 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:

  1. Keep C_i fixed.
  2. Replace previous block with controlled X.
  3. Query oracle on X || C_i (with dynamically solved IV prepended to satisfy modulus check).
  4. Use standard byte-wise PKCS#7 logic from last byte to first byte to recover intermediate bytes I_i = D_k(C_i).
  5. 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 b via math_test(0)
    • Computes gcd of multiple a*d + b - y values to recover p
  • decrypt_oracle(tail)
    • Computes required q
    • Solves for a valid IV modulo p
    • Sends decrypt request and returns boolean padding status
  • recover_block(...)
    • Standard CBC padding-oracle byte recovery

Reproduction#

From challenge directory:

python3 solve_cable.py
bash

Optional verbose logs:

python3 solve_cable.py --debug
bash

Observed 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}
text

Why This Works (Core Reasoning in 5 Bullets)#

  1. math_test leaks affine congruence outputs, enough to recover hidden modulus p by gcd.
  2. b is directly leaked by querying d=0.
  3. decrypt gate checks only int(ciphertext) mod p, not semantic structure.
  4. We can always choose an IV that forces any chosen ciphertext tail to satisfy the modulus gate.
  5. 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}

Cable Temporal loop
https://nahil.xyz/vault/writeups/apoorvctf2026/cryptography/cable-temporal-loop/
Author Nahil Rasheed
Published at March 24, 2026
Disclaimer This content is provided strictly for educational purposes only.