In this CTF challenge we are given an enc.py
file:
from Crypto.Util.number import *
p = getPrime(1024)
bits = 128
m = bytes_to_long(b"REDACTED")
hints = [pow(m , -1 , p) , pow(m+1 , -2 , p)]
hints_leak = [(i>>bits)<<bits for i in hints]
print(f'p = {p}')
print(f'hints_leak = {hints_leak}')
as well as the output p
and hints_leak
in the file out.txt
p = 117593624298425786343779158012820875154822864368382625245527483403432934003483945150470206407456758951269631159296406949363530801144116051024607996020606008637719420473508584102759537549869268380832507998189573147118724711583890139172725884196595640384171883519174624232176171861648257367040001679671930516257
hints_leak = [29532884859848451807856040503801489793449597914559835640013346371615282769039782729995651472190910037139963402884437232479340276830952204736162501040446353868183083550897609990419665664218203589490798227152745073916743432546774880541751765375202866498878181362239845800024263833214003957243156923484070739968, 2240800030522719831440690213801032993267721517756450944809696773586000818511688287641493847808933201477652660185925436211555966348047610258375098042072112054000315861147846986256701531141306392153787106580833282665986451952386428424060514960239609554280495803294023792016130151761105191792899173791341477888]
In enc.py
, the flag m
is turned into bytes and then into a long. The script then generates two hints for us:
These hints are then shifted 128 bits, then shifted back. This essentially zeroes out the lower 128 bits for both hints.
We are then given these hints_leak
values along with p
Our goal is to use these hints to recover m
Since we are missing the lower bits, this seems like a classic coppersmith challenge. We can represent hint1
and hint2
as hint1_leak + x
and hint2_leak + y
We can rewrite the hint1
and hint2
equations to isolate m like so:
and
\[\large\begin{aligned} \text{Hint2} &= (m+1)^{-2} &\mod p \\ Hint2\_leak + y &= (m+1)^{-2} &\mod p \\ (Hint2\_leak + y)^{-2} &= m+1 &\mod p \\ (Hint2\_leak + y)^{-2} - 1 &= m &\mod p \\ \end{aligned}\]Since both are equal to m, we can do one minus the other to get a zero polynomial. We begin by denoting the hints as A
and B
Which finally gives us:
\[\large f = (H 2 \_l e a k+y) \cdot(H 1 \_l e a k+x+1)^2-(H 1 \_l e a k+x)^2 \equiv 0 \quad \bmod p\]We can now use this polynomial $f$ and use bivariate coppersmith’s theorem to solve for the roots x and y. With x and y, we can reconstruct hint1
, compute the modular inverse, and we will have m!
We first of all define our values $p,\; hint1\_leak,\; hint2\_leak$ from the challenge source code:
p = 117593624298425786343779158012820875154822864368382625245527483403432934003483945150470206407456758951269631159296406949363530801144116051024607996020606008637719420473508584102759537549869268380832507998189573147118724711583890139172725884196595640384171883519174624232176171861648257367040001679671930516257
hint1_leak = 29532884859848451807856040503801489793449597914559835640013346371615282769039782729995651472190910037139963402884437232479340276830952204736162501040446353868183083550897609990419665664218203589490798227152745073916743432546774880541751765375202866498878181362239845800024263833214003957243156923484070739968
hint2_leak = 2240800030522719831440690213801032993267721517756450944809696773586000818511688287641493847808933201477652660185925436211555966348047610258375098042072112054000315861147846986256701531141306392153787106580833282665986451952386428424060514960239609554280495803294023792016130151761105191792899173791341477888
Then, before we proceed, we need to find a suitable algorithm for finding the roots. I will utilize the small_roots.sage
script from the following repository: https://github.com/josephsurin/lattice-based-cryptanalysis
The function small_roots
requires a function $f$, an upper bound for the roots, a specified algorithm, and some other values $m$ and $d$.
We can define our function $f$ over the integers:
P.<x, y> = PolynomialRing(ZZ)
f = (hint2_leak + y) * (hint1_leak + x + 1)**2 - (hint1_leak + x)**2
We know x
and y
are less than 128 bits, meaning our upper bound for the roots are $2^{128}$
bounds = (2**128, 2**128)
for the specified algorithm, the small_roots
function supports the groebner
, msolve
, resultants
, and jacobian
algorithms. Generally speaking, the resultants
algorithm is the best for bivariate problems.
We can also optionally specify a lattice_reduction
algorithm. I choose to use flatter
from the same repo. In addition to this, we change the ring of $f$ to Zmod(p)
because the function is congruent to 0 mod p.
From here, we just need to tweak the values m
and d
:
roots = small_roots(f.change_ring(Zmod(p)), bounds, m=7, d=6, algorithm="resultants", lattice_reduction=flatter, verbose=True)
And after finding the roots, we can change the function $f$ back to the ring of integers, retrieve x
to recover hint1
, and calculate the modular inverse to find m!
f.change_ring(ZZ)
solx = ZZ(roots[0][0])
soly = ZZ(roots[0][1])
invmod_leak = hint1_leak + solx
m = invmod_leak ^ -1 % p
print(bytes.fromhex(f'{m:x}'))
After converting from long to hex, we get our flag:
b'H7CTF{thx_for_finding!!}'
Note, this script takes a couple of minutes to run. This is because m
and d
are relatively high, but it is needed to recover the roots.
load('~/tools/coppersmith/lbc/lattice-based-cryptanalysis/lbc_toolkit/problems/small_roots.sage')
load('~/tools/coppersmith/lbc/lattice-based-cryptanalysis/lbc_toolkit/common/flatter.sage')
load('~/tools/coppersmith/lbc/lattice-based-cryptanalysis/lbc_toolkit/common/systems_solvers.sage')
# Given values from out.txt
p = 117593624298425786343779158012820875154822864368382625245527483403432934003483945150470206407456758951269631159296406949363530801144116051024607996020606008637719420473508584102759537549869268380832507998189573147118724711583890139172725884196595640384171883519174624232176171861648257367040001679671930516257
hint1_leak = 29532884859848451807856040503801489793449597914559835640013346371615282769039782729995651472190910037139963402884437232479340276830952204736162501040446353868183083550897609990419665664218203589490798227152745073916743432546774880541751765375202866498878181362239845800024263833214003957243156923484070739968
hint2_leak = 2240800030522719831440690213801032993267721517756450944809696773586000818511688287641493847808933201477652660185925436211555966348047610258375098042072112054000315861147846986256701531141306392153787106580833282665986451952386428424060514960239609554280495803294023792016130151761105191792899173791341477888
bounds = (2**128, 2**128)
# Define the polynomial ring
P.<x, y> = PolynomialRing(ZZ)
f = (hint2_leak + y) * (hint1_leak + x + 1)**2 - (hint1_leak + x)**2
roots = small_roots(f.change_ring(Zmod(p)), bounds, m=7, d=6, algorithm="resultants", lattice_reduction=flatter, verbose=True)
f.change_ring(ZZ)
solx = ZZ(roots[0][0])
soly = ZZ(roots[0][1])
invmod_leak = hint1_leak + solx
m = invmod_leak ^ -1 % p
print(bytes.fromhex(f'{m:x}'))