(Crypto) TSJ CTF 2022 - babyRSA
Created at: 06/03/2025, 23:26:11.: #ctf-writeups, #crypto :.
Table of Contents
Category | Crypto |
Difficulty | Medium |
CTF | TSJ CTF |
Year | 2022 |
Challenge Author | maple3142 |
Description

Synopsis
The point belongs to the curve so we create two multivariate polynomials, eliminate one variable using resultants and run small_roots()
to obtain . Having factored , construct the curve in the composite ring and find the base point by calculating with the inverse of modulo the curve’s order.
Source files
You can download the source files from the original CTF repository.
Overview
Let’s first analyze the main script of the challenge; that is challenge.sage
.
from Crypto.Util.number import *
import os
proof.arithmetic(False) # to make sage faster
flag = b"TSJ{not_real_flag}"
p = getPrime(1024)
q = getPrime(512)
n = p * q
e = 65537
E = EllipticCurve(Zmod(n), [p, q])
while True:
x = ZZ(bytes_to_long(flag + os.urandom(192 - len(flag))))
try:
yp = ZZ(E.change_ring(GF(p)).lift_x(x).xy()[1])
yq = ZZ(E.change_ring(GF(q)).lift_x(x).xy()[1])
y = crt([yp, yq], [p, q])
break
except:
pass
C = e * E(x, y)
print(n)
print(C.xy())
At first glance, we see there is a standard RSA key generation, i.e. the secret primes , the modulo and the public exponent . Then, an elliptic curve is defined over the composite ring with parameters and .
We can describe this curve with the following algebraic relationship:
The flag is randomly padded and stored in the variable . After that, two points with the -coordinate are calculated:
This is a point of the curve that is defined over ; say .
Note that is a multiple of so it is eliminated .
This is a point of the curve that is defined over ; say .
Note that is eliminated .
What the function E.change_ring()
basically does is changing the ring in which the curve is defined.
Then, and are combined with the Chinese Remainder Theorem to get the -coordinate that belongs to .
To summarize, this loop does the same job as the Sage method E.lift_x(x)
. The reason we can’t use this method now is due to being defined over a composite ring and lift_x
would try to factor to lift the point in and , which is a very hard problem since the prime generation is secure. Therefore we conclude that the flag is randomly padded until the point is on , where is the padded flag.
Finally, the encryption is similar to that of RSA but in the additive group:
- is the ciphertext point
- is the scalar
- is the point of that we want to retrieve
Idea on how to recover the base point and the problem that lies
How does decryption work in RSA? One raises the ciphertext to the multiplicative inverse of modulo the order of the multiplicative group ; that is .
Similarly, from here, we know that to solve for we need to multiply by the multiplicative inverse of modulo the order of .
where is the order of the curve .
Pretty straightforward right? … Hmm, not at all.
As aforementioned, is defined over a composite ring so knowing its order is as hard as factoring . is more than 1500 bits long so without a quantum computer, we have no luck here.
Solution
q is half the bit length of p
The key to factor is notice that is half the bit length of . That’s about the bit length of .
? Where did this come from?
That’s because can be written as the product of three 512
-bit integers, say .
Since is 512 bits, we are certain that one (1) of these three (3) variables must be . This makes about of .
Playing with the curve’s formula
Since belongs to , so does .
We know that any point of satisfies the following formula:
Then substituting with coordinates we get:
Let’s rewrite the relation above as follows:
We know everything apart from and but we can’t solve for them because we have one relation and two unknowns. Do we know something else about ? Well, from the RSA part we know that and is known. That’s great! Two equations and two unknowns so there is a unique solution.
Constructing the polynomials
Let’s define the following polynomials over .
Notice that are both roots of these polynomials:
They are multivariate polynomials but maybe we could eliminate one variable? For example, we could substitute in the polynomial. One could substitute with pencil and paper and come up with a univariate polynomial in terms of but that’s a lot of work (however, it is recommended as an exercise for beginners!).
We could use our beloved resultant that basically does the same thing. You can find more about resultants from 1, 2 and some cool Joseph writeups.
Let’s use Sage to find the resultant of these polynomials. We will basically find a univariate polynomial in terms of only.
P.<x,y> = PolynomialRing(Zmod(n))
f = n - x*y
g = Cy^2 - Cx^3 - x*Cx - y
def resultant(f1, f2, var):
return Matrix.determinant(f1.sylvester_matrix(f2, var))
h = resultant(f, g, x) # eliminating x is equivalent to eliminating p
print(h)
The output is:
y^2 + 11913771694063495132568425582147978387779218009404951491138444355803251420750777828581495229803905485508710200822306270492779460893035511452060758726696972877404214806553422280705330092204004616281420566339823476408647786409010040145494297930530259483781466478416269629186356995544407404915722209121439224567698019474188565402069644492370517495662654444038623713130993722823437453577026376201959720791194856979494885237541302217843842247547112767879217639883793*y
We have the polynomial:
where is the large integer.
For the correct value of it holds that:
But what can we do now? This polynomial is defined in so we can’t apply standard techniques that work in the integers .
Recall that is half ’s bit length. This means that is a small root of this polynomial, compared to the size of . It turns out we can use Coppersmith’s algorithm to find the roots of the polynomial above. These roots are also known as small roots
.
Coppersmith’s algortihm
It might be a bit complex to describe how the algorithm works but the intuition behind is that when we are looking for something small defined over something big, then Coppersmith’s algorithm should do the trick.
This is why we cared about being the half ’s bit length.
Let’s get a bit more technical now. Coppersmith’s method will return the small roots of our polynomial modulo a factor of , say , without having to factor at all. Pretty instance, right? That’s lattices for you!
Why is this so important?
While the equations are defined modulo , Coppersmith’s small roots algorithm finds a small root modulo a factor of and in our case modulo .
Sage’s small_roots()
function is an implementation of Coppersmith’s algorithm. However, it requires some parameters:
That’s an upper bound for the small root we are looking for. In our case, is at most so .
beta
(or )That’s a value such that , where is a factor of . We know that or equivalently so:
You can check here for more information.
Factoring
Now it’s time to factor . Let’s run the following code:
roots = h.small_roots(X=2^512, beta=0.66)
print(roots)
[0, 7560550953987228717927043411195097606178780260722416435854220484370855427179572047127883844297336386784419855728350626032040641635456814848906770345908561]
The second root looks like a candidate for .
print(N % q == 0)
True
Boom! We have factored .
q = int(h.small_roots(X=2^512, beta=0.66)[1])
p = N // q
assert N == p * q
print(f'p = {p}')
print(f'q = {q}')
p = 143466851392554970695990704123817779733897135669358867616227016983904822448652872447294618655211767603232776070689066195417250663712048624942911364907905504389182987025672966027320571640054927174423881068214579019052804849273855736938414398455136976371108924577231955088915205951155616111863602559370112932349
q = 7560550953987228717927043411195097606178780260722416435854220484370855427179572047127883844297336386784419855728350626032040641635456814848906770345908561
Now we have to find . Since is defined over a composite ring, it holds that:
where the order of the curve and the order of the curve .
Knowning , we can compute as shown in .
Full solve script
from sage.matrix.matrix2 import Matrix
from Crypto.Util.number import long_to_bytes
def resultant(f1, f2, var):
return Matrix.determinant(f1.sylvester_matrix(f2, var))
Cx = 1079311510414830031139310538989364057627185699077021276018232243092942690870213059161389825534830969580365943449482350229248945906866520819967957236255440270989833744079711900768144840591483525815244585394421988274792758875782239418100536145352175259508289748680619234207733291893262219468921233103016818320457126934347062355978211746913204921678806713434052571635091703300179193823668800062505275903102987517403501907477305095029634601150501028521316347448735695
Cy = 950119069222078086234887613499964523979451201727533569872219684563725731563439980545934017421736344519710579407356386725248959120187745206708940002584577645674737496282710258024067317510208074379116954056479277393224317887065763453906737739693144134777069382325155341867799398498938089764441925428778931400322389280512595265528512337796182736811112959040864126090875929813217718688941914085732678521954674134000433727451972397192521253852342394169735042490836886
N = 1084688440161525456565761297723021343753253859795834242323030221791996428064155741632924019882056914573754134213933081812831553364457966850480783858044755351020146309359045120079375683828540222710035876926280456195986410270835982861232693029200103036191096111928833090012465092747472907628385292492824489792241681880212163064150211815610372913101079146216940331740232522884290993565482822803814551730856710106385508489039042473394392081462669609250933566332939789
e = 65537
P.<p,q> = PolynomialRing(Zmod(N))
f = N - p*q
g = Cy^2 - Cx^3 - p*Cx - q
h = resultant(f, g, p).univariate_polynomial()
q = int(h.small_roots(X=2^512, beta=0.66)[1])
p = N // q
assert N == p * q
E = EllipticCurve(Zmod(N), [p,q])
Ep = EllipticCurve(GF(p), [p,q])
Eq = EllipticCurve(GF(q), [p,q])
On = Ep.order() * Eq.order()
C = E(Cx, Cy)
G = int(pow(e, -1, On)) * C
flag = long_to_bytes(int(G[0]))
flag = re.search(rb'(TSJ{.*})', flag).group(1)
print(flag)
Output:
b"TSJ{i_don't_know_how_to_come_up_with_a_good_flag_sorry}"