以下是一道 CTF 题目: from gmssl import sm3 from random im...
Erstellt am: 26. Juli 2025
Erstellt am: 26. Juli 2025
以下是一道 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()
textdef 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
这是个典型的 WOTS(Winternitz OTS)变种:
签名给出的是 sk[i]
沿着长度 255 的哈希链前进 step[i]
次后的值;验证再把它补完到 255 次,看是否等于公开的链尾 pk[i]
。
因此只要我能拿到每条链上“比目标消息所需步数更靠前(更小)”的位置,就能自己把它“往前推”到目标步数,从而伪造目标消息的签名。
计算目标消息的 step:
你本地就能算出 m = SM3(b'happy for NepCTF 2025')
,再按源码里同样的方法得到 48 个 step_target[i]
。
多次问签名(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]
。本地补链:
对每个 i:qq_target[i] = SM3_n(qq_sign[i], step_target[i] - step_sign[i])
。
提交(case 2):
把 qq_target
(48 个十六进制字符串的 Python 列表)交给服务器,验证通过就打印 flag。
特别注意:
literal_eval
读取的是 Python 字面量,因此你的列表要像:
['deadbeef...', '0123abcd...', ...]
。
不要带0x
,也不要是 bytes。
pythonfrom 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,不难),脚本会自动继续刷。
祝你拿到 flag!有问题继续问我。
靶机地址是nepctf30-nnyx-qoot-msg5-apmehyqsz101.nepctf.com:443,我该如何运行该脚本
可以,按下面步骤跑就行(以 Linux/macOS 终端为例;Windows 用 PowerShell 也一样):
很多 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
ssl=True, sni=host
。ssl=False
(不写参数即可)。不想测也行:先按“明文”跑,不通再改成
ssl=True, sni=host
。
建议用虚拟环境:
bashpython3 -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install --upgrade pip pip install pwntools gmssl
把下面脚本保存为 solve.py
。根据第 1 步的结果设置 USE_TLS
(先尝试 False
,不行再改 True
并带上 SNI)。
pythonfrom 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()
运行:
bashpython solve.py
看到类似 x/48 collected
一路增长,最后会打印出服务器返回的 flag。
卡住/没输出:
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
那就最简单了:不用 TLS。
把我之前脚本里这两处确认一下:
pythonHOST = "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) # 就这一句
或者直接删掉判断,写成:
pythonr = remote(HOST, PORT) # 不要 ssl 参数
然后照常:
bashpython3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install pwntools gmssl python solve.py
如果想先手动看看服务返回什么:
bashnc nepctf30-nnyx-qoot-msg5-apmehyqsz101.nepctf.com 443
应当立刻看到 initializing...
之类提示。
有报错贴出来我继续帮你。