WMCTF2022 Crypto wp

两天的WMCTF,整个做下来感觉密码题出得确实蛮好的,很值得学习与复现。在比赛中也能收获很多知识。很棒!明年还来~

先记录一下解出的题目。ocb磕了一下午没磕出来,就等wp吧(应该会有吧~)。

ecc

题目没有给源码,只给了输出文本:

1
2
3
4
5
6
7
flag bits: 606
e = 0x10001
n = 61262574892917665379101848600282751252633178779864648655116434051615964747592676204833262666589440081296571836666022795166255640192795587508845265816642144669301520989571990670507103278098950563219296310830719975959589061794360407053224254135937766317251283933110936269282950512402428088733821277056712795259
c = 16002162436420434728223131316901476099110904029045408221515087977802746863468505266500673611412375885221860212238712311981079623398373906773247773552766200431323537510699147642358473715224124662007742017000810447999989426207919068340364725395075614636875116086496704959130761547095168937180751237132642548997
G = (3364552845709696244757995625685399274809023621531082895612949981433844727622567352338990765970534554565693355095508508160162961299445890209860508127449468 : 4874111773041360858453223185020051270111929505293131058858547656851279111764112235653823943997681930204977283843433850957234770591933663960666437259499093 : 1)
3G = (8240596254289477251157504980772167439041663401504657696787046343848644902166655624353107697436635678388969190302189718026343959470011854412337179727187240 : 4413479999185843948404442728411950785256136111461847698098967018173326770728464491960875264034301169184074110521039566669441716138955932362724194843596479 : 1)

根据常识可以知道,这里包含了RSA参数和ECC上两个点。这套路岂不是和前段时间的corCTF-threetreasures很相似,RSA和ECC共用同一个因子。猜测本题也是把flag分成三块。因此需要根据ECC上两个点的信息求出ECC的参数,然后分解来解RSA。

我们有ECC上两个点的坐标,因此根据ECC方程可以得到两个方程来求出。由于这里与threetreasures题目有一定区别,无法针对的高位使用coppersmith攻击,但是我们还可以利用两个点之间的关系。

具体来讲,我们先用两个点的坐标得到两个多项式,然后可以先在模的意义下求出,然后利用ECC点乘计算公式得到两点坐标之间的关系,而给的坐标是模意义下的,从而可以与做gcd从而分解出。然后常规解RSA,同时求出的值即可。

解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from Crypto.Util.number import *
from sage.matrix.matrix2 import Matrix

flag_bits=606
e = 0x10001
n = 61262574892917665379101848600282751252633178779864648655116434051615964747592676204833262666589440081296571836666022795166255640192795587508845265816642144669301520989571990670507103278098950563219296310830719975959589061794360407053224254135937766317251283933110936269282950512402428088733821277056712795259
c = 16002162436420434728223131316901476099110904029045408221515087977802746863468505266500673611412375885221860212238712311981079623398373906773247773552766200431323537510699147642358473715224124662007742017000810447999989426207919068340364725395075614636875116086496704959130761547095168937180751237132642548997
Gx,Gy = (3364552845709696244757995625685399274809023621531082895612949981433844727622567352338990765970534554565693355095508508160162961299445890209860508127449468 , 4874111773041360858453223185020051270111929505293131058858547656851279111764112235653823943997681930204977283843433850957234770591933663960666437259499093 )
G3x,G3y = (8240596254289477251157504980772167439041663401504657696787046343848644902166655624353107697436635678388969190302189718026343959470011854412337179727187240 , 4413479999185843948404442728411950785256136111461847698098967018173326770728464491960875264034301169184074110521039566669441716138955932362724194843596479 )

