We’re given a basic RSA encrypted flag with n and ct.
We also have an LCG that generates the lower 3/4ths of p and q (the upper 1/4th uses a different PRNG).
We’re given 6 outputs of the LCG.
This means that somehow, we first need to completely recover the LCG, step backwards to find the lower bits of the primes, then factor n to break the RSA encryption and get the flag. Let’s get started!
Breaking LCGs: The Easy Part
Let’s start by cracking the LCG first. There’s plenty of documentation online on solving LCGs with 0 information, but I’ll try to walk through it here.
Finding the modulus
First we need to recover the modulus of the LCG. To do this, we can use a simple trick to recover multiples of the modulus and gcd them together to find either the modulus itself or a low multiple.
Let’s set up the following equations and define Sn as the nth output of the LCG, as well as a as the multiplier, c as the increment, and m as the modulus:
Meaning that T forms its own LCG with the same modulus and multiplier but no increment. Witht this new LCG, we can now easily find the modulus by just multiplying different items in T to get differences that are k⋅m for some integer k.
If we gather enough of these, we can just gcd them together to find the modulus. Unfortunately, the output is chosen so that we get a multiple of the modulus, but since we know the number has to be around 128 bits, we can easily guess that it is a factor of 5 too large.
Nice! We’ve recovered the modulus, so we can move on to part 2, recovering the multiplier.
Recovering the multiplier
Let’s go back to the T LCG earlier, since the multiplier is the only unknown there.
We should be able to easily recover a like so:
T1a=aT0=T1T0−1modmmodm
But if we try this out in Python, we get an error…
Traceback (most recent call last):
File "test.py", line 16, in <module>
A = (T1 * pow(T0, -1, M)) % M
^^^^^^^^^^^^^^
ValueError: base is not invertible for the given modulus
Unfortunately, our T values aren’t coprime with m, so we can’t just take the modular inverse. Thankfully, we can still recover a very simply by the following identity:
Actually just Univariate in disguise? ⚠️ Sage & math ahead ⚠️
Now we have the low bits of both primes, we can set up a bivariate equation which should give us the high bits like so:
kf=384(offset for high bits)=(plow+phigh2k)(qlow+qhigh2k)−n
But actually, because these equations are over the integers, we can use the monic and small_roots functions in SageMath to
create a univariate polynomial with an easy solution!
x + <very big number here>
51124072313420104261781687898895862037 # our p_high!
In fact, when I was solving this challenge originally, I forgot that monic existed, and kept trying to jank some method for small_roots.
Eventually, I stumbled upon this implementation which I used to solve (It implements this paper).
It creates a bunch of monomials from the original bivariate polynomial and uses LLL to solve for small roots. If you want specifics, I recommended reading the paper and checking out the code.
Anyway, the solve implementation is the same, just call the provided coron method with the correct parameters.
Finally, we have recovered p and q. All we need to do now is calculate d and decrypt the flag.
GG! 😄
Closing Thoughts
I had a lot of fun doing this challenge (cough cough even if vishi lied about our modulus being wrong cough cough), and I feel like I’m actually starting to understand these types of Coppersmith attacks and polynomials a lot more 😎.
It’s been a long journey since I first started doing CTFs so it’s nice to do these challenges where I can see my own progress. I hope you enjoyed reading this writeup, and I’ll see you in the next one! 👋