SEKAICTFの部分密码题

qwq

上周也是抽空打了SEKAICTF,不得不说质量是真的高,感觉最近有点颓废了,快开学了还是尽快找回状态

SSSS

这道题其实跟不久前的idekCTF的一道题方法是一样的,都是利用DFT(离散傅里叶变换)去解决,(写到这想起来还没写idek的博客唉)这里我们就先再次分析一下idek的那道题。

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
from Crypto.Util.number import *
import random
import os

class idek():

def __init__(self, secret : bytes):

self.secret = secret
self.p = None

self.poly = None

def set_p(self, p : int):

if isPrime(p):
self.p = p

def gen_poly(self, deg : int):

s = bytes_to_long(self.secret)
l = s.bit_length()
self.poly = [random.randint(0, 2**l) for _ in range(deg + 1)]
index = random.randint(deg//4 + 1, 3*deg//4 - 1)
self.poly[index] = s

def get_share(self, point : int):

if not self.p or not self.poly:
return None

return sum([coef * pow(point, i, self.p) for i, coef in enumerate(self.poly)]) % self.p

def get_shares(self, points : list[int]):

return [self.get_share(point) for point in points]

def banner():

print("==============================================")
print("=== Welcome to idek Secret Sharing Service ===")
print("==============================================")
print("")

def menu():

print("")
print("[1] Oracle")
print("[2] Verify")
print("[3] Exit")

op = int(input(">>> "))
return op

if __name__ == '__main__':

S = idek(os.urandom(80))
deg = 16
seen = []

banner()

for _ in range(17):

op = menu()
if op == 1:
p = int(input("What's Your Favorite Prime : "))
assert p.bit_length() == 64 and isPrime(p) and p not in seen
seen += [p]
S.set_p(p)
S.gen_poly(deg)
L = list(map(int, input("> ").split(",")))
assert len(L) <= 3*deg//4
print(f"Here are your shares : {S.get_shares(L)}")
elif op == 2:
if S.secret.hex() == input("Guess the secret : "):
with open("flag.txt", "rb") as f:
print(f.read())
else:
print("Try harder.")
elif op == 3:
print("Bye!")
break
else:
print("Unknown option.")

哎才想起来我群好像写了这个wp,顺便引流一下正规子群 • idekCTF 2025 Team WriteUp那我们废话不多说直接来看这道题。

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
import random, os

p = 2 ** 256 - 189
FLAG = os.getenv("FLAG", "SEKAI{}")

def challenge(secret):
t = int(input())
assert 20 <= t <= 50, "Number of parties not in range"

f = gen(t, secret)

for i in range(t):
x = int(input())
assert 0 < x < p, "Bad input"
print(poly_eval(f, x))

if int(input()) == secret:
print(FLAG)
exit(0)
else:
print(":<")

def gen(degree, secret):
poly = [random.randrange(0, p) for _ in range(degree + 1)]
index = random.randint(0, degree)

poly[index] = secret
return poly

def poly_eval(f, x):
return sum(c * pow(x, i, p) for i, c in enumerate(f)) % p

if __name__ == "__main__":
secret = random.randrange(0, p)
for _ in range(2):
challenge(secret)

其实本质上跟idek那道是一模一样的。不过这里我们可以自定义多项式的阶数,然后生成一个全新的、阶数为t的随机多项式,并将secret作为其中一个系数。循环t次:等待我们发送一个x值,服务器则返回f(x) mod p的结果,对于gen函数,为了便于理解,我们加上以下注释

1
2
3
4
5
6
7
8
9
10
11
12
13
# gen函数用于生成多项式。
def gen(degree, secret):
# a. 创建一个包含`degree + 1`个随机系数的多项式。
poly = [random.randrange(0, p) for _ in range(degree + 1)]
# b. 随机选择一个位置。
index = random.randint(0, degree)
# c. 将这个位置的系数替换为真正的secret。
poly[index] = secret
return poly

# poly_eval函数用于计算多项式在点x处的值。
def poly_eval(f, x):
return sum(c * pow(x, i, p) for i, c in enumerate(f)) % p

分析题目我们可以发现以下一些可能对解题有帮助的点。

  1. secret在一次连接中是固定不变的。 它在程序启动时生成,并被两次challenge调用共享。
  2. 多项式是随机变化的:每一次调用challenge,都会生成一个全新的随机多项式secret被藏在其中的位置也是随机的。

那么我们就可以有一个大概的思路:既然secret在两次挑战中是唯一不变的元素,而其他所有系数都是随机变化的,那么我们只要分别找出两次挑战中所有的多项式系数,然后计算这两个系数集合的交集 ,这个交集中唯一的元素必然就是secret。根据DFT相关知识,这里给出了p,我们需要选择一个数,令p-1能整除,这样就有对应次次单位根。传进去的如果是t次单位根那么IDFT恢复的是x$^{i+kt}$的项对应的系数的和,如果我们有t阶的g,常数项和第t项重叠,所以多项式可以用t个点重建.再根据题目给出的条件assert 20 <= t <= 50,那么只有29符合条件。也就是我们需要29个点恢复多项式。

然后,收集齐29个点(x, y)后,我们使用逆离散傅里叶变换 (IDFT) ,可以从这些点值中瞬间计算出多项式f1的所有29个系数。我们将这些系数存入集合 candidates_1,同理第二次一样的操作,最后计算交集。仍然为了便于理解,这里给出ai详细化后的脚本

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

import sys
import socket
import ssl
from tqdm import tqdm

# --- Configuration ---
HOST = "ssss.chals.sekai.team"
PORT = 1337
P_VAL = 2**256 - 189
DEGREE = 28
N_POINTS = DEGREE + 1 # 29

# =============================================================================
# Part 1: A stable, standard library-based SSL socket wrapper
# =============================================================================
class SafeRemote:
"""A robust socket wrapper using Python's standard ssl library."""
def __init__(self, address, timeout=10):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(timeout)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.check_hostname = True
context.verify_mode = ssl.CERT_REQUIRED
context.load_default_certs()
self.wrapped_sock = context.wrap_socket(self.sock, server_hostname=address[0])
self.wrapped_sock.connect(address)
self.f = self.wrapped_sock.makefile('rw', buffering=1)
tqdm.write(f"[*] Securely connected to {address[0]}:{address[1]}.", file=sys.stderr)

def recvline(self):
line = self.f.readline()
if not line:
raise EOFError("Connection closed by server while waiting for a line.")
return line.strip()

def sendline(self, data):
if not isinstance(data, str):
data = str(data)
self.f.write(data + '\n')

def close(self):
try:
self.f.close()
self.wrapped_sock.close()
except:
pass

# =============================================================================
# Part 2: Logic to get candidates and optionally submit the secret
# =============================================================================
def perform_challenge(io, challenge_num, secret_to_submit=None):
tqdm.write(f"[*] Executing Challenge {challenge_num}...", file=sys.stderr)

F = GF(P_VAL)
g = F.multiplicative_generator()
w = g^((P_VAL - 1) // N_POINTS)

io.sendline(N_POINTS)
tqdm.write(f" [+] Sent t={N_POINTS}", file=sys.stderr)

y_values = []
for k in tqdm(range(N_POINTS), desc=" [+] Exchanging points", leave=False, file=sys.stderr):
io.sendline(int(w^k))
y_str = io.recvline()
y_values.append(F(int(y_str)))

tqdm.write(" [+] All points received.", file=sys.stderr)

invN = F(1)/N_POINTS
coeffs = set(int(sum(y_values[k] * w^(-m * k) for k in range(N_POINTS)) * invN) for m in range(N_POINTS))
tqdm.write(f" [+] Recovered {len(coeffs)} candidates for this challenge.", file=sys.stderr)

# If we have a secret to submit, send it. Otherwise, send a dummy guess.
if secret_to_submit is not None:
tqdm.write(f" [+] Submitting final secret: {hex(secret_to_submit)}", file=sys.stderr)
io.sendline(secret_to_submit)
else:
tqdm.write(" [+] Sending dummy guess to continue to next challenge.", file=sys.stderr)
io.sendline(0)
io.recvline() # Consume the ':<'

return coeffs

# =============================================================================
# Part 3: Main solver logic with in-place submission
# =============================================================================
def main():
print("--- Solver Started: Using In-Place Submission Strategy ---", file=sys.stderr)

try:
# Establish ONE connection for all operations
io = SafeRemote((HOST, PORT))

# --- Challenge 1: Just gather candidates ---
candidates_1 = perform_challenge(io, 1)

# --- Challenge 2: Gather candidates AND submit the answer ---
tqdm.write(f"[*] Executing Challenge 2 (and preparing to submit)...", file=sys.stderr)

# We need to get the second set of candidates before we can find the secret
F = GF(P_VAL)
g = F.multiplicative_generator()
w = g^((P_VAL - 1) // N_POINTS)

io.sendline(N_POINTS)
tqdm.write(f" [+] Sent t={N_POINTS}", file=sys.stderr)

y_values_2 = []
for k in tqdm(range(N_POINTS), desc=" [+] Exchanging points", leave=False, file=sys.stderr):
io.sendline(int(w^k))
y_str = io.recvline()
y_values_2.append(F(int(y_str)))

tqdm.write(" [+] All points for Challenge 2 received.", file=sys.stderr)

invN = F(1)/N_POINTS
candidates_2 = set(int(sum(y_values_2[k] * w^(-m * k) for k in range(N_POINTS)) * invN) for m in range(N_POINTS))

# --- Now, find the secret and submit it IMMEDIATELY ---
print("\n[*] Calculating intersection and submitting...", file=sys.stderr)
intersection = candidates_1.intersection(candidates_2)

if len(intersection) != 1:
print(f"\n[-] Error: Expected exactly one common secret, but found {len(intersection)}. Aborting.", file=sys.stderr)
io.close()
return

final_secret = intersection.pop()
print(f" [+] Unique secret found: {hex(final_secret)}", file=sys.stderr)

print(f" [+] Submitting final secret NOW.", file=sys.stderr)
io.sendline(final_secret)

# --- Receive the Flag ---
print("\n" + "="*40, file=sys.stderr)
print("!!! SERVER RESPONSE (FLAG) !!!", file=sys.stderr)
response = io.recvline()
print(response)
print("="*40, file=sys.stderr)

io.close()

except Exception as e:
print(f"\n[!] An unexpected error occurred: {e}", file=sys.stderr)
import traceback
traceback.print_exc()

if __name__ == "__main__":
main()

I Dream of Genni

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from hashlib import sha256
from Crypto.Cipher import AES

x = int(input('Enter an 8-digit multiplicand: '))
y = int(input('Enter a 7-digit multiplier: '))
assert 1e6 <= y < 1e7 <= x < 1e8, "Incorrect lengths"
assert x * y != 3_81_40_42_24_40_28_42, "Insufficient ntr-opy"

def dream_multiply(x, y):
x, y = str(x), str(y)
assert len(x) == len(y) + 1
digits = x[0]
for a, b in zip(x[1:], y):
digits += str(int(a) * int(b))
return int(digits)
assert dream_multiply(x, y) == x * y, "More like a nightmare"

ct = '75bd1089b2248540e3406aa014dc2b5add4fb83ffdc54d09beb878bbb0d42717e9cc6114311767dd9f3b8b070b359a1ac2eb695cd31f435680ea885e85690f89'
print(AES.new(sha256(str((x, y)).encode()).digest(), AES.MODE_ECB).decrypt(bytes.fromhex(ct)).decode())

找出另一组能够满足“梦想乘法”的7位数和8位数,直接剪枝吧,但是我的脚本蛮慢,学习一下其他师傅的快速脚本

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
from __future__ import annotations
from hashlib import sha256
from Crypto.Cipher import AES
from tqdm import tqdm

CT = '75bd1089b2248540e3406aa014dc2b5add4fb83ffdc54d09beb878bbb0d42717e9cc6114311767dd9f3b8b070b359a1ac2eb695cd31f435680ea885e85690f89'
EXCLUDED = 381404224402842

# 我们的位索引约定(与原题数字位位置一致):
# x = x0 x1 x2 x3 x4 x5 x6 x7 (x0 是最高位, x7 最低位)
# y = y0 y1 y2 y3 y4 y5 y6 (y0 最高位, y6 最低位)
#
# LSD 搜索时,我们先确定块 (x7*y6) 作为 dream 的最低块,接着 (x6*y5), ..., 最后 (x1*y0)。
# 块序(低到高) 索引 k: 0..6 对应 (x_{7-k}, y_{6-k})
#
# 在已知低位位数 t 时,已知的 x 低位 digits 列表 lx: lx[0]=x7, lx[1]=x6, ...
# 同理 ly: ly[0]=y6, ly[1]=y5, ...
#
# dream 尾串 = 拼接已确定块(从高到低反转)后的字符串;
# 将其按低位数字顺序拆成 digit 数组 D (D[0] 为最末位)。

########################################################################
# 生成 (a,b) 候选
########################################################################

two_digit_pairs = [(a, b, a*b, str(a*b)) for a in range(2, 10) for b in range(2, 10) if a*b >= 10]
# 按乘积值从大到小,提高冲突剪枝概率
two_digit_pairs.sort(key=lambda x: -x[2])

one_digit_pairs = [(a, b, a*b, str(a*b)) for a in range(0, 10) for b in range(0, 10) if a*b <= 9]
# 同样可按乘积值排序(大的一位数优先)
one_digit_pairs.sort(key=lambda x: -x[2])

########################################################################
# 低位 carry DP 剪枝
########################################################################

def tail_digits_from_blocks(blocks_low_to_high: list[str]) -> list[int]:
"""blocks_low_to_high: 低位到高位的块字符串;拼接成 tail 再转低位 digits 列表"""
if not blocks_low_to_high:
return []
s = "".join(reversed(blocks_low_to_high))
return list(map(int, reversed(s))) # D[0] 低位

def build_known_and_unknown_bounds(lx: list[int], ly: list[int], L_tail: int):
"""
计算:
known_sum[e]: 所有已知对 (xi,yj) 且 i+j=e 的 xi*yj 之和
U_e_max[e]: 位 e 可能由尚未确定的对贡献的最大增量上界 (使用 81 保守估计)
这里只对 e < L_tail 的低位关心。
"""
t = len(lx)
max_e = L_tail - 1
if max_e < 0:
return [], []
known_sum = [0]*(max_e+1)
U_e_max = [0]*(max_e+1)
# 已知对:i< t, j< t
for i in range(t):
for j in range(t):
e = i + j
if e <= max_e:
known_sum[e] += lx[i]*ly[j]
# 未知对:只统计 (i,j) 尚未被双方同时确定的那些
# 我们按实际上限所有 (i,j) i<8, j<7
for i in range(8):
for j in range(7):
e = i + j
if e > max_e:
continue
# 若这个对完全已知则跳过
if i < t and j < t:
continue
# 上界:81
U_e_max[e] += 81
return known_sum, U_e_max

def carry_feasible(digits: list[int], lx: list[int], ly: list[int]) -> bool:
"""
逐位(真实乘积的低位 -> 高位)DP 判断:是否存在未知对的贡献选择
使得低 L_tail 位可以匹配 dream 尾串 digits。
digits: D[e] (e=0..L_tail-1)
"""
L_tail = len(digits)
if L_tail == 0:
return True
known_sum, U_e_max = build_known_and_unknown_bounds(lx, ly, L_tail)
# possible carries 用区间集合表示(初始只有 0)
carry_min = 0
carry_max = 0
for e in range(L_tail):
d = digits[e]
S_known = known_sum[e]
Umax = U_e_max[e]
new_carry_min = None
new_carry_max = None
# 我们有 carry_in ∈ [carry_min, carry_max]
# 对每个可能的 carry_in 需要存在 k ∈ [0,Umax] s.t.
# (S_known + k + carry_in) % 10 == d
# 令 R = (d - (S_known + carry_in)) mod 10 -> 最小 k0 = R
# 若 k0 > Umax -> 不可
# 若可,k 可为 k0 + 10t ≤ Umax,产生的 carry_out = (S_known + carry_in + k)/10
# carry_out 的最小来自最小 k(k0),最大来自最大 k (k0 + 10*floor((Umax-k0)/10))
for carry_in in range(carry_min, carry_max+1):
base = S_known + carry_in
R = (d - (base % 10)) % 10
if R > Umax:
continue
# 最小合法 k
k_min = R
# 最大合法 k
tmax = (Umax - R)//10
k_max = R + 10*tmax
# 计算 carry_out 区间
co_min = (base + k_min)//10
co_max = (base + k_max)//10
if new_carry_min is None or co_min < new_carry_min:
new_carry_min = co_min
if new_carry_max is None or co_max > new_carry_max:
new_carry_max = co_max
if new_carry_min is None:
return False
carry_min, carry_max = new_carry_min, new_carry_max
# 可选:限制 carry 上界(理论上不会太大);保守不裁剪
return True

########################################################################
# 主 DFS
########################################################################

solutions = []

def search_mode(all_two_digit: bool,
single_pos: int | None,
continue_search: bool,
show_progress: bool):
"""
all_two_digit = True : k=7 模式
all_two_digit = False : k=6 模式, single_pos 指定哪一个块(0..6, 低位=0) 为单一位积
single_pos: 块索引(低位=0);只有在 all_two_digit=False 时使用
"""
mode_desc = "k=7(all 2-digit)" if all_two_digit else f"k=6(single at block {single_pos})"
print(f"[MODE] {mode_desc}")

# 预准备第一层候选(最低块);根据模式决定允许集合
if all_two_digit:
first_candidates = two_digit_pairs
else:
# 若最低块恰好是 single_pos,则用一位集合,否则两位
first_candidates = one_digit_pairs if single_pos == 0 else two_digit_pairs

pbar = tqdm(first_candidates, desc="First block", disable=not show_progress)

# 为减少重复,可使用集合记录已探索某些前缀状态 (可选,这里不做以保持清晰)

def dfs(block_index: int,
blocks_low_to_high: list[str],
lx: list[int], ly: list[int]):
# 已放置块数 block_index
# 长度判断
t_blocks = block_index # 已确定的块数
tail_str = "".join(reversed(blocks_low_to_high))
L_tail = len(tail_str)

# 已放块数 = t_blocks, 剩余 r = 7 - t_blocks
r = 7 - t_blocks

# 当前已使用的一位块数
single_used = 0 if all_two_digit else 1 # 在 k=6 模式里我们强制 exactly one; 进入 DFS 时已默认包含 single_pos 之前的统计
if not all_two_digit:
# 统计当前 blocks_low_to_high 中是否已经包含 single_pos 位置的一位块
# 不计数其它(因为其它位置不能是一位块)
# 简化:到调用 dfs 时,已经严格按照模式过滤了候选,不需要重复判断
pass

# 目标长度:
# - k=7 -> dream 总长应=15
# - k=6 -> dream 总长应=14
target_total_len = 15 if all_two_digit else 14
# 当前 dream 总长 = 1 (x0) + L_tail
current_total_min = 1 + L_tail + r*1
current_total_max = 1 + L_tail + r*2
if current_total_min > target_total_len or current_total_max < target_total_len:
return

# 低位 carry DP 剪枝(只针对已经有尾部的情况)
if L_tail > 0:
digits = list(map(int, reversed(tail_str)))
if not carry_feasible(digits, lx, ly):
return

# 若 7 块全部完成 -> 选 x0
if t_blocks == 7:
dream_tail = tail_str # (x1*y0)...(x7*y6)
# dream 总长 = 1 + len(dream_tail)
total_len = 1 + len(dream_tail)
if total_len != target_total_len:
return
# 还需要检查:k=7 模式下所有块是两位;k=6 模式下恰好 single_pos 那块是一位且其它两位;
if all_two_digit:
# quick assert
if any(len(b) != 2 for b in blocks_low_to_high):
return
else:
# single_pos 块必须是一位,其他必须是两位
for idx, blk in enumerate(blocks_low_to_high):
if idx == single_pos:
if len(blk) != 1:
return
else:
if len(blk) != 2:
return

# reconstruct x,y:
if len(lx) != 7 or len(ly) != 7:
return
x_digits = [None]*8
y_digits = [None]*7
# lx[0]=x7,... -> x_digits[7-i] = lx[i]
for i, dval in enumerate(lx):
x_digits[7 - i] = dval
for i, dval in enumerate(ly):
y_digits[6 - i] = dval

blocks_from_high = list(reversed(blocks_low_to_high))
suffix = "".join(blocks_from_high)
# 选 x0
for x0 in range(1, 10):
x_digits[0] = x0
x = int("".join(map(str, x_digits)))
y = int("".join(map(str, y_digits)))
dream_val = int(str(x0) + suffix)
prod = x * y
if prod == dream_val and prod != EXCLUDED:
key = sha256(str((x, y)).encode()).digest()
pt = AES.new(key, AES.MODE_ECB).decrypt(bytes.fromhex(CT))
try:
dec = pt.decode()
except UnicodeDecodeError:
dec = repr(pt)
print(f"[FOUND] mode={mode_desc} x={x} y={y} prod={prod}")
print(f" dream={dream_val}")
print(" Decrypted:", dec)
solutions.append((x, y, prod, dec))
if not continue_search:
raise StopIteration
return

# 选择下一块 (block_index)
k = block_index # 0..6, 低位=0
# 根据模式决定候选集合
if all_two_digit:
cand = two_digit_pairs
else:
cand = one_digit_pairs if k == single_pos else two_digit_pairs

for a, b, pval, s in cand:
# 追加块
new_blocks = blocks_low_to_high + [s]
new_lx = lx + [a]
new_ly = ly + [b]
# 长度快速界
new_tail_len = L_tail + len(s)
new_r = 7 - (t_blocks + 1)
min_total = 1 + new_tail_len + new_r*1
max_total = 1 + new_tail_len + new_r*2
if min_total > target_total_len or max_total < target_total_len:
continue
dfs(block_index + 1, new_blocks, new_lx, new_ly)

try:
for (a, b, pval, s) in pbar:
dfs(1, [s], [a], [b])
if solutions and not continue_search:
break
except StopIteration:
pass
finally:
pbar.close()


def solve(continue_search=False, show_progress=True):
# 1) k=7 全两位
search_mode(all_two_digit=True,
single_pos=None,
continue_search=continue_search,
show_progress=show_progress)
if solutions and not continue_search:
return solutions
# 2) k=6:枚举哪块是一位
for pos in range(7):
if solutions and not continue_search:
break
search_mode(all_two_digit=False,
single_pos=pos,
continue_search=continue_search,
show_progress=show_progress)
return solutions


if __name__ == "__main__":
res = solve(continue_search=False, show_progress=True)
if not res:
print("No solution found in current search configuration.")
else:
print(f"Total solutions: {len(res)}")
for x, y, prod, dec in res:
print(f"x={x} y={y} prod={prod} plaintext={dec}")