# Not Funny

Aperi'CTF 2019 - Cryptography (175 pts).

# Aperi’CTF 2019 - Not Funny

### Challenge details

Event Challenge Category Points Solves
Aperi’CTF 2019 Not Funny Cryptography 175 2

Votre voisin est un fan de cryptographie à tendance sadique. Il ne prend pas vos compétences en cryptanalyse au sérieux et a une dent contre vous depuis que vous avez été embauché dans l’entreprise où il avait postulé et pas lui.

Il vous a volé un document crucial pour votre avenir professionnel et vous a laissé une lettre de moqueries.

Fichiers : letter - md5sum: 2a00764510e267d601331dad192b45d4

### TL;DR

This is a relaxed model of RSA that was broken by Coppersmith and Howgrave-Graham.

### Methodology

Based on the information in the description of the challenge, the MSB of p are known.

This is a relaxed model of RSA that was broken by Coppersmith and Howgrave-Graham.

There is a github page that provides a sage implementation of this attack.

Full script available here

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# This file was *autogenerated* from the file rsaQuarterRecovery.sage
from sage.all_cmdline import *   # import sage library

_sage_const_3 = Integer(3); _sage_const_2 = Integer(2); _sage_const_1 = Integer(1); _sage_const_0 = Integer(0); _sage_const_7 = Integer(7); _sage_const_4 = Integer(4); _sage_const_1024 = Integer(1024); _sage_const_200 = Integer(200); _sage_const_0p5 = RealNumber('0.5')
import time, sys

debug = False

def ntos(x):
n = hex(x)[2:].rstrip("L")
if len(n)%2 != 0:
n = "0"+n
return n.decode("hex")

# display matrix picture with 0 and X
def matrix_overview(BB, bound):
for ii in range(BB.dimensions()[_sage_const_0 ]):
a = ('%02d ' % ii)
for jj in range(BB.dimensions()[_sage_const_1 ]):
a += '0' if BB[ii,jj] == _sage_const_0  else 'X'
a += ' '
if BB[ii, ii] >= bound:
a += '~'
print a

def coppersmith_howgrave_univariate(pol, modulus, beta, mm, tt, XX):
"""
Coppersmith revisited by Howgrave-Graham

finds a solution if:
* b|modulus, b >= modulus^beta , 0 < beta <= 1
* |x| < XX
"""
#
# init
#
dd = pol.degree()
nn = dd * mm + tt

#
# checks
#
if not _sage_const_0  < beta <= _sage_const_1 :
raise ValueError("beta should belongs in (0, 1]")

if not pol.is_monic():
raise ArithmeticError("Polynomial must be monic.")

#
# calculate bounds and display them
#
"""
* we want to find g(x) such that ||g(xX)|| <= b^m / sqrt(n)

* we know LLL will give us a short vector v such that:
||v|| <= 2^((n - 1)/4) * det(L)^(1/n)

* we will use that vector as a coefficient vector for our g(x)

* so we want to satisfy:
2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n)

so we can obtain ||v|| < N^(beta*m) / sqrt(n) <= b^m / sqrt(n)
(it's important to use N because we might not know b)
"""
if debug:
# t optimized?
print "\n# Optimized t?\n"
print "we want X^(n-1) < N^(beta*m) so that each vector is helpful"
cond1 = RR(XX**(nn-_sage_const_1 ))
print "* X^(n-1) = ", cond1
cond2 = pow(modulus, beta*mm)
print "* N^(beta*m) = ", cond2
print "* X^(n-1) < N^(beta*m) \n-> GOOD" if cond1 < cond2 else "* X^(n-1) >= N^(beta*m) \n-> NOT GOOD"

# bound for X
print "\n# X bound respected?\n"
print "we want X <= N^(((2*beta*m)/(n-1)) - ((delta*m*(m+1))/(n*(n-1)))) / 2 = M"
print "* X =", XX
cond2 = RR(modulus**(((_sage_const_2 *beta*mm)/(nn-_sage_const_1 )) - ((dd*mm*(mm+_sage_const_1 ))/(nn*(nn-_sage_const_1 )))) / _sage_const_2 )
print "* M =", cond2
print "* X <= M \n-> GOOD" if XX <= cond2 else "* X > M \n-> NOT GOOD"

