第二届 OpenHarmony CTF 赛后复盘总结

比赛时间:2026 年 8 月 1 日至 8 月 21 日

底座模型:DeepSeek V4 Flash 0731

对agent的学习还尚浅,因此以赛代学,长长见识

这次比赛打了二十天

刚开赛时,我其实没准备一套多复杂的 Agent 架构,最初的想法很直接:把 OpenHarmony 仓库、平台 Skill 和比赛目标交给 Agent,先看看它到底能不能自己读代码、翻 Git、找候选,再把一条漏洞链完整跑通

后面的 Evidence Store、团队进度服务器、Candidate State、Submission Gate,基本都是比赛过程中撞到具体问题以后一点点补出来的

本机原始日志最终能核验到 17 条 Accepted,命中 8 个 track,其中 4 个完成 L1 / L2 / L3 三级闭环

赛题的三级结构可以简单理解成三个问题:

L1:漏洞具体落在哪个文件、哪一行?
L2:这段风险最早由哪个 commit 引入?
L3:平台把它归到哪个 CWE?

也就是说,先找到一个“看起来有问题”的 patch ,位置、历史、类型都要继续往下闭环

开局先让 Agent 自己跑一遍

第一版提示词很朴素

我把工作目录、平台接口和比赛目标交给 Agent,让它自己读仓库、查 Git、调用平台 Skill,同时把运行日志保存下来

当时最关心的就是:

发现候选

核对代码

提交 L1

追 L2

判断 L3

继续下一条

这条链能不能真的跑通

ecmascript/js_hclass.cpp:613 是比较典型的一次

当时 js_hclass.cpptagged_tree.cppjs_stable_array.cpp 都放在 ecmascript/ 根目录。比赛又把题目拆成 ecmascript/basebuiltinscontainers 等 challenge,单看目录很难确定这些根目录文件应该归到哪一题

前面一些根目录候选提交到 containers 被拒以后,我让 Agent 重新判断 challenge 归属。它开始把 ecmascript/base 提到更高优先级,随后提交:

ecmascript/js_hclass.cpp:613

平台返回 Accepted

这次命中马上改变了后面的搜索顺序。根目录核心文件开始优先往 challenge 139 看,不再平均地往几个 challenge 里试。

继续追 L2 时,最开始主要依赖 git blame

很快我发现,blame 更适合回答“最近是谁碰过这行”:一旦中间经过代码重排、迁移、改名,它很容易停在一个后续提交上

于是搜索开始往语义历史靠:

git log --all -S "ProtoIsFastJSArray" -- ecmascript/js_hclass.cpp

最后追到:

72a3e62801ac16b85b09187785df9a4ef8eabf3b

这个 commit 才是相关风险语义真正出现的位置,L2 Accepted,继续完成 L3

但是前期跑得很快,问题也很明显

Agent 当时有比较大的提交自由度:一个候选只要足够像,它就容易继续往平台试。短时间看起来效率很高,比赛一旦拉长,这种自由度会持续消耗 wrongCount 和注意力

第一次改思路

当时是json_parser.cpp 某个后续修复改动了 SkipEndWhiteSpace(),修复父版本和比赛基线之间还有其他提交,同一条语句在两个版本里的行号已经发生偏移。

如果直接拿修复 diff 的行号去交,很容易把版本差异一起带进去

所以当时把两个版本分别取出来核对:

git show <baseline>:ecmascript/base/json_parser.cpp | sed -n '796,800p'
git show <fix-parent>:ecmascript/base/json_parser.cpp | sed -n '796,800p'

最后确认比赛基线里的目标行为在 799 行。

这个候选再次提交后,平台依旧返回:

Duplicate wrong submission, no penalty

代码分析本身没有白做:

修复证据
后续版本确实改过危险行为

基线证据
比赛版本确实存在对应代码

平台证据
这个位置确实属于平台收录的 track

也就是说,Git 历史可以提出一个很强的假设,比赛基线继续压缩范围,平台回执才给最终裁决

所以这个时候我改变了下思路:

安全 patch 先进入候选池

接下来继续核:

比赛基线是否存在
危险操作到底是哪一行
challenge 归属是否合理
有没有已经验证过的错误记录

