Crypto 方向

题目1:GZRSA

审计一下源代码,发现N是仅由flag作为seed生成的,所以是一直不变的,只有e每次打开容器时会根据时间戳变化
N不变,可以用共模攻击,但是需要开两次容器拿到两组数据
最后的结果是:furryCTF{ee74fc3b0b4d_E4SY_RS4_wIth_G2cTf_FRaM3wOrK}

题目2:0x4A

需要找个网站用0x4A做秘钥连续解密3次,最后得到:POFP{2394E9DA555D55D493A28624D901D2CA}
但是写wp的时候找不到那个网站了,当时也没记录

题目3:你是说这是个数学题?

这题在比赛时是PPC板块的,但是应该算是密码题
初始数据:flag的二进制用列表表示,还有一个单位矩阵,对角线全是1
题目做了很多次线性变换,每次做的变换是,在矩阵随机取两个不同的行做异或,然后对二进制列表中相同的元素进行同样的操作
最后得到变化之后的matrix和result
看似很复杂,但是都是线性的,并且每次变换前后的元素都是一一对应的,一定是可逆的
最后逆出来的初始flag二进制直接转码不对,是因为在最开始把flag明文变成二进制的时候去掉了ASCII码的前导0
那么在复原的时候有些位置由于不知道当前位置是字母还是数字,所以需要爆破一下
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
import sys

# 增加递归深度,防止解码长字符串时报错
sys.setrecursionlimit(5000)

# ==========================================
# 区域 1:数据填充区 (请手动修改这里)
# ==========================================

# 1. 打开你的 Encrypt (1).py 文件
# 2. 找到最底部的 #matrix=['...', '...'] 这一行
# 3. 复制中括号 [] 里的所有内容(不要带前面的 #matrix=),粘贴到下面 matrix_source = 的方括号中


matrix_source=[...]
result_source=[1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1]

# ==========================================
# 区域 2:解密逻辑 (无需修改)
# ==========================================

def solve():
# --- 1. 数据预处理与检查 ---
if not matrix_source or not result_source:
print("错误:请先在脚本顶部的 matrix_source 和 result_source 中填入数据!")
return

# 将字符串矩阵转换为整数列表矩阵
# 原始数据是 '0101' 字符串,转换为 [0, 1, 0, 1] 列表
M = [[int(c) for c in row] for row in matrix_source]
R = result_source[:]
n = len(R)

# 维度检查
if len(M) != n:
print(f"数据错误:矩阵行数 ({len(M)}) 与结果向量长度 ({n}) 不一致!")
print("请检查是否完整复制了 matrix 列表。")
return

print(f"数据加载成功。正在解 {n} 元线性方程组 (GF(2))...")

# --- 2. 高斯消元法求解 (GF(2)域) ---
# 构造增广矩阵 [M | R]
aug = [M[i] + [R[i]] for i in range(n)]

# 转换为行阶梯形矩阵
for i in range(n):
# 寻找主元 (Pivot)
pivot = -1
for k in range(i, n):
if aug[k][i] == 1:
pivot = k
break

if pivot == -1:
continue # 该列全为0,跳过(如果是可逆矩阵不应发生)

# 交换行,将主元移到当前对角线位置
aug[i], aug[pivot] = aug[pivot], aug[i]

# 消元:用当前行去异或下面的所有行
for k in range(n):
if k != i and aug[k][i] == 1:
# 优化:Python的切片异或比较慢,但对于几百维是够用的
for j in range(i, n + 1):
aug[k][j] ^= aug[i][j]

# 提取解向量 (最后一列)
solution_bits = "".join(str(aug[i][n]) for i in range(n))
print("解密完成,得到二进制流。")
print(f"二进制长度: {len(solution_bits)}")

# --- 3. 递归解码 (Flag还原) ---
# CTF常见字符集:字母、数字、下划线、花括号
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}"

# 构建 映射表:二进制 -> 字符
# 注意:ord('a')=97 -> 0b1100001 (7位),ord('0')=48 -> 0b110000 (6位)
# 题目中的生成脚本是不定长的,所以我们需要同时匹配 6位 和 7位
code_map = {}
for c in charset:
# 去掉 '0b' 前缀
bits = bin(ord(c))[2:]
code_map[bits] = c

found_flags = []

def decode_recursive(index, current_str):
# 递归出口:二进制流完全匹配完
if index == len(solution_bits):
found_flags.append(current_str)
return

