TRX CTF 25 - factordb.com
Disclaimer: It’s magicfrank speaking at 20 Feb 00:57 2025. I really hope nobody actually uploads the factorization to factordb.com, but let’s see.
Understanding the Leak
This challenge is a standard RSA with a leak where we have to factorize the modulus.
The function generating the leak is particularly bad because each bit of leak_i only depends on the lower (least significant) bits of $p$ and $q$.
This means:
- $\text{LEAK} \mod 2$ only depends on $p \mod 2$ and $q \mod 2$.
- $\text{LEAK} \mod 2^2$ depends only on $p \mod 4$, $q \mod 4$.
- $\text{LEAK} \mod 2^3$ depends only on $p \mod 8$, $q \mod 8$.
Since each step only depends on previously discovered bits, we can reconstruct $p$ and $q$ one bit at a time by trying all possible values and checking against both $N$ and the leak.
Recovering the Factors
We recover $p$ and $q$ bit by bit using brute-force with constraints. We start with the least significant bit (LSB) and systematically build up to the full values.
Step-by-Step Example
Finding the first bit
- Since $N \mod 2 = 1$, we know that both $p$ and $q$ must be 1.
- The possible values for $(p \mod 2, q \mod 2)$ are:
- (0,0) → 0 × 0 = 0 (incorrect)
- (0,1) → 0 × 1 = 0 (incorrect)
- (1,0) → 1 × 0 = 0 (incorrect)
- (1,1) → 1 × 1 = 1 (correct ✅)
So the only valid choice is (1,1).
Finding the second bit
Now, we try all possibilities for the second-least significant bit, while keeping the first bit fixed.
Possible values:
- (1,1)
- (1,3)
- (3,1)
- (3,3)
We check which of these satisfy both:
- $N \mod 4$
- $\text{LEAK} \mod 4$
In this case N%4 = 1
- (1,1) → 1 × 1 = 1 (correct ✅)
- (1,3) → 1 × 3 = 3 (incorrect)
- (3,1) → 3 × 1 = 3 (incorrect)
- (3,3) → 3 × 3 = 1 (correct ✅)
We have two valid choices: (1,1) and (3,3), so let’s check the leak function
LEAK%4 = 2
- (1,1) → (0x1337 + 1 + 1) ^ (0x1337 * 1 * 1) & (1 | 0x1337137) = 2 (correct ✅)
- (3,3) → (0x1337 + 3 + 3) ^ (0x1337 * 3 * 3) & (3 | 0x1337137) = 2 (correct ✅)
Both are valid, so we can continue with both.
Finding the third bit
- Now partial p and q could be either (1,1) or (3,3)
- Possible values:
- (1,1)
- (1,5)
- (5,1)
- (5,5)
- (3,3)
- (3,7)
- (7,3)
- (7,7)
- Check on N%8=5
- (1,1) → 1 × 1 = 1 (incorrect)
- (1,5) → 1 × 5 = 5 (correct ✅)
- (5,1) → 5 × 1 = 5 (correct ✅)
- (5,5) → 5 × 5 = 1 (incorrect)
- (3,3) → 3 × 3 = 4 (incorrect)
- (3,7) → 3 × 7 = 5 (correct ✅)
- (7,3) → 7 × 3 = 5 (correct ✅)
- (7,7) → 7 × 7 = 1 (incorrect)
- Check on LEAK%8=2
- (1,5) → (0x1337 + 1 + 5) ^ (0x1337 * 1 * 5) & (1 | 0x1337137) = 6 (incorrect)
- (5,1) → (0x1337 + 5 + 1) ^ (0x1337 * 5 * 1) & (5 | 0x1337137) = 6 (incorrect)
- (3,7) → (0x1337 + 3 + 7) ^ (0x1337 * 3 * 7) & (3 | 0x1337137) = 2 (correct ✅)
- (7,3) → (0x1337 + 7 + 3) ^ (0x1337 * 7 * 3) & (7 | 0x1337137) = 2 (correct ✅)
- We have two valid choices: (1,5) and (3,7)
- Note that the leak in this case actually helped us by eliminating half of the possibilities!
Finding the fourth bit
- …
…. …
BFS Implementation
We won’t do it by hand (it’s certainly possible, but humans invented computers for a reason).
from collections import deque
def bfs(n, leak):
start = (0, 0, 1) # (p_guess, q_guess, bit_position)
queue = deque([start])
ccc = 0
while queue:
pk, qk, k = queue.popleft()
ccc += 1
if ccc % 100 == 0:
print(f"\r{k}", end='')
if pk * qk > n:
continue
if pk * qk == n:
print("FOUND", pk, qk)
return pk, qk
# Try all possible combinations of the next bit
poss = [(0,0), (0,2**(k-1)), (2**(k-1),0), (2**(k-1),2**(k-1))]
for pos in poss:
new_pk, new_qk = pk + pos[0], qk + pos[1]
if (F(new_pk, new_qk) % 2**k == leak % 2**k and
(new_pk * new_qk) % 2**k == n % 2**k):
queue.append((new_pk, new_qk, k+1))