bi0sCTF 2024

#CTF-Writeup

Table of Contents

Last weekend, I played bi0sCTF with my team @1337%Yogurt. Here is my writeup for all crypto challenges.

My first CTF writeup in 2024!

lalala

chall.sage

from random import randint
from re import search

flag = "bi0sctf{%s}" % f"{randint(2**39, 2**40):x}"

p = random_prime(2**1024)
unknowns = [randint(0, 2**32) for _ in range(10)]
unknowns = [f + i - (i%1000)  for i, f in zip(unknowns, search("{(.*)}", flag).group(1).encode())]

output = []
for _ in range(100):
    aa = [randint(0, 2**1024) for _ in range(1000)]
    bb = [randint(0, 9) for _ in range(1000)]
    cc = [randint(0, 9) for _ in range(1000)]
    output.append(aa)
    output.append(bb)
    output.append(cc)
    output.append(sum([a + unknowns[b]^2 * unknowns[c]^3 for a, b, c in zip(aa, bb, cc)]) % p)

print(f"{p = }")
print(f"{output = }")

In this challenge, flag’s characters are hidden in $unknowns$ variables, so we need to find it.

Lets take a look at this line:

output.append(sum([a + unknowns[b]^2 * unknowns[c]^3 for a, b, c in zip(aa, bb, cc)]) % p)

We can get lots of equations contain only $unknowns_i^2 * unknowns_j^3$ by subtract $a$ from this. Then we will treat $unknowns_i^2 * unknowns_j^3$ as a variable and solve systems of linear equations. After that, we can find $unknown_i$ from $unknowns_i^2 * unknowns_i^3$ by calculate 5th-roots. When we have $unknowns_i$, we can find flag !

from Crypto.Util.number import *
from out import output, p
from tqdm import *

aas = []
bbs = []
ccs = []
ss = []

for i in range(0, 400, 4):
    out = output[i:i+4]
    aa, bb, cc, s = out   
    aas.append(aa)
    bbs.append(bb)
    ccs.append(cc)
    ss.append(s)

remains = [(s - sum(aa)) % p for s, aa in zip(ss, aas)]

#(s[0]^2*s[0]^3, s[0]^2*s[1]^3,..) s[i]^2*s[j]^3, i <= j
mt = []

for bb, cc in zip(bbs, ccs):
    equation = [0 for _ in range(100)]
    for b, c in zip(bb, cc):
        idx = 10 * b + c
        equation[idx] += 1
    mt.append(equation)

from sage.all import *

M = Matrix(mt)
v = vector(remains)

ans = list(M.solve_right(v))

flag = []
for i in range(100):
    try:
        u = ans[i].nth_root(5)
        flag.append(u)
        print(i)
    except:
        continue

print("".join([bytes([c%1000]).decode() for c in flag]))

rr

chall.py

from Crypto.Util.number import *
from FLAG import flag
from random import randint

flag = bytes_to_long(flag)
n = ..
rr = ..
ks = [randint(0, rr**(i+1)) for i in range(20)]
c1 = pow(sum(k*flag**i for i, k in enumerate(ks)), (1<<7)-1, n)
c2 = pow(flag, (1<<16)+1, n)
ks = [pow(69, k, rr**(i+2)) for i, k in enumerate(ks)]
print(f"{ks = }")
print(f"{c1 = }")
print(f"{c2 = }")

Recover coefficients

To recover coefficient $coeff_i$, we need to solve this discrete logarithm problem:

$$ 69^{coeff_i} = ks_i \mod rr^{i+2} $$

After google, I found this link and I will explain what I did

The main idea to solve are the isomormophism

$$\mathbb{Z}^\ast _{p^s} \cong \mathbb{Z}^\ast _{p^{s-1}} \times \mathbb{Z}^\ast _{p-1}$$

and the homomorphism

$$\theta: \mathbb{Z}^\ast _{p^s} \to \mathbb{Z}^+ _{p^{s-1}}$$

where $\theta(k) = \left[ \frac{k^{(p-1)p^{s-1}} - 1}{p^s} \right] \bmod p^{s-1}$, and note that we compute numerator inside the bracket modulo $p^{2s-1}$

