Zukane CTF

That one RSA challenge (TCP1P 2024)

Coppersmith small roots RSA
Challenge overview

In this CTF challenge, we are given the following python source code:

from Crypto.Util.number import *
from secret import flag
z = 567
p = getPrime(1024)
q = getPrime(1024)
n = p*q
c = pow(bytes_to_long(flag), 65537, n)
tot = (p-1) * (q-1)
d = int(pow(65537, -1, tot))
dinv = int(pow(d, -1, n))

h = int(dinv >> z)
hpq = (int((p+q)>> z))

with open('out.txt', 'w+') as f:
    f.write(f'{n=}\n')
    f.write(f'{h=}\n')
    f.write(f'{hpq=}\n')
    f.write(f'{c=}\n')

as well as the file out.txt We are given the typical n, e and c parameters, as well as two hints h and hpq.

Deriving the solution

We are given two hints for this RSA challenge; h and hpq.

These kinds of challenges can usually be solved using coppersmith lattice attacks. We have to derive some function $f$ that contains the hints with roots $x, y$. Solving for the roots, we can recover $d^{-1}\mod N$ and $p+q$.

for hpq, we can expand $\varphi(N)$ in the following equation:

\[\large\begin{aligned} e \cdot d &\equiv 1 \mod \varphi (N) \\ e \cdot d &= 1 + k \cdot (N + 1 - (p+q)) \\ d &= \frac{1 + k \cdot (N + 1 - (p+q))}{e} \end{aligned}\]

Here we can see the term $p+q$ appear after expanding $\varphi(N)$. We also find an equation for $d$. We can then substitute $d$ into the following equation:

\[\large\begin{aligned} d \cdot d^{-1} &\equiv 1 \mod N \\ d^{-1} \cdot d - 1 &\equiv 0 \mod N \\ d^{-1} \cdot\frac{1 + k \cdot (N + 1 - (p+q))}{e} - 1 &\equiv 0 \mod N \\ d^{-1} \cdot (1 + k \cdot (N + 1 - (p+q))) - e &\equiv 0 \mod N \end{aligned}\]

We can now substitute $p+q$ and $d^{-1}$ for our hints:

\[\large\begin{aligned} d^{-1} \cdot (1 + k \cdot (N + 1 - (p+q))) - e &\equiv 0 \mod N \\ ((h<<z) + x) \cdot (1 + k \cdot (N + 1 - ((hpq<<z)+y))) - e &\equiv 0 \mod N \end{aligned}\]

We can use this as our function $f$ and solve for the roots. We have the roots $k,x,y$ so we will have to use multivariate coppersmith.

For the bounds, we know $x$ and $y$ are both upper bounded by $2^z$, but $k$ is different. We can take into consideration this equation:

\[\large\begin{aligned} e \cdot d & \equiv1 \quad(\bmod \varphi(N)) \\ \Longrightarrow e \cdot d & =1 + k \cdot \varphi(N) \\ \Longrightarrow e \cdot d &\approx k \cdot \varphi(N) \end{aligned}\]

Since $d$ and $\varphi(N)$ have almost the same bit-size, it must mean that $e$ and $k$ also have almost the same bit-size. We can use $e$ as the upper bound for $k$.

Implementing the solution

Since this is a multivariate coppersmith problem, I will take in use the useful scripts from kiona’s git repo. Specifically, I will use the coppersmith_multivariate_heuristic function.

load('~/tools/coppersmith/kiona/coppersmith_multivariate_heuristic.py')

We can load in our values

