DASCTF X GFCTF 2022 Crypto Writeup

RSA

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

def encrypt1(n):
    n1 = hex(n>>200).encode()
    n2 = str(hex(n))[20:].encode()
    return n1,n2


def encrypt2(m , n_1):
    c_1 = pow(m,e_1,n_1)
    print('c_1 = '+str(c_1))


def encrypt3(m , n_2):
    c_2 = pow( m , e_2 , n_2)
    print('c_2 = '+str(c_2))


def encrypt4(m):
    k = getPrime(512)
    m = m % k
    c_3 = pow(m, e_2, n_3)
    print('c_3 = ' + str(c_3))
    print('m = ' + str(m))
    print('k = ' + str(k))


m1,m2 = encrypt1(flag)
m1 = bytes_to_long(m1)
m2 = bytes_to_long(m2)


print('n_2 = ' + str(n_2))
print('n_3 = ' + str(n_3))
print('e_1 = ' + str(e_1))
print('e_2 = ' + str(e_2))


encrypt2(m1,n_1)
encrypt3(n_1,n_2)
encrypt4(m2)

'''
n_2 = 675835056744450121024004008337170937331109883435712066354955474563267257037603081555653829598886559337325172694278764741403348512872239277008719548968016702852609803016353158454788807563316656327979897318887566108985783153878668451688372252234938716250621575338314779485058267785731636967957494369458211599823364746908763588582489400785865427060804408606617016267936273888743392372620816053927031794575978032607311497491069242347165424963308662091557862342478844612402720375931726316909635118113432836702120449010
n_3 = 91294511667572917673898699346231897684542006136956966126836916292947639514392684487940336406038086150289315439796780158189004157494824987037667065310517044311794725172075653186677331434123198117797575528982908532086038107428540586044471407073066169603930082133459486076777574046803264038780927350142555712567
e_1 = 65537
e_2 = 3
c_1 = 47029848959680138397125259006172340325269302342762903311733700258745280761154948381409328053449580957972265859283407071931484707002138926840483316880087281153554181290481533
c_2 = 332431
c_3 = 11951299411967534922967467740790967733301092706094553308467975774492025797106594440070380723007894861454249455013202734019215071856834943490096156048504952328784989777263664832098681831398770963056616417301705739505187754236801407014715780468333977293887519001724078504320344074325196167699818117367329779609
m = 9530454742891231590945778054072843874837824815724564463369259282490619049557772650832818763768769359762168560563265763313176741847581931364
k = 8139616873420730499092246564709331937498029453340099806219977060224838957080870950877930756958455278369862703151353509623205172658012437573652818022676431
'''

普通的RSA,flag分成$m_1$和$m_2$按照encrypt1的规则分成两部分,再根据encrypt2、encrypt3、encrypt4进行变换:

已知$e_2=3, n_2,n_3,c_1,c_2,c_3, m=m_2\bmod k, k$,先看最后一个同余式:$m=k*i+m_2$,其中$i$为整数,指数太小,因此可以尝试找到这样的$i$求出$m_2$:

from Crypto.Util.number import *
import gmpy2

c_3 = 11951299411967534922967467740790967733301092706094553308467975774492025797106594440070380723007894861454249455013202734019215071856834943490096156048504952328784989777263664832098681831398770963056616417301705739505187754236801407014715780468333977293887519001724078504320344074325196167699818117367329779609
m = 9530454742891231590945778054072843874837824815724564463369259282490619049557772650832818763768769359762168560563265763313176741847581931364
k = 8139616873420730499092246564709331937498029453340099806219977060224838957080870950877930756958455278369862703151353509623205172658012437573652818022676431

i = 0
while 1:
    if(gmpy2.powmod(m+i*k, 3, n_3) == c_3):
        print(long_to_bytes(m+i*k))
        break
    i += 1

# b'383539643865383534633466363030636231323735376262663966357d'
# f_2 = b'859d8e854c4f600cb12757bbf9f5}'

对第二个同余式用小指数可直接开3次方求出$n_1$:

from Crypto.Util.number import *
import gmpy2

