Teaser Dragon CTF 2019 rsachained

Keys are generated in the standard way with the default setup...

task.py

output.txt

import os
import gmpy2

flag = int(open('flag.txt').read().encode("hex"), 16)

def genPrime(bits):
    data = os.urandom(bits/8)
    number = int(data.encode("hex"), 16)
    return gmpy2.next_prime(number)


e = 1667


# rsa1: p - 700 bits q - 1400 bits

p = genPrime(700)
q = genPrime(1400)

n = p*q
phi = (p-1)*(q-1)
d = gmpy2.powmod(e, -1, phi)

rsa1 = (n, d)


# rsa2: p - 700 bits, q - 700 bits, r = 700 bits

p = genPrime(700)
q = genPrime(700)
r = genPrime(700)

n = p*q*r
phi = (p-1)*(q-1)*(r-1)
d = gmpy2.powmod(e, -1, phi)

rsa2 = (n, d)

# rsa3: p - 700 bits, q - 700 bits, r = 700 bits

p = genPrime(700)
q = genPrime(700)

n = p*q*r
phi = (p-1)*(q-1)*(r-1)
d = gmpy2.powmod(e, -1, phi)

rsa3 = (n, d)

# rsa4: p - 700 bits, q - 700 bits

p = genPrime(700)
q = genPrime(700)

n = p*q*q
phi = (p-1)*(q-1)*q
d = gmpy2.powmod(e, -1, phi)

rsa4 = (n, d)

rsa = sorted([rsa1, rsa2, rsa3, rsa4])


for n, d in rsa:
    print 'pubkey:', n, d % (2**1050)
    flag = pow(flag, e, n)

print 'encrypted flag', flag

4つのRSAと4つのd'が与えられる。 d' = d \mod 2^{1050} なのでd'はおよそ1050bit?

rsa1 n=p*q で 700bits と 1400bits

rsa2 n = p*q*r でそれぞれ700bits

rsa3 n = p*q*r でrsa2とrを共有している

rsa4 n = p * q * q でそれぞれ700bits

rsa1

ここで説明されているPartial Key Exposure Attackみたいなものができるらしい(著者 Boneh と DurfeeだけどBoneh Durfee Attackとはおそらく別)。これはeがある程度小さくてdの下位bitが1/4程度わかっているときにd全体を復元できるという攻撃っぽい。また、eが N^{1/4} \leq e  \leq N^{1/2}程度の素数の場合にも似たような結果が得られるとか。

今回はLow exponent RSAの場合のみを考える。前提として ed \equiv 1 \mod \phi(n)のとき、 ed - \phi(n)k = 1であるような整数kが存在する。そして、 d_0 = d \mod 2^{n/4}、つまりdの下位1/4bitであるようなd_0が与えられているとする。

このとき、

 ed_0 \equiv  1 + k (N - s + 1) \mod 2^{n/4}

である。ここで、 0 \le k \le eであり、これを探索して整数sを求める。sがわかると以下の式を解いてpがわかる。(何でかはpaperを読んでくれ)

 p^2 - sp + N \equiv 0 \mod 2^{n/4}

わからん

import sys
import gmpy2

N1=859120656206379031921714646885063910105407651892527052187347867316222596884434658659405822448255608155810241715879240998190431705853390924506125009673776539735775578734295308956647438104082568455604755897347443474837969627723792555180605570061543466558710984713039896691188382997755705425087392982168984185229388761020298907843938651747237132404355197739247655148879984476412365078220648181358131779226613410257975180399575865809381114503102493767755197618305439592514756307937482535824886677028869214462143121897901687818277055753103973809215799348093165770108731899625440232334370794010877047571087634088643565878666814823597
N2=1311485515090222718982495198730831073955174624382380405255882886012541076751694096664143914783345786808989030195914045177845164364400711539537456739170589346033729436625658871146633553503774866142568716068953663977222002088477902235884717082069070796901652820969777833174034685816622756524282480580399883160755030115364291412718061758142515305389808681261201028647918631486855998332674824264495109641835596608336454102370944531225289276734441189842117114187272485368222698210402692987316946307402124818683662256285425749745044473836534317911693431019535047343774304237969249813708212575639444584092633692220540287380100087406907
d1lower = 6404011758157972703612261793681759246464215350955339624160911161422611520145549655575457659757671312362520534084082807541566604214336116674150997699501607769566852492157994984170248823333294918447786457066546086171046532955584494739314351490726886444164048184241514125678560723782999029659135170314199032519015517483
d2lower = 6762547460602241256253304403057840010356965995658332151464306076734886668348338229477220486851971695831025738065540621944954803087639187605826246170852109559079074614119808221929013953607066074389809368603757307759386199129306765569186327590211123849053154881101151177686828760168745679159348065176015209027306930795

