今天是2025.07.19,"期待已久"的熵密杯正式开赛了
早上10点正式开赛,但是7点半就要上车了,明明只有40min左右的车程,但是不知道是为什么主办方要我们那么早起床,来比赛场地还打了一会游戏(bushi)

等了好一会,开幕式终于开始了,该说不说,赛场布置的大屏幕还是非常好看的,高刷新率、高帧率,把领导讲话的气场展现得淋漓尽致
开幕式结束,比赛就随之开始了,上来硬控5min下载3道初始赛题的zip,3个文件整整齐齐都是49.7MB,第一反应就是先下出来看一眼。比赛第3分钟左右,当时大部分队伍连赛题都还没下载完,中科大的Nebula已经拿下初始赛题3的一血了,当时隔壁的队伍惊呼“我们都还在下赛题!”。当然,这是整场比赛最简单的一题,也是我们队唯一能做的一题了(主要还是HvAng的功劳,反正我没看出来)
初始赛题3题目代码如下:

from 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

    @staticmethod
    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

初始赛题3.exe

这题主要思路就是审代码之后想办法绕过哈希加密,因为一旦进行了哈希加密就会因为不可逆而不可控(基本不可能借助哈希来构造一个新的消息摘要和签名),因此在看懂代码之后就明白,需要在get_pubkey_from_signature函数中调用_chain函数时的哈希操作次数为0,而终止值是self.w-1=15,即输入ffffffff作为消息摘要就可以让初始值为15,从而绕开哈希操作,此时从get_pubkey_from_signature函数得到的公钥就是签名内容,验证就是要公钥和签名一致就行,所以就直接把公钥当作签名输入即可通过验证得到flag
随后的数个小时我们把能做的事都做了一遍,因为题目做不出来,又没网,自然是非常无聊,睡觉也是姿势各异,除了个别的队伍在埋头做题,剩下的都已经呈现溃败之势了
另外必须吐槽的点必然是主办方提供的中午饭——一个华莱士牛肉汉堡,窜不窜的就先不说了,一个小汉堡压根不够塞牙缝,说实话在这方面确实有点抠门了
今年真的全方面坐牢,明年得好好考量了,这几个钟真的很难熬,还好能写写blog(haha)