以下是一道 CTF 题目: from gmssl import sm3 from random im...

作成日: 2025年7月26日

使用モデル O3 by Chat01

質問

以下是一道 CTF 题目:
from gmssl import sm3
from random import SystemRandom
from ast import literal_eval
import os
flag = os.environ["FLAG"]
def SM3(data):
d = [i for i in data]
h = sm3.sm3_hash(d)
return h
def SM3_n(data, n=1, bits=256):
for _ in range(n):
data = bytes.fromhex(SM3(data))
return data.hex()[:bits // 4]

class Nepsign():
def init(self):
self.n = 256
self.hex_symbols = '0123456789abcdef'
self.keygen()

text
def keygen(self): rng = SystemRandom() self.sk = [rng.randbytes(32) for _ in range(48)] self.pk = [SM3_n(self.sk[_], 255, self.n) for _ in range(48)] return self.sk, self.pk def sign(self, msg, sk=None): sk = sk if sk else self.sk m = SM3(msg) m_bin = bin(int(m, 16))[2:].zfill(256) a = [int(m_bin[8 * i: 8 * i + 8], 2) for i in range(self.n // 8)] step = [0] * 48; qq = [0] * 48 for i in range(32): step[i] = a[i] qq[i] = SM3_n(sk[i], step[i]) sum = [0] * 16 for i in range(16): sum[i] = 0 for j in range(1, 65): if m[j - 1] == self.hex_symbols[i]: sum[i] += j step[i + 32] = sum[i] % 255 qq[i + 32] = SM3_n(sk[i + 32], step[i + 32]) return [i for i in qq] def verify(self, msg, qq, pk=None): qq = [bytes.fromhex(i) for i in qq] pk = pk if pk else self.pk m = SM3(msg) m_bin = bin(int(m, 16))[2:].zfill(256) a = [int(m_bin[8 * i: 8 * i + 8], 2) for i in range(self.n // 8)] step = [0] * 48; pk_ = [0] * 48 for i in range(32): step[i] = a[i] pk_[i] = SM3_n(qq[i], 255 - step[i]) sum = [0] * 16 for i in range(16): sum[i] = 0 for j in range(1, 65): if m[j - 1] == self.hex_symbols[i]: sum[i] += j step[i + 32] = sum[i] % 255 pk_[i + 32] = SM3_n(qq[i + 32], 255 - step[i + 32]) return True if pk_ == pk else False

print('initializing...')
Sign = Nepsign()
while 1:
match int(input('> ')):
case 1:
msg = bytes.fromhex(input('msg: '))
if msg != b'happy for NepCTF 2025':
print(Sign.sign(msg))
else:
print("You can't do that")
case 2:
qq = literal_eval(input('give me a qq: '))
if Sign.verify(b'happy for NepCTF 2025', qq):
print(flag)
解密flag

思考

Verifying SM3 process

Okay, I’m thinking through the SM3 process. I need to create a target value (qq_target) and can compute SM3_n iteratively forward from the known step. But since I can’t do it backwards from a later step and we only have values after ‘sign,’ I'll need to apply it for each index up to 47. From there, I’ll compute forward using the SM3_n hash function. Verifying the current state with the qq bytes will help get the right target value. Makes sense, but checking the steps closely is key here.

Evaluating SM3 process further

To construct the desired qq_target, I need to compute it with SM3 iterations using known values like q_known. If step values have the right relation, I can advance through the chain properly. I also thought about using certain simple step values—maybe a step of 0 could help, as it seems like a potential leak. Alternatively, querying multiple messages could reveal smaller steps. However, I still need to ensure s_known ≤ s_target to make it work. I can compute step_target offline as the message is known.

Explaining process for solving CTF

The task revolves around querying messages to build up the required “signature” by controlling the SM3 hashing process with chosen messages. The idea is to adaptively select messages and compute step values, checking them against a target until all conditions are met. I’d use random messages to cover each index, optimizing by crafting messages to make small steps. The approach would involve scripting, using the SM3 algorithm, and handling edge cases—like when we can't step back for certain indices. A strategy and pseudocode is provided for how to proceed and build the final signature.

Breaking down the CTF strategy

The method aims to generate a valid signature for a target message by manipulating the SM3 hashing process. By customizing the bytes and step values, we perform queries until all target indices are satisfied — sometimes using random messages or adjusting each byte strategically. Special attention is given to certain edge cases, like when the step value is zero, which can be rare but manageable through multiple attempts. This process can be done offline through scripting with SM3 computations, allowing us to craft the required signature to solve the challenge efficiently.

Thought for 47 seconds

回答

这是个典型的 WOTS(Winternitz OTS)变种:
签名给出的是 sk[i] 沿着长度 255 的哈希链前进 step[i] 次后的值;验证再把它补完到 255 次,看是否等于公开的链尾 pk[i]
因此只要我能拿到每条链上“比目标消息所需步数更靠前(更小)”的位置,就能自己把它“往前推”到目标步数,从而伪造目标消息的签名。


攻击思路

  1. 计算目标消息的 step:
    你本地就能算出 m = SM3(b'happy for NepCTF 2025'),再按源码里同样的方法得到 48 个 step_target[i]

  2. 多次问签名(case 1):
    对任意其它消息(随便发随机字节的 hex)请求签名,得到 qq_sign[i] = SM3_n(sk[i], step_sign[i])

    • step_sign[i] ≤ step_target[i],就保存下来;以后可以自己再哈希 step_target[i] - step_sign[i] 次,得到目标需要的 qq_target[i]
    • 否则丢弃这个下标,继续刷下一条消息。
      重复直到 48 个位置全都满足。
  3. 本地补链:
    对每个 i:qq_target[i] = SM3_n(qq_sign[i], step_target[i] - step_sign[i])

  4. 提交(case 2):
    qq_target(48 个十六进制字符串的 Python 列表)交给服务器,验证通过就打印 flag。

特别注意:literal_eval 读取的是 Python 字面量,因此你的列表要像:
['deadbeef...', '0123abcd...', ...]
不要带 0x,也不要是 bytes。


参考脚本(pwntools 版本)

python
from pwn import * from gmssl import sm3 import os, ast, random hex_symbols = '0123456789abcdef' def SM3(data: bytes): return sm3.sm3_hash(list(data)) def SM3_n(data: bytes, n: int): for _ in range(n): data = bytes.fromhex(SM3(data)) return data.hex() def get_steps(msg: bytes): m = SM3(msg) m_bin = bin(int(m, 16))[2:].zfill(256) a = [int(m_bin[8*i:8*i+8], 2) for i in range(32)] step = [0]*48 for i in range(32): step[i] = a[i] for i in range(16): s = 0 for j in range(1, 65): if m[j-1] == hex_symbols[i]: s += j step[32+i] = s % 255 return step # ---------- 连接 ---------- host, port = 'your.host', 31337 r = remote(host, port) # 初始化读取 r.recvuntil(b'initializing...') target_msg = b'happy for NepCTF 2025' steps_t = get_steps(target_msg) got = [False]*48 sig_store = [None]*48 step_store = [None]*48 def ask_sign(msg_bytes: bytes): r.sendline(b'1') r.recvuntil(b'msg: ') r.sendline(msg_bytes.hex().encode()) line = r.recvline().decode().strip() # 可能返回 "You can't do that" if "You can't do that" in line: return None sig_list = ast.literal_eval(line) return sig_list while not all(got): # 随机消息 m = os.urandom(32) sig_list = ask_sign(m) if sig_list is None: continue steps_m = get_steps(m) for i in range(48): if got[i]: continue if steps_m[i] <= steps_t[i]: sig_store[i] = sig_list[i] step_store[i] = steps_m[i] got[i] = True print(f"{sum(got)}/48 collected") # 补链得到目标签名 final_sig = [] for i in range(48): delta = steps_t[i] - step_store[i] q = sig_store[i] if delta: q = SM3_n(bytes.fromhex(q), delta) final_sig.append(q) # 提交 r.sendline(b'2') r.recvuntil(b'give me a qq: ') r.sendline(str(final_sig).encode()) print(r.recvline().decode()) print(r.recvline().decode())

如果有个别 step_target[i] 为 0,你也必须刷到一次 step_sign[i] == 0 才行(概率 1/256,不难),脚本会自动继续刷。


小结

  • 本题实质是“哈希链型一次性签名”的典型弱点:允许多次签名 + 可知/可控的哈希迭代步数 → 可以“往前推”伪造别的消息。
  • 解决方案即:收集每条链上不大于目标步数的节点,然后本地补齐。
  • 最终用 forged qq 过验证即可拿到 flag。

祝你拿到 flag!有问题继续问我。

質問

靶机地址是nepctf30-nnyx-qoot-msg5-apmehyqsz101.nepctf.com:443,我该如何运行该脚本

思考

Instructing on exploit script setup

To run the exploit against a remote host on port 443, we need to clarify whether SSL/TLS is required. It's common for CTFs to use port 443 without actual HTTPS. Start by testing with openssl s_client -connect host:443 to check for a TLS handshake. If TLS is needed, use remote(host, port, ssl=True), or omit it to disable SSL. You can also try tools like netcat or nmap to confirm if SSL is needed. Provide two script versions — one with SSL and one without.

Providing exploit script instructions

To run the attack script, ensure you have Python 3.10+ and the gmssl and pwntools packages installed. Run pip install gmssl pwntools. The gmssl library is used for SM3 hashing. If you encounter issues with SSL, try connecting with and without SSL: use openssl s_client -connect host:443 to check if TLS is enabled based on the handshake. If it’s detected, enable SSL in your script. For Cloudflare, SNI might be needed. We’ll provide both SSL-enabled and plaintext options for the script.

Thought for 15 seconds

回答

可以,按下面步骤跑就行(以 Linux/macOS 终端为例;Windows 用 PowerShell 也一样):


1) 先判断服务是不是 TLS(因为端口是 443)

很多 CTF 把明文 TCP 放在 443 端口。你可以快速探测:

bash
# 成功握手并打印证书等 => 服务走 TLS openssl s_client -connect nepctf30-nnyx-qoot-msg5-apmehyqsz101.nepctf.com:443 -servername nepctf30-nnyx-qoot-msg5-apmehyqsz101.nepctf.com -quiet
  • 如果能握手成功(看到证书信息、没有 “wrong version number” 之类错误),后面脚本里需要 ssl=True, sni=host
  • 如果握手失败,那就是明文服务,用默认的 ssl=False(不写参数即可)。

不想测也行:先按“明文”跑,不通再改成 ssl=True, sni=host


2) 准备环境

建议用虚拟环境:

bash
python3 -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install --upgrade pip pip install pwntools gmssl

3) 保存并运行脚本

