Try   HackMD

HITCON CTF 2022 - Easy NTRU

prob.sage

from Crypto.Util.number import bytes_to_long as b2l from Crypto.Util.number import long_to_bytes as l2b import random Zx.<x> = ZZ[] def convolution(f,g): return (f * g) % (x^n-1) def balancedmod(f,q): g = list(((f[i] + q//2) % q) - q//2 for i in range(n)) return Zx(g) % (x^n-1) def randomdpoly(d1, d2): result = d1*[1]+d2*[-1]+(n-d1-d2)*[0] random.shuffle(result) return Zx(result) def invertmodprime(f,p): T = Zx.change_ring(Integers(p)).quotient(x^n-1) return Zx(lift(1 / T(f))) def invertmodpowerof2(f,q): assert q.is_power_of(2) g = invertmodprime(f,2) while True: r = balancedmod(convolution(g,f),q) if r == 1: return g g = balancedmod(convolution(g,2 - r),q) def keypair(): while True: try: f = randomdpoly(61, 60) f3 = invertmodprime(f,3) fq = invertmodpowerof2(f,q) break except Exception as e: pass g = randomdpoly(20, 20) publickey = balancedmod(3 * convolution(fq,g),q) secretkey = f return publickey, secretkey, g def encode(val): poly = 0 for i in range(n): poly += ((val%3)-1) * (x^i) val //= 3 return poly def encrypt(message, publickey): r = randomdpoly(18, 18) return balancedmod(convolution(publickey,r) + encode(message), q) n, q = 263, 128 publickey, _, _ = keypair() flag = open('flag', 'rb').read() print(publickey) for i in range(24): print(encrypt(b2l(flag), publickey))

NTRU Cryptosystem

The question is implementing the NTRU Cryptosystem, and it encrypts the flag with the same public-key 24 times.

NTRUEncrypt procress is described here: https://en.wikipedia.org/wiki/NTRUEncrypt. And the encryptions are generated as such:

ei=hri+mmodq

This equation is under the Quotient Ring:

Z128[X]/(X2631).
h
is the public key,
m
is the message (in this case the flag) and
ri
is a randomly generated polynomial with coeffcients in the set
{1,0,1}
.

Retransmission Attack

According this pdf: https://ntru.org/f/tr/tr006v1.pdf, if you encrypt the same message more than once with the same public key, then NTRU is vulnerable to the retransmission attack.

Using the differences in the encryptions, once can recover each individual term of the random polynomial

r0, which once fully recovered we can solve for
m
.

In other words if we take the difference between every encryption and the first one:

eie0=h(rir0)modqh1(eie0)=rir0modq

As long as we can calculate

h1(eie0), we will reveal the value of
rir0
, and with enough of these values we can recover the entirety of
r0
.

Calculating the public-key inverse

Unforunately the question isn't that easy, because

h1 isn't defined in the Quotient Ring. The reason being because if you try to execute (from code given) invertmodpowerof2(h), it will throw an exception saying that an inverse doesn't exist in
Z2[X]/(X2631)
.

The way the code attempts to calculate an inverse in

Z128[X]/(X2631) is by first calculating the inverse in
Z2[X]/(X2631)
and then using a reformation of Newton's formula for Hensel lifting to "lift" the inverse from
Z2
to
Z128
. So the code fails to find an inverse in
Z2
, it obviously doesn't exist in
Z128
.

The reason the inverse doesn't exist is because

X2631=(X1)(X262+X261+X260++X2+X+1)
In
Z2
,
1=1
, so the factorization is really
(X+1)(X262+X261+X260++X2+X+1)
.

If you factor the public key under

Z2[X], you will see that it also contains a
(X+1)
factor, meaning that it shares a common factor with the modulus
X2631
, and that's why an inverse doesn't exist.

However, that doesn't mean we can't solve for a "psuedo-inverse".

The modulus

X2631 isn't irreducible under
Z2[X]
, it decomposes into 3 factors. This means we decompose the Quotient Ring into the direct product of 3 Quotient Rings, where
(X1)P2P3=X2631
.

Z2[X]/(X2631)Z2[X]/(X1)×Z2[X]/(P2)×Z2[X]/(P3)

Now we can take the equation

eie0=h(rir0)modq

And solve for

rir0 over each of the individual Quotient Rings in
P1,P2
, and
P3
, and then getting a solution in
Z2[X]/(X2631)
using the Chinese Remainder Theorem.

Then we can use a Hensel's lifting method to lift the solution to

Z128[X]/(X2631).

We actually only need to lift twice to

Z8[X]/(X2631), since we know the coefficients are in the set
{2,1,0,1,2}
.

The good news is that

h1 is well defined in
Z2[X]/(P3)
and
Z2[X]/(P2)
. However it doesn't exist in
Z2[X]/(X1)
(for the reasons described above).

We don't need to know the inverse to solve for what we need, though. The quotient ring

Z2[X]/(X1) is isomorphic to
Z2
, because the only values possible are
0
and
1
. Going back to our original equation, we basically have the following under that quotient ring:

eie0=h(rir0)mod20=0(rir0)

This means that

rir0 is either
0
or
1
in
Z2[X]/(X1)
, so we can try both values when we calculate the CRT value, and get 2 choices for
rir0
in
Z2[X]/(X2631)
.

Onto Hensel lifting.

Lifting from
Z2
to
Z128

Using a techinique similar to what was described here: https://math.stackexchange.com/questions/448946/lifting-linear-solutions-mod-p-to-mod-p2, we can do the following.

Using the above process, we can solve for

rir0 in the equation below:
eie0=h(rir0)mod2

To lift our solution for

rir0 from
mod2
to
mod4
, let
ci
be the solution to
rir0mod2
.
eie0=hcimod2

Now we subsitute

rir0=ci+2di:

eie0=h(ci+2di)mod4eie0hci=2hdimod4
We know that
eie0hci=0mod2
, so we can divide both sides of the equation by
2
.

eie0hci2=hdimod2h1eie0hci2=dimod2

We can use the exact same process we did earlier to solve for

dimod2, as our definition
h1
hasn't changed. We still solve for
di
over the 3 Quotient Rings, and for the
Z2[X]/(X1)
ring we just assume the solution is either
0
or
1
.

After solving for

di, we lift to
mod8
. Let
rir0=ci+2di+4fi
.
eie0=h(ci+2di+4fi)mod8eie0hci2hdi4=hfimod8h1eie0hci2hdi4=fimod8

And now we have

rir0=cd+2di+4fimod8.

So in total we get 2 solutions in

mod2, 4 in
mod4
, and 8 in
mod8
. Only one of them is correct for the value of
rir0
, and it's very easy to check since the amount of terms will be very small compared to the rest.

Once we solve for

rir0 we can proceed with the rest of the retransmission attack as described in the PDF.

PoC

from Crypto.Util.number import bytes_to_long as b2l from Crypto.Util.number import long_to_bytes as l2b import random Zx.<x> = ZZ[] def convolution(f,g): return (f * g) % (x^n-1) def balancedmod(f,q): g = list(((f[i] + q//2) % q) - q//2 for i in range(n)) return Zx(g) % (x^n-1) def randomdpoly(d1, d2): result = d1*[1]+d2*[-1]+(n-d1-d2)*[0] random.shuffle(result) return Zx(result) def invertmodprime(f,p): T = Zx.change_ring(Integers(p)).quotient(x^n-1) return Zx(lift(1 / T(f))) def invertmodpowerof2(f,q): assert q.is_power_of(2) g = invertmodprime(f,2) while True: r = balancedmod(convolution(g,f),q) if r == 1: return g g = balancedmod(convolution(g,2 - r),q) def keypair(): while True: try: f = randomdpoly(61, 60) f3 = invertmodprime(f,3) fq = invertmodpowerof2(f,q) break except Exception as e: pass g = randomdpoly(20, 20) publickey = balancedmod(3 * convolution(fq,g),q) secretkey = f return publickey, secretkey, g def encode(val): poly = 0 for i in range(n): poly += ((val%3)-1) * (x^i) val //= 3 return poly def encrypt(message, publickey): r = randomdpoly(18, 18) return r, balancedmod(convolution(publickey,r) + encode(message), q) n, q = 263, 128 publickey, _, _ = keypair() flag = b'HITCON{test_flag_123141241}' ris = [] eis = [] for _ in range(24): rr, ee = encrypt(b2l(flag), publickey) ris.append(rr) eis.append(ee) h = publickey def balancedmod_new(f,q,quo): g = list(((f[i] + q//2) % q) - q//2 for i in range(n)) return Zx(g) % quo def convolution_new(f,g,quo): return (f * g) % quo def invertmodprime_new(f,p,quo): T = Zx.change_ring(Integers(p)).quotient(quo) return Zx(lift(1 / T(f))) def invertmodpowerof2_new(f,q,quo): #print(f, q, quo) assert q.is_power_of(2) g = invertmodprime_new(f,2,quo) #print("g:", g) while True: r = balancedmod_new(convolution_new(g,f,quo),q,quo) if r == 1: return g g = balancedmod_new(convolution_new(g,2 - r,quo),q,quo) factors = [x^131 + x^129 + x^126 + x^123 + x^122 + x^118 + x^117 + x^116 + x^115 + x^114 + x^111 + x^109 + x^108 + x^107 + x^106 + x^104 + x^103 + x^102 + x^98 + x^97 + x^96 + x^95 + x^94 + x^93 + x^90 + x^89 + x^88 + x^86 + x^82 + x^81 + x^80 + x^78 + x^77 + x^76 + x^75 + x^73 + x^72 + x^71 + x^70 + x^67 + x^66 + x^63 + x^62 + x^61 + x^60 + x^59 + x^58 + x^57 + x^56 + x^53 + x^51 + x^48 + x^46 + x^39 + x^35 + x^32 + x^30 + x^29 + x^28 + x^25 + x^24 + x^22 + x^21 + x^19 + x^18 + x^17 + x^16 + x^15 + x^14 + x^13 + x^11 + x^10 + x^9 + x^8 + x^7 + x^6 + x^4 + x + 1, x^131 + x^130 + x^127 + x^125 + x^124 + x^123 + x^122 + x^121 + x^120 + x^118 + x^117 + x^116 + x^115 + x^114 + x^113 + x^112 + x^110 + x^109 + x^107 + x^106 + x^103 + x^102 + x^101 + x^99 + x^96 + x^92 + x^85 + x^83 + x^80 + x^78 + x^75 + x^74 + x^73 + x^72 + x^71 + x^70 + x^69 + x^68 + x^65 + x^64 + x^61 + x^60 + x^59 + x^58 + x^56 + x^55 + x^54 + x^53 + x^51 + x^50 + x^49 + x^45 + x^43 + x^42 + x^41 + x^38 + x^37 + x^36 + x^35 + x^34 + x^33 + x^29 + x^28 + x^27 + x^25 + x^24 + x^23 + x^22 + x^20 + x^17 + x^16 + x^15 + x^14 + x^13 + x^9 + x^8 + x^5 + x^2 + 1] moduli = [] residues = [] for factor in factors: Ttmp = Zx.change_ring(Integers(2)).quotient(factor) h_part = Ttmp(h) moduli.append(Ttmp.modulus()) residues.append(lift(Ttmp(invertmodprime_new(h, 2, factor))*Ttmp(eis[1] - eis[0]))) residues.append(0) moduli.append(x + 1) ci_cands = [] ci_cands.append(Zx(crt(residues, moduli))) # r_1 - r_0 in Z2/<x^n - 1> # r_1 - r_0 can be either 0 or 1 mod (x + 1) we don't know residues[-1] = 1 ci_cands.append(Zx(crt(residues, moduli))) # now we lift from Z2 to Z4 then Z4 to Z8 #print("guess mod 2:", balancedmod(ci_cands[0], 2)) #print("guess mod 2:", balancedmod(ci_cands[1], 2)) #print("correct:", balancedmod(ris[1] - ris[0], 2)) cidi_cands = [] for ci_cand in ci_cands: xx3 = balancedmod(eis[1] - eis[0] - balancedmod(convolution(h, ci_cand), 4), 4)//2 moduli = [] residues = [] for factor in factors: Ttmp = Zx.change_ring(Integers(2)).quotient(factor) h_part = Ttmp(h) moduli.append(Ttmp.modulus()) residues.append(lift(Ttmp(invertmodprime_new(h, 2, factor))*Ttmp(xx3))) residues.append(0) moduli.append(x + 1) cidi_cands.append((ci_cand, Zx(crt(residues, moduli)))) residues[-1] = 1 cidi_cands.append((ci_cand, Zx(crt(residues, moduli)))) #for ci_cand, di_cand in cidi_cands: # print("guess mod 4:", balancedmod(ci_cand + 2*di_cand, 4)) #print("correct:", balancedmod(ris[1] - ris[0], 4)) cidifi_cands = [] for ci_cand, di_cand in cidi_cands: xx5 = balancedmod(eis[1] - eis[0] - balancedmod(convolution(h, ci_cand), 8) - balancedmod(convolution(h, 2*di_cand), 8), 8) // 4 moduli = [] residues = [] for factor in factors: Ttmp = Zx.change_ring(Integers(2)).quotient(factor) h_part = Ttmp(h) moduli.append(Ttmp.modulus()) residues.append(lift(Ttmp(invertmodprime_new(h, 2, factor))*Ttmp(xx5))) residues.append(0) moduli.append(x + 1) cidifi_cands.append((ci_cand, di_cand, Zx(crt(residues, moduli)))) residues[-1] = 1 cidifi_cands.append((ci_cand, di_cand, Zx(crt(residues, moduli)))) for ci_cand, di_cand, fi_cand in cidifi_cands: print("guess mod 8:", balancedmod(ci_cand + 2*di_cand + 4*fi_cand, 8)) print("correct:", balancedmod(ris[1] - ris[0], q))