Challenge: Fun With RSA · CTF: DalCTF 2026 · Category: Crypto · Difficulty: Hard
“Made my own rsa implementation. Hoping its safe enough that no one can crack it. I even xored things to make it safer”
Whenever a challenge description brags about a homemade RSA implementation and “xored things to make it safer,” you can be pretty confident the crypto is about to roll over. Let’s dig in.
The Challenge
We’re given the encryption script and its output.
FunWithRSA.py
| |
fwr.txt
| |
Step 1 — Read the code, not the README
The first thing to notice is that this isn’t really an encryption challenge — it’s a signing challenge. The script computes a ciphertext c = pow(m, e, n), sure, but it never gives c to us directly (it hides it inside ct). Instead, the values we actually receive are s and spz, which are produced by an RSA-CRT signing routine.
Let’s unpack what CRT signing does.
A quick refresher on RSA-CRT
Plain RSA decryption/signing is s = m^d mod n. That modular exponentiation over the full modulus n is slow, so real implementations use the Chinese Remainder Theorem (CRT) to do it ~4× faster. They precompute:
| |
Then they compute the signature mod each prime separately and glue them back together (this is Garner’s formula):
| |
This is exactly what the challenge does to produce s. So:
| |
s is nothing more than the textbook RSA signature of the flag.
Step 2 — The free win (signature ↔ message)
Here’s the thing that makes this challenge fall apart in one line.
RSA signing and verifying are inverse operations. Signing with the private exponent d and then verifying with the public exponent e brings you right back to the message:
| |
We have s, e, and n. So we can recover the message without factoring anything:
| |
| |
Output:
| |
Done. dalctf{s3gf4u17_r54_m1x3d_w17h_x0r}
That’s the fastest path — but the flag name (“segfault RSA”) is a big hint that the intended solution is something juicier: a fault-injection attack. Let’s do it the intended way too, because it’s a beautiful, classic attack.
Step 3 — The intended solution: the Bellcore / RSA-CRT fault attack
Look again at this block:
| |
The script gives us two signatures of the same message:
s— computed normally.spz— computed with a corruptedsp. The linespz = sp ^ 1XORs the p-half with 1, flipping its lowest bit. This simulates a hardware fault injection (a glitch, a cosmic ray, a Rowhammer bit-flip — exactly the kind of thing the Bellcore attack was designed to exploit).
This is the textbook setup for the RSA-CRT fault attack, and it’s catastrophic.
Why one faulty signature breaks everything
Look carefully at where the fault lands. The faulty signature is built from a corrupted sp but a correct sq. So:
Modulo q: both signatures used the correct
sqand only differ by multiples ofq. The error termh*qandhf*qboth vanish modq. Therefore:1s ≡ spz (mod q) → s - spz ≡ 0 (mod q)Modulo p: the fault changed
sp, so the two signatures genuinely disagree:1s ≢ spz (mod p) → s - spz ≢ 0 (mod p)
Put those together: s - spz is a multiple of q but not a multiple of p. That means the greatest common divisor of (s - spz) and n = p·q is exactly q:
| |
We just factored a 512-bit modulus with a single gcd. 💀
| |
| |
Once you have p and q, the rest is standard RSA:
| |
Step 4 — What about that XOR’d ciphertext?
The “I even xored things to make it safer” line refers to this:
| |
The author takes the real ciphertext c, converts it (and p and q) to binary strings with bin(...), XORs all three together using pwntools’ xor, and prints the result as ct. The idea was to hide c so you couldn’t just decrypt it.
But this “protection” is hollow:
XOR is trivially reversible.
a ⊕ b ⊕ c ⊕ b ⊕ c = a. Once you knowpandq(which we recovered in Step 3), you simply XOR them back out to recoverc:1 2 3 4 5from pwn import xor # ct = c ^ p ^ q (over their binary string representations) c = int(xor(bin(ct), bin(p), bin(q)), 2) m = pow(c, d, n) print(long_to_bytes(m)) # dalctf{s3gf4u17_r54_m1x3d_w17h_x0r}And as we saw in Step 2, you never even needed
cat all —salready leaks the message vias^e mod n.
So the XOR layer adds zero security. It just gives the flag its name: RSA segfault mixed with XOR.
Full Solver
| |
Both paths print:
| |
Takeaways
- Roll-your-own crypto loses. The “homemade RSA” warning in the description is the whole challenge.
- Never expose two CRT signatures of the same message when one might be faulty. A single faulty RSA-CRT signature lets an attacker factor
nwith onegcd— this is the real-world Bellcore attack, and it’s why production libraries verify every CRT signature (s^e ≟ m) before releasing it. The fix is one line: recompute and reject if the result doesn’t verify. - XOR is not encryption. Obfuscating a value by XORing it with other values you also leak (here
pandq) provides no security whatsoever. - Signatures and ciphertexts are dual. Handing out
m^d mod nis handing out the decryption oracle output for that message.s^e mod ngivesmstraight back.
Flag: dalctf{s3gf4u17_r54_m1x3d_w17h_x0r}