# 尝试匹配 7 位编码 (字母、符号通常是7位)
if index + 7 <= len(solution_bits):
chunk = solution_bits[index : index+7]
if chunk in code_map:
decode_recursive(index + 7, current_str + code_map[chunk])

# 尝试匹配 6 位编码 (数字通常是6位)
if index + 6 <= len(solution_bits):
chunk = solution_bits[index : index+6]
if chunk in code_map:
decode_recursive(index + 6, current_str + code_map[chunk])

print("\n正在尝试解码文本...")

# 优化:已知 flag 以 "furryCTF{" 开头
# 我们先验证解密出的二进制前缀是否匹配这个头,匹配则直接从后面开始解
prefix = "furryCTF{"
start_ptr = 0
prefix_match = True

for char in prefix:
bits = bin(ord(char))[2:]
if solution_bits.startswith(bits, start_ptr):
start_ptr += len(bits)
else:
prefix_match = False
break

if prefix_match:
print(f"前缀 '{prefix}' 匹配成功,加速解码中...")
decode_recursive(start_ptr, prefix)
else:
print(f"警告:前缀 '{prefix}' 不匹配。尝试全量解码(可能会很慢或失败)...")
decode_recursive(0, "")

# 输出结果
print("\n" + "="*30)
if found_flags:
print("找到可能的 Flag:")
for f in found_flags:
print(f"-> {f}")

# 自动筛选最可能的 (根据题目提示)
best_flag = max(found_flags, key=len) # 通常最长的是正确解析
print("\n推荐提交: " + best_flag)
else:
print("未找到符合字符集的 Flag,请检查 matrix 数据是否正确。")
print("="*30)

if __name__ == '__main__':
solve()

最后得到512个候选flag,出题人把这512个都设置成正确答案了,随便选一个交就行

题目4:迷失

代码审计:
本质上是一个二分查找构成的明文与密文一一对应的映射关系,而且保证若了P1 < P2,则C1 < C2
虽然有AES的随机扰动,但是key是固定的,而且对于每个待加密的字符,seed也是固定的,所以每次的干扰都是固定的,且不影响一一对应的关系和保序
根据已知的明密文对应关系,可以确定一些位置,剩下未知的插值恢复即可
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
def solve():
# 1. 题目提供的密文 m
m_hex = "4ee06f407770280066806d00609167402800689173402800668074f17200720079004271550046e07b0050006d0065c06091734074f1720065c05f4050f174f165c0720079005f404f7072003a6065c072005f405000720065c0734065c03af0768068916e8067405f406295720079007000740068916f406e805f406f4077706f407cf128002f4928006df06091650065c0280061e17900280050f150f13c5938d4382039403940379037903b8039d038203b802800714077707140"

# 2. 已知明文信息
# 前缀:根据 flag 格式
prefix = "Now flag is furryCTF{"
# 后缀:根据源代码中的 print 语句
suffix = "} - made by QQ:3244118528 qwq"

# 3. 将密文 Hex 转换为 2 字节整数列表
# 每个块 4 个 hex 字符 (16 bit = 2 bytes)
cipher_ints = [int(m_hex[i:i+4], 16) for i in range(0, len(m_hex), 4)]

# 4. 建立已知映射表 (Cipher -> Char)
known_map = {}

# 映射前缀
for i, char in enumerate(prefix):
cipher_val = cipher_ints[i]
known_map[cipher_val] = char

# 映射后缀
# 后缀从列表末尾向前对应
suffix_start_index = len(cipher_ints) - len(suffix)
for i, char in enumerate(suffix):
cipher_val = cipher_ints[suffix_start_index + i]
known_map[cipher_val] = char

# 对已知密文进行排序,方便二分查找/范围判断
sorted_ciphers = sorted(known_map.keys())

# 5. 解密未知部分
# 未知部分位于前缀和后缀之间
flag_content_indices = range(len(prefix), len(cipher_ints) - len(suffix))

decrypted_chars = []

print(f"[*] 已恢复 {len(known_map)} 个字符的映射关系。")
print("[*] 开始推导未知字符...\n")

for idx in flag_content_indices:
c_val = cipher_ints[idx]

# 如果这个密文正好在已知表中(比如重复出现的字符)
if c_val in known_map:
decrypted_chars.append(known_map[c_val])
continue

# 如果不在表中,寻找它的上下界
# 找到 c_val 在 sorted_ciphers 中的插入位置
# prev_c < c_val < next_c
import bisect
pos = bisect.bisect_left(sorted_ciphers, c_val)

prev_c = sorted_ciphers[pos - 1]
next_c = sorted_ciphers[pos]