把下面脚本保存为 solve.py根据第 1 步的结果设置 USE_TLS(先尝试 False,不行再改 True 并带上 SNI)。

python
from pwn import * from gmssl import sm3 import os, ast import random # --------- 配置 --------- HOST = "nepctf30-nnyx-qoot-msg5-apmehyqsz101.nepctf.com" PORT = 443 USE_TLS = False # 若第 1 步表明服务是 TLS,就改为 True # ----------------------- hex_symbols = '0123456789abcdef' def SM3(data: bytes): return sm3.sm3_hash(list(data)) def SM3_n(data: bytes, n: int): for _ in range(n): data = bytes.fromhex(SM3(data)) return data.hex() def get_steps(msg: bytes): m = SM3(msg) m_bin = bin(int(m, 16))[2:].zfill(256) a = [int(m_bin[8*i:8*i+8], 2) for i in range(32)] step = [0]*48 for i in range(32): step[i] = a[i] for i in range(16): s = 0 for j in range(1, 65): if m[j-1] == hex_symbols[i]: s += j step[32+i] = s % 255 return step def ask_sign(r, msg_bytes: bytes): r.sendline(b'1') r.recvuntil(b'msg: ') r.sendline(msg_bytes.hex().encode()) line = r.recvline(timeout=5) if line is None: return None text = line.decode(errors='ignore').strip() if "You can't do that" in text: return None return ast.literal_eval(text) def main(): context.log_level = 'error' # 静默一点 if USE_TLS: r = remote(HOST, PORT, ssl=True, sni=HOST) else: r = remote(HOST, PORT) # 读到初始化提示 r.recvuntil(b'initializing...') target_msg = b'happy for NepCTF 2025' steps_t = get_steps(target_msg) got = [False]*48 sig_store = [None]*48 step_store = [None]*48 collected = 0 while collected < 48: m = os.urandom(32) sig_list = ask_sign(r, m) if not sig_list: continue steps_m = get_steps(m) for i in range(48): if not got[i] and steps_m[i] <= steps_t[i]: sig_store[i] = sig_list[i] step_store[i] = steps_m[i] got[i] = True collected += 1 print(f"{collected}/48 collected") # 本地补链 final_sig = [] for i in range(48): delta = steps_t[i] - step_store[i] q = sig_store[i] if delta: q = SM3_n(bytes.fromhex(q), delta) final_sig.append(q) # 提交验证 r.sendline(b'2') r.recvuntil(b'give me a qq: ') r.sendline(str(final_sig).encode()) # 打印返回(flag 应该在这里) try: while True: line = r.recvline(timeout=2) if not line: break print(line.decode(errors='ignore').strip()) finally: r.close() if __name__ == "__main__": main()

