smb坐牢实录
今天是2025.07.19,”期待已久”的熵密杯正式开赛了</br>
早上10点正式开赛,但是7点半就要上车了,明明只有40min左右的车程,但是不知道是为什么主办方要我们那么早起床,来比赛场地还打了一会游戏(bushi)</br>
等了好一会,开幕式终于开始了,该说不说,赛场布置的大屏幕还是非常好看的,高刷新率、高帧率,把领导讲话的气场展现得淋漓尽致</br>
开幕式结束,比赛就随之开始了,上来硬控5min下载3道初始赛题的zip,3个文件整整齐齐都是49.7MB,第一反应就是先下出来看一眼。比赛第3分钟左右,当时大部分队伍连赛题都还没下载完,中科大的Nebula已经拿下初始赛题3的一血了,当时隔壁的队伍惊呼“我们都还在下赛题!”。当然,这是整场比赛最简单的一题,也是我们队唯一能做的一题了(主要还是HvAng的功劳,反正我没看出来)</br>
初始赛题3题目代码如下: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
109from typing import List, Callable
from hashlib import sha256
def hex_to_32byte_chunks(hex_str):
# 确保十六进制字符串长度是64的倍数(因为32字节 = 64个十六进制字符)
if len(hex_str) % 64 != 0:
raise ValueError("十六进制字符串长度必须是64的倍数")
# 每64个字符分割一次,并转换为字节
return [bytes.fromhex(hex_str[i:i + 64]) for i in range(0, len(hex_str), 64)]
def openssl_sha256(message: bytes) -> bytes:
return sha256(message).digest()
class WOTSPLUS:
def __init__(
self,
w: int = 16, # Winternitz 参数,控制空间与时间的复杂度
hashfunction: Callable = openssl_sha256, # 哈希函数
digestsize: int = 256, # 摘要大小,单位为比特
pubkey: List[bytes] = None,
) -> None:
self.w = w
if not (2 <= w <= (1 << digestsize)):
raise ValueError("规则错误:2 <= w <= 2^digestsize")
# 消息摘要所需的密钥数量(默认8个)
self.msg_key_count = 8
# 校验和密钥数量
self.cs_key_count = 0
# 总密钥数量 = 消息密钥 + 校验和密钥
self.key_count = self.msg_key_count + self.cs_key_count
self.hashfunction = hashfunction
self.digestsize = digestsize
self.pubkey = pubkey
def number_to_base(num: int, base: int) -> List[int]:
if num == 0:
return [0] # 如果数字是 0,直接返回 0
digits = [] # 存储转换后的数字位
while num:
digits.append(int(num % base)) # 获取当前数字在目标进制下的个位,并添加到结果列表
num //= base # 对数字进行整除,处理下一位
return digits[::-1] # 返回按顺序排列的结果
def _chain(self, value: bytes, startidx: int, endidx: int) -> bytes:
for i in range(startidx, endidx):
value = self.hashfunction(value) # 每次迭代对当前哈希值进行哈希操作
return value
def get_signature_base_message(self, msghash: bytes) -> List[int]:
# 将消息哈希从字节转换为整数
msgnum = int.from_bytes(msghash, "big")
# 将消息的数字表示转换为特定进制下的比特组表示
msg_to_sign = self.number_to_base(msgnum, self.w)
# 校验消息比特组的数量是否符合预期
if len(msg_to_sign) > self.msg_key_count:
err = (
"The fingerprint of the message could not be split into the"
+ " expected amount of bitgroups. This is most likely "
+ "because the digestsize specified does not match to the "
+ " real digestsize of the specified hashfunction Excepted:"
+ " {} bitgroups\nGot: {} bitgroups"
)
raise IndexError(err.format(self.msg_key_count, len(msg_to_sign)))
return msg_to_sign
def get_pubkey_from_signature(
self, digest: bytes, signature: List[bytes]
) -> List[bytes]:
msg_to_verify = self.get_signature_base_message(digest)
result = []
for idx, val in enumerate(msg_to_verify):
sig_part = signature[idx]
chained_val = self._chain(sig_part, val, self.w - 1)
result.append(chained_val)
return result
def verify(self, digest: bytes, signature: List[bytes]) -> bool:
pubkey = self.get_pubkey_from_signature(digest, signature)
return True if pubkey == self.pubkey else False
if __name__ == "__main__":
pubkey_hex = "5057432973dc856a7a00272d83ea1c14de52b5eb3ba8b70b373db8204eb2f902450e38dbade5e9b8c2c3f8258edc4b7e8101e94ac86e4b3cba92ddf3d5de2a2b454c067a995060d1664669b45974b15b3423cec342024fe9ccd4936670ec3abaae4f6b97279bd8eb26463a8cb3112e6dcbf6301e4142b9cdc4adfb644c7b114af4f0cf8f80e22c3975ba477dc4769c3ef67ffdf2090735d81d07bc2e6235af1ee41ef332215422d31208c2bc2163d6690bd32f4926b2858ca41c12eec88c0a300571901a3f674288e4a623220fb6b70e558d9819d2f23da6d897278f4056c346d7f729f5f70805ad4e5bd25cfa502c0625ac02185e014cf36db4ebcdb3ed1a38"
pubkey_list_bytes = hex_to_32byte_chunks(pubkey_hex)
wots = WOTSPLUS(pubkey = pubkey_list_bytes)
digest_hex = "84ffb82e"
signature_hex = "25d5a0e650d683506bfe9d2eca6a3a99b547a4b99398622f6666ce10131e971b6bd36841c9074fe9b4de2900ebe3fadb3202a173be486da6cf8f3d8c699c95c3454c067a995060d1664669b45974b15b3423cec342024fe9ccd4936670ec3abaae4f6b97279bd8eb26463a8cb3112e6dcbf6301e4142b9cdc4adfb644c7b114a4966398a789b56bdb09ea195925e7e8cde372305d244604c48db08f08a6e8a38951030deb25a7aaf1c07152a302ebc07d5d0893b5e9a5953f3b8500179d138b9aa90c0aaacea0c23d22a25a86c0b747c561b480175b548fcb1f4ad1153413bc74d9c049d43ffe18ceee31e5be8bdb9968103ef32fb4054a4a23c400bbfe0d89f"
digest_bytes = bytes.fromhex(digest_hex)
signature = hex_to_32byte_chunks(signature_hex)
valid = wots.verify(digest_bytes,signature)
print(valid)
# 历史消息摘要(16进制):
# bf0acbfe
# 历史签名(16进制):
# 35d9a79d19cf652305d28050af95153940b71f6c61933bed734be6f9afea37bd241254a53dd5ed3510465d16d1ce7ee10554b3a2111c20084a532fa30fbcb15264038b633d2eaf369b412dff26c89d626327ba544cfe51f3402d4a753c2a442052175f5330c0c9d5d64069ad106654e6d2f5ce867f607f0780fac7f9368366fa948e84cc3508fc89b04e78afd601015eb98e7d2bea2d8c9ad3972b953985e6a900f893a3377702524cd2ddb1325b2e1b5783e870fe8e8a7bf4c940b6f62e90907ff32a2ddfd83a6374e26ccc5af543a63dca9bb1d0070ac35d41783b367a504b88fed1cf56eeca2edc03acf977fac8348dae33f6a225caeb9d8e1c3c6b3baaf4
# 公钥(16进制):
# 907358fef9d0788ef921c9821993e60e983e8c783d2f4fb86e17e5f276fac684241254a53dd5ed3510465d16d1ce7ee10554b3a2111c20084a532fa30fbcb152d1a5a25a525328df2ee563a37f7f495eaea51fd26bf821110558d140d05d83b0b6d0914b2c2e6b7d10b34bb45a164cd5c34c0729059369b4e09bc85446057c1e83374a93363a35136dbb948cbefd81f2e029412688ddf6f2ba01c63c82b6299264d815a38800d4b843a8fca739caca135a2aa2d7f8c478206947bfba2dd2a61d7ff32a2ddfd83a6374e26ccc5af543a63dca9bb1d0070ac35d41783b367a504b62f4a270f10d6dfdcb1fa858c2a6990176ed96010c9328107d95d36a234ded78
这题主要思路就是审代码之后想办法绕过哈希加密,因为一旦进行了哈希加密就会因为不可逆而不可控(基本不可能借助哈希来构造一个新的消息摘要和签名),因此在看懂代码之后就明白,需要在get_pubkey_from_signature
函数中调用_chain
函数时的哈希操作次数为0,而终止值是self.w-1=15,即输入ffffffff
作为消息摘要就可以让初始值为15,从而绕开哈希操作,此时从get_pubkey_from_signature
函数得到的公钥就是签名内容,验证就是要公钥和签名一致就行,所以就直接把公钥当作签名输入即可通过验证得到flag
</br>
随后的数个小时我们把能做的事都做了一遍,因为题目做不出来,又没网,自然是非常无聊,睡觉也是姿势各异,除了个别的队伍在埋头做题,剩下的都已经呈现溃败之势了</br>
另外必须吐槽的点必然是主办方提供的中午饭——一个华莱士牛肉汉堡,窜不窜的就先不说了,一个小汉堡压根不够塞牙缝,说实话在这方面确实有点抠门了
</br>
今年真的全方面坐牢,明年得好好考量了,这几个钟真的很难熬,还好能写写blog(haha)