n_3 = 91294511667572917673898699346231897684542006136956966126836916292947639514392684487940336406038086150289315439796780158189004157494824987037667065310517044311794725172075653186677331434123198117797575528982908532086038107428540586044471407073066169603930082133459486076777574046803264038780927350142555712567
c_2 = 332431
n_2 = 675835056744450121024004008337170937331109883435712066354955474563267257037603081555653829598886559337325172694278764741403348512872239277008719548968016702852609803016353158454788807563316656327979897318887566108985783153878668451688372252234938716250621575338314779485058267785731636967957494369458211599823364746908763588582489400785865427060804408606617016267936273888743392372620816053927031794575978032607311497491069242347165424963308662091557862342478844612402720375931726316909635118113432836702120449010

i = 0
while 1:
    if(gmpy2.iroot(c_2+i*n_2, 3)[1] == 1):
        print(gmpy2.iroot(c_2+i*n_2, 3)[0])
        break
    i += 1

# n_1 = 70406706457855863712635967741447303613971473150228480705119773604469794649140239446237334040048504811343327173817296308781190911727763110615393368497803655390445303946160971

发现$n_1$可以分解成三个素数,即可得到另一部分$m_1$:

from Crypto.Util.number import *
import gmpy2

c_1 = 47029848959680138397125259006172340325269302342762903311733700258745280761154948381409328053449580957972265859283407071931484707002138926840483316880087281153554181290481533
e_1 = 65537

p = 2224243981
q = 2732337821
r = 11585031296201346891716939633970482508158508580350404805965250133832632323150440185890235814142601827544669601048550999405490149435265122374459158586377571
n_1 = p * q * r
phi = (p - 1) * (q - 1) * (r - 1)
d = inverse(e_1, phi)
f_1 = pow(c_1, d, n_1)
print(long_to_bytes(f_1))
# b'0x666c61677b3230366538353964'
print(long_to_bytes(0x666c61677b3230366538353964))
# b'flag{206e859d'

# flag{206e859d8e854c4f600cb12757bbf9f5}'

CryptoRecover_Secret

Problem

import gmpy2 as gy
import random
import functools
import libnum
from Crypto.Util import number
from datetime import datetime
import os
import sys

of = open('output','w')
sys.stdout = of
_N = gy.next_prime(2 ** 512)
_rand_int = functools.partial(random.SystemRandom().randint, 0)

class Encryptor():
    def __init__(self):
        self.pubKey = None
        self.priKey = None

    def __gen_prime__(self, rs, n_bits):
        p = gy.mpz_urandomb(rs, n_bits)
        while not gy.is_prime(p):
            p += 1
        return p

    def __key_gen__(self, n_bits=512):
        while True:
            rs = gy.random_state(datetime.now().microsecond)
            p = self.__gen_prime__(rs, n_bits)
            q = self.__gen_prime__(rs, n_bits)
            n = p * q
            lmd = (p - 1) * (q - 1)
            if gy.gcd(n, lmd) == 1:
                break

        g = n + 1
        mu = gy.invert(lmd, n)
        self.pubKey = [n, g]
        self.priKey = [lmd, mu]
        return (self.pubKey, self.priKey)

    def encipher(self, plaintext):
        m = plaintext
        n, g = self.pubKey
        r = gy.mpz_random(gy.random_state(datetime.now().microsecond), n)
        while gy.gcd(n, r) != 1:
            r += 1
        ciphertext = gy.powmod(g, m, n ** 2) * gy.powmod(r, n, n ** 2) % (n ** 2)
        return ciphertext

class Commit:
    def __init__(self):
        self.param = None

    def __key_gen__(self, n):
        p = n
        r = 1
        while True:
            q = r * p + 1
            if gy.is_prime(q):
                break
            r += 1
        x = q - 1
        while True:
            g = x ** r % q
            if g != 1:
                break
            x -= 1
        while True:
            h = x ** r % q
            if(g != h and h != 1):
                break
            x -= 1
        self.param = q, g, h

    def commit(self, m):
        q, g, h = self.param
        r = number.getRandomRange(1, q - 1)
        c = (pow(g, m, q) * pow(h, r, q)) % q
        return c, r

