2025 第三届熵密杯 - WriteUp & 游记


碎碎念

由于这次熵密杯跟国赛时间上冲突了,所以老大哥晨曦只能被迫含泪让我 替父行军 代为参加这次的熵密杯。虽然说在这之前对密码是一点都不懂啊,但还好这次熵密杯说是难度提高了,大家都在坐牢,所以俺最后嗯是混到了一个优胜奖(好耶)通过这次比赛也成功见到了su里的一些师傅,希望以后有更多的机会去线下跟各位大佬面基捏
以后估计是不会再有机会参加了,所以还是要好好珍惜这次经历,简单记录一下

WP

初始谜题

初始谜题3

附件如下,简单分析可以看到他实现了一个简单的WOTS验证器,但是存在一定的结构漏洞,验证签名时,wots会将传入的digest拆分后,将其转化成对应的base解码值后,进行特定的哈希运算次数,再对签名进行验证

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)

本题中wots的w值为16,对应的base即为base16,也就是16进制,而验证时,会对签名进行hash处理,具体的处理公式为(w-1-digest每位对应值),所以就是15-digest对应值,那么我们只需要让对应值为15,那么他所需要进行hash的次数就是0,即不进行hash,保持为原值

image-20250719151946789

所以我们直接利用公钥本身作为签名数据,并且让digest值为最大的0xffffffff即可(f就是15)

exp:

    pubkey = '84c9811997f0a41ff892a7b0c1ab4d2c518b510df556000e266e5a496b98a4dcafab13dc42eb090b9ca546d1b0771d2f73b1f6da6a94491ec0bdb2f4bc77b89d187036599e03e7f9a72e675db99952e82e21f4073ad0c135a5c39609d42f732f465315ded5128869da630008471a364919455a776e1d2338b65db4034698f392b22c6b8dcd442c42bb271cf938ab3e3e713f3f2bada82fc313efd6f2e682c7a704626476dd88ad0947d683e63e21fb25942d4637e1a2a98f0eb8be8d0d53533ba98a90eecfea1e612f29520be526d41b444b434b6902eff78d67d401b5353caa36798b991524cd0fd4dd4f5c62d92cd2dd4a33ebac94fea3689406a343323c52'
    pubkey_list_bytes = hex_to_32byte_chunks(pubkey)
    wots = WOTSPLUS(pubkey=pubkey_list_bytes)
    digest_hex = "daa4a0eb"
    signature_hex = "df4411caa406cb6c3d296980576c771c1a7d5cf6549e072d0b781b6b03e91d5cb5384250400829e0585a49617fd4164aae639d0bac3647130fbfb7bc34e67d335834e4ae341dae13c6c53b36bb60bf4627c8325061408cf08a2559f594172ed3bd20705f91358b693d2369af29eb3523daa1962063905a68c4779ce4b6172ce77c562cb70a95458f75e5c3996b47725f1b8324b15fafea5b8ee3493d333ec02d5b2f702c34a3948a44d42dde6f562f8eefec58b2ff4c392a843ca198ae8222c6b2944c97708f4bb505f792948de01832a7393c56a73148a70a08102a99857b7edada3caf8c20b122f635e9900c4f51b7bead30a5473cd66765fa6294ed47b63b"


    fake_signature = ''.join(data.hex() for data in pubkey_list_bytes)
    fake_digest  = (2 ** 32 - 1).to_bytes(4, 'big')

    print(fake_digest.hex())
    print(fake_signature)
    print(wots.verify(fake_digest, hex_to_32byte_chunks(fake_signature)))

image-20250719150106674

夺旗闯关

flag1

根据题目引导,flag1不难想到在TSP服务器中,如果要登录TSP服务器则需要验证以下信息:

其中服务器端返回的挑战值似乎是用不上的(后面没用上)

然后则是需要我们输入用户名,鉴别信息以及证书

image-20250719141832848

完成初始条件登录gitee后,可以看到仓库内有若干文件,其中与TSP登录相关的内容如下,可以看到有个历史抓包数据

image-20250719141922733

打开抓包数据,简单审视一下发现有一个证书,将他提取出来即可,要注意每一轮指令的第一个字节都是标志位,也就是无用数据,不将其提出

image-20250719142033756

提取后,将证书内容丢给厨子,会发现里面有一个shangmibeiadmin,经过尝试发现就是用户名(因为如果用户名不对的话会提示用户不存在)

image-20250719142134047

最后就是鉴别信息,根据给的提示直接找即可,长度分别为64字节

image-20250719142257544

解完拼接后,提交即可登录成功

image-20250719141742077

游记

这次熵密杯举办地点是在咱们大中国的山城重庆,俺长这么大也是从来都没去过这旮沓,去之前只能说是期待满满(顺带一提重庆吃辣是全国皆知,而我作为一名广door人是真不爱吃辣,所以去之前也是做足了心理准备)

但好死不死,这次举办的场馆离重庆市区可以说是非常的远了,更重量级的是举办方给我们定的酒店还要离市区再远一段路,导致我们平时在酒店想要去市区玩的话,如果直接打车需要开几十公里的路,一趟下来可能上百的车费,太伤了(也没有直达的地铁)所以总的来说其实并没有一个非常好的旅游体验,更多的时候其实反而是在场馆以及酒店这两块地方游荡