# solution possible?
print "\n# Solutions possible?\n"
detL = RR(modulus**(dd * mm * (mm + _sage_const_1 ) / _sage_const_2 ) * XX**(nn * (nn - _sage_const_1 ) / _sage_const_2 ))
print "we can find a solution if 2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n)"
cond1 = RR(_sage_const_2 **((nn - _sage_const_1 )/_sage_const_4 ) * detL**(_sage_const_1 /nn))
print "* 2^((n - 1)/4) * det(L)^(1/n) = ", cond1
cond2 = RR(modulus**(beta*mm) / sqrt(nn))
print "* N^(beta*m) / sqrt(n) = ", cond2
print "* 2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n) \n-> SOLUTION WILL BE FOUND" if cond1 < cond2 else "* 2^((n - 1)/4) * det(L)^(1/n) >= N^(beta*m) / sqroot(n) \n-> NO SOLUTIONS MIGHT BE FOUND (but we never know)"

print "\n# Note that no solutions will be found _for sure_ if you don't respect:\n* |root| < X \n* b >= modulus^beta\n"

#
# Coppersmith revisited algo for univariate
#

# change ring of pol and x
polZ = pol.change_ring(ZZ)
x = polZ.parent().gen()

# compute polynomials
gg = []
for ii in range(mm):
for jj in range(dd):
gg.append((x * XX)**jj * modulus**(mm - ii) * polZ(x * XX)**ii)
for ii in range(tt):
gg.append((x * XX)**ii * polZ(x * XX)**mm)

# construct lattice B
BB = Matrix(ZZ, nn)

for ii in range(nn):
for jj in range(ii+_sage_const_1 ):
BB[ii, jj] = gg[ii][jj]

# display basis matrix
if debug:
matrix_overview(BB, modulus**mm)

# LLL
BB = BB.LLL()

# transform shortest vector in polynomial
new_pol = _sage_const_0
for ii in range(nn):
new_pol += x**ii * BB[_sage_const_0 , ii] / XX**ii

# factor polynomial
potential_roots = new_pol.roots()
# print "potential roots:", potential_roots

# test roots
roots = []
for root in potential_roots:
if root[_sage_const_0 ].is_integer():
result = polZ(ZZ(root[_sage_const_0 ]))
if gcd(modulus, result) >= modulus**beta:
roots.append(ZZ(root[_sage_const_0 ]))

#
return roots

if __name__ == "__main__":
# when partially know p or q
N = Integer(21475596280939174139431514855487916848280293757877221629494000423065740039343165568040911365501931709899474457901218636286613252968091838444984682784515413643636881148238753087604560665820935690301341977536386047275665017274334379698691769474381442859960487730730928509631882014534379721535881772034345790413578890481396738792233363159809423111473169292217241145616333232083518177326358298078048628120127179798565879267980968193362945481320953732076711233595860392612661434058587237963218791159823174473621469045407472255263769629994834663464420068453008215132214063751047917187048914808372378795396554061907744836817)
e = 65537
c = 20754532737949147742223373902498179634482610091973401913965157562215452429498008912598985786021744450617885408505714822236352995311810091314580467491358612250746471365079581324215426926430950841351364153354904788124987738423489564283310714390723281967378568516974010529197725382065316878282378564285294967866976464084855095285979327325380990150508854537668589744102474681630473341864971730762504436287798866508217036746708084200623440756971882983470032827345378642632637403720063632023394983904708937936062655036947907107788458010769639935212088808550212043364417102917046970788178629455261301698308788491373995670906

qbar = Integer(136032115030209730841795989556533149289798564864039724005919755642131398781579565419215111294655920896216631707218214703673379732583990633646915047476669276370539105011413159892850208470199569891528387660241874080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000)

length_N = len(N.digits(2))

F = PolynomialRing(Zmod(N), implementation='NTL', names=('x',)); (x,) = F._first_ngens(1);
pol = x + qbar
dd = pol.degree()

# PLAY WITH THOSE:
beta = RealNumber('0.4')                             # we should have q >= N^beta
epsilon = beta / _sage_const_7                      # <= beta/7
mm = ceil(beta**_sage_const_2  / (dd * epsilon))    # optimized
tt = floor(dd * mm * ((_sage_const_1 /beta) - _sage_const_1 ))   # optimized
XX = ceil(N**((beta**_sage_const_2 /dd) - epsilon)) # we should have |diff| < X

# Coppersmith
start_time = time.time()
roots = coppersmith_howgrave_univariate(pol, N, beta, mm, tt, XX)

# output
if len(roots) >= 1:
r = roots[0]
q = qbar + int(r)
p = N/q
o = (p-1)*(q-1)
d = inverse_mod(e,o)
print ntos(int(pow(c,d,N)))


### Flag

APRK{c0pP3rSm1th_H0wgr4v3_b1g_fl4G_suCh_sk1lls_mucH_R3w4rd1ng}

ENOENT