走完这些步骤,候选才值得进入下一层

中期:我开始把 Agent 的记忆搬出对话

比赛跑到中段以后,单次分析已经不算最麻烦的部分。

麻烦的是状态越来越多,而且此时主办方也对比赛做了一些调整:对有爆破行为的队伍进行了惩罚,并且给每道题目规定了总尝试次数上限

同一时间可能有:

  • 已经提交并明确错误的 L1;
  • 基线不存在的修复;
  • 还缺一段证据的候选;
  • 已经过 L1、正在追 L2 的 track;
  • L2 已过、正在判断 CWE 的 track;
  • 不同 Agent 重复看过的 commit;
  • wrongCount、cooldown、activeTrack 等平台状态

这些东西全塞在聊天里,跑几轮以后就会变成:

这个好像试过
那个之前似乎排除了
这个 commit 应该有人看过

这种模糊记忆不能拿来支撑提交

所以中期开始把长期状态落到本地文件。

logs/          原始运行记录
candidates/   候选与证据
ledger/       已经发生的提交
rejected/     已排除候选及原因
snapshots/     提交前后平台状态
strategy/     当前筛选策略

这里刻意把 candidaterejected 分开

一个候选暂时没做完,只能算待复核。只有出现明确理由,比如:

比赛基线没有这段代码
父版本已经存在同样的风险语义
平台已经明确返回 WrongAnswer
这次改动只有重构,没有危险行为变化

才进入 rejected

候选状态也逐渐固定下来:

待核验

├── 证据完整 ─────────→ ready
│                         │
│                         ├── Accepted ─→ 已命中
│                         ├── 暂缓 ─────→ 待复核
│                         └── 明确否定 ─→ 已排除

└── 基线不成立 ────────→ 已排除

这时候我对上下文的使用方式也变了。

对话负责当前推理
文件负责长期事实

换一个新会话以后,Agent 先读当前状态,再接着做。

这样比让它重新回忆上一轮到底干了什么稳定很多

关于团队协作

做到中途,我和队友又碰到另一个问题

自己的 Agent 有 ledger 以后,确实能少做很多重复工作,但队友也在并行推进,本机看不到其他人的最新结果

实际运行中出现过这种情况:

提交一个答案

Duplicate wrong submission

继续查

发现并行者已经试过

也出现过某个 track 已经被队友推进,本机还按照旧状态继续分析

于是经过队内商量后,我们搭了一个团队服务器,用来记录已经确认的比赛成果和当前进度

我没有把本机所有分析细节都往服务器里塞

两边保存的东西不一样:

本机工作区
└─ 完整候选、Git 证据、AI 日志、原始提交回执

团队进度服务器
└─ 已确认结果、track 进度、团队共享状态

本机回答:

这个结论为什么成立?

团队服务器回答:

这条线现在推进到哪了?

也就是说,一个负责证据,一个负责同步

后面再开新分析任务时,我会先看自己的 ledger,再看团队进度,很多重复分析在这一层就能提前停掉

后期:我开始主动收 Agent 的提交权限

比赛后半段,我对“自动化”的理解又变了一次

前期更接近:

Agent 分析

Agent 判断

Agent 提交

后期逐渐变成:

Agent 分析

生成精确候选

基线 / 历史 / CWE 证据核验

Candidate = ready

Submission Gate

单次提交

保存 pre / response / post

原因很简单

读代码错了可以重来

Git 搜索错了可以换关键词

但是平台提交会直接改变 wrongCount、cooldown 和 track 状态

所以我后来把“分析权限”和“提交权限”拆开。Agent 可以大胆找候选,真正写平台状态之前必须过一层闸门。

提交前至少要确认:

challenge / track / level 是否明确
L1 是否回到比赛基线逐行核过
L2 是否追到风险语义真正的引入位置
L3 是否有代码行为支撑
ledger 是否已经出现同一答案
团队进度里是否已经有人推进
wrongCount / cooldown / lock 是否允许
pre-submit 快照是否已保存

能程序化的条件就直接程序化

例如:

