这次 tough cookie 分类没有全部做出来,有一道题卡住了,想到了可能的解法,但是没有去实践

实际上就是事后诸葛亮

Asemoon

题目描述

题目基于 $GF(2^{64})$ 中的运算,首先生成了一个模多项式 $g$ 保证 $g$ 不可约,然后代码中的 CP 是 $g$ 的 反向二进制 表示,即,如果 $g(x) = x^{64} + \sum_{i=0}^{63} a_i x^i$,则 $CP = \sum_{i=0}^{63} a_i 2^{63-i}$

然后是 CT 的计算,遍历 $b = 0, 1, \ldots, 255$,代码中的位运算也要 反着看 才会比较符合“多项式取模”的运算:

  • if cb & 1: 也就是如果 $b$ 对应的多项式高位是 $x^{63}$
  • cb = (cb >> 1) ^^ CP: $b$ 对应的多项式乘以 $x$,再模 $g(x)$
  • cb >>= 1: 否则 $b$ 对应的多项式乘以 $x$,因为乘完之后的多项式高位不是 $x^{64}$,所以没必要模 $g(x)$

所以对应的多项式也要反着看,譬如

  • $b=1$ 对应的多项式是 $f(x) = x^{63}$
  • $b=2$ 对应的多项式是 $f(x) = x^{62}$
  • $b=5$ 对应的多项式是 $f(x) = x^{63} + x^{61}$

然后算出来的 CT 就是 $x^8 f(x) \bmod g(x)$

题目生成一个随机的 64 位数 $s$,并提供 2 个运算:

recheck 一个根据 CT 进行迭代运算的“哈希函数”,接受一个消息 $m$ 和一个初始值 $v$,用以下代码计算

把上面的操作转换为 $GF(2^{64})$ 上的多项式运算,就是

消息的分量 $m_i$ 对应多项式的法则和上面相同,也是把 $1$ 映射到 $x^{63}$,把 $5$ 映射到 $x^{63} + x^{61}$……

第二行的转换比较好理解,第三行的转换可以把右移 8 位看成是把至多位 $x^{55}$ 的乘上 $x^8$,CT 的代换则是把 $x^{56}$ 到 $x^{63}$ 的部分先乘了个 $x^8$,再模多项式 $g(x)$(注意 CT 的定义),所以把两部分合起来就是在乘一个 $x^8$

举个例子,如果消息是 CNM,那么

  • C 对应 67,01000011,对应多项式 $x^{63} + x^{62} + x^{57}$
  • N 对应 78,01001110,对应多项式 $x^{62} + x^{61} + x^{60} + x^{57}$
  • M 对应 77,01001101,对应多项式 $x^{63} + x^{61} + x^{60} + x^{57}$

所以 recheck(CNM, 0) 迭代到最后的多项式是

也就是

然后加上常数项,recheck(CNM, v) 迭代到最后,还要加个常数项 $x^{24} v(x)$

verify 输入初始值 $v$ 和当前时间戳 $t$,验证 recheck(s xor t, v) 是否等于 $v$。由之前对 recheck 的分析容易知道当 $t = s$ 的时候,上式恒成立。 但是 $s$ 是生成的随机未知数,$t$ 是当前时间戳,由此可见上句话在说个 JB。

然后题目给出了两个交互选项:

检验 token 输入 $v$ 使得 verify 通过。若能通过,则给 flag

获取信息 返回以下值:

  • recheck('CCTF', recheck(s xor t, 0))
  • 一个随机 64 位素数 $r$
  • $5^{CP} \bmod r$

试获取 flag

我的解答

首先是获取 $CP$。因为 $r$ 是 64 位素数,所以离散对数是可以直接求出来的。而且因为可以多次获取信息,还可以只挑那些光滑的来解离散对数。

然后是通过上面的两个 recheck 嵌套获取 $s \oplus t$ 的值。

由上面对 recheck 函数的分析,实际上就是减去(加上,反正这两运算在 $GF(2^{64})$ 里面一个意思) CCTF 对应的多项式即可

然后是通过上面的 recheck 检测,由上面的分析等价于解一个方程:

所以就是直接解就行了:

因为 $t$ 是 10 秒一变的时间戳,我们这里没有啥耗时的操作,所以就当作是定值,懒得调了。(要是在代码运行的过程中 $t$ 变了那就证明比较倒霉)。然后就是实现和 debug 了:

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
from pwn import *
from sage.all import *

class Gao:
def __init__(self):
self.conn = process(['sage', 'another.sage'])

def get_public_params(self):
self.conn.sendlineafter('[Q]uit\n', 'P')
m = []
for i in range(3):
self.conn.recvuntil('is:')
mi = int(self.conn.recvline())
m.append(mi)
return m

def solve_cp(self):
mi, ni = [], []
for i in range(2):
_, r, c = self.get_public_params()
g = Zmod(r)(5)
c = Zmod(r)(c)
d = c.log(g)
mi.append(d)
ni.append(g.multiplicative_order())
m = crt(mi, ni)

PR, x = PolynomialRing(GF(2), 'x').objgen()
fm = PR(ZZ(m).digits(2, padto=64)[::-1]) + x**64
F, x = GF(2**64, 'x', modulus=fm).objgen()

self.F = F
self.x = x