R.<a,b>=PolynomialRing(Zmod(n))
I=[]
I.append(Gx^3+a*Gx+b-Gy^2)
I.append(G3x^3+a*G3x+b-G3y^2)
res=Ideal(I).groebner_basis()
a=int(-res[0].constant_coefficient())
b=int(-res[1].constant_coefficient())
lam1=(3*Gx^2+a)*pow(2*Gy,-1,n)
G2x=lam1^2-2*Gx
G2y=lam1*(Gx-G2x)-Gy
lam2=(G2y-Gy)*pow(G2x-Gx,-1,n)
G3x_=lam2^2-G2x-Gx
p=int(gcd(G3x_-G3x,n))
assert isPrime(p)
q=n//p
d=inverse(e,(p-1)*(q-1))
m=int(pow(c,d,n))
a=a%p
b=b%p
flag=(a<<404)+(b<<202)+m
print(long_to_bytes(flag))
#$$U_c0u1d_s01v3_e11iptiCurv3_s0_34sily$$0f19d82199a0db0dee31fa12330307ea90aa

homo

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from Crypto.Util.number import * 
from secret import flag
from random import *
assert flag.startswith(b'wmctf{') and flag.endswith(b'}')
flaglen = len(flag) - 7
flag = bytes_to_long(flag[6:-1])
pbit = 114
qbit = 514
rbit = 191
keylen = 981
nouse = 0
def keygen():
pk = []
sk = getPrime(qbit)
for i in range(keylen):
print(i)
p = getPrime(pbit)
r = getPrime(rbit)
pk.append(p*sk + 2*r)
return pk , sk

def enc(m , pk):
c = []
m = [int(i) for i in bin(m)[2:]]
print(m)
for i in m:
tempc = i
for j in range(keylen):
if randint(0 , 1):
tempc += pk[j]
c.append(tempc)
return c

pk , sk = keygen()
print(sk)
c = enc(flag , pk)

f = open('./pubkey.txt' , 'w')
f.write(str(pk))
f.close()

f = open('./cipher.txt' , 'w')
f.write(str(c))
f.close()
print([(i%sk)%2 for i in c])
print(sk)

怎么哪里都有homo啊!(恼)

还有你这个参数是怎么回事啊喂!

说正经的,本题的homo应该指的是同态加密(Homomorphic Encryption, HE),而本题也正是整数上基于近似最大公因子(Approximate-GCD, AGCD)的全同态加密方案(Fully Homomorphic Encryption, FHE)。而该加密方案早在2012年被Gu Chunsheng指出存在一种启发式攻击方法,能够借助格基规约算法,能在多项式时间内无需恢复secret key(即本题中的)而直接还原明文。详情可具体参考论文,论文链接在此。

论文攻击的大致思想是,添加一种小的噪声项来消除部分的影响,从而根据奇偶性直接判断明文的比特位。整个过程可以巧妙利用公钥根据参数设置来构造格,并对规约出的最短基向量进行判断来完成。

好吧,其实原理也只是略懂一点,但是做题已经够了。根据该攻击方案可以写出对应的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from Crypto.Util.number import *

cipher=eval(open('cipher.txt','r').read())
pubkey=eval(open('pubkey.txt','r').read())
l=28
res=''
for c in cipher:
M=Matrix(ZZ,1+l,2+l)
col0=vector([c]+pubkey[1:l]+[pubkey[0]])
M.set_column(0,col0)
for i in range(1+l):
M[i,i+1]=1
ML=M.LLL()
c1=[ i%2 for i in ML.column(0)]
c2=[ i%2 for i in ML.column(1)]
if c1[:-1]==c2[:-1]:
res+='1'
else:
res+='0'
flag=long_to_bytes(int(res,2))
print(flag)
#sodayo>A<!!$%!$_Easy_G@CDp_ATtaCk

nanoDiamond & nanoDiamond-rev

nanoDiamond-rev没搞出来,呜呜还是太菜了,这里先蹲一手大佬们的wp,到时复现一下。

nanoDiamond就先不码了,毕竟能打通后者肯定能打通前者。

INTERCEPT

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import os
import string
import signal
from gmpy2 import *
from Crypto.Util.number import *
from hashlib import sha256, sha512, md5
from random import randint, seed, choice
from SECRET import FLAG

class KGC:
def __init__(self, lam):

self.lam = lam
while 1:
self.z = 2 * randint(1 << 63, 1 << 64)
self.p = self.genPrime(self.z)
self.q = self.genPrime(2)
self.N = (self.z * self.p + 1) * (2 * self.q + 1)

self.s1, self.s2 = randint(1, self.N), randint(1, self.N)
if self.s1 % 2 == self.s2 % 2:
self.s1 += 1