status != ready           -> 拒绝提交
ledger 已存在同答案       -> 拒绝提交
缺少 baseline evidence   -> 拒绝提交
缺少 pre snapshot       -> 拒绝提交

提示词继续影响 Agent 怎么想

程序负责决定它能不能真的按下 submit

这个边界对长时间 Agent 很重要

最终架构:agent核心

比赛后半段,我把前面零零散散加进去的东西重新整理了一次

核心其实只有四块:

屏幕截图 2026-08-28 174158

Platform I/O

所有和比赛平台直接打交道的动作从这里走。

包括:

challenge 查询
track 状态
submit
wrongCount
cooldown
lock
pre / post snapshot

说简单一点,就是平台现在到底是什么状态

Evidence Store

这里保存已经确认过的东西:

candidates
ledger
rejected
logs
snapshots
strategy

它解决的是长期记忆问题

Agent 可以忘掉一段聊天,已经确认过的事实不能跟着一起丢

Analysis Workers

这里是真正烧推理资源的部分

主要负责:

安全修复筛选
比赛基线核验
危险操作定位
L2 引入历史追溯
CWE 判断

Worker 可以并行

我后面更倾向按“候选”或者“验证任务”去拆,而不让几个 Agent 同时扫同一批 commit

Submission Gate

这里统一处理提交

它检查:

候选状态
证据完整性
重复答案
团队进度
提交预算
平台锁定状态

通过以后才放行

团队服务器放在协作层

我没有塞进四块核心里

它更适合挂在外层:

                   Team Progress Server
                    │             │
                    │             │
                    ▼             ▼
            Evidence Store   Submission Gate

它给 Evidence Store 补充团队已经确认的结果,也给 Submission Gate 提供团队侧去重信息

这样核心工作流仍然可以单机运行

团队协作存在时,再接这一层

我觉得这种拆法更干净,项目内部逻辑和多人协作不会绑死在一起

平台回执开始反过来影响下一轮策略

后期还有一个变化:Accepted 和 WrongAnswer 不再只看成结果,它们也变成了下一轮搜索的输入

因为我发现,有些commit里面可能不止一条track,会出很多条L1答案

流程大概是:

提交

Accepted / WrongAnswer / Duplicate

更新 ledger / snapshot,从已找到的L1答案中找线索,尝试去探究比赛方的出题侧重

调整候选权重与扫描策略

进入下一轮 Analysis Workers

变化发生在 Agent 周围的状态和策略上

下一轮它看到的上下文、候选排序和限制条件已经和上一轮不同

最后

比赛结束时,整套东西已经可以压成:

模型负责提出假设
Git 和比赛基线负责提供证据
Evidence Store 负责保存状态
团队服务器负责同步进度
Analysis Workers 负责并行验证
Submission Gate 负责限制提交
平台反馈负责调整下一轮策略

底座模型 DeepSeek V4 Flash 0731 主要承担高吞吐的代码阅读、diff 分析、历史追溯和候选判断

我没有要求模型每一次都判断正确

要求的是一个错误判断不能轻易穿透整套流程,直接变成平台提交

开赛时,我手里基本只有一条提示词、一套平台工具和本地仓库

我主要想知道:

Agent 能不能自己找到答案?

所以给了它比较大的搜索空间和工具权限

跑到中段以后,问题变成:

前面做过什么,还能不能准确找回来?

于是加 Evidence Store,把长期状态从对话里搬出去。

队友一起并行以后,又多了一个问题:

其他人现在做到哪里?

于是搭团队进度服务器。

到了后期,我更关心:

这个候选到底够不够资格提交?

于是 Candidate State 和 Submission Gate 开始成为核心

到比赛结束,工作流已经长成:

Platform I/O
Evidence Store
Analysis Workers
Submission Gate
Team Progress Server
Feedback Loop

这些模块单独看都不复杂

有价值的是边界

Agent 会忘,会重复,会误判;长时间运行以后,上下文也会越来越脏

把长期状态、证据、团队同步和提交权限分别放到合适的位置以后,模型就可以把主要精力留给当前候选本身

总结下来就是:让 Agent 负责思考,让工作流负责记住它做过什么,并限制它什么时候真的动手

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