def to_field(self, m):
return self.F(ZZ(m).digits(2, padto=64)[::-1])

def from_field(self, f):
fl = ZZ(f.integer_representation()).digits(2, padto=64)
return ZZ(fl[::-1], base=2)

def solve_sxort(self):
F, x = self.F, self.x

res, _, _ = self.get_public_params()
c = self.to_field(res)

msg = b'CCTF'
f = 0
for m in msg:
my_bits = ZZ(m).digits(2, padto=8)[::-1]
my_part = sum([my_bits[i] * x**i for i in range(8)])
f += x**56 * my_part
f *= x**8
sxort = (c - f) / x**32

self.sxort = sxort

def solve_v(self):
F, x = self.F, self.x
sxort = self.sxort

v = sxort / (x**64 + 1)
vnum = self.from_field(v)

self.conn.sendlineafter('[Q]uit\n', 'L')
self.conn.sendlineafter('here:\n', f'{vnum:x}')

def gao(self):
self.solve_cp()
self.solve_sxort()
self.solve_v()
self.conn.interactive()

if __name__ == '__main__':
g = Gao()
g.gao()

然后在题目的 gen_CP 中,有可能 $c$ 的高位不为 0,转换会失败。不过这个概率比较小,碰上了就再来一次就行了。flag 为:

1
CCTF{CRC_0v3R_quO7i3nT_r!n9S}

Goliver II (x)

题目描述

题目给了一个特殊版本的 ECDSA。记已知曲线为 $E_p$,并已知生成元 $G$。每次连接的时候生成一个私钥 $x$,并且算出对应的、可获取的公钥 $P = xG$。签名过程均对 flag 的前半 $t$ 签名,过程如下:

  1. 输入一个 ID $k$
  2. 算出消息的哈希 $h = H(x_h)$
  3. 算出 $Q = (t+k)G$
  4. 记 $r = x_Q$
  5. 记 $s = (t + k)^{-1} (h + r x) \bmod n$
  6. 服务器仅给出 $s$ 的值

每次连接服务器可以执行至多 3 次的签名,且单次连接中,$k$ 的选择不能重复。

若能在某次连接时,把对应私钥 $x$ 给求出来,那么就能得到 flag。

我们的尝试

因为 $t$ 为 flag 的前半部分,为定值,不难想到多次连接时可能能利用这个不变量做一些文章。

重新写一下问题形式:记执行签名次数为 $j = 1, 2, 3$,记服务器连接次数为 $i = 1, 2, 3, \ldots$。因为服务器仅给出 $s$ 的值,所以只能依赖式子

这里主要难搞的有两点

  • $r^{(i)}_j$ 未知
  • $h$ 未知

