2025D^3CTF-Crypto-WP

赛中三道题只做出了一道,题目质量都非常好,所以写篇博客记录一下。

d3fnv

题目描述

1
Guessing is easy. Trusting is hard. Winning? That’s another story.

题目附件

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
#!/usr/bin/env python3
from Crypto.Util.number import getPrime
from hashlib import sha256
from secret import FLAG
import socketserver
import signal
import random
import string
import os


BANNER = br"""
____ //|_____ __________________ ___ ____ ___ ______
/ __ \/||__ / / ____/_ __/ ____/ |__ \ / __ \__ \ / ____/
/ / / / /_ < / / / / / /_ __/ // / / /_/ //___ \
/ /_/ / ___/ / / /___ / / / __/ / __// /_/ / __/____/ /
/_____/ /____/ \____/ /_/ /_/ /____/\____/____/_____/
"""


MENU = br"""
1. Get p
2. H4sh
3. Flag
4. Exit
"""


class FNV():
def __init__(self):
self.pbit = 1024
self.p = getPrime(self.pbit)
self.key = random.randint(0, self.p)

def H4sh(self, value:str):
length = len(value)
x = (ord(value[0]) << 7) % self.p
for c in value:
x = ((self.key * x) % self.p) ^ ord(c)

x ^= length

return x


class Task(socketserver.BaseRequestHandler):
def _recvall(self):
BUFF_SIZE = 2048
data = b''
while True:
part = self.request.recv(BUFF_SIZE)
data += part
if len(part) < BUFF_SIZE:
break
return data.strip()

def send(self, msg, newline=True):
try:
if newline:
msg += b'\n'
self.request.sendall(msg)
except:
pass

def recv(self, prompt=b'> '):
self.send(prompt, newline=False)
return self._recvall()

def close(self):
self.send(b"Bye~")
self.request.close()

def handle(self):
signal.alarm(30)

self.send(BANNER)

n = 32
cnt = 67
str_table = string.ascii_letters + string.digits
self.send(b'Welcome to D^3CTF 2025')
self.send(b'Could you break my modified fnv hash function?')
self.fnv = FNV()

for _ in range(cnt):
self.send(MENU)
option = self.recv(b'option >')
if option == b'G':
p = self.fnv.p
self.send(f'p = {p}'.encode())

elif option == b'H':
random_token = ''.join(random.choices(str_table, k=n))
random_token_hash = self.fnv.H4sh(random_token)
self.send(b'Token Hash: ' + str(random_token_hash).encode())

elif option == b'F':
random_token = ''.join(random.choices(str_table, k=n))
self.send(b'Here is a random token x: ' + random_token.encode())
ans = self.recv(b'Could you tell the value of H4sh(x)? ').strip().decode()
if int(ans) % self.fnv.p == self.fnv.H4sh(random_token) % self.fnv.p:
self.send(b'Congratulations! Here is your flag: ')
self.send(FLAG)
else:
self.send(b'Try again~')

else:
break

self.close()


class ThreadedServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass


class ForkedServer(socketserver.ForkingMixIn, socketserver.TCPServer):
pass


if __name__ == "__main__":
HOST, PORT = '0.0.0.0', 10007
server = ForkedServer((HOST, PORT), Task)
server.allow_reuse_address = True
server.serve_forever()

思路

题目基于FNV,一共给了67次交互,首先肯定要2次来拿p和flag。剩下的65次都用来获取hash值。hash值可以由下面的式子表示: \[ x_i = v_{i,0}*2^7*k^{32} + v_{i,0}'*k^{31} + v_{i,1}'*k^{30} + \cdots + v_{i,30}*k + v_{i,31} \mod P \]