n=13986357905153484822874300783445968480194277882812317554826224241536479785567487956712558237728345348661360577246137576216953724039680969623887884690471844396542763308129517234365819619617071449273126659007918716307793788623728052337632935762139796688014791419718949572448772521789488223910450877828732015095423443037519388747356327730350934152781671783952028215703864406564741666179193772037496984854699143314813242721157017296866888522135989818414587193505121794302821401677072507471357592358012342178011963104524959087968374300060349343826214249928530346877968114749229074874962737714935221065368318487049394644831
h=10474216468878927114435400909130676124750910912012236182806861194655854223324539867768381265996955193355030239325750528328250897464859373863289680002879536341349759323910048168674147097644573874679268018966497862685092382336865554114348153248267599439087357199554652601126191061921516650448119261614064051599968120061991607030873881013657693987836636730528537557619595799676312850875727477092697270452300532360780188724484703363561848754770976459
hpq=492124417091708682668644108145880307537308922842816506360717440112116492381514432506339907757228214359689270777951081610062506962769167209
c=4715651972688371479449666526727240348158670108161494767004202259402013317642418593561463200947908841531208327599049414587586292570298317049448560403558027904798159589477994992384199008976859139072407664659830448866472863679123027179516506312536814186903687358198847465706108667279355674105689763404474207340186200156662095468249081142604074167178023479657021133754055107459927667597604156397468414872149353231061997958301747136265344906296373544580143870450924707559398134384774201700278038470171319329716930036843839101955981274793386611943442507144153946307781795085665793554799349509983282980591388585613674226899
e = 65537
z = 567

and set up our function $f$ with roots $k,x,y$:

P.<k, x, y> = PolynomialRing(Zmod(n))
f = (1 + k*(n+1-((hpq<<z)+y)))*((h<<z)+x)-e

Using kiona’s multivariate heuristic function, we can recover the roots:

roots = coppersmith_multivariate_heuristic(f, (e, 2**z, 2**z), 1.0)

And lastly, we can take the modular inverse $d^{-1}$ to derive the private key $d$ and decrypt the ciphertext $c$

d = pow(((h<<z)+ roots[0][1]), -1, n)
print(bytes.fromhex(f'{pow(c, d, n):x}').decode())

This gives us our flag: TCP1P{AmEeeeeEE33333eeee333_T_T_8883938ef7571cc2}

solve.sage
load('~/tools/coppersmith/kiona/coppersmith_multivariate_heuristic.py')

n=13986357905153484822874300783445968480194277882812317554826224241536479785567487956712558237728345348661360577246137576216953724039680969623887884690471844396542763308129517234365819619617071449273126659007918716307793788623728052337632935762139796688014791419718949572448772521789488223910450877828732015095423443037519388747356327730350934152781671783952028215703864406564741666179193772037496984854699143314813242721157017296866888522135989818414587193505121794302821401677072507471357592358012342178011963104524959087968374300060349343826214249928530346877968114749229074874962737714935221065368318487049394644831
h=10474216468878927114435400909130676124750910912012236182806861194655854223324539867768381265996955193355030239325750528328250897464859373863289680002879536341349759323910048168674147097644573874679268018966497862685092382336865554114348153248267599439087357199554652601126191061921516650448119261614064051599968120061991607030873881013657693987836636730528537557619595799676312850875727477092697270452300532360780188724484703363561848754770976459
hpq=492124417091708682668644108145880307537308922842816506360717440112116492381514432506339907757228214359689270777951081610062506962769167209
c=4715651972688371479449666526727240348158670108161494767004202259402013317642418593561463200947908841531208327599049414587586292570298317049448560403558027904798159589477994992384199008976859139072407664659830448866472863679123027179516506312536814186903687358198847465706108667279355674105689763404474207340186200156662095468249081142604074167178023479657021133754055107459927667597604156397468414872149353231061997958301747136265344906296373544580143870450924707559398134384774201700278038470171319329716930036843839101955981274793386611943442507144153946307781795085665793554799349509983282980591388585613674226899
e = 65537
z = 567

P.<k, x, y> = PolynomialRing(Zmod(n))
f = (1 + k*(n+1-((hpq<<z)+y)))*((h<<z)+x)-e
roots = coppersmith_multivariate_heuristic(f, (e, 2**z, 2**z), 1.0)
d = pow(((h<<z)+ roots[0][1]), -1, n)
print(bytes.fromhex(f'{pow(c, d, n):x}').decode())