def shuffle(X, Y, x):
    res = []
    for i in range(len(X)):
        res.append([i, X[i], Y[i]])
        for j in range(x):
            res.append([i, X[i] + random.randint(-X[i], X[i]), Y[i] + random.randint(-Y[i], Y[i])])
    random.shuffle(res)
    return res

def main():
    k = 6
    sk = []
    flag = os.environ["flag for GFCTF2022-Crypto Recover Secret"]
    secret = libnum.s2n(flag)
    assert len(bin(secret)) < 514
    enc = Encryptor()
    sk = [enc.__key_gen__() for i in range(k)]
    print(sk)

    s = [_rand_int(_N - 1) for i in range(k)]
    s[0] = secret

    v = Commit()
    v.__key_gen__(_N)
    print(v.param)
    cr = [v.commit(s[i]) for i in range(k)]
    c = [i[0] for i in cr]
    print(c) 

    X = []
    Y = []
    for i in range(1, k + 1):
        xs = 1
        enc.pubKey = sk[i - 1][0]
        n_2q = enc.pubKey[0] ** 2
        for j in range(k):
            xs *= pow(enc.encipher(i ** j), cr[j][1], n_2q)
        X.append(xs)

        ys = 1
        for j in range(k):
            ys *= pow(enc.encipher(i ** j), s[j], n_2q)
        Y.append(ys)

    X_Y = shuffle(X, Y, 51)
    print(X_Y)

if __name__ == '__main__':
    main()

Encryptor类实现Paillier加解密,Commit类实现Pedersen可验证秘密分享中的承诺方案。首先Paillier是一种概率加密,但它具有同态性:对一个消息$m$来说,加密过程引入了随机数$r$,相同的明文可能对应不同的密文,而密文只能解密到它所对应的明文,而与引入的随机数无关。并且对于两个不同的$m_1,m_2$,用同一把公钥$(g,n)$加密成它们的密文$c_1,c_2$,使用私钥可以将$c_1c_2$解密到$m_1+m_2$:

其次,Pedersen VSS是Shamir秘密共享(SSS)的一种并行版本。一般我们会把SSS方案中选择随机多项式的人叫做dealer,由dealer向参与方分发秘密份额。Pedersen VSS在SSS中加入可验证步骤,也就是在dealer选定随机多项式后,对多项式进行承诺(Commit),在分发份额前,广播给所有参与方,参与方在收到秘密份额后通过承诺来检查份额是否正确。承诺部分采用了Pedersen承诺的形式:$c=g^s h^t$,同样具有同态的性质。这两部分其实都有许多值得讨论的问题,现在这里只针对这道题目。

一共有6个参与方,dealer选择的随机模$N$下($N$为素数)多项式为:

$s_0$为要恢复秘密值,$f(i)$作为dealer分发给每个参与方$P_i$的份额,$i$是集合下标。$f(x)$阶为5,那么只要知道6个$f(x)$上的点$(i,f(i))$,那么可以由拉格朗日插值恢复出$f(x)$,即所有系数。

最终列表中的数据格式为$X_Y=[i,X_i,Y_i]$。用$r_x$和$R_x$表示未知的随机数,E表示Paillier加密,D表示Paillier解密,每个参与方$P_i$的Paillier公私钥$(pk_i,sk_i)$,$X_i$和$Y_i$生成的形式为:

注意到$f(i)=\sum_{j=0}^{5}i^js_j$。已经有Paillier的私钥,虽然随机数不一样,由Paillier同态的性质,解密后:

最终列表使用了shuffle函数加入了50个$X_i$和$Y_i$的干扰项,并打乱了顺序。但是利用承诺$c_j=g^{s_j}h^{r_j}$,$P_i$验证从而找出自己正确的$X_i,Y_i$:

完整代码如下。

from gmpy2 import mpz, next_prime
from Crypto.Util.number import *
from scipy.interpolate import lagrange

with open('output', 'r') as f:
    content = list(filter(lambda x:len(x)>0, f.read().split('\n')))

key = eval(content[0])
param = eval(content[1])
commit = eval(content[2])
X_Y = eval(content[3])
_N = next_prime(2 ** 512)