将65个\(x_i\)写成矩阵形式: \[ \begin{pmatrix} x_0 \\ x_1 \\ \vdots \\ x_{64} \end{pmatrix} = \begin{pmatrix} v_{0, 0} & v_{0, 0}' & v_{0, 1}' & \dots & v_{0, 31}' \\ v_{1, 0} & v_{1, 0}' & v_{1, 1}' & \dots & v_{1, 31}' \\ \vdots & \vdots & \vdots & &\vdots \\ v_{64, 0} & v_{64, 0}' & v_{64, 1}' & \dots & v_{64, 31}' \\ \end{pmatrix} \begin{pmatrix} 2^7*k^{32} \\ k^{31} \\ \vdots \\ k^{0} \end{pmatrix} \]

下面简记为\(\mathbf{x} = \mathbf{Vk}\)
用格基规约求出\(\mathbf{x}\)的左核空间的话,前32个一定是 \(\mathbf{V}\)的左核空间,所以再求左核的右核就是 \(\mathbf{V}\),但是由于\(\mathbf{V}\)还不够小,所以求出来的实际是\(\mathbf{V}\)经过一些微小的初等变换,这个变换可以如下表示为: \[\mathbf{V = V'B}\]\(\mathbf{BB^T}\)可能的值非常少,测试发现\(\mathbf{BB^T}\)经常是对角矩阵,而且前2个元素是2,后面的全是1。
所以: \[ \begin{gather} \mathbf{x = V'Bk} \end{gather} \]

解这个线性方程组得到\(\mathbf{Bk}\),然后让这个向量和自身点积得到\(\mathbf{k}\mathbf{B}\mathbf{B}^T\mathbf{k}^T\),然后只需要求解多项式方程的根就行。
求出来的多个根应该可以用求\(\mathbf{V}\),然后通过看明文是否落在给定范围来判断是否正确。但是这里我直接猜测是最后一个根了,只要多爆几遍就行。

exp

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
# exp.sage
import string
from Crypto.Util.number import getPrime, getRandomNBitInteger
from hashlib import sha256
import random
from pwn import remote, process
from tqdm import trange

class FNV():
def __init__(self, p, key):
self.pbit = 1024
self.p = p
self.key = key

def H4sh(self, value:str):
length = len(value)
x = (ord(value[0]) << 7) % self.p
for c in value:
x = ((self.key * x) % self.p) ^^ ord(c)

x ^^= length

return x

def once():
io = remote("35.241.98.126", 30104)
io.recvuntil(b'Could you break my modified fnv hash function?\n')

io.recvuntil(b"option >")
io.sendline(b"G")
p = int(io.recvline().split(b"=")[1])
print(p)

xl = []
for i in trange(65):
io.recvuntil(b"option >")
io.sendline(b"H")
xi = int(io.recvline().split(b": ")[1])
xl.append(xi ^^ (32))

M = column_matrix(xl + [p]) * getPrime(1024)
M = M.augment(identity_matrix(M.nrows() - 1).stack(zero_vector(M.nrows() - 1)))
ML = M.LLL()

K = ML[:32, 1:]

K = K.right_kernel_matrix().LLL().T
print(K.dimensions())
Bk = K.change_ring(GF(p)).solve_right(vector(xl))
PR.<x> = PolynomialRing(GF(p))
f = 2 * x^64 * (1 << 14)
f += 2 * x^(62)
for i in range(31):
f += x^(2 * i)
f -= (Bk * Bk)

key = f.roots()
if key == []:
io.close()
return
print(key)
key = int(key[-1][0])
print(key)

io.recvuntil(b"option >")
io.sendline(b"F")
token = (io.recvline().split(b": ")[1].strip()).decode()
print(token)
fnv = FNV(p, key)
ans = fnv.H4sh(token)
io.recvuntil(b'Could you tell the value of H4sh(x)? ')
io.sendline(str(ans).encode())
print(io.recvall(3))

io.close()
for _ in range(50):
once()

*d3guess

题目描述

1
Guessing is easy. Trusting is hard. Winning? That’s another story.

题目附件

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
#!/usr/bin/env python3

from random import *
from secret import FLAG

N = 2**32
times = 64
r = .1
h1 = 'your number is too big'
h2 = 'your number is too small'
h3 = 'you win'
print("=== Welcome to D3CTF 2025 ===")
print("You have at most 1 hour to solve this challenge.")
print("Can you defeat the biased oracle?\n")
rr = Random()

def challge(rounds, times, N, r, mode=0):
wins = 0
f = lambda x: [0.075, 0.15, 0.225, 0.3, 0.375, 0.45][5 - x * 6 // 2**32]
print(["Now let's play a simple number-guessing game", "Let's play a relatively simple number-guessing game again"][mode])
for round_idx in range(rounds):
x = rr.randint(1, N - 1)
print(f"[*] Starting Round {round_idx + 1} of {rounds}")
# print(x)
for _ in range(times):
try:
guess = int(input('[d3ctf@oracle] give me a number > '))
except:
print("[!] Invalid input detected. Session terminated.")
exit()
if guess > x:
print([f(abs(guess - x)), [h1, h2][rr.random() < r]][mode])
elif guess < x:
print([f(abs(guess - x)), [h2, h1][rr.random() < r]][mode])
else:
print(h3)
wins += 1
break
return wins


if challge(350, 32, N, r) == 350 and challge(2200, 64, N, r, mode=1) > 2112:
print(f"[!] You have proven your power over probability. This is your {FLAG}. Congratulations!")
else:
print("[X] The oracle remains unbeaten. Try again, challenger.")
exit()

这题要解决mode0和mode1两个猜数游戏

mode0

需要猜测一个32bit的数,一共有32次机会。每次猜测能得到猜测的数g和被猜数x的距离通过f函数处理过的值,其实就是告诉你k,使得: \[ \frac{2^{32}k}{6} \leq \mid g - x\mid < \frac{2^{32}(k + 1)}{6}, \ \ \ k \in \{0, 1, 2, 3, 4, 5\} \] 可以发现这个区间的大小是恒定的,下面记区间长度是gap。解决这个问题的思路还是二分,具体如下:
首先第一次可以发送边界\(2^{32} - 1\)然后根据k就可以二分区间。现在假设我们已知的区间边界是(a, b),那么可以发送(a + b) / 2 + gap,根据返回的k值来二分区间(a, b)。这样不断二分就能得到x。

mode1

每次交互都会告诉你偏大还是偏小,而且结果有10%的概率出错。
主要参考了洛谷上这题的题解。 大体做法是每次发送概率分布的中位数,然后使用贝叶斯公式更新概率分布,原文中使用的动态开点线段树维护概率分布的数组,但是这里是python,所以就直接用列表模拟了。
测试下来有80%的正确率,是不够题目要求的正确率的,但是加上MODE0的350个机数,MODE1只需要赢274次,后面用MT19937就行。

exp

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
# exp.py

from sage.all_cmdline import *
from pwn import remote, process, context
from tqdm import trange
from math import ceil, floor
import random
import numpy as np

N = 2**32
h1 = 'your number is too big'
h2 = 'your number is too small'
h3 = 'you win'
f = [0.075, 0.15, 0.225, 0.3, 0.375, 0.45]
path = "/d/CTF/Chall/2025/D^3CTF/Crypto/d3guess/chal/chal.py"
io = process(["python", path])
io.recvuntil(b"Can you defeat the biased oracle?\n\n")

DATA = []

# ======================== MODE 0 ========================
def MODE_0():
def sendnumber(num):
io.recvuntil(b'[d3ctf@oracle] give me a number > ')
g = num
io.sendline(str(g).encode())
recv = io.recvline()
if b"win" in recv:
return 0, 0, 0, 1
else:
k = 5 - f.index(eval(recv))
a = ceil((k << 32) / 6)
b = floor(((k + 1) << 32) / 6)
return k, a, b, 0

def mode0_playonce():
io.recvline()
g = N - 1
k, a, b, win = sendnumber(g)
if win:
return g, -0
left = g - b
right = g - a

gap = (2 ** 32) / 6
for i in range(30):
mid= (right + left) / 2
k, _, _, win = sendnumber(int(mid + gap))
if win:
return int(mid + gap, -(i + 1))
if (k == 1):
right = mid
else:
assert k == 0
left = mid
# print(flag, end=" : ")
# print(f"%f %f" % (left, right))

mid= ((right) + (left)) / 2
# print(f"%f" % mid)
k, _, _, win = sendnumber(int(mid))
if win:
return int(mid), -31

return None

io.recvuntil(b"Now let's play a simple number-guessing game\n")
rounds = 350
for _ in trange(rounds):
data = mode0_playonce()
DATA.append(data[0] - 1)
DATA.append(data[1])

# ======================== MODE 1 ========================
def MODE_1():
COUNT = 64
K = 1/10
BOUND = (1, 2**32 - 1)

def MT19937Predictor(data):
v = []
for i in range(len(data)):
if data[i] >= 0:
x = data[i]
v.extend([int(i) for i in bin(x)[2:].zfill(32)])
v = vector(GF(2), v)
print(f"{len(v) = }")
rr = random.Random()
Mat = np.zeros((19968, len(v)), dtype=int)
for i in trange(19968):
state = [0] * 624
state[i // 32] = 1 << (31 - i % 32)
rr.setstate((3, tuple(state + [int(624)]), 624))
tmp = 0
for j in range(len(data)):
if data[j] < 0:
if data[j] == -128:
rr.getrandbits(32)
continue
rr.getrandbits(64 * -data[j])
else:
x = rr.getrandbits(32)
for k in range(32):
Mat[i][tmp * 32 + k] = (x >> (31 - k)) & 1
tmp += 1

print("Construct matrix and solve")
Mat = matrix(GF(2), Mat)
print(f"{Mat.rank() = }")
s = Mat.solve_left(v)
seed = []
for i in range(0, 19968, 32):
x = 0
for j in range(32):
x <<= 1
x += int(s[i + j])
seed.append(int(x))
RR = random.Random()
RR.setstate((3, tuple(seed + [int(624)]), None))
return RR

def sendonce(guess):
io.recvuntil(b'[d3ctf@oracle] give me a number > ')
io.sendline(str(guess).encode())
res = io.recvline()
if b"big" in res:
return 1
elif b"small" in res:
return -1
elif b"win" in res:
return 0

def median(Prob):
total = 0
for i in range(len(Prob)):
if total < 1 / 2:
if total + Prob[i][2 ]>= 1 / 2:
return int(((1 / 2 - total) / Prob[i][2]) * (Prob[i][1] - Prob[i][0] + 1) + Prob[i][0])
total += Prob[i][2]

def maxim(Prob: list):
res = 0
p = 0
for i in range(len(Prob)):
left, right, pb = Prob[i]
if (pb / (right - left + 1)) > p:
p = pb / (right - left + 1)
res = (left, right)
return res, p

def update(Prob: list, guess, result):
ind = None
for i in range(len(Prob)):
if Prob[i][0] <= guess <= Prob[i][1]:
ind = i
break

left, right, p = Prob.pop(ind)
if (left <= guess - 1):
Prob.insert(ind, (left, guess - 1, (guess - left) * p / (right - left + 1)))
Prob.insert(ind + 1, (guess, guess, 0))
if (right >= guess + 1):
Prob.insert(ind + 2, (guess + 1, right, ((right - guess) * p / (right - left + 1))))

a = 0
b = 0
for i in range(0, ind + 1):
a += Prob[i][2]
for i in range(ind + 2, len(Prob)):
b += Prob[i][2]

for i in range(0, ind + 1):
left, right, p = Prob[i]
if result == 1:
Prob[i] = (left, right, (1 - K) * p / ((1 - K) * a + K * b))
else:
Prob[i] = (left, right, K * p / ((1 - K) * b + K * a))

for i in range(ind + 2, len(Prob)):
left, right, p = Prob[i]
if result == 1:
Prob[i] = (left, right, K * p / ((1 - K) * a + K * b))
else:
Prob[i] = (left, right, (1 - K) * p / ((1 - K) * b + K * a))

def play():
prob = [(BOUND[0], BOUND[1], 1)]
for i in range(COUNT):
if i == COUNT - 1:
guess_range, _ = maxim(prob)
guess = (guess_range[0] + guess_range[1]) // 2
res = sendonce(guess)
if res == 0:
return guess, -63
break
guess = median(prob)
res = sendonce(guess)
if res == 0:
return guess, -i
update(prob, guess, res)
return -128, -64

io.recvuntil(b"Let's play a relatively simple number-guessing game again")
rounds = 2200
win = 0
while(True):
if win == 274:
break
x = play()
if x[0] == -128:
DATA.append(x[0])
DATA.append(x[1])
else:
win += 1
DATA.append(x[0] - 1)
DATA.append(x[1])
print(f"{len(DATA) // 2}")
print("lost rounds:", (len(DATA) // 2) - 350 - win)
print("Start to creak MT...")
MyR = MT19937Predictor(DATA)
print("MT has been cracked")
for i in range(len(DATA)):
if DATA[i] < 0:
if DATA[i] == -128:
MyR.getrandbits(32)
continue
MyR.getrandbits(64 * -DATA[i])
else:
MyR.getrandbits(32)
remain = 350 + 2200 - len(DATA) // 2
for _ in range(remain):
io.recvuntil(b'[d3ctf@oracle] give me a number > ')
io.sendline(str(MyR.randint(1, N - 1)).encode())
assert b"win" in io.recvline()
print(io.recvline())

import time
print("MODE_0 begin...")
begin = time.time()
MODE_0()
end = time.time()
print("MODE_0 Over...")
print("MODE_0 spent time:", end - begin)
print()

print("MODE_1 begin...")
begin = time.time()
MODE_1()
end = time.time()
print("MODE_1 Over...")
print("MODE_1 spent time:", end - begin)

io.close()

*d3sys[p2_2.0]

题目描述

1
2
3
Everything is in attachments! Let's go and solve it!

Author: deebato @ L-team x Ele3tronic x D^3CTF

题目附件

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
from Crypto.Util.number import bytes_to_long, getPrime, inverse
from hashlib import sha256
from secret import FLAG
import socketserver
import signal
import os


banner = br"""
_ .-') _ .-') _
( ( OO) ) ( OO) )
\ .'_ ,---. .-----. .-----./ '._ ,------.
,`'--..._) / \ \ / -. \ ' .--./|'--...__)('-| _.---'
| | \ '`--' `--''-' _' | | |('-.'--. .--'(OO|(_\
| | ' | |_ < /_) |OO ) | | / | '--.
| | / : .-. | | || |`-'| | | \_)| .--'
| '--' / \ `-' /(_' '--'\ | | \| |_)
`-------' `----'' `-----' `--' `--'
.-----. .----. .-----. .------.
/ ,-. \ / .. \ / ,-. \| ___|
'-' | |. / \ .'-' | || '--.
.' / | | ' | .' / `---. '.
.' /__ ' \ / ' .' /__ .- | |
| | \ `' / | || `-' /
`-------' `---'' `-------' `----''
"""


MENU2 = br'''
====------------------------------------------------------------------------------------------------------====
| | +---------------------------------------------------------------------+ | |
| | | [G]et_dp_dq [F]lag [T]ime [E]xit | | |
| | +---------------------------------------------------------------------+ | |
====------------------------------------------------------------------------------------------------------====
'''

class CRT_RSA_SYSTEM:
nbit = 3072
blind_bit = 153
unknownbit = 983
e_bit = 170

def __init__(self):
e = getPrime(self.e_bit)
p,q = [getPrime(self.nbit // 2) for _ in "D^3CTF"[:2]]
n = p * q
self.pub = (n,e)

dp = inverse(e,p - 1)
dq = inverse(e,q - 1)
self.priv = (p,q,dp,dq,e,n)
self.blind()

def blind(self):
p,q,dp,dq,e,n = self.priv
rp,rq = [getPrime(self.blind_bit) for _ in "D^3CTF"[:2]]
dp_ = (p-1) * rp + dp
dq_ = (q-1) * rq + dq
self.priv = (p,q,dp_,dq_,e,n)

def get_priv_exp(self):
p,q,dp,dq,e,n = self.priv
dp_ = dp >> self.unknownbit
dq_ = dq >> self.unknownbit
return (dp_,dq_)

def encrypt(self,m):
n,e = self.pub
return pow(m,e,n)

def decrypt(self,c):
p,q,dp,dq,e,n = self.priv
mp = pow(c,dp,p)
mq = pow(c,dq,q)
h = inverse(q, p) * (mp - mq) % p
m = mq + h * q
# m = crt([mp,mq],[p,q])
assert pow(m,e,n) == c
return m

class D3_SYS(socketserver.BaseRequestHandler):
def _recvall(self):
BUFF_SIZE = 2048
data = b''
while True:
part = self.request.recv(BUFF_SIZE)
data += part
if len(part) < BUFF_SIZE:
break
return data.strip()

def send(self, msg, newline=True):
try:
if newline:
msg += b'\n'
self.request.sendall(msg)
except:
pass

def recv(self, prompt=b''):
self.send(prompt, newline=False)
return self._recvall()

def get_dp_dq(self):
return self.crt_rsa.get_priv_exp()

def enc_token(self):
token = os.urandom(380)
n,e = self.crt_rsa.pub
enc_token = pow(bytes_to_long(token),e,n)
return enc_token, sha256(token).hexdigest()

def handle(self):
signal.alarm(5)
self.send(banner)
self.send(b"Welcome to D^3CTF 2025")
self.send(b"Hello player... This year I give you a new challenge which is similar as the second part in d3sys[D^3CTF 2023].")
self.crt_rsa = CRT_RSA_SYSTEM()
for __ in 'D^3CTF'[:2]:
self.send(MENU2)
option = self.recv(b'option >')
if option == b'F':
cip,tokenhash = self.enc_token()
self.send(b'Encrypted Token: ' + hex(cip).encode())
tokenhash_checked = self.recv(b'Token Hash: ')
if tokenhash_checked.decode() == tokenhash:
self.send(b'Correct!')
self.send(FLAG.encode())
else:
self.send(b'Wrong Token Hash!')
elif option == b'G':
dp,dq = self.get_dp_dq()
self.send(f'dp,dq:{[dp,dq]}'.encode())
self.send(f'n,e:{[self.crt_rsa.pub]}'.encode())
elif option == b'T':
pass # self.time()
else:
break
self.request.close()

class ThreadedServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass

class ForkedServer(socketserver.ForkingMixIn, socketserver.TCPServer):
pass

if __name__ == "__main__":
HOST, PORT = '0.0.0.0', 10001
print("HOST:POST " + HOST+":" + str(PORT))
server = ForkedServer((HOST, PORT), D3_SYS)
server.allow_reuse_address = True
server.serve_forever()

主要参考了官方WP和这篇论文

题目基于CRT-RSA的私钥泄露攻击。
CRT-RSA即使用了CRT来加速RSA解密过程的RSA系统。加速过程需要用到私钥: \[ \begin{gather} d_p = d \mod (p - 1)\\ d_q = d \mod(q - 1) \end{gather} \] 常数\(k, l\)满足: \[ \begin{gather} ed_p = 1 + k(p - 1)\\ ed_q = 1 + l(q - 1) \end{gather} \]

为了防止侧信道攻击,通常会为私钥添加盲因子\(r_p, r_q\),即新的私钥为: \[ \begin{gather} d_p' = d_p + r_p(p - 1)\\ d_q' = d_q + r_q(q - 1) \end{gather} \] 常数\(k', l'\)满足: \[ \begin{gather} ed_p' = 1 + k'(p - 1)\\ ed_q' = 1 + l'(q - 1)\\ k' = k + er_p \\ l' = l + er_q \end{gather} \]

思路

本题泄露了\(d_p',d_q'\)的高位\(d_p'^{(M)}, d_q'^{(M)}\)\[ \begin{gather} d_p' = 2^id_p'^{(M)} + d_p'^{(L)}\\ d_q' = 2^id_q'^{(M)} + d_q'^{(L)} \end{gather} \] 按照论文中的思路,分为2步。

step1

首先求出\(k', l'\)。 因为: \[ \begin{gather} ed_p' = 1 + k'(p - 1)\\ ed_q' = 1 + l'(q - 1)\\ \end{gather} \] 所以:

\[ \begin{gather} k'l'N = (ed_p' - 1 + k')(ed_q' - 1 + l')\\ k'l' \approx \frac{e^22^{2i}d_p'^{(M)}d_q'^{(M)}}{N} \end{gather} \]

拿到\(k',l'\)后,如果按照论文中使用分解算法的话时间是不够的,而本题的e较大,可以解下面的同余方程拿到\(k,l\)\[ \begin{gather} kl \equiv k'l' \mod e\\ klN \equiv (k - 1)(l - 1) \mod e \end{gather} \] 然后根据\(k' = k + er_p,l' = l + er_q\),在模\(k'l'\)的情况下使用coppersmith方法求出\(r_p,r_q\)。然后求出\(k',l'\)

step2

拿到\(k'\)后,根据如下等式: \[ \begin{gather} e(d_p^{(M)}2^i + d_p^{(L)}) = 1 + k'(p - 1)\\ e(d_p^{(M)}2^i + d_p^{(L)}) \equiv 1 \mod k'p \end{gather} \] 再使用coppersmith方法求出\(d_p^{(L)}\)即可。

exp

因为我的flatter好像有点问题(用起来比LLL慢),所以用的sage自带的small_roots,题目给的5s比较极限,加上服务器延迟,得多试几遍。

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
# sage.py
from Crypto.Util.number import *
from subprocess import check_output
from re import findall
import time
from pwn import remote, process
from hashlib import sha256

nbit = 3072
blind_bit = 153
unknownbit = 983
e_bit = 170


# io = remote("localhost", 9998)
io = remote("node6.anna.nssctf.cn", 24225)
begin = time.time()

io.recvuntil(b"option >")
io.sendline(b"G")
dp_msb, dq_msb = eval(io.recvline().decode().split(":")[1])
n, e = eval(io.recvline().decode().split(":")[1])[0]

kl_ = (e**2 * 2**(2 * unknownbit) * dp_msb * dq_msb) // n + 1
PR.<x> = PolynomialRing(GF(e))
f = kl_ * n * x - (kl_ - x)*(x - 1)
k = int(f.roots(multiplicities=False)[0])
PR.<x> = PolynomialRing(Zmod(kl_))
f = e*x + k
f = f.monic()

a = time.time()
rp = int(f.small_roots(X=2**blind_bit, beta=0.49, epsilon = 0.024)[0])
k_ = e * rp + k
l_ = kl_ // k_

PR.<x> = PolynomialRing(Zmod(k_ * n))
f = e*((dp_msb*2**unknownbit) + x) - 1 + k_
f = f.monic()
r = f.small_roots(X=2**unknownbit, beta=0.493, epsilon=0.02)
print(r)
end = time.time()
print(end - begin)
p = int(gcd(int(f(r[0])), n))
q = n // p
d = pow(e, -1, (p - 1) * (q - 1))

io.recvuntil(b"option >")
io.sendline(b"F")
c = int(io.recvline().decode().split(":")[1], 16)
m = pow(c, d, n)
m = long_to_bytes(m)
m_h = sha256(m).hexdigest()
io.recvuntil(b'Token Hash: ')
io.sendline(m_h.encode())
print(io.recvall(2).decode())