运行:

bash
python solve.py

看到类似 x/48 collected 一路增长,最后会打印出服务器返回的 flag。


4) 常见问题 & 小贴士

  • 卡住/没输出

    • 先试 USE_TLS=False。不行再设为 True 并加 sni=HOST
    • 网络波动可重跑;也可在脚本里加重连逻辑(本题循环在一个会话内足够)。
  • ModuleNotFoundError: gmssl:忘记安装 gmssl,按上面 pip install gmssl

  • 速度
    收集 48 条满足 step_sign[i] ≤ step_target[i] 的链节点是概率事件,通常几百到一两千次签名请求就能收集齐,耐心等。

  • 服务器速率限制
    若远端有限流,适当在采样处 time.sleep(0.01~0.05) 放慢。

需要我帮你把脚本改成自动探测 TLS 并重试的版本吗?或者你在本地遇到具体报错,把报错贴出来我一起看。

質問

靶机是 TCP

思考

Clarifying TCP setup for script

The user clarified that the target machine uses TCP, not TLS. I’ll need to update the instructions to reflect that change. I'll suggest setting USE_TLS=False or removing SSL params and confirm that when using pwntools, they can simply use remote(HOST, PORT). If using `socat

Thought for a couple of seconds

回答

那就最简单了:不用 TLS

把我之前脚本里这两处确认一下:

python
HOST = "nepctf30-nnyx-qoot-msg5-apmehyqsz101.nepctf.com" PORT = 443 USE_TLS = False # 明文 TCP ... if USE_TLS: r = remote(HOST, PORT, ssl=True, sni=HOST) else: r = remote(HOST, PORT) # 就这一句

或者直接删掉判断,写成:

python
r = remote(HOST, PORT) # 不要 ssl 参数

然后照常:

bash
python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install pwntools gmssl python solve.py

如果想先手动看看服务返回什么:

bash
nc nepctf30-nnyx-qoot-msg5-apmehyqsz101.nepctf.com 443

应当立刻看到 initializing... 之类提示。

有报错贴出来我继续帮你。

このQ&Aを共有