但是由 $r$ 的计算过程,可以把 $r$ 定死在某些变量里面:

  • 令 $k = n, 2 n, 3n$,则 $r$ 的值相同,由上面那个式子,会发现 $s_j{i}$ 也相同,也就是说没有通过 3 次输入,获取到任何有价值的信息。

  • 令 $k = k_0, n - 2 t - k_0$,这样算出的 $Q$ 恰好一个是 $Q$,一个是 $-Q$。它们的横坐标相等,所以可以获得相同的 $r$,可以消掉一个变量。但是因为 $t$ 未知,所以没法操作。

    • 因为 $t$ 是 flag 的前半段,所以 赌 flag 较短,也就是可以通过枚举 CCTF{xxx 来确定 $t$ 的值。如果这样的话,$h$ 的值相当于也有了。则有:
    • $(t+k_0) s_1 \equiv h + r x \pmod{n}$
    • $-(t+k_0) s_2 \equiv h + r x \pmod{n}$
    • 也就是要检查两式的左边是否相等;如果相等,则 flag 前半段 $t$ 值直接被猜出
    • 很遗憾,对不上,寄
  • 令 $k = -1, 0, 1$,则 3 次交互的 $r$ 分别为 $x_{(k-1)G}, x_{kG}, x_{(k+1)G}$。当然这里有一个问题:题目会检验 $k>0$ 要满足,但是没有检验其他的,所以实际输入的时候可以改为 $k = n-1, n, n+1$,则题目式子可以被写成

    • $(t + k_j) s_j^{(i)} \equiv h + r_j x^{(i)} \pmod{n}$

如果我们一共有 $N$ 次连接,那么一共 $3N$ 次交互就有 $3N$ 个式子,但是未知量仅有 $t, h, r_j, x^{(i)}$,数量仅有 $5 + N$ 个,所以是管够的。

然后 沛公 就做了一版尝试:

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
from goliver_ii import *

p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
a, b = 0, 7
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
x = 0x4F22E22228BD75086D77AE65174C000F132BFD4EF3E28BEF20AC476997D4444F
y = 0x3456B224247A4F73BF187AC25864F8F694C078380E6BDDF51379AC33F18BD829


def _prepare(sign_id, msg):
k = sign_id + int.from_bytes(msg, "big")
r, _ = MUL(G, k)
hmsg = int.from_bytes(sha256(msg).digest(), "big")
return k, r, hmsg

def sign(sign_id, msg):
k, r, hmsg = _prepare(sign_id, msg)
s = (inverse(k, n) * (hmsg + r * skey)) % n
return s, k, r, hmsg

def m_resultant(p1, p2, var, field):
p1 = p1.change_ring(QQ)
p2 = p2.change_ring(QQ)
var = var.change_ring(QQ)
r = p1.resultant(p2, var)
return r.change_ring(field)

P.<k,hmsg, r1,r2,r3,sk> = PolynomialRing(Zmod(n))

skey = getRandomRange(1, p)
s1, k1, rr1, hmsg1 = sign(1, flag[:len(flag) >> 1])
s2, k2, rr2, hmsg2 = sign(2, flag[:len(flag) >> 1])
s3, k3, rr3, hmsg3 = sign(3, flag[:len(flag) >> 1])
skey = getRandomRange(1, p)
s4, k4, rr4, hmsg4 = sign(1, flag[:len(flag) >> 1])
s5, k5, rr5, hmsg5 = sign(2, flag[:len(flag) >> 1])
s6, k6, rr6, hmsg6 = sign(3, flag[:len(flag) >> 1])
skey = getRandomRange(1, p)
s7, k7, rr7, hmsg7 = sign(1, flag[:len(flag) >> 1])
s8, k8, rr8, hmsg8 = sign(2, flag[:len(flag) >> 1])
s9, k9, rr9, hmsg9 = sign(3, flag[:len(flag) >> 1])
assert k1 == k4 == k7 and k2 == k5 == k8 and k3 == k6 == k9
assert rr1 == rr4 == rr7 and rr2 == rr5 == rr8 and rr3 == rr6 == rr9
assert hmsg1 == hmsg2 == hmsg3 and hmsg1 == hmsg1 == hmsg1 and hmsg1 == hmsg1 == hmsg1


P.<k, hmsg, r1, r2, r3, sk1, sk2, sk3> = PolynomialRing(Zmod(n))
f1 = hmsg + r1* sk1 - k*s1
f2 = hmsg + r2* sk1 - (k+1)*s2
f3 = hmsg + r3* sk1 - (k+2)*s3
f4 = hmsg + r1* sk2 - k*s4
f5 = hmsg + r2* sk2 - (k+1)*s5
f6 = hmsg + r3* sk2 - (k+2)*s6
f7 = hmsg + r1* sk3 - k*s7
f8 = hmsg + r2* sk3 - (k+1)*s8
f9 = hmsg + r3* sk3 - (k+2)*s9

g1 = m_resultant(f1, f2, sk1, Zmod(n))
g2 = m_resultant(f1, f3, sk1, Zmod(n))
g3 = m_resultant(f4, f5, sk2, Zmod(n))
g4 = m_resultant(f4, f6, sk2, Zmod(n))
g5 = m_resultant(f7, f8, sk3, Zmod(n))
g6 = m_resultant(f7, f9, sk3, Zmod(n))

结果打印出来的多项式都不太行。

赛后复盘

赛后发现,其实就该这样做,不过当时没有继续细想,所以就放弃了。

其实感觉和去年那道 Rehawk 一样,在碰到多元多次方程的时候,没搞懂自己到底要什么东西。其实这里最关键的就是 $t$

所以就不管其他那些乱七八糟的东西,直接上 groebner basis 就行了,然后就会发现 grobner basis 里面分别有关于 $h$ 和 关于 $t$ 的一元一次多项式,直接解就行了,然后稍微检查一下解的前缀满足 CCTF{ 格式。

如果 $t$ 和 $h$ 的值已经求出来了,相当于 $t+k$ 也知道,$r$ 也知道,相当于可以直接解出 $x$ 了。代码如下:

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
from pwn import *
from sage.all import *
from Crypto.Util.number import *

class Gao:
def __init__(self):
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
a, b = 0, 7

self.E = EllipticCurve(Zmod(p), [a, b])
self.n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
x = 0x4F22E22228BD75086D77AE65174C000F132BFD4EF3E28BEF20AC476997D4444F
y = 0x3456B224247A4F73BF187AC25864F8F694C078380E6BDDF51379AC33F18BD829
self.G = self.E(x, y)

self.cnt = 7

def init_conn(self):
self.conn = process(['python3', 'another_2.py', '-x', f'{self.cnt}'])
self.cnt += 7

def get_data(self):
self.init_conn()
slist = []
for i in range(4):
self.conn.sendlineafter('[Q]uit\n', 'S')
self.conn.sendlineafter('sign_id:\n', f'{self.n + i}')
self.conn.recvuntil('s = ')
si = int(self.conn.recvline())
slist.append(si)
self.conn.close()
return slist

def gao(self):
N = 4
var_names = ['h', 't', 'r_0', 'r_1', 'r_2', 'r_3'] + [f'x_{i}' for i in range(N)]
PR, my_vars = PolynomialRing(Zmod(self.n), var_names).objgens()
my_vars = list(my_vars)
h, t = my_vars[:2]
r = my_vars[2:6]
x = my_vars[6:]

eqs = []
ans = [0] * (6 + N)
ans[0] = 74193004650945221374126951237762386983759461703132362510748817470699398372824
ans[1] = 349249487944061721355322256519033970
for i in range(N):
slist = self.get_data()
for j in range(4):
sij = slist[j]
eqs.append((t + j) * sij - (h + r[j] * x[i]))
# CHECK
E = self.E
G = self.G
t_ = ans[0]
h_ = ans[1]
k_ = t_ + j
R = k_ * G
rj_ = ZZ(R.xy()[0])
xi_ = (i+1) * 7
LHS = k_ * sij % self.n
RHS = (h_ + rj_ * xi_) % self.n
ans[2 + j] = rj_
ans[6 + i] = xi_

B = Ideal(eqs).groebner_basis()
print(B)
h_eq = B[1]
t_eq = B[2]

h = ZZ(-h_eq.coefficients()[1])
t = ZZ(-t_eq.coefficients()[1])
print(f'{long_to_bytes(int(t)) = }')

self.init_conn()
self.conn.sendlineafter('[Q]uit\n', 'S')
self.conn.sendlineafter('sign_id:\n', f'{self.n}')
self.conn.recvuntil('s = ')
s = int(self.conn.recvline())
Q = t * self.G
r = Q.xy()[0]
x = (t * s - h) * inverse_mod(r, self.n) % self.n
self.conn.sendlineafter('[Q]uit\n', 'G')
self.conn.sendlineafter('private key: \n', f'{x}')
self.conn.interactive()

if __name__ == '__main__':
g = Gao()
g.gao()

真滴可惜,我他妈比赛的时候也不知道干啥去了,也没有拿着沛公的代码多调两下

然后就是,不知道为什么自己手搓的通过 sylvester_matrix 再求行列式的土味 resultant 不能解出 $t$ 来,麻

Juliax

题目描述

题目基于 CRT-RSA,$p$, $q$ 均为 512 bit,$e$ 为 72 bit,有 $d_p$ 和 $d_q$ 的低 199 位泄露,其中

已知 $N = pq$,$e$,$c = m^e \bmod{N}$,求 $m$。

我的解答

想到 CRT-RSA,就想到了之前 0CTF 有过的一道 ezRSA++,直接把 idek 的 脚本 抄来跑了,不过要改一些地方

  • 各未知量的界($p, k, l$)
  • 参考 论文 的 Table 1,当 $\beta=0.5$ 的时候,$\sigma$ 为 1
  • 额外需要测试规约后可用的多项式个数

改好了之后如下:

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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
m = 3

# Generate test data set
scale_factor = 1
p_size = int(512 * scale_factor)
l_size = int(199 * scale_factor)
d_size = int(512 * scale_factor)
M = 2^l_size

N, e = (113512878655961571626610562291692317083167898593072246908072509473338669866931624486434843922077792562235492835323939380660867587409311081240029070350808655984402585845023288249807250489084430773691893497493957878187939757801622886103893275017257035278212160216032814012251157961899906789943525036078018769313, 4680013789992958764661)
dp_tilde, dq_tilde = (1931999207628789396725122770203483408911326042952326921451, 799504796180001663308018451701479236857150404193865300422493)

def nearest_below(x):
c = floor(x)
if not (c < x):
c -= 1
return c

############# PARAMETERS #################
delta = float((d_size - l_size) / int(N).bit_length())
beta = float((d_size) / int(N).bit_length())
alpha = float((int(e).bit_length()) / int(N).bit_length())
# sigma = .35
sigma = 1.0
tau = float(max(1/2, 1 - 2*beta))
print(f'{beta = }')

X = int(e*2^(d_size - p_size))
Y = int(2^p_size)

print(f'beta: {float(beta)}')
print(f'(beta - delta)/beta: {float((beta - delta)/beta)}')
print(f'sigma: {sigma}')
print(f'delta: {delta}')
print(f'{X.bit_length() = }')
print(f'{Y.bit_length() = }')

P.<xp, xq, yp, yq, zp, zq> = PolynomialRing(ZZ, 6, order = 'lex')
index_map = {g:i for i,g in enumerate(P.gens())}

######################## HELPER DATA #########################
import itertools
M_sigma = [(a,b,c) for a, c, b in itertools.product(range(m + 1), range(m + 1), range(nearest_below(2*sigma*m) + 1))]

M_1 = [(a,b,c) for c in range(m + 1) for a in range(c + 1) for b in range(c - a + 1)]
M_2 = [(a,b,c) for c in range(m + 1) for a in range(c + 1, m + 1) for b in range(a - c)]
M_3 = [(a,b,c) for a in range(m + 1) for c in range(m + 1) for b in range(a + c + 1) if ((a,b,c) not in M_1 and (a,b,c) not in M_2 and (a + b + c) % 2 == 0)]
M_4 = [(a,b,c) for a in range(m + 1) for c in range(m + 1) for b in range(a + c + 1) if ((a,b,c) not in M_1 and (a,b,c) not in M_2 and (a,b,c) not in M_3)]
MM = [(a,b,c) for a in range(m + 1) for c in range(m + 1) for b in range(a + c + 1)]

M_tilde = [(a,b,c) for a, c, b in itertools.product(range(m + 1), range(m + 1), range(2*m + 1))]

# Sort M_sigma
M_tilde.sort(key = lambda p: xp^p[0]*yp^p[1]*zp^p[2])
M_sigma.sort(key = lambda p: xp^p[0]*yp^p[1]*zp^p[2])
MM.sort(key = lambda p: xp^p[0]*yp^p[1]*zp^p[2])

def E_f(a,b,c):
if (a,b,c) in M_1:
return 0
elif (a,b,c) in M_2:
return b
elif (a,b,c) in M_3:
return (a + b - c)//2
elif (a,b,c) in M_4:
return (a + b - c + 1)//2
else:
return a


def E_g(a, b, c):
if (a,b,c) in M_1:
return b
elif (a,b,c) in M_2:
return 0
elif (a,b,c) in M_3:
return (-a + b + c)//2
elif (a,b,c) in M_4:
return (-a + b + c - 1)//2
else:
return c

def E_h(a, b, c):
if (a,b,c) in M_1:
return a
elif (a,b,c) in M_2:
return c
elif (a,b,c) in M_3:
return (a - b + c)//2
elif (a,b,c) in M_4:
return (a - b + c - 1)//2
else:
return 0

def E_x(a, b, c):
if (a,b,c) in M_1:
return 0
elif (a,b,c) in M_2:
return a - b - c
elif (a,b,c) in M_3:
return 0
elif (a,b,c) in M_4:
return 0
else:
return 0

def E_z(a, b, c):
if (a,b,c) in M_1:
return -a - b + c
elif (a,b,c) in M_2:
return 0
elif (a,b,c) in M_3:
return 0
elif (a,b,c) in M_4:
return 1
else:
return 0

def trans(F):
ypi = index_map[yp]
yqi = index_map[yq]
xpi = index_map[xp]
zpi = index_map[zp]
xqi = index_map[xq]
zqi = index_map[zq]

F = P(F)

# Replace all instances of yp*yq by N
new_dict = {}
for t, v in F.dict().items():
num = min(t[ypi], t[yqi])
new_t = list(t)
new_t[ypi] -= num
new_t[yqi] -= num
new_t = tuple(new_t)

if new_t not in new_dict:
new_dict[new_t] = 0

new_dict[new_t] += int(N)^num * int(v)
F = P(new_dict)

# Step 2
F_ = P(0)
for t, v in F.dict().items():
if t[ypi] != 0:
F_ += P({t: v})
continue

new_t = list(t)
xp_pow = t[xpi]
zp_pow = t[zpi]
new_t[xpi] = 0
new_t[zpi] = 0
new_t = tuple(new_t)

mm = P({new_t: int(v)})
F_ += mm*(xq + 1)^xp_pow*(zq - 1)^zp_pow
F = F_

# Step 3
F_ = P(0)
for t, v in F.dict().items():
if t[ypi] == 0:
F_ += P({t: int(v)})
continue

new_t = list(t)
xq_pow = t[xqi]
zq_pow = t[zqi]
new_t[xqi] = 0
new_t[zqi] = 0
new_t = tuple(new_t)

mm = P({new_t: v})
F_ += mm*(xp - 1)^xq_pow*(zp + 1)^zq_pow
F = F_

return F

def lambda_abc(a, b, c):
if b % 2 == 0:
return xq^a*yq^(b//2)*zq^c
else:
return xp^a*yp^((b + 1)//2)*zp^c

# Highest monomial in the (zp, xp, yp) ordering
def highest_monomial(p):
monomials = p.monomials()
monomials.sort(key = lambda g: g(xp, xp, yp, yp, zp, zp))

return monomials[-1]

# Rescale a poly to make the determinant of lattice smaller
def rescale_poly(poly):
g = gcd(N - 1, e*M)

monomials = poly.monomials()
highest = highest_monomial(poly)

d = int(poly[highest])
t = next(iter(highest.dict().keys()))

Xpow = t[index_map[xp]] + t[index_map[xq]] + t[index_map[zp]] + t[index_map[zq]]
Ypow = t[index_map[yp]] + t[index_map[yq]]
xy_pow = X^Xpow * Y^Ypow
assert d % xy_pow == 0
d //= xy_pow

E4 = 0
while d % N == 0:
E4 += 1
d //= N
E5 = 0
while d % (N - 1) == 0:
E5 += 1
d //= N - 1

new_d = int(d) * int(g)^E5 * int(xy_pow)

multiplier = pow(int(N), E4, (e*M)^(2*m))*pow(int((N - 1)//g), E5, (e*M)^(2*m))
multiplier = pow(int(multiplier), -1, (e*M)^(2*m))

p = P(0)
for mm in monomials:
if mm == highest:
p += new_d * mm
else:
p += int(poly[mm]) * int(multiplier) * mm

return p


################ PKE SHIFT POLYS #################
f_tilde = xp*yp - xq - e*dp_tilde
g_tilde = yp*zp - N*zq + e*dq_tilde*yp
h_tilde = N*xp*zq - xq*zp - e^2*dp_tilde*dq_tilde - e*dp_tilde*zp - e*dq_tilde*xq


def p_tilde(a, b, c):
res = f_tilde^E_f(a, b, c)*g_tilde^E_g(a, b, c)*h_tilde^E_h(a, b, c)*xp^E_x(a, b, c)*zp^E_z(a, b, c)*(e*M)^(2*m - E_f(a, b, c) - E_g(a, b, c) - E_h(a, b, c))
return P(res)

def pke_row(a, b, c):
if (a,b,c) in MM:
return trans(p_tilde(a, b, c)*yq^(b//2))(X*xp, X*xq, Y*yp, Y*yq, X*zp, X*zq)
else:
if b % 2 == 0:
return trans(p_tilde(a, b, c)*yq^((a + c)//2)*yq^((b - a - c + 1)//2))(X*xp, X*xq, Y*yp, Y*yq, X*zp, X*zq)
else:
return trans(p_tilde(a, b, c)*yq^((a + c)//2)*yp^((b - a - c + 1)//2))(X*xp, X*xq, Y*yp, Y*yq, X*zp, X*zq)

################# TLP SHIFT POLYS #####################
f = M*(xp*yp - xq)
g = M*(yp*zp - N*zq)
h = M*(N*xp*zq - xq*zp)

def p(a, b, c):
res = f^E_f(a, b, c)*g^E_g(a, b, c)*h^E_h(a, b, c)*xp^E_x(a, b, c)*zp^E_z(a, b, c)*(e*M)^(2*m - E_f(a, b, c) - E_g(a, b, c) - E_h(a, b, c))
return P(res)

def p_ast(a, b, c, i, y):
return trans(p(a, b, c)*yq^(b//2)*y^i)(X*xp, X*xq, Y*yp, Y*yq, X*zp, X*zq)

def tlp_row(a, b, c):
return trans(p(a, b, c)*yq^(b//2))(X*xp, X*xq, Y*yp, Y*yq, X*zp, X*zq)

############ FETCH SHIFT POLYS ##################
# PKE polys
PKE_polys = []
for t in M_sigma:
PKE_polys.append(pke_row(*t))

# TLP polys
TLP_polys = []
for t in MM:
TLP_polys.append(tlp_row(*t))
a, b, c = t
if b == a + c:
for i in range(1, floor(tau*b) - b//2 + 1):
TLP_polys.append(p_ast(a, b, c, i, yq))
for i in range(1, floor(tau*b) - (b + 1)//2 + 1):
TLP_polys.append(p_ast(a, b, c, i, yp))

# Filter out un-needed tlp polys
for i in range(len(TLP_polys) - 1, -1, -1):
p = TLP_polys[i]

mm = highest_monomial(p)
mm = next(iter(mm.dict().keys()))
if mm[index_map[yq]] + mm[index_map[yp]] <= sigma*m:
TLP_polys.pop(i)


############## COMBINE BOTH #################
shift_polys = PKE_polys + TLP_polys
# Rescale them for LLL
shift_polys = list(map(rescale_poly, shift_polys))

# Sort the monomials
monomials = set()
for p in shift_polys:
monomials |= set(p.monomials())
monomials = list(monomials)
monomials.sort(key = lambda q: q(xp, xp, yp, yp, zp, zp))

# Build the coeff matrix
B = [[0 for _ in range(len(monomials))] for __ in range(len(shift_polys))]
for i, p in enumerate(shift_polys):
for j, mm in enumerate(monomials):
B[i][j] = p[mm]

B = matrix(ZZ, B)

############### APPLY COPPERSMITH TECHNIQUE TO FINISH ################
print(f'dim: {B.nrows()}')
B = B.LLL()
print('Finished LLL')
B = B.change_ring(QQ)

# Rescale columns back
for i, mm in enumerate(monomials):
t = next(iter(mm.dict().keys()))
Xpow = t[index_map[xp]] + t[index_map[xq]] + t[index_map[zp]] + t[index_map[zq]]
Ypow = t[index_map[yp]] + t[index_map[yq]]
d = X^Xpow * Y^Ypow

assert all(map(lambda j: int(B[j][i]) % d == 0, range(B.nrows())))

B.rescale_col(i, 1/d)

# Look at ideal gen by rows
P = P.change_ring(QQ)
H = Sequence([], P)
monomials = vector(P, monomials)

save((B, monomials), 'LLL_result.sobj')

amount = 14
for i, h in enumerate(list(B*monomials)[:amount]):
H.append(h)

print('Solving variety')
I = H.ideal()

B = I.groebner_basis()
print(B)
roots = []
for root in I.variety(): # ring = ZZ
roots.append(root)
print(roots)

# k = xq + 1

for root in roots:
p = int(root[yp])
if N % p == 0 and p != 1 and p != N:
print(f'p: {p}')

然后发现只能解出 $k$ 和 $l$ 来,不能解出 $p$。这个时候 V 给了另一个代码:

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
"""
This code finds unknown MSBs of dp, dq
when LSBs of dp & dq are known using our idea. It will reproduce Table 2 and Table 3 of our paper.

"""


m_2 = 40 #Parameter for 2nd Lattice
t_2 = 20 #Parameter for 2nd Lattice


# n is the size of primes, alpha is the size of e. dSize is bit size of dp & dq. Number of unknown bits of dp & dq is Unknown_MSB.

n = 512
dSize = 512
alpha = 72
Unknown_MSB = 512-199

#TWO_POWER is the left shift of 2. This value corresponds to the knowledge of LSBs
TWO_POWER = 2^(dSize - Unknown_MSB)

xq = 4198425198326169691466
k = xq + 1
e = 4680013789992958764661
n = 113512878655961571626610562291692317083167898593072246908072509473338669866931624486434843922077792562235492835323939380660867587409311081240029070350808655984402585845023288249807250489084430773691893497493957878187939757801622886103893275017257035278212160216032814012251157961899906789943525036078018769313
U = 1931999207628789396725122770203483408911326042952326921451
V = 799504796180001663308018451701479236857150404193865300422493
c = 94105129348907954980205351665290609913865320383526984688577432708537003146181471259880907643772804194349299707552600926808992628679380768658711570812064692302538521952981150231103309549852666196113547591789678339722493939214907786911484309585843582998176263433226474196365066591224488571958002184788519619403



Q = 2**199

# Alias
LSB_dp = U
TWO_POWER = Q
N = n

"""
From here 2nd step starts. After 1st step we know k. Now we try to find unknown
MSBs of dp

"""
R.<x>=QQ[]


f = (e*(TWO_POWER*x+LSB_dp)-1+k)
IN_k = (e*TWO_POWER).inverse_mod(k*N)

f = x+IN_k*(e*LSB_dp-1+k) # Make f monic by inverting the coefficient of x
X = 2^Unknown_MSB


#Generate shift polynomials and store these polynomials in F. Store monomials of shift polynomials in S
F = []
S = []
for i in range(m_2+1):
h = f^i*k^(m_2-i)*N^(max(0,t_2-i))
F.append(h)
S.append(x^i)


"""
Form a matrix MAT. Entries of MAT are coming from the coefficient
vector from shift polynomials which are stored in F
"""

print('2nd lattice dimension', len(F))


MAT = Matrix(ZZ, len(F))

for i in range(len(F)):
f = F[i]
f = f(x*X)

coeffs = (f.coefficients(sparse=False))
for j in range(len(coeffs), len(F)):
coeffs.append(0)
coeffs = vector(coeffs)
MAT[i] = coeffs


from time import process_time
TIME_Start = process_time()
tt = cputime()
MAT = MAT.LLL()
TIME_Stop = process_time()
print('2nd LLL time', TIME_Stop-TIME_Start)

#After reduction identify polynomials which have root MSB_dp over integer and store them in a set A.

A = []
for j in range(len(F)):
f = 0
for i in range(len(S)):
cij = MAT[j,i]
cij = cij/S[i](X)
cj = ZZ(cij)
f = f + cj*S[i]
if(j < 40):
print(f'OK {j = }')
A.append(f)
else:
break

#Find the root MSB_dp using Groebner basis techenique over integer

I = ideal(A)
tt = cputime()
B = I.groebner_basis()

print(B)

然后就解出了 $d_p$ 高位。然后直接模 $p$ 下解密即可(为什么不模 $N$?因为我懒)

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

xq = 4198425198326169691466
k = xq + 1
e = 4680013789992958764661
n = 113512878655961571626610562291692317083167898593072246908072509473338669866931624486434843922077792562235492835323939380660867587409311081240029070350808655984402585845023288249807250489084430773691893497493957878187939757801622886103893275017257035278212160216032814012251157961899906789943525036078018769313
U = 1931999207628789396725122770203483408911326042952326921451
V = 799504796180001663308018451701479236857150404193865300422493
c = 94105129348907954980205351665290609913865320383526984688577432708537003146181471259880907643772804194349299707552600926808992628679380768658711570812064692302538521952981150231103309549852666196113547591789678339722493939214907786911484309585843582998176263433226474196365066591224488571958002184788519619403

dp_M = 11589004150669115023857901117883258359392123003952932835988862957971135362774131734145547016673

dp = 2**199 * dp_M + U

p = (e * dp - 1) // k + 1
assert n % p == 0
q = n // p

d = inverse(e, (p-1)*(q-1))
m = pow(c, d, n)
print(long_to_bytes(m))

得到 flag 为

1
CCTF{f4c70r1ng_w!7h_0nlY_a_thIrD_0F_7H3_53creT_CR7-3xp0n3n75!}

赛后复盘

实际上 V 给的这个代码就是在利用下面这个等式

同时要先把上面的式子模 $k$ 分析,得到 $d_{ph} \bmod k$ 的值:

再把等式去做模 $N$ 的 coppersmith,把未知的 $p$ 当作 $N$ 给模掉就行了(可以算一下界,利用完上面的模 $k$ 分析后,未知数的位数仅为 $512 - 199 - 72 = 241$ 位,大概是 $N^{1/4}$ 这个级别的):

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
xq = 4198425198326169691466
k = xq + 1
e = 4680013789992958764661
n = 113512878655961571626610562291692317083167898593072246908072509473338669866931624486434843922077792562235492835323939380660867587409311081240029070350808655984402585845023288249807250489084430773691893497493957878187939757801622886103893275017257035278212160216032814012251157961899906789943525036078018769313
U = 1931999207628789396725122770203483408911326042952326921451
V = 799504796180001663308018451701479236857150404193865300422493
c = 94105129348907954980205351665290609913865320383526984688577432708537003146181471259880907643772804194349299707552600926808992628679380768658711570812064692302538521952981150231103309549852666196113547591789678339722493939214907786911484309585843582998176263433226474196365066591224488571958002184788519619403

Q = 2**199

PR.<u> = PolynomialRing(Zmod(n))

dphmodk = (inverse_mod(e, k) - U) * inverse_mod(Q, k) % k
x = u * k + dphmodk

x_ = 11589004150669115023857901117883258359392123003952932835988862957971135362774131734145547016673
assert x_ % k == dphmodk


f = e * (Q * x + U) + k - 1
f = f.monic()

X = 2**(512-199) // k

beta = 0.48
sol = f.small_roots(X=X, beta=beta, epsilon=0.02)
print(sol)

print(gcd(ZZ(f(sol[0])), n))

然后比赛的时候还试了直接用 cuso 去解,好像解不出来

flag 提到的内容应该是 A Third is All You Need: Extended Partial Key
Exposure Attack on CRT-RSA with Additive
Exponent Blinding,也是一篇蛮经典的 论文

Expulsion is All You Need

Lowdown

题目描述

记 $\mathbb{F} = GF(2^8), k=10$

密钥生成 随机生成矩阵 $G \in \mathbb{F}^{k \times k}$,以及 $2 < r, a < 2^{192}$。计算 $A = G^{ar}$。公钥为 $(G, A)$,私钥为 $r$

签名 输入一个长度小于 10 的消息 $m$

  1. 算出其 SHA1 哈希 $h = H(m)$
  2. 计算 $G_2 = G^r$
  3. 生成随机数 $2 < n < 2^{192}$
  4. 计算 $S = A G_2^{-n h}$
  5. 计算 $T = G_2^n$
  6. 服务器返回 $(S, T)$

验签 输入消息 $m$,以及待验证签名 $(S, T)$

  1. 算出其 SHA1 哈希 $h = H(m)$
  2. 验证 $S T^{h} = A$ 是否成立

挑战 服务器给一个长度为 40 的随机消息,需要给签名 $(S, T)$ 使得

  1. 签名通过
  2. $S$ 转成整数表示要以 13 开头
  3. $T$ 转成整数表示要以 37 开头

通过挑战即给 flag

我的解答

看到矩阵乘法运算和幂运算容易想到 特征分解,但是好像用不着

本地试了一下,$G$ 的阶能求出来,也就是离散对数能求。但是直接调用 sagemath 的官方 API 貌似不能直接求,因为没实现矩阵类的 __hash__ 方法,那就手撕一下 Pohlig-Hellman 得了。

然后假设我们已经知道 $A = G^k$,那么要构造满足条件的 $S, T$ 就很简单:

  1. 随机满足限制的 $T$ 矩阵
  2. 计算 $S = A T^{-h}$
  3. 验证 $S$ 矩阵是否满足限制,这个概率应该是 $1/256$

然后分析一下他的那个限制,他是用 startswith 来限制的,实际上就是限制整数的范围,也就是限制 $S_{00}, T_{00}$ 就能满足。估算一下,$S$ 转成整数的话大概是 $2^{8 \times 10 \times 10}=2^{800} \approx 10^{240}$ 这个量级,应该能估算出个 $S_{00}, T_{00}$ 限制的范围,或者用下面的代码来做个简单 check:

1
2
3
4
5
for i in range(256):
a = i * 2**792
b = (i+1) * 2**792
if str(a).startswith('13') and str(b).startswith('13'):
print(f'{i = }')

然后发现

  • i = 50, 51, 52 的时候,满足整数首位为 13 的限制
  • i = 143, 144 的时候,满足整数首位为 37 的限制

这样的话,实际上上面所说的离散对数也不用求了,整个就一乱搞,代码如下:

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
from pwn import *
from sage.all import *
from hashlib import sha1
from Crypto.Util.number import bytes_to_long

F = GF(2**8)
k = 10


def random_oracle(msg):
_h = sha1(msg).digest()
return bytes_to_long(_h)

def h(a):
if a == 0:
return 0
else:
g = F.gen()
for _ in range(1, 256):
if g ** _ == a:
return _

def H(M):
assert M.nrows() == M.ncols()
k, _H = M.nrows(), []
for i in range(k):
for j in range(k):
_h = h(M[i, j])
_H.append(bin(_h)[2:].zfill(8))
return ''.join(_H)

def Hinv(m, k):
B = bin(m)[2:].zfill(8 * k**2)
g = F.gen()
_H = [int(B[8*i:8*i + 8], 2) for i in range(k**2)]
_M = [0 if _h == 0 else g ** _h for _h in _H]
M = Matrix(F, [[a for a in _M[k*i:k*i + k]] for i in range(k)])
return M

def M2i(M):
_H = H(M)
return int(_H, 2)

class Gao:
def __init__(self):
self.conn = process(['sage', 'another.sage'])

def get_public(self):
self.conn.sendlineafter('[Q]uit\n', 'p')
self.conn.recvuntil('ga = ')
A = int(self.conn.recvline())
self.A = Hinv(A, k)

def get_flag(self):
self.conn.sendlineafter('[Q]uit\n', 'g')
self.conn.recvuntil('Message = ')
h0 = eval(self.conn.recvline())
h0 = bytes_to_long(sha1(h0).digest())
g = F.gen()
while True:
T = random_matrix(F, k, k)
T[0, 0] = g ** 143
if T.det() == 0:
continue
S = self.A * T**-h0
S00 = h(S[0, 0])
if 50 <= S00 <= 52:
break
else:
print(f"Failed {S00 = }...")
s, t = M2i(S), M2i(T)
self.conn.sendlineafter('message: \n', f'{s},{t}')

def gao(self):
self.get_public()
self.get_flag()
self.conn.interactive()

if __name__ == '__main__':
g = Gao()
g.gao()