prev_char = known_map[prev_c]
next_char = known_map[next_c]

# 核心逻辑:保序性推导
# 如果上下界字符 ASCII 码只差 2,那中间一定差 1
if ord(next_char) - ord(prev_char) == 2:
guess_char = chr(ord(prev_char) + 1)
print(f"位置 {idx}: 密文 {c_val:04x} 位于 '{prev_char}'({prev_c:04x}) 和 '{next_char}'({next_c:04x}) 之间 -> 推断为 '{guess_char}'")
decrypted_chars.append(guess_char)

else:
# 如果跨度较大(比如数字到字母),使用线性插值估算
# 假设密文分布是相对均匀的
total_gap = next_c - prev_c
my_gap = c_val - prev_c
ratio = my_gap / total_gap
char_gap = ord(next_char) - ord(prev_char)

estimated_offset = int(round(char_gap * ratio))
guess_char = chr(ord(prev_char) + estimated_offset)

print(f"位置 {idx}: 密文 {c_val:04x} 位于 '{prev_char}' 和 '{next_char}' 之间 (线性插值) -> 推断为 '{guess_char}'")
decrypted_chars.append(guess_char)

# 6. 拼凑最终结果
full_flag = prefix + "".join(decrypted_chars) + suffix

# 提取花括号内的内容
start = full_flag.find("{")
end = full_flag.find("}")

print("\n" + "="*50)
print(f"完整解密结果: {full_flag}")
print(f"最终 Flag: {full_flag[start:end+1]}")
print("="*50)

if __name__ == "__main__":
solve()

最后结果:furryCTF{Pleasure_Query_Or6er_Prese7ving_cryption_owo}

题目5:hide

HNP问题,将题目的矩阵转换成整数方程后构建格,用LLL算法规约出来
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
from Crypto.Util.number import long_to_bytes

# --- 题目数据 ---
x = 110683599327403260859566877862791935204872600239479993378436152747223207190678474010931362186750321766654526863424246869676333697321126678304486945686795080395648349877677057955164173793663863515499851413035327922547849659421761457454306471948196743517390862534880779324672233898414340546225036981627425482221
A = [7010037768323492814068058948174853511882398276332776121585079407678330793092800035269526181957255399672652011111654741599608887098109580353765882969176288829698783809623046145668133636075432524440915257579561871685314889370489860185806532259458628868370653070766497850259451961004644017942384235055797395644, 74512008367681391576615422563769111304299667679061047768808113939982483619544887008328862272153828562552333088496906580861267829681506163090926448703049851520594540919689526223471861426095725497571027934265222847996257902446974751505984356357598199691411825903191674839607030952271799209449395136250172915515, 25171034166045065048766468088478862083654896262788374008686766356983492064821153256216151343757671494619313358321028585201126451603499400800590845023208694587391285590589998721718768705028189541469405249485448442978139438800274489463915526151654081202939476333828109332203871789408483221357748609311358075355, 52306344268758230793760445392598730662254324962115084956833680450776226191926371213996086940760151950121664838769606693834086936533634419430890689801544767742709480565738473278968217081629697632917059499356891370902154113670930248447468493869766005495777084987102433647416014761261066086936748326218115032801, 2648050784571648217531939202354197938389512824250133239934656370441229591673153566810342978780796842103474408026748569769289860666767084333212674530469910686231631759794852701142391634889712214232039601137248325291058095314745786903631551946386508619385174979529538717455213294397556550354362466891057541888, 4166766374977094264345277893694623030532483103866451849932564813429296670145052328195058889292880408332777827251072855711166381389290737203475814458557602354827802370340106885546253665151376153287179701847638247208647055846230060548340862356687738774258116075051088973344675967295352247188827680132923498399]
C = [96354217664113218713079763550257275104215355845815212539932683912934781564627, 30150406435560693444237221479565769322093520010137364328243360133422483903497, 70602489044018616453691889149944654806634496215998208471923855476473271019224, 48151736602211661743764030367795232850777940271462869965461685371076203243825, 103913167044447094369215280489501526360221467671774409004177689479561470070160, 84110063463970478633592182419539430837714642240603879538426682668855397515725]

# --- 构造格 (Hidden Number Problem) ---
shift = 2^256
inv_shift = inverse_mod(shift, x)

# 转换系数
alphas = [(Ai * inv_shift) % x for Ai in A]
gammas = [(Ci * inv_shift) % x for Ci in C]

# 权重设置
Weight = 2^256
Const_Val = 2^768

