grammar nazi
6 minutes to read
We are given the following source code in Python, with the output as a multi-line string:
from Crypto.Util.number import *
FLAG = 'maltactf{???????????????????????????????}'
assert len(FLAG) == 41
p = getPrime(128)
q = getPrime(128)
N = p * q
e = 65537
m = f'The flag is {FLAG}'
c = pow(bytes_to_long(m.encode()), e, N)
# ERROR: Sentences should end with a period.
m += '.'
c += pow(bytes_to_long(m.encode()), e, N)
# All good now!
print(f'{N = }')
print(f'{c = }')
'''
N = 83839453754784827797201083929300181050320503279359875805303608931874182224243
c = 32104483815246305654072935180480116143927362174667948848821645940823281560338
'''
Source code analysis
The source code is short and straight-forward:
- We know the flag format and its length
- The program generates two 128-bit prime numbers
and (RSA private key) - and uses that to create an RSA public key
and - After that, the program encrypts a message that contains the flag:
- Then, it updates
in a weird way to “add a period”
We are given N
and the updated c
. With this information, we need to find the flag.
Solution
Let
As a result, the given c
value is
In brief, we can define a polynomial
In a secure RSA implementation, such as RSA-2048 or RSA-4096, this would be impossible to solve in a reasonable time. However, this time the server uses RSA-256, since the primes
$ sage -q
sage: N = 83839453754784827797201083929300181050320503279359875805303608931874182224243
sage: c = 32104483815246305654072935180480116143927362174667948848821645940823281560338
sage:
sage: e = 65537
sage:
sage: p = 276784813000398431755706235529589161781
sage: q = 302904819256337380397575865141537456903
sage: N == p * q
True
So, given that we know
In other words, we can easily find
Implementation
In SageMath, we could be tempted to use the following code:
sage: Pp.<xp> = PolynomialRing(GF(p))
sage: Pq.<xq> = PolynomialRing(GF(q))
sage:
sage: fp = xp ** e + (256 * xp + 46) ** e - c
sage: fq = xq ** e + (256 * xq + 46) ** e - c
sage:
sage: fp.roots()
sage: fq.roots()
However, this will take a long time to finish. A fast way to get a root is to use any_root
:
sage: fp.any_root()
38745752538982310497402322032299730998
We can do the same with any_root
several times until getting the desired one. Nevertheless, even if we find the desired roots, we won’t be able to get the expected message. Notice that the flag is 41 bytes long, so the message "The flag is "
. Remember that
The way to address this is issue to consider the known bytes, so that "The flag is maltactf{\0\0...\0}"
, and "maltactf{}"
), which is less than
sage: M = int.from_bytes(b'The flag is maltactf{???????????????????????????????}'.replace(b'?', b'\0'))
sage:
sage: fp = (M + 256 * yp) ** e + (256 * (M + 256 * yp) + 46) ** e - c
sage: fq = (M + 256 * yq) ** e + (256 * (M + 256 * yq) + 46) ** e - c
Now, using any_root
several times on both fp
and fq
and trying the CRT, we will eventually find the flag.
Trick for finding roots
However, there is another way to compute roots of a polynomial under a finite field, which is an implementation trick in SageMath.
Let’s consider
Let’s return to the challenge. Theoretically,
However, we can’t simply define yp ** p - yp
, because it will complain that the exponent is huge. Instead, remember this GCD property: pow(yp, p, fp) - yp
.
So, this way we can get all possible roots of
sage: roots_p = fp.gcd(pow(yp, p, fp) - yp).roots(multiplicities=False)
sage: roots_q = fq.gcd(pow(yq, q, fq) - yq).roots(multiplicities=False)
Flag
At this point, we only need to consider each possible pair, compute the CRT and see if the result is the expected flag:
sage: from itertools import product
sage:
sage: for rp, rq in product(roots_p, roots_q):
....: if (data := long_to_bytes(crt([int(rp), int(rq)], [p, q]))).isascii():
....: print((b'maltactf{' + data + b'}').decode())
....:
maltactf{Ferm4ts_littl3_polyn0mial_tr1ck}