def modinv(a, mod):
    return gmpy2.divm(1, a, mod)

def crt(p,q, dmp, dmq):
    prod = p * q
 
    ret = dmp * modinv(q, p) % p * q
    ret += dmq * modinv(p, q) % q * p
    return ret % prod

# P egy ismert prímosztója N-nek, nem az eredeti támadás része
def d_partial_exposure(_dlower, N, p):
    assert N%p == 0
    e = 1667
    p1 = p-1
    dmp1 = modinv(e, p1)
    assert e*dmp1%p1 == 1
    assert gmpy2.powmod(2, dmp1*e, p) == 2

    p1x = p1
    while p1x%2 == 0:
        p1x //= 2
    dmp1x = dmp1%p1x
    dlower = crt(p1x, 2**1050, dmp1x, _dlower)
    assert dlower % (2**1050) == _dlower
    assert dlower % p1 == dmp1
    assert dlower < (1<<1750)
    assert dlower > (1<<1460)

    MOD = p1x<<1050
    for k in range(e+1):
        dupper = (k*N+1)//e
        dupper = dupper//MOD*MOD
        for x in range(-2,3):
            dcand = dupper + dlower + x*MOD
            if dcand < 0:
                continue
            assert dcand % p1 == dmp1
            assert dcand % (1<<1050) == _dlower
            if gmpy2.powmod(2, dcand*e,N) == 2:
                return dcand
    assert(False)

# egyik prim
P = gmpy2.gcd(N1,N2)
assert P != 1

e = 1667
#d1 = d_partial_exposure(d1lower, N1, P)
d1 = 536499461974109521434016165211368644522933032765519292937629951935325568900237840230618752950590334787161644646808812165036736296216784614523620956850870652588447736930054839006520685702669438369696791175248139333899178109132857129498644463812879241313217838272698759059618670904301503031370482593128601064601988543628242477255811668562806753373863919467532704381724235908139965837261439492025443764063209607138449745387083882085791921688501309079739936184841144642836768681165552475426747942922033566756338240250913366448461590599540047734659944902532315561198549877167966775516159467684027796694761374788473470169610840165675
#d2 = d_partial_exposure(d2lower, N2, P)
d2 = 689965684903494495829423088954372436627887309888030963053034967626273859814778477969078712216553242370415944500189932586064912506046445123079993737404083297223503728806660365924173741105465241515916475100463296876910366439207642483647683954896773567500287530655465704536301654714603709153441934810480180612520736719117071180018835175344146545698946933779921074459607196808941644511778420505254623841265774408774443891495012930540080236965626719028359450733805499546172601502884736802703320296093816630611677176285813425155240682413132686810513081547455776384558547313469435113077571280358641899861943541720182054572347130790507
from crypto_commons.rsa.rsa_commons import hensel_lifting
def solve_rsa4(n, e, d_):
    def check_k(k):
        ka = (k*n + 1 - e*d_) % 2**1050
        f =  lambda x: (k*x**3 - k*x**2 - ka*x + k*n) % 2**1050
        df = lambda x: (3*k*x**2 - 2*k*x - ka) % 2**1050
        sols = [p for p in hensel_lifting(f, df, 2, 1050, 1) if n % p == 0]
        if sols:print(sols)
    for k in tqdm.trange(1, e):
        check_k(k)

https://www.jaybosamiya.com/blog/2019/09/25/rsachained/