第一天我们是早上八点半的高铁,下午两点多到的,到了重庆后我们直接打车到了场馆进行了报到,不得不说这次的场馆除开偏僻这一点,是真的挺高级的,好像是一个刚建成的场馆,就是一个字,壕

d1aa570c05d8a3d060f33b40e642c482

报到完之后我们就打车去酒店了,在酒店跟xrntkk歇了会搞了杯奶茶然后就去吃饭了(这会在看下一个是谁最新一季第一集)

7bf725abcb711b64b18a0033285fec65

既然来了重庆,首当其冲肯定是去吃吃他们的火锅,在队内妹子的推荐下我们去吃了就在附近镇上就有的一个评价还不错的火锅店——朝天嘴棒棒老火锅,虽然说这个锅底看着很红,但其实辣度还在能承受的范围内,他们家的分量我觉得还行,咱们四个人最后吃的都挺撑的,人均大概在七八十左右,腰片挺好吃的,耗儿鱼也不错很嫩hhh

吃完饭后我们随便逛了逛就步行回酒店了,然后在酒店待到了第二天比赛,晚上还跟xrntkk点了宵夜烧烤吃)

b7e494b0cdb453d4230e8712f3893e5b

第二天一大早就起了,去酒店的一楼简单搞了点早餐(这酒店早餐很一般),然后就坐着安排的大巴去场馆打比赛了,不得不说这场馆确实赞,比赛过程就没啥好说的了,纯坐牢,搞完两道题就到极限了,其他题是一点不会,中午的午饭原以为会有什么好吃的,没想到居然只是一个华莱士汉堡(悲)。最后只能硬撑到下播了,不过也是没想到大家都黏住了,基本都卡在了第三道题上,20多名开外都是一个分数,因为我们解得算比较快的,所以最后的排名锁定在了41名,也是成功拿下了优胜奖(至少跟去年比没退步就胜利了)

01344c39fd30fe45665d004c1333c973

比赛打完后,我们回酒店放了放行李,然后就去找指导老师xrntkk了(在现场太无聊了他就只能先去市区玩了)(本来说是获奖要彩排的,结果因为大家都没空就取消了)。到市区那块咱们简简单单搞了个晚饭,吃了个重庆小面和抄手,我寻思来重庆至少整点微辣品品吧,没想到要辣飞了wok,好不容易才给吃完了)搞完后我们又去简单逛了逛解放碑步行街,然后决定去看个电影——罗小黑战记2,不得不说这是真好看,捡到宝了说是,怎么会有这么好看的国漫,你要是看到这里了请一定要去看看这部电影(另外别问为什么出去旅游还看电影)

a24430f54478da39b4425370a0f21e80

看完电影就刚好到晚上12点,我们就去早上订好(咸鱼找了个代排的哥们)过夜的金色印象店去足疗去了(金色印象也是很推荐来这边玩的朋友去试试),以前去长沙特种兵的时候去过一次,体验非常不错,这次也算是圆了之前没有过夜的梦,不过由于第二天要早起去颁奖,感觉体验感还是少了一些,但按摩还是很不错的,凌晨看了两部电影,一个蜡笔小新剧场版(忘记叫啥了反正是跟涂鸦有关的),另一个是宇宙编辑部(有点抽象,甚至还没看完,但还挺有意思)

4ad8dbe9063f239888e7b2167d11580c

快进到第三天颁奖仪式,一大早又起来了,然后收拾好行李后就打车回场馆了,然后就是惯例的开场,介绍,颁奖,也是终于有机会站上颁奖台了呜呜,奖杯还是那么好看啊,太喜欢了。顺带一提颁奖仪式是跟着一个学术会议一起举办的,所以还有茶歇吃,这波ak茶歇了)

106f27406d827798178f9451bfa9e9a2

顺带分享一下听会议无聊画的画(我朋友的头像和xrntkk的头像

7318a7e4480bba52bbcc585744ae0290

茶歇时间,也是跟su的师傅成功会师了(就是不太齐人),然后在签名板上写下了咱们的id(当然了也包括没到场的)

6e8daa035c19972750fee4bc900bc0e9

一直到中午,干完准备的会场自助午饭后,我们就打车回酒店休息了,本来来打熵密的suer们约着晚上吃饭的,结果我睡醒起来的时候都6点了,最后只能赶过去收尾了,不过至少拍上了合照(某hy不在我不好评价)跟suer们吃完火锅后就去逛了逛洪崖洞那块,进行了一些简单的景点打卡、人挤人and爬坡运动,最后坐地铁返程

74fa915f666b89021d79e3477aeda34f

正如前面说的酒店非常的偏,回到酒店都已经11点多了,本来想跟xrntkk点奶茶的,结果发现那旮沓的店全都打烊了,甚至酒店楼下小卖部都关了(没有夜生活说是)最后只能在美团点了些喝的还有些第二天返程吃的泡面,然后美滋滋地看天禄夺冠了

00e293fc869b657e3754c8581d3c2720

最后一天总算不用起一大早了,十点多睡醒,收拾好后跟队友们集合,然后就去高铁站了,在那边放下行李后去附近的商场逛了逛,简单吃了个午饭后,就返程回家了

总的来说,希望以后还有机会来重庆好好的玩一次吧,这次错过的东西还是太多了,很多好玩的好吃的都没机会去尝试,也希望以后有更多机会去线下玩qwq

qwq