# 构造矩阵
# 格式:
# [W, a0, a1 ... a5, 0]
# [0, x, 0 ... 0, 0]
# ...
# [0, -g0, -g1 ... -g5, Const]

B_rows = []
# Row 0
row0 = [Weight] + alphas + [0]
B_rows.append(row0)

# Rows 1-6 (Modulus)
for i in range(6):
r = [0] * 8
r[i+1] = x
B_rows.append(r)

# Row 7 (Constants)
row7 = [0] + [(-g % x) for g in gammas] + [Const_Val]
B_rows.append(row7)

# 创建矩阵并规约
M = Matrix(ZZ, B_rows)
# SageMath 的 LLL 是 C++ 实现的,非常快
L = M.LLL()

# 提取结果
vec = L[0]
# 向量第一位是 m * Weight,所以除以 Weight
possible_m = abs(vec[0]) // Weight

# 打印
print(long_to_bytes(int(possible_m)))

flag:pofp{8bbda68c-9a6f-41dd-bf27-a143d2644a9aaa}

题目6:lazy signer

经典的ECDSA共用随机数k_nonce攻击,是交互模式的
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
from pwn import *
from hashlib import sha256
from Crypto.Cipher import AES
from Crypto.Util.number import inverse

# 配置连接信息
HOST = 'ctf.furryctf.com'
PORT = 33058

# SECP256k1 曲线的阶 n
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

def solve():
# 连接服务器
io = remote(HOST, PORT)

# 1. 获取加密的 Flag
io.recvuntil(b"Encrypted Flag (hex): ")
enc_flag_hex = io.recvline().strip().decode()
print(f"[+] Encrypted Flag: {enc_flag_hex}")

# 2. 签名第一条消息 "1"
msg1 = b"1"
io.sendlineafter(b"Option: ", b"1")
io.sendlineafter(b"Enter message to sign: ", msg1)

io.recvuntil(b"Signature (r, s): (")
sig1 = io.recvline().strip().decode().strip(')').split(', ')
r = int(sig1[0])
s1 = int(sig1[1])
print(f"[+] Sig 1: r={r}, s={s1}")

# 3. 签名第二条消息 "2"
msg2 = b"2"
io.sendlineafter(b"Option: ", b"1")
io.sendlineafter(b"Enter message to sign: ", msg2)

io.recvuntil(b"Signature (r, s): (")
sig2 = io.recvline().strip().decode().strip(')').split(', ')
# r 应该是不变的,我们只需要 s2
s2 = int(sig2[1])
print(f"[+] Sig 2: r={r}, s={s2}")

# 4. 计算消息哈希 z1, z2
z1 = int.from_bytes(sha256(msg1).digest(), 'big')
z2 = int.from_bytes(sha256(msg2).digest(), 'big')

# 5. 恢复随机数 k
# k = (z1 - z2) * (s1 - s2)^-1 mod n
numerator = (z1 - z2) % n
denominator = inverse(s1 - s2, n)
k = (numerator * denominator) % n
print(f"[+] Recovered k: {k}")

# 6. 恢复私钥 d
# d = r^-1 * (s1 * k - z1) mod n
r_inv = inverse(r, n)
d = (r_inv * (s1 * k - z1)) % n
print(f"[+] Recovered d: {d}")

# 7. 解密 Flag
# 根据题目代码:aes_key = hashlib.sha256(str(d).encode()).digest()
aes_key = sha256(str(d).encode()).digest()
cipher = AES.new(aes_key, AES.MODE_ECB)

encrypted_bytes = bytes.fromhex(enc_flag_hex)
flag = cipher.decrypt(encrypted_bytes)

print("\n-------------------------------------------")
# 去除 padding 后打印
try:
print(f"FLAG: {flag.decode().strip()}")
except:
print(f"FLAG (bytes): {flag}")
print("-------------------------------------------")

io.close()

if __name__ == '__main__':
solve()

结果:
![](/img/2026-furryCTF-crypto-Writeup/lazy signer/flag.png)

题目7:Tiny random

标准的ECDSA椭圆曲线数字签名要求随机数k是[0,n-1]内的强随机数,但是本题限制k<2^128.那么k的大小远小于n,是HNP问题,可以用LLL算法算出
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
from pwn import *
import hashlib
import json
from sage.all import *

# === 配置 ===
HOST = 'ctf.furryctf.com'
PORT = 33059
# 如果连接不稳定,可以适当增加 timeout
context.timeout = 5