self.g = randint(1, self.N)
check = pow(self.g, self.p * self.q, self.N) != 1 and pow(self.g, self.z * self.p, self.N) != 1 and pow(self.g, self.z * self.q, self.N) != 1
while not check:
self.g = randint(1, self.N)
check = pow(self.g, self.p * self.q, self.N) != 1 and pow(self.g, self.z * self.p, self.N) != 1 and pow(self.g, self.z * self.q, self.N) != 1

break

self.g1, self.g2, self.g3 = (pow(self.g, self.p * self.s1, self.N),
pow(self.g, self.p * self.s2, self.N),
pow(self.g, self.p ** 2, self.N))

def genPrime(self, delta):
prime = 1
while not isPrime(prime):
prime = delta * getPrime(self.lam // 2 - delta.bit_length()) + 1
return (prime - 1) // delta

def H1(self, m):
return int(sha256(m.encode()).hexdigest(), 16)

def H2(self, m):
return int(sha512(m.encode()).hexdigest(), 16)

def H3(self, m):
return int(md5(m.encode()).hexdigest(), 16)

def KeyGen(self, username):
h1, h2 = self.H1(username), self.H2(username)
y1 = invert(self.p, self.z * self.q) * h1 % (self.z * self.q)
y2 = invert(self.p, self.z * self.q) * h2 % (self.z * self.q)
d = invert(y1 * self.s1 + y2 * self.s2, self.z * self.q)
e = pow(self.g1, h1, self.N) * pow(self.g2, h2, self.N) % self.N

return (y1, y2, d, h1, h2, e)

def enc(self, m, user):
length_limit = 2**128
r = randint(1, self.N)
F = pow(self.g3, r, self.N)
c1 = (m % length_limit) ^ self.H3(str(F))
c2 = pow(user.e, r, self.N)
return (c1, c2)

def dec(self, c, user):
c1, c2 = c
F = pow(c2, user.d, self.N)
m = long_to_bytes(self.H3(str(F)) ^ c1)
return m


class USER:
def __init__(self, id_, kgc):
self.id_ = id_
self.y1, self.y2, self.d, self.h1, self.h2, self.e = kgc.KeyGen(id_)

def print_info(self):
print(f'e, d, h1, h2 = ({self.e}, {self.d}, {self.h1}, {self.h2})')

def register(username, kgc):
user = USER(username, kgc)
return user

def intercept():
while True:
try:
informant = 'WMCTF_IS_FUN_' + str(getRandomNBitInteger(512)) # randomize a big string
USERLIST[informant] = register(informant, kgc)
INFORMATION = randint(1 << 127, 1 << 128)
return informant, INFORMATION
except:
pass

def proof_of_work():
seed(os.urandom(8))
proof = ''.join([choice(string.ascii_letters + string.digits) for _ in range(20)])
digest = sha256(proof.encode()).hexdigest()
print("sha256(XXXX+%s) == %s" % (proof[4:], digest))
print('Give me XXXX: ', end = '')
x = input().strip()
if len(x) != 4 or sha256((x + proof[4:]).encode()).hexdigest() != digest:
return False
return True

def BANNER():
print(
'''
|--------------|
| [P]arams |
| [R]egister |
| [E]ncrypt |
| [D]ecrypt |
| [I]ntercept |
| [G]etflag |
|--------------|
''')


if __name__ == "__main__":

if not proof_of_work():
exit()


LAM = 512
LIMITED_USER_NUM = 6
kgc = KGC(LAM)
USERLIST = {}
informant, INFORMATION = intercept()

signal.alarm(5)
BANNER()
while True:
op = input('Input your choice please: ')
if op == 'P':
print(f'Here gives you some params about the KGC: {kgc.N}, ({kgc.g1}, {kgc.g2}, {kgc.g3})')

elif op == 'R':
if len(USERLIST) >= LIMITED_USER_NUM:
print('No more new registration allowed!')
continue

username = input('Input your name: ').strip()
try:
if username not in USERLIST:
USERLIST[username] = register(username, kgc)
print('Registration success, keep your account well!')
USERLIST[username].print_info()
else:
print('This username has been taken!')
except:
print('Registration Not Allowed!')

elif op == 'E':
username = input('Input your name: ').strip()
try:
if username in USERLIST:
message = input('Input your message in HEX: ').strip()
cipher = kgc.enc(int(message, 16), USERLIST[username])
print(f'Here is the ciphertext: {cipher}')
else:
print('This user does not exist!')
except:
print('Bad Request!')

elif op == 'D':
username = input('Input your name: ').strip()
prikey = input('Input your private key: ').strip()
try:
if username in USERLIST and USERLIST[username].d == prikey:
c1, c2 = input('Input ciphers(c1, c2) in HEX: ').strip()[1:-1].split(', ')[0:2]
c1, c2 = int(c1, 16), int(c2, 16)
m = kgc.dec((c1, c2), USERLIST[username])
print(f'Here is the message: {m}')
else:
print('Bad Request!')
except:
print('Bad Request!')

elif op == 'I':
CIPHER = kgc.enc(INFORMATION, USERLIST[informant])
print(f'Here you intercepted a piece of message {CIPHER} sent by {informant}...')

elif op == 'G':
message = str(input('Input right messages in HEX and you\'ll get your bonus: '))

try:
message = int(message, 16)
except:
print('Illegal input!')
continue

if INFORMATION == message:
print(f'Here is your flag: {FLAG}!')
else:
print('You really let me down..')
exit()

else:
print('Have a nice day!')
exit()

题目代码很长,参数很多,看到第一眼就有种看不下去想放弃的感觉,但还是凭着学习的本心硬着头一点点啃下去了。

这里先大致捋一下题目的逻辑。

首先有一个全局KGC用于产生用户密钥,其中KGC中包含以下参数: KGC公开参数

然后提供了用户注册功能,根据用户的id,KGC会为其分配公私钥,产生的公私钥的参数如下: 其中参数用户保留可见。

接下来是用户加密消息功能,过程如下: 密文为

同时提供解密功能,需要提供私钥,其对应解密为: 加解密的过程可以稍微手动推一下,是没有问题的~

然后回到题目的意图,题目模拟拦截了一个随机用户发送消息的流量,已知的只有该用户的id和该随机消息的密文。我们需要还原出该消息来提供给系统获取flag。可以利用的条件包括不超过6次的用户注册所提供的相关信息(用户id不能有相同),以及任意次数的加密解密功能。

常规思路就是尝试去获取该用户的私钥了。我们从现有的已知条件和需求出发,已知不超过6组的,尝试获取指定的(用户id的哈希)对应的

这看起来似乎是可行的,我们先对一些公式做一些变形: 这样一来,我们将未知参数用已知参数来表示。

由于是KGC的共用参数,那么对于任意两组,我们有: 这里未知。但仔细观察该式特点,我们再使用两组来构造以上关系,然后等式两边交换相乘即可同时消去!也就是说我们可以拿到仅用已知参数来表示的,且模为0的式子!那么故技重施再算一次,然后求gcd,即可拿到模数

后面就比较简单了,我们可以根据 得到之间的线性关系(无需求出的值),假设为

因此有: 因此对于指定,对任意我们有: 这样就求出了该用户的私钥

注意这里由于gcd和取模运算等原因,该可能不是准确值,因此提交到系统的解密功能中可能会出错。但我们手动进行解密仍然能计算出正确的消息明文。

该题至此已经解出。然而比赛时还有坑的一点是,由于用户id和消息是随机的,因此很多地方的求逆运算结果都是不存在逆元,因此想要使程序按正确路线跑完还是比较费劲的,得多跑几次(一度怀疑自己的推理逻辑对不对,还重新看了好几遍)。加之服务器产生KGC参数有时非常慢,当时我是做吐了—_—!

解出后和出题人交流过程中,发现该解法竟是非预期。预期解是打穿KGC???再蹲一波预期解好吧~

解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
from pwn import *
import re
import string
from hashlib import sha256,sha512,md5
from itertools import product,combinations,permutations
from functools import reduce
from collections import Counter
from gmpy2 import invert
from Crypto.Util.number import *

context.log_level = 'debug'

sh=remote('1.13.154.182',30126)
l64 = lambda :u64(sh.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
l32 = lambda :u32(sh.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
sla = lambda a,b :sh.sendlineafter(str(a),str(b))
sa = lambda a,b :sh.sendafter(str(a),str(b))
lg = lambda name,data : sh.success(name + ": 0x%x" % data)
se = lambda payload: sh.send(payload)
rl = lambda : sh.recvline()
rv = lambda n : sh.recv(n)
sl = lambda payload: sh.sendline(payload)
ru = lambda a :sh.recvuntil(str(a))
rud = lambda a :sh.recvuntil(str(a),drop=True)

def solve_pow():
s=rl().decode()
suffix,val=re.findall('sha256\(XXXX\+(\w{16})\) == ([0-9a-f]{64})',s)[0]
tab=string.digits + string.ascii_letters
for item in product(tab,repeat=4):
ans=''.join(item)
if sha256((ans+suffix).encode()).hexdigest()==val:
sla('XXXX: ',ans)
return

def Param():
sla('choice please: ','P')
ru('about the KGC: ')
N=int(rud(', ('))
g1=int(rud(', '))
g2=int(rud(', '))
g3=int(rud(')\n'))
return N,g1,g2,g3

def Intercept():
sla('choice please: ','I')
ru('of message ')
cipher=rud(' sent by ')
informant=rud('...\n').decode()
return eval(cipher),informant

def Register(name):
sla('choice please: ','R')
sla('Input your name: ',str(name))
if b'Not Allowed!' in rl():
name=bytes_to_long(name.encode())
new_name=long_to_bytes(name+1)
return Register(new_name.decode())
info=rl()[16:-2].decode()
e,d,h1,h2=list(map(int,info.split(', ')))
return e,d,h1,h2,name

def Decrypt(name,d,c1,c2):
sla('choice please: ','D')
sla('Input your name: ',name)
sla('private key: ',d)
cip=f'({c1}, {c2})'
if b'Bad' in rl():
return None
sl(cip)
ru('message: ')
message=rl()[:-1].decode()
return message

def GetFlag(m):
sla('choice please: ','G')
sla('bonus: ',hex(m)[2:])

def solve():
solve_pow()
N,g1,g2,g3=Param()
CIPHER,informant=Intercept()
c1,c2=CIPHER
H1=int(sha256(informant.encode()).hexdigest(), 16)
H2=int(sha512(informant.encode()).hexdigest(), 16)
e1,d1,h11,h21,name1=Register('zealot_aaa')
e2,d2,h12,h22,name2=Register('zealot_bbb')
e3,d3,h13,h23,name3=Register('zealot_ccc')
e4,d4,h14,h24,name4=Register('zealot_ddd')
e5,d5,h15,h25,name5=Register('zealot_eee')
arr=[(e1, d1, h11, h21),(e2, d2, h12, h22),(e3, d3, h13, h23),(e4, d4, h14, h24),(e5, d5, h15, h25)]
tmp=[]
for a,b,c in permutations(arr,3):
l=(a[1]*a[2]-b[1]*b[2])*(c[1]*c[3]-a[1]*a[3])
r=(b[1]*b[3]-a[1]*a[3])*(a[1]*a[2]-c[1]*c[2])
tmp.append(l-r)
zq=reduce(GCD,tmp)
#print(zq)
#print(zq.bit_length())
#s1=k*s2
klist=[]
for a,b in permutations(arr,2):
try:
t=((a[1]*a[3]-b[1]*b[3])*invert(b[1]*b[2]-a[1]*a[2],zq))%zq
klist.append(t)
except:
continue
klist=list(set(klist))
print(klist)
if(len(klist)!=1):
exit(0)
D=d1*(h11*klist[0]+h21)*invert(H1*klist[0]+H2,zq)%zq
F=pow(c2,D,N)
HF=int(md5(str(F).encode()).hexdigest(), 16)
m=c1^HF
GetFlag(m)

solve()

sh.interactive()
#WMCTF{cracking_such_a_toy_system_is_so_easy!}
Author

zealot

Posted on

2022-08-22

Updated on

2022-08-27

Licensed under