Now, because $coeff_i < rr^{i + 1}$, so we only need to calculate discrele logarithm over $\mathbb{Z}^\ast _{p^{s-1}}$. To compute discrete logarithm, we use the homomorphism $\theta$ and our problem will become

$$coeff_i * \theta(69) = \theta(ks_i) \mod rr^{i+1}$$

And we can find $coeff_i$ easily !

def omega(x, p, e):
    numerator = (pow(x, (p - 1) * p**(e-1), p**(2 * e - 1)) - 1) % p**(2 * e - 1)
    denominator = pow(p, e)
    ans = (numerator // denominator) % p**(e-1)
    return ans

coeffs = []
for i, k in enumerate(ks):
    base = omega(69, rr, i+2)
    target = omega(k, rr, i+2)
    coeff = target * inverse_mod(base, rr**(i + 1)) % rr**(i+1)
    coeffs.append(coeffs)

Finding flag

After recovered all coefficients, now we can construct two polynomials:

$$ f(x) = (\sum_{i=0}^{19}coeff_i * x^i)^{127} - c_1 \mod n \newline g(x) = x^{65537} - c_2 \mod n $$

We can see both $f(x)$ and $g(x)$ have the same root $flag$, it means that they have the same factor $(x - flag)$, and we can calculate $GCD(f(x), g(x))$ to get this factor. To speed up, I use half-GCD, which was implemented at jvdsn’s repository

Script

from sage.all import *
from Crypto.Util.number import *
from data import n, rr, ks, c1, c2
import sys
sys.path.append("../../../Tools/crypto-attacks") #https://github.com/jvdsn/crypto-attacks
from shared.polynomial import fast_polynomial_gcd
import logging

logging.basicConfig(level=logging.DEBUG)

#https://math.stackexchange.com/questions/1863037/discrete-logarithm-modulo-powers-of-a-small-prime
def omega(x, p, e):
    numerator = (pow(x, (p - 1) * p**(e-1), p**(2 * e - 1)) - 1) % p**(2 * e - 1)
    denominator = pow(p, e)
    ans = (numerator // denominator) % p**(e-1)
    return ans

coeff = []
for i, k in enumerate(ks):
    base = omega(69, rr, i+2)
    target = omega(k, rr, i+2)
    kk = target * inverse_mod(base, rr**(i + 1)) % rr**(i+1)
    coeff.append(kk)

x = PolynomialRing(Zmod(n), 'x').gen()

f1 = 0
for i, k in enumerate(coeff):
    f1 += k*x**i

f1 = f1**127 - c1
f2 = x**65537 - c2
h = fast_polynomial_gcd(f1, f2)
m = -h[0] / h[1]
print(long_to_bytes(int(m)))

Challengename

chall.py

from ecdsa.ecdsa import Public_key, Private_key
from ecdsa import ellipticcurve
from hashlib import md5
import random
import os
import json

flag = open("flag", "rb").read()[:-1]

magic = os.urandom(16)

p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
a = ###REDACTED###
b = ###REDACTED###
G = ###REDACTED###

q = G.order()

def bigsur(a,b):
    a,b = [[a,b],[b,a]][len(a) < len(b)]
    return bytes([i ^ j for i,j in zip(a,bytes([int(bin(int(b.hex(),16))[2:].zfill(len(f'{int(a.hex(), 16):b}'))[:len(a) - len(b)] + bin(int(b.hex(),16))[2:].zfill(len(bin(int(a.hex(), 16))[2:]))[:len(bin(int(a.hex(), 16))[2:]) - len(bin(int(b.hex(), 16))[2:])][i:i+8], 2) for i in range(0,len(bin(int(a.hex(), 16))[2:]) - len(bin(int(b.hex(), 16))[2:]),8)]) + b)])

def bytes_to_long(s):
    return int.from_bytes(s, 'big')

def genkeys():
    d = random.randint(1,q-1)
    pubkey = Public_key(G, d*G)
    return pubkey, Private_key(pubkey, d)

def sign(msg,nonce,privkey):
    hsh = md5(msg).digest()
    nunce = md5(bigsur(nonce,magic)).digest()
    sig = privkey.sign(bytes_to_long(hsh), bytes_to_long(nunce))
    return json.dumps({"msg": msg.hex(), "r": hex(sig.r), "s": hex(sig.s)})

def enc(privkey):
    x = int(flag.hex(),16)
    y = pow((x**3 + a*x + b) % p, (p+3)//4, p)
    F = ellipticcurve.Point('--REDACTED--', x, y)
    Q = F * privkey.secret_multiplier
    return (int(Q.x()), int(Q.y()))

pubkey, privkey = genkeys()
print("Public key:",(int(pubkey.point.x()),int(pubkey.point.y())))
print("Encrypted flag:",enc(privkey))

nonces = set()

for _ in '01':
    try:
        msg = bytes.fromhex(input("Message: "))
        nonce = bytes.fromhex(input("Nonce: "))
        if nonce in nonces:
            print("Nonce already used")
            continue
        nonces.add(nonce)
        print(sign(msg,nonce,privkey))
    except ValueError:
        print("No hex?")
        exit()

This challenge using ECDSA to sign messages, we are given two points in a hidden curve, and one of them contains information about flag. We can also provide our nonce and messages to sign.

Recover curve’s parameters

Because we have two points $P$ and $Q$, which are public key and multiply of another point in curve $(E)$, we will have two equations with unknown $a, b$:

$$ y_P^2 = x_P^3 + a * x_P + b \mod p \newline y_Q^2 = x_Q^3 + a * x_Q + b \mod p $$

Then we can solve system of linear equations to get $a$ and $b$:

Recover privkey

Note that we can provide nonce to server to sign a message, we will find a way to make server use same nonce with different messages. We need to know how server generate nunce and use it to sign message

magic = os.urandom(16)

def bigsur(a,b):
    a,b = [[a,b],[b,a]][len(a) < len(b)]
    return bytes([i ^ j for i,j in zip(a,bytes([int(bin(int(b.hex(),16))[2:].zfill(len(f'{int(a.hex(), 16):b}'))[:len(a) - len(b)] + bin(int(b.hex(),16))[2:].zfill(len(bin(int(a.hex(), 16))[2:]))[:len(bin(int(a.hex(), 16))[2:]) - len(bin(int(b.hex(), 16))[2:])][i:i+8], 2) for i in range(0,len(bin(int(a.hex(), 16))[2:]) - len(bin(int(b.hex(), 16))[2:]),8)]) + b)])

...

def sign(msg,nonce,privkey):
    hsh = md5(msg).digest()
    nunce = md5(bigsur(nonce,magic)).digest()
    sig = privkey.sign(bytes_to_long(hsh), bytes_to_long(nunce))
    return json.dumps({"msg": msg.hex(), "r": hex(sig.r), "s": hex(sig.s)})

We can see that server provides bytearray magic and calculate nunce from it. The vulnerable here is the function bigsur. If we choose nonce_1 = b"\x00" and nonce_2 = b"\x00\x00", we can get the same nunce - the nonce that server use to sign a message.

Recover private key

In ECDSA, if we use the same nonce with different messages, we can recover private key. Specifically, we will have $r_1 = r_2 = R$, where $(r_1, s_1)$ is the signature of message $m_1$, and $(r_2, s_2)$ is the signature of message $m_2$. Now, based on equation to calculate $s$, we have

$$ \begin{aligned} s_1 * k - H(m_1) &= s_2 * k - H(m_2) ( = R * privkey) \newline k &= \frac{s_1 - s_2}{H(m_1) - H(m_2)} \end{aligned} $$

Then we can calculate $privkey = \frac{s_1 * k - H(m_1)}{R}$

Note that all computation are under modulo $q$, which is the order of the curve

Find flag

When we have privkey, we can easily recover $F$, which is the point that contains flag by calculate pow(privkey, -1, q) * Q

from sage.all import *
from Crypto.Util.number import *
from pwn import *
from ast import literal_eval
import json
from hashlib import md5
from ecdsa.curves import NIST256p

p = 115792089210356248762697446949407573530086143415290314195533631308867097853951
target = remote("13.201.224.182", int(30474))
dG = literal_eval(target.recvline().decode()[len("Public key: "):])
fG = literal_eval(target.recvline().decode()[len("Encrypted flag: "):])

dGx, dGy = (99122053878685444817852582103585646482441799670468212049632161370423019963573, 49681263796445807694244738028189208770171168855624587289690892802435841601423)
fGx, fGy = (22455982735997721923198309515515820680837002550923840212531066823876108860098, 49955453626898315794129063911602706078234097783588068635922441060010795905908)

target.sendlineafter(b"Message: ", b"1337")
target.sendlineafter(b"Nonce: ", b"00")
resp1 = json.loads(target.recvline().decode())
msg1, r1, s1 = resp1.values()
h1 = bytes_to_long(md5(bytes.fromhex("1337")).digest())

target.sendlineafter(b"Message: ", b"133700")
target.sendlineafter(b"Nonce: ", b"0000")
resp2 = json.loads(target.recvline().decode())
msg2, r2, s2 = resp2.values()
h2 = bytes_to_long(md5(bytes.fromhex("133700")).digest())


M = Matrix(GF(p), [[dGx, 1], [fGx, 1]])
v = vector(GF(p), [dGy**2 - dGx**3, fGy**2 - fGx**3])

a, b = M.solve_right(v)

assert dGy**2 % p == (dGx**3 + a * dGx + b) % p
assert fGy**2 % p == (fGx**3 + a * fGx + b) % p

E = EllipticCurve(GF(p), [a, b])
n = int(E.order())

k = ((h1 - h2) * pow(s1 - s2, -1, n)) % n

privkey = ((s2 * k - h2) * pow(r2, -1, n)) % n

fG = E(fGx, fGy)
dG = E(dGx, dGy)

privkey_ = pow(privkey, -1, n)
F = privkey_ * fG
print(long_to_bytes(int(F.xy()[0])))

daisy_bell

chall.py

from Crypto.Util.number import *
from FLAG import flag

p = getPrime(1024)
q = getPrime(1024)
n = p*q
c = pow(bytes_to_long(flag), 65537, n)

print(f"{n = }")
print(f"{c = }")
print(f"{p>>545 = }")
print(f"{pow(q, -1, p) % 2**955 = }")

"""
n = 13588728652719624755959883276683763133718032506385075564663911572182122683301137849695983901955409352570565954387309667773401321714456342417045969608223003274884588192404087467681912193490842964059556524020070120310323930195454952260589778875740130941386109889075203869687321923491643408665507068588775784988078288075734265698139186318796736818313573197531378070122258446846208696332202140441601055183195303569747035132295102566133393090514109468599210157777972423137199252708312341156832737997619441957665736148319038440282486060886586224131974679312528053652031230440066166198113855072834035367567388441662394921517
c = 7060838742565811829053558838657804279560845154018091084158194272242803343929257245220709122923033772911542382343773476464462744720309804214665483545776864536554160598105614284148492704321209780195710704395654076907393829026429576058565918764797151566768444714762765178980092544794628672937881382544636805227077720169176946129920142293086900071813356620614543192022828873063643117868270870962617888384354361974190741650616048081060091900625145189833527870538922263654770794491259583457490475874562534779132633901804342550348074225239826562480855270209799871618945586788242205776542517623475113537574232969491066289349
p>>545 = 914008410449727213564879221428424249291351166169082040257173225209300987827116859791069006794049057028194309080727806930559540622366140212043574
pow(q, -1, p) % 2**955 = 233711553660002890828408402929574055694919789676036615130193612611783600781851865414087175789069599573385415793271613481055557735270487304894489126945877209821010875514064660591650207399293638328583774864637538897214896592130226433845320032466980448406433179399820207629371214346685408858
"""

This is a RSA challenge. In this one, we are given MSB of $p$ and LSB of $u = q^{-1} \mod p$, and we need to factor $n$.

Construct the equation

$$ \begin{aligned} u &= q^{-1} \mod p \newline uq &= 1 + kp \newline uq^2 &= q + kpq \newline uq^2 - q &= 0 \mod n \end{aligned} $$

From the information we have, we will have bivariate polynomial

$$P(x, y) = (u_{hi} * 2^{955} + u_{low}) * (q_{hi} * 2**545 + q_{low})^2 - (q_{hi} * 2^{545} + q_{low}) = 0 \mod n$$

and we can use bivariate coppersmith to find roots $(q_{low}, u_{hi})$. I use kiona’s implementation to solve.

When we found roots, we can recover $q$, and everything is trivial

from sage.all import *
from Crypto.Util.number import *
import itertools
import sys
sys.path.append("../../../Tools/coppersmith")
from coppersmith_multivariate_heuristic import coppersmith_multivariate_heuristic
from lll import *

lllopt = {'algorithm':FPLLL}

n = 13588728652719624755959883276683763133718032506385075564663911572182122683301137849695983901955409352570565954387309667773401321714456342417045969608223003274884588192404087467681912193490842964059556524020070120310323930195454952260589778875740130941386109889075203869687321923491643408665507068588775784988078288075734265698139186318796736818313573197531378070122258446846208696332202140441601055183195303569747035132295102566133393090514109468599210157777972423137199252708312341156832737997619441957665736148319038440282486060886586224131974679312528053652031230440066166198113855072834035367567388441662394921517
c = 7060838742565811829053558838657804279560845154018091084158194272242803343929257245220709122923033772911542382343773476464462744720309804214665483545776864536554160598105614284148492704321209780195710704395654076907393829026429576058565918764797151566768444714762765178980092544794628672937881382544636805227077720169176946129920142293086900071813356620614543192022828873063643117868270870962617888384354361974190741650616048081060091900625145189833527870538922263654770794491259583457490475874562534779132633901804342550348074225239826562480855270209799871618945586788242205776542517623475113537574232969491066289349

msb_p = 914008410449727213564879221428424249291351166169082040257173225209300987827116859791069006794049057028194309080727806930559540622366140212043574
lsb_u = 233711553660002890828408402929574055694919789676036615130193612611783600781851865414087175789069599573385415793271613481055557735270487304894489126945877209821010875514064660591650207399293638328583774864637538897214896592130226433845320032466980448406433179399820207629371214346685408858
msb_q = (n // (msb_p << 545)) #calculate msb_q * 2**545

x, y = PolynomialRing(Zmod(n), "x, y").gens()

qq = msb_q + x
uu = y * 2**955 + lsb_u
f = uu*qq**2 - qq 

ans = coppersmith_multivariate_heuristic(f, [2**545, 2**68], 1.0, **lllopt)
lsb_q, msb_u = ans[0]

q = int(qq(lsb_q))
p = n // q
d = pow(65537, -1, (p - 1) * (q - 1))
m = pow(c, d, n)
print(long_to_bytes(m))

P/s: I also use kiona’s implementation to solve campervan. Really surprise that the implementation works in both challenges

Katyusha’s Campervan

chall.py

from Crypto.Util.number import *
from random import randint
from FLAG import flag

p = getPrime(1024)
q = getPrime(1024)
e = getPrime(132)
n = p*q
hint = pow(e, -1, (p-1)*(q-1))
hint %= p-1
hint %= 2**892
c = pow(3, int.from_bytes(flag), n**5) * pow(randint(0, n**5), n**4, n**5) % n**5

print(f"{n = }")
print(f"{e = }")
print(f"{c = }")
print(f"{hint = }")

"""
n = 9722343735487336242847355367175705096672092545117029199851527087227001665095112331406581010290318957921703096325328326862768861459201224096506317060919486835667369908780262880850949861734346363939614200227301344831209845565227637590016962469165064818450385339408084789219460490771570003649248250098125549751883777385917121014908647963900636814694225913533250242569263841750262192296795919177720443516042006972193940464844059718044438878017817432336475087436031866077325402373438547950481634275773767410248698596974769981162966656910136575149455523084473445761780201089182021418781347413453726696240548842411960178397
e = 5323153428600607366474827268153522064873
c = 9128106076400211790302891811252824557365859263295914819806672313027356017879597156259276057232557597614548050742418485365280305524694004426832069896531486671692562462063184624416012268348059935087037828161901243824582067161433586878141008884976330185561348052441637304755454643398179479215116505856245944555306345757777557395022121796068140566220391012921030768420736902592726104037200041403396506760483386523374366225161516294778224985920562226457769686733422726488624795847474711454397538514349555958637417188665977095558680525349235100286527905789576869572972662715040982208185436819557790062517857608731996417066519220133987864808243896151962316613504271341630230274953625158957103434031391582637694278277176886221304131078005240692954168656292222792833722555464070627220306776632641544334188357810067577550784029449834217848676080193960627138929032912578951880151284003878323853182114030012207949896373695734783631698004600675811512726913649141626146115066425891236975554237158682938964099745220780884079884347052906073216530490633243676915134831324804418410566989306886192743687590855529757605789691981493863292029273401139254934543448966341439303948513266699261650278938684067402860913507689842621595391519090227639907684629841162983852454124546030986411283762938101536264676221777904450717178547838152674410566294280937400196290368544481636850750666313771438253636667634601122561235018292316232335633111595474772273810349284893171480302604833719250453453781210093266339454843926482821341993360016434693250661347303203216948599305102121353574445652764255573536572077762409837628479280331295047290459975370026620838169978316921035609492162085052786943829915442906137063599836720584533200385074702683101049336194258783047318183521466098437420153628598968954236332678203275614402446435216223033804260963642393142002417568855964535316709986640977596845897721671783670070696907220894520837335160816494605130683705587464386202643385688263935088026204614056121745160246499509455752793089324629215884008499726564579763845757062068182946721730306128755414268910929410742220199282343421146810430121947827801171056425435942640932150535954546458772114121498557119913825127286832860975814307160175273154886250581960709573672488119996389986116735407178214281982766051391618187878672106737928646489671994503814871652107136752677107398141842179907758909246276653861569864776043204134345135427118784118473462309509988521112691717301811627018054555866015966545532047340607162395739241626423495285835953128906640802690450118128515355353064004001500408400502946613169130088974076348640048144323898309719773358195921400217897006053213222160549929081452233342133235896129215938411225808985658983546168950790935530147276940650250749733176085747359261765601961315474656996860052862883712183817510581189564814317141703276878435707070103680294131643312657511316154324112431403040644741385541670392956841467233434250239028068493523495064777560338358557481051862932373791428839612299758545173203569689546354726917373906408317003812591905738578665930636367780742749804408217333909091324486584514813293
hint = 27203100406560381632094006926903753857553395157680133688133088561775139188704414077278965969307544535945156850786509365882724900390893075998971604081115196824585813017775953048912421386424701714952968924065123981186929525951094688699758239739587719869990140385720389865
"""

Another RSA challenge in this competition! We are given LSB of $d_p = d \mod p - 1$ and the flag was encrypted like Damgard-Jurik cryptosystem

Factor n

We have:

$$ \begin{aligned} d_p &= d \mod p - 1 \newline e * d_p &= 1 \mod p - 1 \newline e * d_p &= 1 + k * (p - 1) \newline e * (d_{hi} * M + d_{low}) &= 1 + k * (p - 1) \newline e * d_{hi} * M + e * d_{low} &= 1 + k * (p - 1) (1) \end{aligned} $$

Let $E = (eM)^{-1} \mod p-1$, then there exists $c \in \mathbb{N}$ such that $E * eM = 1 + cN$, than we can transform $(1)$ to:

$$d_{hi} + E * (ed_{low} + k - 1) = (Ek - cqd_{hi})*p$$

So we can construct a bivariate polynomial:

$$P(x, y) = x + E * (ed_{low} + y - 1) \mod n$$

and our roots will be $$(d_{hi}, k)$$

When we found the roots, we can find factor $p$ of $n$ by calculate $GCD(P(d_{hi}, k), n)$

How to decrypt ?

When we have $p$, we can calculate private key $d$. Now to decrypt, we will follow this paper to decrypt.

from sage.all import *
from Crypto.Util.number import *
import itertools
sys.path.append("../../../Tools/coppersmith")
from coppersmith_multivariate_heuristic import coppersmith_multivariate_heuristic
from lll import *

lllopt = {'algorithm':FPLLL}

n = 9722343735487336242847355367175705096672092545117029199851527087227001665095112331406581010290318957921703096325328326862768861459201224096506317060919486835667369908780262880850949861734346363939614200227301344831209845565227637590016962469165064818450385339408084789219460490771570003649248250098125549751883777385917121014908647963900636814694225913533250242569263841750262192296795919177720443516042006972193940464844059718044438878017817432336475087436031866077325402373438547950481634275773767410248698596974769981162966656910136575149455523084473445761780201089182021418781347413453726696240548842411960178397
e = 5323153428600607366474827268153522064873
c = 9128106076400211790302891811252824557365859263295914819806672313027356017879597156259276057232557597614548050742418485365280305524694004426832069896531486671692562462063184624416012268348059935087037828161901243824582067161433586878141008884976330185561348052441637304755454643398179479215116505856245944555306345757777557395022121796068140566220391012921030768420736902592726104037200041403396506760483386523374366225161516294778224985920562226457769686733422726488624795847474711454397538514349555958637417188665977095558680525349235100286527905789576869572972662715040982208185436819557790062517857608731996417066519220133987864808243896151962316613504271341630230274953625158957103434031391582637694278277176886221304131078005240692954168656292222792833722555464070627220306776632641544334188357810067577550784029449834217848676080193960627138929032912578951880151284003878323853182114030012207949896373695734783631698004600675811512726913649141626146115066425891236975554237158682938964099745220780884079884347052906073216530490633243676915134831324804418410566989306886192743687590855529757605789691981493863292029273401139254934543448966341439303948513266699261650278938684067402860913507689842621595391519090227639907684629841162983852454124546030986411283762938101536264676221777904450717178547838152674410566294280937400196290368544481636850750666313771438253636667634601122561235018292316232335633111595474772273810349284893171480302604833719250453453781210093266339454843926482821341993360016434693250661347303203216948599305102121353574445652764255573536572077762409837628479280331295047290459975370026620838169978316921035609492162085052786943829915442906137063599836720584533200385074702683101049336194258783047318183521466098437420153628598968954236332678203275614402446435216223033804260963642393142002417568855964535316709986640977596845897721671783670070696907220894520837335160816494605130683705587464386202643385688263935088026204614056121745160246499509455752793089324629215884008499726564579763845757062068182946721730306128755414268910929410742220199282343421146810430121947827801171056425435942640932150535954546458772114121498557119913825127286832860975814307160175273154886250581960709573672488119996389986116735407178214281982766051391618187878672106737928646489671994503814871652107136752677107398141842179907758909246276653861569864776043204134345135427118784118473462309509988521112691717301811627018054555866015966545532047340607162395739241626423495285835953128906640802690450118128515355353064004001500408400502946613169130088974076348640048144323898309719773358195921400217897006053213222160549929081452233342133235896129215938411225808985658983546168950790935530147276940650250749733176085747359261765601961315474656996860052862883712183817510581189564814317141703276878435707070103680294131643312657511316154324112431403040644741385541670392956841467233434250239028068493523495064777560338358557481051862932373791428839612299758545173203569689546354726917373906408317003812591905738578665930636367780742749804408217333909091324486584514813293
hint = 27203100406560381632094006926903753857553395157680133688133088561775139188704414077278965969307544535945156850786509365882724900390893075998971604081115196824585813017775953048912421386424701714952968924065123981186929525951094688699758239739587719869990140385720389865
M = 2**892
E = pow(e * M, -1, n)

x, y = PolynomialRing(Zmod(n), "x, y").gens()
f = x + E * (e * hint + y - 1)


ans = coppersmith_multivariate_heuristic(f, [2**132, e], 1.0, **lllopt)[0]
#ans = (1364278824202792998093019636227517188336, 2238131335516129175817357831521181270929)
d_hi, k = ans

p = GCD(int(f(d_hi, k)), n)
assert n % p == 0
q = n // p
d = lcm(p - 1, q - 1)

#From https://github.com/p4-team/ctf/tree/master/2016-09-09-asis-final/dam
def decrypt(ct, d, n, s):
    ns1 = pow(n, s + 1)
    a = pow(ct, d, ns1)
    i = 0
    for j in range(1, s + 1):
        t1 = ((a % pow(n, j + 1))-1) // n
        t2 = i
        for k in range(2, j + 1):
            i -= 1
            t2 = (t2 * i) % pow(n, j)
            factorial = 1
            factorial = int(gmpy2.factorial(k))
            up = (t2 * pow(n, k - 1))
            down = gmpy2.invert(factorial, pow(n, j))
            t1 = (t1 - up * down) % pow(n, j)
        i = t1
    return i

jmd = decrypt(c, d, n, 5)
jd = decrypt(3, d, n, 5)
print(long_to_bytes((jmd * pow(jd, -1, n**4)) % (n**4)))

* Predictable

A blackbox cryptography challenge! We are not given the source code, we only know this is a Dual_EC_DRBG system to generate random number, and we need to predict the next output.

I couldn’t solved this challenge when the competition happened. After competition, I ask the author and know my idea was right, just only my coding issue :((

Timing attack

The description of this challenge mentioned about Dual_EC_DRBG backdoor. After researched about it, I found that I need to find $d$ such that $P = d * Q$. Because server uses safe curves like secp192k1 or secp256k1, so calculate discrete logarithm is impossible, but a loading part to generate parameter is really suspicious. Sometimes it’s long, and sometimes it’s not. It reminds me about timing attack.

If we choose option 1 and calculate time between two “\r” appear (We can know it by recvuntil("\r") in pwntools library in Python), our result will be ~0.5s or ~3s, and this reminds me to double-and-add algorithm. This video explains the algorithm well.

We can guess that server are calculating $P = d * Q$ by double-and-add algorithm, it means that i-th bit of $d$ will be 1 if the time between two “\r” appear is ~3s, otherwises it’s 0

Dual_EC_DRBG Backdoor

When we have d, we can calculate the next output of random generator. This was described at here

Solve script

This is my solve script after competition end. Thanks @ctfguy to help me fix

Caveat: My code only work when server using secp192k1. For secp256k1, you need to bruteforce 16 bits to get correct x-coordinate.

from sage.all import *
from pwn import *
import time

target = remote("13.201.224.182", int(32553))
target.recvline()
target.sendlineafter(b">", b"1")
target.recvline()

loading = ""
d = ""

while check + 0.7 < 100:
    start = time.time()
    loading = target.recvuntil("\r").decode()
    end = time.time()
    check = float(loading[:-2])
    if "%" in loading:
        t = end - start
        if t > 2:
            d += "1"
        else:
            d += "0"

d = int(d, 2)
p = 0xfffffffffffffffffffffffffffffffeffffffffffffffff
K = GF(p)
a = K(0xfffffffffffffffffffffffffffffffefffffffffffffffc)
b = K(0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1)
E = EllipticCurve(K, (a, b))
curve = target.recvline().decode()
if "secp192k1" in curve:
    Px, Py = [int(c) for c in target.recvline().decode()[1:-2].split(",")]
    Qx, Qy = [int(c) for c in target.recvline().decode()[1:-2].split(",")]
    P = E(Px, Py)
    Q = E(Qx, Qy)
    assert P == d * Q
    for _ in range(4):
        target.recvline()
    target.sendline(b"1")
    r = int(target.recvline().decode()[1:-1])
    R = E.lift_x(K(r))
    d_inv = inverse_mod(d, int(P.order()))
    s2 = int((d_inv*R).xy()[0])
    r2 = int((s2*Q).xy()[0])
    print(r2)
    target.interactive()

My dumbest error

Why I can’t solve this one ? Turn out, this is about my coding issue. Instead of this one

d = ""
...
t = end - start
if t > 2:
    d += "1"
else:
    d += "0"

I code in another way

d = 0
...

t = end - start
if t > 2:
    d *= 2
else:
    d += 1

and my code doesn’t work because it calculates wrong $d$. How dumb am I :(((