q, g, h = param

pk = [i[0] for i in key]
sk = [i[1] for i in key]

def eval_poly_hx(i, commit):
    c = 1
    for j in range(len(commit)):
        c = (c * pow(commit[j], (i+1)**j, q)) % q
    return c

def pillier_dec(i, sk, c):  
    m = ((((pow(c, sk[i][0], pk[i][0]**2) - 1) // pk[i][0]) % pk[i][0]**2) * sk[i][1]) % pk[i][0]
    # m_ = ((((pow(c, sk[i][0], pk[i][0]**2) - 1) // pk[i][0]) % pk[i][0]**2) * inverse((((pow(pk[i][1], sk[i][0], pk[i][0]**2) - 1) // pk[i][0]) % pk[i][0]**2), pk[i][0]**2)) % pk[i][0]
    # assert m == m_
    return m

f = [0] * 6

for item in X_Y:
    i, x, y = item
    t = pillier_dec(i, sk, x)
    s = pillier_dec(i, sk, y)
    if (pow(g, s, q) * pow(h, t, q)) % q == eval_poly_hx(i, commit) % q:
        f[i] = s

F = GF(_N)
points = list(zip([i+1 for i in range(6)], f))
R = F['x']
print(long_to_bytes(R.lagrange_polynomial(points)[0]))

# b'DASCTF{fe22ca94-c002-d10b-7104-1c5eff3f335c}'

CryptoProof_Yourself

import gmpy2 as gy
import random
import functools
from datetime import datetime
from Crypto.Cipher import AES
import base64
import os

_N = gy.next_prime(2 ** 512)
_rand_int = functools.partial(random.SystemRandom().randint, 0)

class Encryptor():
    def __init__(self):
        self.pubKey = None
        self.priKey = None
        self.r = None

    def __gen_prime__(self, rs, n_bits):
        p = gy.mpz_urandomb(rs, n_bits)
        while not gy.is_prime(p):
            p += 1
        return p

    def __key_gen__(self, n_bits=512):
        while True:
            rs = gy.random_state(datetime.now().microsecond)
            p = self.__gen_prime__(rs, n_bits)
            q = self.__gen_prime__(rs, n_bits)
            n = p * q
            lmd = (p - 1) * (q - 1)
            if gy.gcd(n, lmd) == 1:
                break

        g = n + 1
        mu = gy.invert(lmd, n)
        self.pubKey = [n, g]
        self.priKey = [lmd, mu]
        return (self.pubKey, self.priKey)

    def encipher(self, plaintext):
        m = plaintext
        n, g = self.pubKey
        if self.r is None:
            r = gy.mpz_random(gy.random_state(datetime.now().microsecond), n)
            while gy.gcd(n, r) != 1:
                r += 1
            self.r = r
        else:
            r = self.r
        ciphertext = gy.powmod(g, m, n ** 2) * gy.powmod(r, n, n ** 2) % (n ** 2)
        return ciphertext

    def decipher(self, ciphertext):
        # The decryption process is hidden
        return 0xffffffff


class Proof():
    def __init__(self):
        self.pubKey = None
        self.proof_param = None
        self.pre_param = None
        self.d = None

    def setup(self, pk, c1, c2, c3, m1, m2, r1, r2, r3):
        self.pubKey = pk
        self.proof_param = [c1, c2, c3, m1, m2, r1, r2, r3]

    def pre_work_verify(self):
        c1, c2, c3, m1, m2, r1, r2, r3 = self.proof_param
        m4 = gy.mpz_random(gy.random_state(datetime.now().microsecond), self.pubKey[0])
        pai = Encryptor()
        pai.pubKey = self.pubKey
        c4 = pai.encipher(m4)
        r4 = pai.r
        pai.r = None

        c5 = pai.encipher(m2 * m4)
        r5 = pai.r
        pai.r = None

        self.pre_param = [c4, m4, r4, c5, r5]
        return c4, c5

    def set_d(self, d):
        self.d = d

    def verify(self):
        n_2q = self.pubKey[0] ** 2
        c1, c2, c3, m1, m2, r1, r2, r3 = self.proof_param
        c4, m4, r4, c5, r5 = self.pre_param
        d = self.d

        e = d * m1 + m4
        a1 = pow(r1, d, n_2q) * r4 % n_2q
        b1 = r5 * pow(r3, d, n_2q) % n_2q
        a2 = pow(r2, e, n_2q) * gy.invert(b1, n_2q) % n_2q
        b2 = pow(c3, d, n_2q) * c5 % n_2q

        enc = Encryptor()
        enc.pubKey = self.pubKey
        enc.r = pow(r1, d, n_2q) * r4 % n_2q
        if pow(c1, d, n_2q) * c4 % n_2q != enc.encipher(d * m1 + m4):
            return False
        enc.r = a2
        return pow(c2, e, n_2q) * gy.invert(b2, n_2q) % n_2q == enc.encipher(0)

class MProof():
    def __init__(self):
        self.pubKey = None
        self.priKey = None
        self.cs = None
        self.rs = None
        self.k = None
        self.t = 9

    def generate_param(self, pk, cs, rs, k):
        self.pubKey = pk[0]
        self.priKey = pk[1]
        self.cs = cs
        self.rs = rs
        self.k = k

    def verify(self, d):
        enc = Encryptor()
        enc.pubKey = self.pubKey
        enc.priKey = self.priKey
        cs = self.cs
        rs = self.rs
        k = self.k
        t = self.t
        assert k == enc.decipher(cs[0])
        for i in range(1, t - 1):
            c1 = cs[0]
            c2 = cs[i - 1]
            c3 = cs[i]
            m2 = enc.decipher(cs[i - 1])
            r1 = rs[0]
            r2 = rs[i - 1]
            r3 = rs[i]

            proof = Proof()
            proof.setup(self.pubKey, c1, c2, c3, k, m2, r1, r2, r3)

            proof.pre_work_verify()
            proof.set_d(d)

            if not proof.verify():
                return False
        return True

def main():
    flag = os.environ["flag for GFCTF2022-Crypto Proof Yourself"]
    k = _rand_int(_N - 1)
    print("k: ", k)
    d = _rand_int(_N - 1)
    print("d: ", d)
    param = eval(input("please proof yourself: "))

    cs = param['cs']
    rs = param['rs']
    pk = param['pk']

    print(pk[0])
    print(rs)
    mp = MProof()
    mp.generate_param(pk, cs, rs, k)
    if mp.verify(d):
        s = 1
        for i in cs:
            s *= i
            s %= _N
        aes = AES.new(str(s)[:32].encode(), AES.MODE_ECB)
        flag = flag.encode()
        while len(flag) % AES.block_size != 0:
            flag += b'\x00'
        c = aes.encrypt(flag)
        c = base64.b64encode(c)
        print(c)
    else:
        print("you're not yourself!")

if __name__ == '__main__':
    main()

同样是Paillier,开始以为是ZKP,然而不是,没看出来题目原型是什么证明。首先给定一些证明的参数$c_1, c_2, c_3, k, m_2, r_1, r_2, r_3$,验证输入$d$时会经过一个验证的参数预处理阶段Proof.pre_work_verify(),生成预处理参数$c_4, m_4, r_4, c_5, r_5$,调用验证函数在MProof.verify(),真正验证的地方在Proof.verify()中。其中已知的列表rs,依次提供$r_1,r_2,r_3$且$r_1$始终为rs[0];列表cs依次提供$c_1,c_2,c_3$,$c_1$始终为cs[0],并且作为$m_1=k$的密文,列表cs所有元素的乘积作为AES的密钥,目标是获得完整的cs。

分析Proof.pre_work_verify()生成参数的关系(在模$n^2$下):

分析Proof.verify()验证中(在模$n^2$下):

验证通过需要满足两个条件:

显然第一个等式成立。对第二个式子代入变形,用$\hat{r}$表示Paillier加密时的未知随机数:

在模$n^2$下,任意的$\hat{r}$都满足${(\hat{r}})^n\equiv 1(\bmod n^2)$,结合$D(c_2)=m_2$,所以真正验证的是如下关系:

因为$m_1=k$,第一轮$m_1\cdot m_1=m_2$,依次算出所有的$m_i=k^{i}$。现在知道了Paillier加密的所有明文,cs列表是其对应的密文集合。题目不太严谨的地方是并没有说明加密中所用的随机数是rs,只能猜测:

import base64 
from Crypto.Util.number import *
from Crypto.Cipher import AES
from functools import reduce
import gmpy2 as gy

k = 11279504534075664648994853756208309836888259081316987836068134565254532996039360827546417609514423428910384542842117375028420780633866653159695275393368525
d = 10221665185656075870670995630062725196664430105025904882602057721789662854574978018794504264934209067929011475979200882531626936657436063908347321906634698
pk = [48818225666351727287207574448645102762537417687444983843100304919285725062598967011116889972414214836241381602471131430626879759533369143920487299838261148460925610689261484965443528954172944323770181595455493018560585886136460076911284321702606299935385905187490733877700172236929823996030434548431963845417, 48818225666351727287207574448645102762537417687444983843100304919285725062598967011116889972414214836241381602471131430626879759533369143920487299838261148460925610689261484965443528954172944323770181595455493018560585886136460076911284321702606299935385905187490733877700172236929823996030434548431963845418]
rs = [7017835444643249654271082822123622319668823350427792339866057586668389112104752762656197513200543360993520983724957230042201508973227162755834455588213955577427129770220944523362675094151087610149577572745132843096125306714661937578722687485657829513505193501779196974947234280267216675125252078851200384511, 11926419785881734844311665235521692826579887231998046623960612782040667947241499738956574205257924912658501445776452678070116549164041547529876699253294424780763676555577471679864402445858336882095147665188840642566915806261768356998922735189200720213585973193760859762565499482788728104626011895319246070666, 42849056994192303821377400100288133996583852078950720125488290595760149056455393645072193770289570081775019724341661792638180771853532605128714550478335577195726229688940314874397525687446284173037582151644404387768530054222357908795147990398041926171515316417608763369700129500666902309717940977358807727525, 569456034348887900543839220414996643340590767934767190156995758515165897120739581783360541347732807472270912131379832279120488198273011491688831841399829273608264595546111772893429063435258947915507670361144515456182164834954215193382047579447153761545329658264080907041640135001986168270265476743567482692, 3882702856238247498776796661311784006527802920550510293689922477675526354665322642479849874736259591551374176143098918760791897417239069537473431992448934909453262242143947844529634470612342500819944459175262578570970986439077317400529787443727039336276584314877540411569781766947088676007567558786204756622, 34782235652997768305926510863494232383450368915240504241174297907018847182768375070693257841105403770877838315164696945795337955510221866283665914601590428884905858716454103802020812133141150426745257298797890891272342044576267512614330845263269719988690131903885166576787904418689220619969803389055644879283, 41592967491176047854989428284345162501007190601227408901070248204262133730525966700579489348638677097892234799114781714483501047161590906530561145043956218048476973681993844049727301616425300184917461748554561251157129032137452031152035104390819324726028213814331358281835310137578997975640720667608761964678, 2578609445428238934369429276761562264275224240462388961086309951471476260768389780485799474959219453117497280460546287104687546269870223859389440049894002336889078632572487931627548008400365190508858547984095575105551066221392543458691134989381950316158817283668533035090342510306180189918351739923749743570]
c = b'Oq5bkAPCCT6W3CskX+2uqtY5wrhs+2DqupzjIJ0u/Yl0m/Ig1/uvrSWdezrqLxHG'
_N = gy.next_prime(2 ** 512)

s = reduce(lambda x,y: (x*y)%_N, [gy.powmod(pk[1], k**(i+1), pk[0]**2) * gy.powmod(rs[i], pk[0], pk[0]**2) % (pk[0]**2) for i in range(8)])
aes = AES.new(str(s)[:32].encode(), AES.MODE_ECB)
f = aes.decrypt(base64.b64decode(c))
print(f)

# b'DASCTF{6c2d3ab0-cd47-ba82-5c51-fd7d4ea332c0}\x00\x00\x00\x00'