第四届熵密杯——初始谜题"

admin 2026-10-17 11:31:04 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

第四届熵密杯——初始谜题

原创

Van1sh
Van1sh

Van1sh

2026年7月29日 10:45
中国香港

在小说阅读器读本章

去阅读

第三次参加熵密杯了,在同事的助力下,这次侥幸拿下一等奖。美中不足的是最后一道题只差临门一脚,赛后检查发现是代码中有一些数据处理的小bug,可惜。 由于篇幅原因,这次题解会分成三篇文章,分别是初始谜题、工程师站路线和日志管理服务器路线。

初始谜题一

题目代码

import secrets

default_table = {
    'n': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123',
    'p': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF',
    'g': '32c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589334c74c7'
         'bc3736a2f4f6779c59bdcee36b692153d0a9877cc62a474002df32e52139f0a0',
    'a': 'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC',
    'b': '28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93',
}

class Crypt():

    def __init__(self, private_key, public_key, mode=0):
        self.private_key = private_key
        if public_key.startswith(\"04\"):
            self.public_key = public_key[2:]
        else:
            self.public_key = public_key
        self.para_len = len(default_table['n'])
        self.ecc_a3 = (
            int(default_table['a'], base=16) + 3) % int(default_table['p'], base=16)
        assert mode in (0, 1), 'mode must be one of (0, 1)'
        self.mode = mode

    def _kg(self, k, Point):  # kP运算
        if k == 0:
            return None #无穷远点
        Point = '%s%s' % (Point, '1')
        mask_str = '8'
        for i in range(self.para_len - 1):
            mask_str += '0'
        mask = int(mask_str, 16)
        Temp = Point
        flag = False
        for n in range(self.para_len * 4):
            if (flag):
                Temp = self._double_point(Temp)
            if (k & mask) != 0:
                if (flag):
                    Temp = self._add_point(Temp, Point)
                else:
                    flag = True
                    Temp = Point
            k = k << 1
        return self._convert_jacb_to_nor(Temp)

    def _double_point(self, Point):  # 倍点
        if Point is None:
            return None
        l = len(Point)
        len_2 = 2 * self.para_len
        if l < self.para_len * 2:
            return None
        else:
            x1 = int(Point[0:self.para_len], 16)
            y1 = int(Point[self.para_len:len_2], 16)
            if l == len_2:
                z1 = 1
            else:
                z1 = int(Point[len_2:], 16)

            T6 = (z1 * z1) % int(default_table['p'], base=16)
            T2 = (y1 * y1) % int(default_table['p'], base=16)
            T3 = (x1 + T6) % int(default_table['p'], base=16)
            T4 = (x1 - T6) % int(default_table['p'], base=16)
            T1 = (T3 * T4) % int(default_table['p'], base=16)
            T3 = (y1 * z1) % int(default_table['p'], base=16)
            T4 = (T2 * 8) % int(default_table['p'], base=16)
            T5 = (x1 * T4) % int(default_table['p'], base=16)
            T1 = (T1 * 3) % int(default_table['p'], base=16)
            T6 = (T6 * T6) % int(default_table['p'], base=16)
            T6 = (self.ecc_a3 * T6) % int(default_table['p'], base=16)
            T1 = (T1 + T6) % int(default_table['p'], base=16)
            z3 = (T3 + T3) % int(default_table['p'], base=16)
            T3 = (T1 * T1) % int(default_table['p'], base=16)
            T2 = (T2 * T4) % int(default_table['p'], base=16)
            x3 = (T3 - T5) % int(default_table['p'], base=16)

            if (T5 % 2) == 1:
                T4 = (T5 + ((T5 + int(default_table['p'], base=16)) >> 1) - T3) % int(
                    default_table['p'], base=16)
            else:
                T4 = (T5 + (T5 >> 1) - T3) % int(default_table['p'], base=16)

            T1 = (T1 * T4) % int(default_table['p'], base=16)
            y3 = (T1 - T2) % int(default_table['p'], base=16)

            form = '%%0%dx' % self.para_len
            form = form * 3
            return form % (x3, y3, z3)

    def _add_point(self, P1, P2):  # 点加函数,P2点为仿射坐标即z=1,P1为Jacobian加重射影坐标
        if P1 is None:
            return P2
        if P2 is None:
            return P1
        len_2 = 2 * self.para_len
        l1 = len(P1)
        l2 = len(P2)
        if (l1 < len_2) or (l2 < len_2):
            return None
        else:
            X1 = int(P1[0:self.para_len], 16)
            Y1 = int(P1[self.para_len:len_2], 16)
            if (l1 == len_2):
                Z1 = 1
            else:
                Z1 = int(P1[len_2:], 16)
            x2 = int(P2[0:self.para_len], 16)
            y2 = int(P2[self.para_len:len_2], 16)

            T1 = (Z1 * Z1) % int(default_table['p'], base=16)
            T2 = (y2 * Z1) % int(default_table['p'], base=16)
            T3 = (x2 * T1) % int(default_table['p'], base=16)
            T1 = (T1 * T2) % int(default_table['p'], base=16)
            T2 = (T3 - X1) % int(default_table['p'], base=16)
            T3 = (T3 + X1) % int(default_table['p'], base=16)
            T4 = (T2 * T2) % int(default_table['p'], base=16)
            T1 = (T1 - Y1) % int(default_table['p'], base=16)
            Z3 = (Z1 * T2) % int(default_table['p'], base=16)
            T2 = (T2 * T4) % int(default_table['p'], base=16)
            T3 = (T3 * T4) % int(default_table['p'], base=16)
            T5 = (T1 * T1) % int(default_table['p'], base=16)
            T4 = (X1 * T4) % int(default_table['p'], base=16)
            X3 = (T5 - T3) % int(default_table['p'], base=16)
            T2 = (Y1 * T2) % int(default_table['p'], base=16)
            T3 = (T4 - X3) % int(default_table['p'], base=16)
            T1 = (T1 * T3) % int(default_table['p'], base=16)
            Y3 = (T1 - T2) % int(default_table['p'], base=16)

            form = '%%0%dx' % self.para_len
            form = form * 3
            return form % (X3, Y3, Z3)

    def _convert_jacb_to_nor(self, Point):  # Jacobian加重射影坐标转换成仿射坐标
        if Point is None:
            return None
        len_2 = 2 * self.para_len
        x = int(Point[0:self.para_len], 16)
        y = int(Point[self.para_len:len_2], 16)
        z = int(Point[len_2:], 16)
        z_inv = pow(
            z, int(default_table['p'], base=16) - 2, int(default_table['p'], base=16))
        z_invSquar = (z_inv * z_inv) % int(default_table['p'], base=16)
        z_invQube = (z_invSquar * z_inv) % int(default_table['p'], base=16)
        x_new = (x * z_invSquar) % int(default_table['p'], base=16)
        y_new = (y * z_invQube) % int(default_table['p'], base=16)
        z_new = (z * z_inv) % int(default_table['p'], base=16)
        if z_new == 1:
            form = '%%0%dx' % self.para_len
            form = form * 2
            return form % (x_new, y_new)
        else:
            return None

    def verify(self, Sign, data):
        if Sign is None or Sign == '':
            return None
        r = int(Sign[0:self.para_len], 16)
        s = int(Sign[self.para_len:2*self.para_len], 16)
        e = int(data.hex(), 16)
        # 参数合法性
        if not (1 <= r < int(default_table['n'], base=16) and 1 <= s < int(default_table['n'], base=16)):
            return False
        t = r + s
        if t == 0:
            return False
        else:
            t = t % int(default_table['n'], base=16)

        if self.public_key is None or self.public_key == '':
            return None

        P1 = self._kg(s, default_table['g'])
        if P1 is None:
            return False
        P2 = self._kg(t, self.public_key)
        if P1 == P2:
            P1 = '%s%s' % (P1, 1)
            P1 = self._double_point(P1)
        else:
            P1 = '%s%s' % (P1, 1)
            P1 = self._add_point(P1, P2)
            P1 = self._convert_jacb_to_nor(P1)

        x = int(P1[0:self.para_len], 16)
        return r == ((e + x) % int(default_table['n'], base=16))

    def sign(self, data):
        k = secrets.randbelow(int(default_table['n'], 16) - 1) + 1
        if not (1 <= k <= int(default_table['n'], base=16)- 1):
            return None
        E = data.hex()
        e = int(E, 16)
        if self.private_key is None or self.private_key == '':
            return None

        d = int(self.private_key, 16)
        P1 = self._kg(k, default_table['g'])

        x = int(P1[0:self.para_len], 16)
        A = ((e + x) % int(default_table['n'], base=16))
        if A == 0 or A + k == int(default_table['n'], base=16):
            return None
        d_1 = pow(
            d+1, int(default_table['n'], base=16) - 2, int(default_table['n'], base=16))
        B = (d_1*(k + A) - A) % int(default_table['n'], base=16)
        if B == 0:
            return None
        else:
            return '%064x%064x' % (A, B)

if __name__ == '__main__':
    crypt = Crypt(
        # 公钥格式为64字节的16进制字符串
        public_key='',
        # 私钥格式为32字节的16进制字符串
        private_key=''
    )
    data = '919535c3ef53d3fa359196b5229c4bbb386ce209f5905d33fc7bcdff46ae27c2'
    sign = crypt.sign(bytes.fromhex(data))
    print(sign)
    verify = crypt.verify(sign,bytes.fromhex(data))
    print(verify)

题目要求

构造一组可以通过验签的摘要值以及对应的签名值,并提交答案。

题目考点

SM2签名自实现缺陷

解题思路

初始谜题一自实现了一个 SM2 的签名,并要求我们构造一组可以通过验签的摘要值以及对应的签名值,并提交答案。

由于题目并没有给出公钥,因此可以断定我们需要构造一组能够绕过Verify的消息签名,即对于任意公钥均能验签成功,因此我们关注 Verify 函数的实现。

这段代码虽然检查了  不能等于0,但是取余运算却放在了最后,因此我们可以构造

,使得  值先通过判断,然后再取余后为0。方便起见,我们可以设 。

于是根据验签公式,我们有

最后验签是要求

而正好,这里的消息哈希 e 我们也是能够直接控制的,于是我们设  即可

(当然也可以设,,此时摘要就是,答案不唯一)

参考答案

摘要值:r-G.x % n
cd3b51d2e0e67ee6a066fbb995c6366ae220d3ab2f5ff949e261ae800688cc5b
签名值:n-1||1
FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D541220000000000000000000000000000000000000000000000000000000000000001

初始谜题二

题目代码

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import struct

def encrypt_card_data(item) -> str:
    result = f\"{item.card},{item.pin},{item.id}\"
    plaintext = result.encode('utf-8')
    key = get_key()
    # item.index 数据序号
    nonce = get_iv_byte(item.index)
    tmp = encrypt(key,nonce,plaintext).hex()
    return tmp

def get_iv_byte(index: int, prefix=b'', length=16) -> bytes:
    if not (0 <= index < 2**32):
        raise ValueError(\"Index 必须在 0 到 2^32-1 之间\")
    index_bytes = struct.pack('>I', index)
    pad_len = length - len(prefix) - len(index_bytes)
    if pad_len < 0:
        prefix=b''
        pad_len = length - len(index_bytes)
    return prefix + (b'\\x00' * pad_len) + index_bytes

def encrypt(key,nonce,plaintext) -> bytes:
    algorithm = algorithms.SM4(key)
    mode = modes.CTR(nonce)
    cipher = Cipher(algorithm, mode)
    encryptor = cipher.encryptor()
    ciphertext = encryptor.update(plaintext) + encryptor.finalize()
    return ciphertext

相关数据

序号  密文(十六进制)
1000  7d2a3dc99109ff913cc52877cf3578621eaf51d256b043550eac8c377c98357de1b0d616039e6f821850386d86d96057f195539c28b8ed31430f27b16ca55f
1001  1cae58cb5cb947500aa4963f7d93607eeeb4d717069f678b0053226887cd6754fdc24cd32bebb8395f0a72b86ee90c278c4323c288be33e5d9282884acf9fb
1002  eeb6d40e049f6f8003523e6f8fc33854f0ce46d229e3bf3d45083cb96efd0b25d4453c8e8db930ecc67c2ed7a8edff97a668a0c45d36b0c2b36257a2039117
序号    银行卡号                PIN码     用户ID
1000   6205310997298382439    984631   019f1942-4000-7650-a25c-b7ce812783a2
1001   6205310379680232603    118954   019f1942-4000-7b11-a3a2-a17392cc649e
1002

题目要求

提交序号为1003用户的银行卡号、PIN码

题目考点

SM4-CTR 模式 nonce 选取不正确导致密钥流复用

解题思路

题目利用SM4-CTR 模式对明文数据进行加密,但是注意到加密时nonce的取值来自item.index,而明文数据中的index是连续递增的。因此加密第一份明文的密钥流的第二组,就是加密第二份明文密钥流的第一组,如下图所示

于是想要恢复第三组明文,我们可以取第一组的明文和密文,异或得到密钥流;从第三组开始,即截取32字节后的部分,用于解密第三组密文,即可得到题目所需银行卡号与PIN码(用户ID我们无法恢复完全)

(当然也可以取第一组的明文和密文,异或得到密钥流;从第二组开始,即截取16字节后的部分,用于解密第三组密文)

解题代码

from pwn import xor
from Crypto.Util.number import *

a = b\"6205310997298382439,984631,019f1942-4000-7650-a25c-b7ce812783a2\"
b = b\"6205310379680232603,118954,019f1942-4000-7b11-a3a2-a17392cc649e\"

c = bytes.fromhex('7d2a3dc99109ff913cc52877cf3578621eaf51d256b043550eac8c377c98357de1b0d616039e6f821850386d86d96057f195539c28b8ed31430f27b16ca55f')
d = bytes.fromhex('eeb6d40e049f6f8003523e6f8fc33854f0ce46d229e3bf3d45083cb96efd0b25d4453c8e8db930ecc67c2ed7a8edff97a668a0c45d36b0c2b36257a2039117')
print(xor(xor(a,c)[32:],d))

初始谜题三

题目代码

\"\"\"

参数:
    n     = 17669   (环维度,要求为素数(保证 X^n-1 在 GF(2) 上仅有两个不可约因子))
    k     = 16      (消息长度,字节)
    n1    = 46      (外码 RS[46, 16, 31] 的码长)
    n2    = 384     (内码重复 RM 的码长(RM(1,7)×3 = [384, 8, 192]))
    delta = 15      (RS纠错能力)
    w     = 66      (私钥向量 x, y 的 Hamming 重量)
    w_r   = 75      (随机向量 r1, r2 的 Hamming 重量)
    w_e   = 75      (误差向量 e 的 Hamming 重量)
    级联码结构:RS[46,16,31] over GF(2^8) ⊗ RM(1,7)×3
\"\"\"

import os, hashlib, random, reedsolo
from typing import Tuple, List

# ============================================================
# 参数
# ============================================================
PARAM_N     = 17669     # 环 R = GF(2)[X]/(X^17669 - 1) 的维度
PARAM_K     = 16        # 消息长度 (字节)
PARAM_N1    = 46        # 外码 RS[46, 16, 31] 的码长
PARAM_N2    = 384       # 内码重复 RM 的码长(RM(1,7)×3 = [384, 8, 192])
PARAM_DELTA = 15        # RS可纠错数
PARAM_W     = 66        # 私钥向量 x, y 的 Hamming 重量
PARAM_W_R   = 75        # 随机向量 r1, r2 的 Hamming 重量
PARAM_W_E   = 75        # 误差向量 e 的 Hamming 重量
PARAM_RM_M  = 7         # RM(1,m)
PARAM_RM_LEN = 128      # RM(1,7) 码长 = 2^7
PARAM_REP   = 3         # RM 码重复次数

# GF(2^8) 不可约多项式: x^8 + x^4 + x^3 + x^2 + 1 = 0x11D
GF256_MOD = 0x11D

# ============================================================
# Reed-Muller RM(1,7) 编码与解码
# ============================================================
# RM(1,7) 是一阶 Reed-Muller 码,参数为 [128, 8, 64]:
#   码长  n = 2^7 = 128
#   维度  k = 1 + 7 = 8(常数项 + 7 个坐标函数)
#   最小距离 d = 2^(7-1) = 64

def rm17_encode(symbol: int) -> int:
    \"\"\"
    RM(1,7) 编码:8 位符号 → 128 位码字。
    符号格式:bit7 = 常数项,bits[6:0] = 线性项系数。
    \"\"\"
    codeword = 0
    const_bit = (symbol >> 7) & 1  # 最高位 = 常数项
    for pt in range(128):  # 在所有点评估仿射函数
        val = const_bit
        for bit in range(7):
            if (symbol >> bit) & 1:
                val ^= (pt >> bit) & 1
        if val:
            codeword |= (1 << pt)
    return codeword

def rm17_decode(codeword: int) -> int:
    \"\"\"
    RM(1,7) 解码:128 位码字 → 8 位符号(Walsh-Hadamard 变换)。
    \"\"\"
    # Map {0,1} -> {+1,-1}
    signal = [1 - 2 * ((codeword >> i) & 1) for i in range(128)]
    # 快速Walsh-Hadamard变换
    wht = list(signal)
    h_step = 1
    while h_step < 128:
        for i in range(0, 128, h_step * 2):
            for j in range(i, i + h_step):
                x, y = wht[j], wht[j + h_step]
                wht[j], wht[j + h_step] = x + y, x - y
        h_step *= 2
    # 找到绝对值最大值
    best_idx = max(range(128), key=lambda i: abs(wht[i]))
    sign_bit = 1 if wht[best_idx] < 0 else 0
    return (sign_bit << 7) | best_idx

# ============================================================
# 重复 Reed-Muller 码 [384, 8, 192] = RM(1,7) × 3
# ============================================================
# 将 RM(1,7) 码重复 3 次:
#   编码:128 位码字复制 3 份,拼接为 384 位
#   解码:将 3 份 128 位段逐位取多数表决(bitwise majority vote),
#          再调用 RM(1,7) 解码
# 重复设计将最小距离从 64 提升至 192,代价是码率降低为 1/3

def dup_rm_encode(symbol: int) -> int:
    \"\"\"重复 RM(1,7)×3 编码:8 位符号 → 384 位码字。\"\"\"
    base = rm17_encode(symbol)
    return base | (base << 128) | (base << 256)

def dup_rm_decode(bits: int) -> int:
    \"\"\"重复 RM(1,7)×3 解码:384 位码字 → 8 位符号。\"\"\"
    mask = (1 << 128) - 1
    seg0 = bits & mask
    seg1 = (bits >> 128) & mask
    seg2 = (bits >> 256) & mask
    # 按位多数表决
    majority = (seg0 & seg1) | (seg1 & seg2) | (seg0 & seg2)
    return rm17_decode(majority)

# ============================================================
# Reed-Solomon RS[46, 16, 31] over GF(2^8)
# ============================================================
# RS 码参数:
#   码长  n1 = 46,信息位 k = 16,最小距离 d = 31
#   纠错能力 δ = (d-1)/2 = 15 个 GF(2^8) 符号
# 系统码形式:码字 = [消息符号 | 校验符号],前 16 个符号即为原始消息
_rs_codec = reedsolo.RSCodec(nsym=PARAM_N1 - PARAM_K, fcr=1, c_exp=8)  # 默认 GF(2^8)

def rs_encode(msg: bytes) -> List[int]:
    \"\"\"编码:消息 -> 系统码码字(消息 + 校验)\"\"\"
    if len(msg) != PARAM_K:
        raise ValueError(f\"消息长度必须为 {PARAM_K} 字节\")
    encoded_bytes = _rs_codec.encode(msg)
    return list(encoded_bytes)

def rs_decode(symbols: List[int]) -> bytes:
    if len(symbols) != PARAM_N1:
        raise ValueError(f\"接收码字长度必须为 {PARAM_N1} 个符号\")
    received_bytes = bytes(symbols)
    try:
        # 兼容不同版本 reedsolo 的返回值
        decoded_bytes = _rs_codec.decode(received_bytes)[0]
    except reedsolo.ReedSolomonError as e:
        raise ValueError(f\"RS解码失败:{e}\") from e
    return decoded_bytes[:PARAM_K]

# ============================================================
# 级联码 Concatenated Code 编码与解码
# ============================================================
# 编码流程(外码先行):
#   16 字节消息
#   → RS[46,16,31] 外码编码 → 46 个 GF(2^8) 符号
#   → 每个符号经 dup_rm_encode → 46 × 384 = 17664 位
#   → 嵌入 n=17669 维环 R 中
#
# 解码流程(内码先行):
#   17664+ 位码字
#   → 按 384 位分组为 46 组,每组经 dup_rm_decode → 46 个 GF(2^8) 符号
#   → RS 解码(综合征检验 + 可选 BM 纠错)→ 16 字节消息

def concat_encode(msg: bytes) -> int:
    \"\"\"级联码编码:16 字节消息 → 17664 位码字(Python 整数形式)。\"\"\"
    rs_symbols = rs_encode(msg)
    codeword = 0
    for i, sym in enumerate(rs_symbols):
        inner = dup_rm_encode(sym)
        codeword |= (inner << (i * PARAM_N2))
    return codeword

def concat_decode(codeword: int) -> bytes:
    \"\"\"级联码解码:17664+ 位码字(Python 整数)→ 16 字节消息。\"\"\"
    symbols = []
    for i in range(PARAM_N1):
        chunk = (codeword >> (i * PARAM_N2)) & ((1 << PARAM_N2) - 1)
        symbols.append(dup_rm_decode(chunk))
    return rs_decode(symbols)

# ============================================================
# 多项式环 R = GF(2)[X]/(X^n - 1) 上的运算
# ============================================================
# 全部公钥/密文运算均在此环上进行。
# 元素以 Python 整数表示:第 i 位对应 X^i 的系数。
# 重要性质:
#   加法 = XOR(GF(2) 特征为 2,加减等价)
#   乘法 = 循环卷积(模 X^n - 1 的多项式乘法)
#   公钥 s = x + h·y 的安全性依赖于求解 QCSD 问题的困难性

def poly_add(a: int, b: int) -> int:
    return a ^ b

def poly_mul_mod(a: int, b: int, n: int = PARAM_N) -> int:
    \"\"\"
    循环多项式乘法:(a · b) mod (X^n - 1)。
    实现策略:枚举 a 的非零位(稀疏乘法),对 b 施加相应的循环移位后 XOR 累加。
    时间复杂度 O(wt(a) · n),适合 Hamming 重量较小的向量(如私钥和随机向量)。
    \"\"\"
    result = 0
    mask = (1 << n) - 1
    # 优化:遍历a的置位
    temp = a
    i = 0
    while temp:
        if temp & 1:
            shifted = b << i
            result ^= (shifted & mask) ^ (shifted >> n)
        temp >>= 1
        i += 1
    return result & mask

def poly_weight(a: int) -> int:
    return bin(a).count(\"1\")

def poly_to_bytes(p: int) -> bytes:
    return p.to_bytes((PARAM_N + 7) // 8, \"little\")

def poly_from_bytes(data: bytes) -> int:
    return int.from_bytes(data, \"little\") & ((1 << PARAM_N) - 1)

# ============================================================
# 随机向量采样
# ============================================================
# 安全性要求密钥和随机向量的 Hamming 重量精确固定。
# 采样算法:从 {0,...,n-1} 中均匀无重复地抽取 w 个位置,置为 1。

def sample_fixed_weight(n: int, w: int, rng=None) -> int:
    \"\"\"采样 Hamming 重量恰好为 w 的随机二进制向量。\"\"\"
    if rng is None: rng = random.SystemRandom()
    positions = rng.sample(range(n), w)
    result = 0
    for p in positions: result |= (1 << p)
    return result

def sample_random_poly(n: int, rng=None) -> int:
    \"\"\"采样均匀随机的 n 位多项式(用于公钥 h 的生成)。\"\"\"
    nbytes = (n + 7) // 8
    if rng is None:
        raw = os.urandom(nbytes)
    else:
        raw = bytes(rng.getrandbits(8) for _ in range(nbytes))
    return int.from_bytes(raw, \"little\") & ((1 << n) - 1)

# ============================================================
# 密钥生成/封装
# ============================================================

def keygen(seed: bytes = None) -> Tuple[dict, dict]:
    \"\"\"
    密钥生成。
    公钥 pk = (h, s):h 为均匀随机多项式,s = x + h·y
    私钥 sk = (x, y):两个 Hamming 重量为 w 的低权重多项式
    安全性基于求解 2-QCSD 问题的困难性。
    \"\"\"
    if seed:
        rng = random.Random(seed)
    else:
        rng = random.SystemRandom()
    h = sample_random_poly(PARAM_N, rng)
    x = sample_fixed_weight(PARAM_N, PARAM_W, rng)
    y = sample_fixed_weight(PARAM_N, PARAM_W, rng)
    s = poly_add(x, poly_mul_mod(h, y))
    return {\"h\": h, \"s\": s}, {\"x\": x, \"y\": y}

def encapsulate(pk: dict, m: bytes, r1: int, r2: int, e: int):
    h, s = pk[\"h\"], pk[\"s\"]
    u = poly_add(r1, poly_mul_mod(h, r2))
    v = poly_add(poly_add(concat_encode(m), poly_mul_mod(s, r2)), e)
    shared_key = hashlib.sha512(m).digest()[:32]
    return u, v, shared_key

相关数据

密钥封装过程中参数(数字很长,篇幅原因就不全展示了,不影响解题思路,可利用代码本地自行生成一组数据)

h
bef1c1d67d3e1e8039dcd...
s
75b9007fbaf0946a16d19...
u
14506bf660b1098c4cd35...
v
1be57933bc5febab3dc71...

题目要求

计算上述密钥封装过程中所使用的协商密钥。

题目考点

HQC 密钥封装随机向量重用

解题思路

初始谜题三 实现了HQC(Hamming Quasi-Cyclic)码基的密钥封装(赛后查的,其实我并没有见过)

但是我们能根据 keygen 和 encapsulate 函数的代码写出下面的公式

然后我们尝试调用题目提供的密钥封装服务,会发现密钥封装过程参量中的  是不变的

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
u
14506bf660b109...
v
be40236919fab1...

根据 ,也就意味着每次加密, 的值都是不变的 ( 是随机向量和误差向量)

w_r   = 75      (随机向量 r1, r2 的 Hamming 重量)
w_e   = 75      (误差向量 e 的 Hamming 重量)

甚至有理由怀疑  中的 e 也是不变的

于是我们设题目的共享密钥为 ,我们自己设定 ,分别进行密钥封装,则有

两式相减

注意上述的减法运算其实是GF(2) 下多项式减法,等价于加法,等价于异或(所以多项式下的 ,在转为整数后就是 )代码中的注释倒是也说的很明白了

通过本地生成密钥、加解密测试数据,我们会发现,对  做一个解码,能得到 ,不过需要注意题目中  的存储是小端序的,在将  从十六进制转为十进制时,不能直接用 int(x,16)去转。不然解码会报错

解题代码

v1 = ''
v2 = ''

v1 = int.from_bytes(bytes.fromhex(v1),'little')
v2 = int.from_bytes(bytes.fromhex(v2),'little')

import sys
sys.set_int_max_str_digits(100000000)

from pwn import xor
data = bytes.fromhex('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')

m = xor(concat_decode(v1^v2),data)
shared_key = hashlib.sha512(m).digest()[:32]
print(shared_key.hex())

第四届熵密杯——初始谜题" 网络安全文章

第四届熵密杯——初始谜题"

本文档为第四届熵密杯初始谜题题解。谜题一剖析了自实现SM2签名的逻辑缺陷,其验证函数在取模前校验参数,攻击者设r为n减1且s为1使取模后为零,触发错误倍点逻辑,成功构造万能签名。谜题二涉及AES加密但分析中断。核心建议是密码学开发务必在取模后执行参数校验以防范逻辑绕过。
[BSidesCF2019]Kookie" 网络安全文章

[BSidesCF2019]Kookie"

本文记录了CTF题目Kookie的解题过程。作者登录靶场抓包发现服务端仅依赖Cookie中的username字段验证身份,将其修改为admin即成功伪造管理员获取Flag。此案例揭示了客户端Cookie校验的脆弱性,建议开发者鉴权时切勿信任客户端凭据,必须采用服务端Session等安全机制。
服务器的“后门管理口”暴露公网:为什么重装系统也解决不了" 网络安全文章

服务器的“后门管理口”暴露公网:为什么重装系统也解决不了"

针对超三万个公网暴露的BMC接口因IPMI协议缺陷可被离线猜解口令的问题,文章指出重装系统无法清除该威胁,因BMC独立于操作系统掌控底层控制台。建议企业立即执行四项应急:阻断公网IPMI流量、盘点暴露资产、轮换唯一凭据、建立隔离的带外管理网络,并将管理面作为核心基础设施进行严格治理。
评论:0   参与:  0