# === SECP256k1 曲线参数 ===
p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
a = 0
b = 7
n = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
E = EllipticCurve(GF(p), [a, b])
G = E.point((0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,
0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8))

def attack():
# 1. 连接服务器
io = remote(HOST, PORT)

# 接收公钥
# 题目代码显示发送公钥在 handle() 开头
try:
# 有可能服务器会先发送一些 banner,如果第一行不是 json,多读几行
line = io.recvline().strip()
print(f"[*] Received: {line}")
data = json.loads(line)
pub_x = int(data['x'])
pub_y = int(data['y'])
pub_key = E(pub_x, pub_y)
print(f"[+] PubKey: ({hex(pub_x)}, {hex(pub_y)})")
except Exception as e:
print(f"[-] Error parsing pubkey: {e}")
io.close()
return

# 2. 收集签名样本
# Nonce 是 128bit,N 是 256bit。
# 信息泄露率 approx 50%,理论上 2-3 个样本即可,取 4 个保证稳定
sigs = []
print("[*] Collecting signatures...")

for i in range(4):
msg = f"pwn_{i}"
payload = json.dumps({"op": "sign", "msg": msg})
io.sendline(payload.encode())

try:
resp = json.loads(io.recvline().strip())
r_val = int(resp['r'], 16)
s_val = int(resp['s'], 16)
h_val = int(resp['h'], 16)
sigs.append((r_val, s_val, h_val))
print(f" Sample {i+1}: captured")
except:
print("[-] Error receiving signature")
break

# 3. 构造格 (Lattice Construction)
print("[*] Building Lattice...")
m = len(sigs)
B = 2**128 # Nonce 的上界

# 我们有方程: k - t*d - a = 0 (mod n)
# 其中 t = r/s, a = h/s
# 构造矩阵求解隐数问题 (HNP)

ts = []
as_ = []
for (rr, ss, hh) in sigs:
sinv = inverse_mod(ss, n)
ts.append((sinv * rr) % n)
as_.append((sinv * hh) % n)

# 矩阵维度 (m+2)
# Rows 0..m-1: modulus constraint
# Row m: private key constraint
# Row m+1: constants
M = Matrix(QQ, m + 2, m + 2)

for i in range(m):
M[i, i] = n
M[m, i] = ts[i]
M[m+1, i] = as_[i]

# 权重调整,使得目标向量的最后一项约为 B
# 目标向量 v = (k1, k2, ..., km, d*B/n, B)
M[m, m] = QQ(B) / QQ(n) # 对应 d 的列
M[m+1, m+1] = B # 对应常数 1 的列

print("[*] Running LLL algorithm (this might take a second)...")
L = M.LLL()

priv_key = None

# 4. 从基向量中寻找私钥
# LLL 规约后,寻找倒数第二列包含 d 信息,倒数第一列是 B 的行
for row in L:
# 检查最后一列是否为 +/- B (可能存在符号翻转)
if abs(row[m+1]) == B:
# 对应的倒数第二列元素应该是 +/- d * (B/n)
potential_scaled_d = row[m]

# 反解 d: d = potential / (B/n)
# 注意处理符号
try:
d_candidate = abs(int(round(potential_scaled_d * n / B)))

# 验证私钥
if d_candidate * G == pub_key:
priv_key = d_candidate
break
# 有时候是 n - d
if (n - d_candidate) * G == pub_key:
priv_key = n - d_candidate
break
except:
continue

if not priv_key:
print("[-] Attack failed. Private key not found.")
io.close()
return

print(f"[+] Private Key Found: {hex(priv_key)}")

# 5. 伪造签名获取 Flag
print("[*] Forging signature for 'give_me_flag'...")
target_msg = b'give_me_flag'
h_target = int(hashlib.sha256(target_msg).hexdigest(), 16)

# 使用恢复的私钥进行标准 ECDSA 签名
k_forge = 123456789 # 任意随机数
r_forge = int((k_forge * G).xy()[0])
s_forge = (inverse_mod(k_forge, n) * (h_target + r_forge * priv_key)) % n

payload = json.dumps({
"op": "flag",
"r": hex(r_forge),
"s": hex(s_forge)
})

io.sendline(payload.encode())

# 6. 读取 Flag
try:
flag_resp = io.recvline().strip().decode()
print(f"\n[SUCCESS] {flag_resp}")
except Exception as e:
print(f"[-] Failed to get flag response: {e}")

io.close()

if __name__ == '__main__':
attack()

结果:
![](/img/2026-furryCTF-crypto-Writeup/Tiny random/flag.png)