r0re 自动逆向分析工具实战记录
fyrlove
fyrlove
看雪学苑
2026年9月28日 17:59
上海
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
做 Android CTF 时经常碰到同一个问题:前面用 JADX、strings 很快能摸到入口,后面一碰到壳、动态注册 JNI、native 自解密或反调试,就开始在好几个终端和工具之间来回切。最后即便拿到一串“像答案”的内容,过几天再看,往往说不清它从哪一层来的,也不知道有没有真正走到校验点。
所以我把 Cairn 用 Java 重写,再把 Android 逆向常用的工具、提示词和验证环节接进去,做成了 r0re。它干的事情很简单:把任务拆小,把每一步看到的东西留下来;候选错了,就从错误处继续,而不是重新开一个聊天窗口。
这次用它跑完了看雪论坛公开 Android CTF 题集。AliCrackme\_2、AliCrackme\_4 都出现过首个候选不对、补 Hint 后才重新收敛的情况,我把这些过程保留下来了。对我来说,逆向 Agent 能不能第一次完成不是最关键的;更重要的是错误后能不能留下原因,并继续往下走。
说明:文中的样本仅用于公开 CTF/CrackMe 学习和授权逆向场景。源码、配置模板和测试记录随帖附件提供;发布时请将自己的模型 Key、设备地址和本机绝对路径从配置文件中移除。
一、测试结果:9 题通过
本次测试覆盖题集中的 9 道题,下图是最终记录。后面只展开 5 道:我想把篇幅留给出错后的处理、Hint 怎么生效,以及最终结果怎么验,而不是逐题堆截图。
二、我为什么从%20Cairn%20开始改
r0re%20不是凭空起的项目。它基于 Cairn 的架构思想进行%20Java%20重实现,再针对%20Android%20逆向补齐工具、提示词、运行态观测和验证环节。
Cairn%20的开发者是 淚笑,并在开源组织「起零衍迹%20/%20Oritera」发布;Bytex 是他参加腾讯云黑客松时使用的战队名。该系统在那次比赛中完成%2054/54,是该届唯一%20AK%20队伍。我参考它,主要不是因为这个成绩,而是因为它把复杂任务压缩成了一个很清楚的模型:已知 origin,设定 goal,中间路径未知,就持续补事实、拆下一步。Cairn%20源码与作者复盘在这里。
Cairn%20的核心很克制:黑板上只有%20Fact、Intent%20和%20Hint%20三种对象。Fact%20是已确认的发现;Intent%20是下一步准备探索的方向;Hint%20是人随时可以写入的判断。Worker%20围绕同一张黑板读取信息、探索、再写回事实,不靠“信息收集%20Agent%20/%20利用%20Agent%20/%20报告%20Agent”这样的固定角色切分工作。
这个思路刚好适合%20Android%20逆向。一个%20APK%20的校验路径可能横跨%20Java、资源、JNI%20和运行时内存,起手时谁也画不准流程图。先把确定的东西记下来,再盯着当前缺口往下挖,比硬规定一套“第一步、第二步、第三步”更实际。
不过,原版%20Cairn%20面向通用问题求解与渗透测试,Android%20逆向还有自己的硬约束:JADX%20和%20JNI%20映射要可回查,动态分析要能连设备,候选%20Flag%20不能只凭一段自然语言结论完成。因此%20r0re%20保留了%20Cairn%20的黑板、serve%20+%20dispatch 双进程和任务图思想,同时增加了%20Android%20CTF%20专用%20prompt、产物索引、runtime%20event、Frida/ADB%20工作路径,以及最终候选的验证门控。
所以%20r0re%20不是要另起一套理论。Cairn%20给了问题推进的骨架;我做的部分,是把它接到%20APK、so、设备和验证脚本上。
三、做%20r0re%20时,我盯着的几个问题
脚本适合把已经知道的算法批量化;大模型比较擅长在陌生二进制里找方向。两者之间还差一个能长期保存上下文、允许人中途纠偏的工作台。r0re%20就是补这个空档。
它把一个项目拆为三类可追踪对象:
- Fact
已经观察到的事实,例如%20Manifest%20入口、某个%20JNI%20映射、一次%20Frida%20hook%20的参数或已导出的%20dex;
*%20Intent
下一步可独立完成的工作,例如“定位 RegisterNatives 的注册表”或“复现%20AES%20解密链”;
*%20Hint
人为补充的约束和方向,例如“先检查 libmobisec.so”“候选必须在真机输入通过后才可结束”。
Worker%20不能只写一段长篇结论就宣布完成。它需要说明依据哪些%20Fact,产出了什么文件,哪些结论已经由真实工具输出验证。候选答案随后进入单独的%20verification/finalize%20流程;验证失败,候选会保留失败痕迹而不是悄悄覆盖。
这套设计直接针对%20Android%20CTF%20里最常见的三个坑:
- 可见字符串、Base64、Morse%20表只是线索,不等于%20Flag;
- 静态分析卡在壳或自修改%20native%20时,需要切到设备动态分析;
- 一个“看起来正确”的答案必须能通过原%20App、重建算法或独立验证器中的至少一种验证。
四、源码拆解:从创建项目到验证收口
源码仓库:github.com/fyrlove/r0re。r0re%20使用%20Java%2017、Spring%20Boot%20和%20SQLite;服务进程负责保存黑板、项目和完成状态,dispatcher%20是独立进程,按项目状态调用容器中的%20Worker。
这个拆分不是为了把架构画得复杂,而是为了把“模型调用”与“分析现场”分离:模型超时、容器退出、机器休眠,都不应该让已经得到的证据消失。
WebUI%20/%20HTTP%20API
%20 %20 %20 │
%20 %20 %20 ▼
SQLite%20黑板:projects%20/%20facts%20/%20intents%20/%20hints%20/%20runtime_events
%20 %20 %20 │
%20 %20 %20 ▼
Dispatcher:bootstrap%20→%20reason%20→%20explore%20→%20verify%20→%20finalize
%20 %20 %20 │
%20 %20 %20 ▼
Docker%20Worker:jadx%20/%20apktool%20/%20readelf%20/%20Frida%20/%20ADB%20/%20自定义%20solver
%20 %20 %20 │
%20 %20 %20 └──%20analysis%20产物、trace、报告回写到项目目录
4.1%20先把状态落下来
最核心的%20schema%20在 src/main/resources/schema.sql。projects 保存项目生命周期;facts、intents、intent_sources 和 hints 组成可回放的事实图;runtime_events 记录每个%20Worker%20在什么阶段、用了什么工具、生成了什么产物;completion_candidates 与 verification_intents 则把“模型声称完成”拆成待验证的候选和独立验证任务。
这个数据结构解决了一个很现实的问题:同样一句“已找到%20Flag”,在逆向里含金量完全不同。它可能是%20strings%20里看见的值,也可能是对 memcmp 参数的%20hook%20结果,还可能是本地脚本正向/逆向都跑通的输入。r0re%20用 verified、kind 和%20artifact%20路径把这些状态分开保存。dead_end 也会作为事实保留——例如“静态%20dump%20没有解密代码页”,后续任务就不会再沿同一条路无休止搜索。
从源码看,所有黑板写入集中在 src/main/java/com/fyr/r0re/server/R0reService.java,而 DispatcherRunner 只通过%20HTTP%20API%20获取项目、认领任务和回传结果,不直接碰%20SQLite。
这样%20WebUI、dispatcher%20和%20Worker%20看到的是同一个事实来源,不会出现“页面里已经加了%20Hint,容器还在拿旧上下文”的两套状态。
4.2%20调度循环怎么转起来
src/main/java/com/fyr/r0re/dispatcher/DispatcherRunner.java 是整个系统的心脏。每次%20tick%20会回收结束任务、读取活跃项目、刷新运行态、处理容器清理,然后为可执行项目补位。任务只有三种:
bootstrap
项目刚创建时做首轮侦察,建立%20APK%20的基本认识;
*%20reason
读取当前事实图,判断目标是否达成,或生成下一批更小的%20Intent;
*%20explore
认领一个%20Intent,真正调用工具完成分析,并把结果写回为新的%20Fact。
把它写成伪代码,大致就是:
项目创建%20→%20bootstrap
新%20Fact%20/%20新%20Hint%20/%20有空闲任务槽位%20→%20reason
reason%20产生%20Intent%20→%20Worker%20认领%20→%20explore
explore%20产出%20Fact%20或%20dead_end%20→%20再次%20reason
候选答案%20→%20verifier%20→%20finalize
这里最重要的是 reason 不负责“替人写一份计划书”,而是只提出当前证据支持、且可以验收的下一步。例如已经发现 System.loadLibrary,下一条%20Intent%20应该是“枚举%20JNI%20声明并定位%20RegisterNatives”,而不是“继续深入分析”。一条%20Intent%20只要能产生一个明确事实,就值得被调度;无法完成的宽泛描述则不应进入队列。
在 dispatch.kimi.swarm.yaml 中可以设置同一项目的%20Worker%20并发。它们不是固定分工的角色,而是围绕不同%20Intent%20并行探索:一个追%20Java%20入口,一个追%20native%20注册,一个准备动态%20hook。谁先把可验证证据写回黑板,其他路径就能利用它或被及时否定。这也是%20r0re%20从%20Cairn%20继承得最完整的一点。
4.3%20Worker%20如何拿到上下文,又如何留下现场
ExternalCliWorkerAdapter.java 负责将一次任务变成容器内的%20CLI%20执行。它创建/复用项目容器,渲染%20prompt,启动外部%20Agent%20CLI,持续上报心跳,解析结构化结果,并将%20trace、workspace%20snapshot、脚本和报告同步回 output/live-analysis/<project_id>/。因此网页上看到的不是“模型最后一段回答”,而是可以回到本地查看的%20JADX%20输出、Frida%20脚本、内存%20dump%20或%20solver。
runtime_events 是这层的旁路记录。每条事件可以带 phase、step、工具名、当前进度和%20artifact。遇到长时间没有更新的%20native%20任务时,使用者能判断它是在跑 jadx、卡在%20CLI,还是已经开始调设备,而不用盯着一段没有意义的“分析中”。这也是文章里%20AliCrackme\_4%20能从静态分析转向%20Frida%20的基础:方向变化会留下事件、Hint%20和产物,不靠口头交接。
4.4%20Android%20题的%20Prompt%20为什么要拆开
PromptFactory.java 并非简单地把整张黑板塞给模型。它会为 reason 和 explore 导出更紧凑的事实图,附带最近产物、可检索目录和%20JNI%20摘要;再根据%20Intent%20选择模板。Android%20CTF%20的模板位于 src/main/resources/prompts/android-ctf/,native%20相关任务进一步分为:
explore_native_jni.md
Java/Smali%20native%20声明、System.loadLibrary 与 RegisterNatives 对应关系;
*%20explore_native_constants.md
密钥、查表、比较缓冲区等常量恢复;
*%20explore_native_smc.md
mprotect、解密、按需执行等自修改代码路线;
*%20explore_native_packed.md
壳、动态%20dex、Godot%20或资源包的恢复;
*%20explore_native_verify.md
把候选输入回放到已恢复的校验链中。
这类拆分看似只是%20prompt%20工程,实际是在给分析过程设“可交付物”。比如%20JNI%20任务要交出方法映射,常量恢复要交出偏移/字节和提取方法,验证任务要交出可以运行的命令或脚本。只说“发现了%20AES”不能让项目结束;只有证明密钥、密文与最终比较之间的调用关系,才是能复用的事实。
4.5%20Flag%20为什么不能直接宣布完成
CTF%20题最危险的假阳性,是模型用流畅的文字把猜测说成答案。r0re%20因而没有接受原始 complete 后立即结束项目的做法。CompletionPolicyService.java 会对逆向项目检查四类东西:是否存在%20verifier、是否写入验证时间、分析目录是否有真实产物、最近运行事件中是否有%20verifier%20或%20solver%20的通过证据。任一项缺失,候选只能停留在%20pending,而不能%20finalize。
这个约束对应前面的测试记录:AliCrackme\_2、AliCrackme\_4%20的首个候选被%20App%20否决后,错误没有被“覆盖”;它们留在事件和事实图里,新的%20Hint%20再驱动下一轮%20reason。对使用者来说,多出来的不是一个漂亮状态,而是一张可以解释“为什么这次答案可信”的收据。
4.6%20跑一题样本
本地准备好%20Java%2017、Docker%20和%20Android%20分析镜像后,可以这样启动:
./scripts/r0re-preflight.sh
./start-r0re.sh%20--no-bridge%20--config=dispatch.kimi.swarm.yaml
需要真机/模拟器动态分析时,显式指定%20ADB%20设备,避免多设备误连:
./start-r0re.sh%20\\
--adb-device%20<serial-or-ip:port>%20\\
--adb-mode%20wifi%20\\
--config=dispatch.kimi.swarm.yaml
打开 http://127.0.0.1:8001 创建项目即可。APK%20放在 container-android/test_apk/,任务中使用容器内路径 /home/reverser/workspace/test_apk/<sample>.apk。详细启动、排障和检查命令见仓库的 operations-manual.md 与 docs/r0re-swarm-操作手册.md。
4.7%20Fact、Intent、Hint%20为什么分开存
在%20r0re%20里,三者表面上都是文本,职责却完全不同。facts 表示已经掌握的状态,intents 表示从已知状态向未知状态迈出的一步,hints 则是人类对搜索方向的干预。如果把它们都塞进“任务日志”,模型下一轮只能阅读长篇上下文;而拆成事实图后,系统可以问得更精确:哪些事实已验证?哪条%20Intent%20还没有认领?某个%20Hint%20是在什么时候改变了搜索方向?
facts 有三个值得读源码时重点关注的字段:
kind
普通事实为 fact,已排除路线为 dead_end;
*%20verified
仅表示真实工具输出已确认,不因模型自述而自动置位;
*%20creator
记录事实由哪个%20Worker%20或人工写入。
普通 conclude 会生成新的%20Fact,但默认不是%20verified。Worker%20如果通过%20Frida、solver、真机运行等方式得到硬证据,应通过%20Blackboard%20API%20显式写入已验证%20Fact。这个区分看起来严格,却是防止报告“越写越真”的基础:候选%20Flag、历史%20writeup%20线索、JADX%20观察和真实运行结果可以共存,但它们的证据等级不同。
intents 则是一个没有显式%20status%20字段的小状态机。worker%20IS%20NULL%20AND%20concluded_at%20IS%20NULL 代表%20open;有%20worker%20但未%20conclude%20代表%20claimed;concluded_at 有值即闭环。Worker%20第一次%20heartbeat%20就是认领,租约到期会释放%20worker,使%20Intent%20回到%20open。reason%20的项目级租约同样保存在 projects 的 reason_worker、reason_trigger、reason_started_at、reason_last_heartbeat_at 四列中,保证同一时间一个项目只跑一个规划任务。
runtime_events 是唯一的事件总线,不只是调试日志。心跳载荷、任务启动/完成、候选验证、人工恢复都会落在这里,并可带 phase、step、tool、artifact、progress_current、progress_total。completion_candidates 与 verification_intents 则将结果验证独立出来;project_health 存放正常、待验证、需恢复、需人工复核等项目级信号;automation_jobs 记录周期性的冲突候选扫描、待验证重试、调度健康检查等维护任务。
4.8%20Origin%20/%20Goal%20/%20Hint%20的完整调用链
把%20WebUI%20的“创建项目”按钮展开,调用链如下:
index.html:createProject()
%20→%20POST%20/projects
%20→%20ProjectsController.createProject()
%20→%20R0reService.createProject()
%20→%20INSERT%20projects
%20→%20INSERT facts(origin)、facts(goal)
%20→%20INSERT%20hints(创建表单中携带的%20Hint)
%20→%20返回%20ProjectDetail
Origin%20和%20Goal%20在这里首先是数据库中的两条种子%20Fact,随后才被 PromptFactory.bootstrap() 注入给%20Worker。这个先后顺序决定了它们不是一次性%20prompt%20内容:即使项目跑了几小时,Origin、Goal%20仍是图的固定锚点,可以被导出、复核和重新打开的项目继续使用。
Goal%20有三重特殊性。第一,它不能被作为%20Intent%20的 from,服务端会拒绝“因为目标是找%20Flag,所以我已经有%20Flag”这种循环论证;第二,完成时生成的%20Intent%20会把 to_fact_id 指向 goal,表示图真正闭合;第三,Goal%20和标题、Origin%20一起参与逆向项目识别,从而决定是否启用严格完成门控。也就是说,Goal%20不只是写作提示,也是系统的验收契约。
运行中添加%20Hint%20的路径更有意思:HintsController 转发到 R0reService.createHint() 后,服务端会保留%20Hint,并从最近的非%20Goal%20Fact%20中选出来源,生成一条 Human%20hint%20follow-up:%20... 的开放%20Intent;同时释放旧%20reason%20租约并唤醒%20dispatcher。于是一个%20Hint%20会同时影响执行和规划:新的%20follow-up%20Intent%20可以马上被%20explore,Hint%20计数变化也会触发下一轮%20reason。这解释了为什么在错误候选出现后,加一条足够具体的%20Hint%20比“重新运行一遍”更有效。
4.9%20Classic%20与%20Swarm:两种调度方式如何共存
classic%20模式采用“dispatcher%20先认领再执行”的方式。tryDispatchProject() 依次检查项目是否%20active、是否在清理、同项目并发是否达到上限、是否有待处理%20candidate;然后在%20bootstrap、reason%20与未认领%20explore%20Intent%20之间选择。每个任务开始前,dispatcher%20都会通过%20API%20取得租约,再提交给本地线程池。
swarm%20模式的变化不是简单把 max_workers 调大。冷启动时%20dispatcher%20会启动%20race,让多个%20Worker%20同时读黑板;后续%20Worker%20自己通过%20Blackboard%20skill%20竞争%20claim%20Intent。抢到的%20Worker%20执行,没抢到的%20Worker%20换一条开放%20Intent。执行过程中它们可以直写%20Fact、dead\_end%20和结论,服务端会按归一化文本做去重,避免三个%20Worker%20因为同一句观察而把黑板刷屏。
两种模式的共性是:没有预设“反编译专家”“Frida%20专家”或“写报告专家”。逆向问题的关键路径在开始前不可知,角色分工容易使%20Worker%20在自己职责边界内空转。r0re%20更看重共享的事实图和可竞争的任务:能证明%20JNI%20映射的人先写入证据,另一个%20Worker%20就可以立刻用它去做%20native%20还原或动态验证。
这里也要把当前边界说清楚:源码中已经有按阶段记录进展、识别%20stalled%20task、准备恢复%20Hint%20的辅助逻辑,但当前主%20tick%20为保持%20Cairn%20调度行为,并没有直接调用这套%20stalled%20恢复路径。现阶段真正生效的恢复主要是租约过期后的释放,以及%20swarm%20对 recovery_required 项目的自动%20resume;手动停止的项目不会被系统擅自拉起。把“已实现的观测能力”和“已经接入主循环的自动恢复”分开,是阅读和使用这套系统时应有的判断。
4.10%20从%20Prompt%20到容器命令:一次%20Worker%20执行的实际形态
以%20bootstrap%20或%20explore%20为例,ExternalCliWorkerAdapter 会依次执行以下动作:
- 调用
DockerCli.ensureRunning(),确保项目对应的长驻容器可用; - 通过
PromptFactory选择模板,并把%20Origin、Goal、Hint%20或%20compact%20graph%20填入; - 由相应
WorkerDriver构造%20Kimi、Codex、Claude%20Code、Pi%20等%20CLI%20的命令与会话; - 注入
R0RE_BLACKBOARD_URL、项目%20ID、Worker%20名等环境变量,使容器内工具可以回写黑板; docker%20exec
运行命令,同时由 HeartbeatLease 续约;
*%20提取事件流中的%20assistant%20结果,检查配额、可重试错误和%20API%20错误,再交给 JsonOutputParser;
*%20保存%20execution%20trace%20与工作区产物,最后把受限的 WorkerResult 交回%20dispatcher。
这里的“受限”非常重要。解析器接受的不是任意自然语言,而是%20Fact、Intents、Complete、Rejected%20等有限结果。它会依次尝试完整%20JSON、JSON%20代码块和文本中的对象片段;无法解析时,bootstrap/explore%20可以在保留%20session%20的前提下进行一次%20conclude%20fallback,要求%20Worker%20立即停止并提交结构化总结。
reason%20刻意没有这条兜底:规划为空或不合约应当失败并等待新的事实/Hints,而不是被一段补救文字伪装成成功。
4.11%20完成状态机:从“有答案”到“可以发布答案”
下面这条链路是%20r0re%20专门为逆向结果增加的约束,也是我认为最值得保留的源码改造:
Worker%20/%20人工提交%20complete
%20 %20 %20 ↓
completion_candidates:pending
%20 %20 %20 ↓
approve%20/%20start-verification
%20 %20 %20 ↓
verifier、artifact、通过事件满足门控
%20 %20 %20 ↓
finalizeCompletion()
%20 %20 %20 ↓
写入%20to_fact_id='goal' 的完成%20Intent
%20 %20 %20 ↓
projects.status =%20completed
complete() 只创建候选,不会直接改变项目状态。候选保存候选值、来源%20Fact、证据摘要、产物路径和验证状态。CompletionPolicyService 对被识别为%20reverse/CTF/APK/JNI%20等场景的项目执行更严格检查:必须有%20verifier%20Worker、验证时间、真实分析产物,以及带有通过语义的%20verifier/solver%20运行事件。任一条件缺失,finalize%20都会被拒绝。
最终收口时,finalizeCompletion() 会再检查候选来源的%20Fact%20是否仍有效,写入一条目标为 goal 的完成%20Intent,更新%20candidate%20和%20project%20health,最后才把项目标为%20completed。图上的最后一条边由此可见、可导出。验证失败则不是“静默重试”:它会进入可重试失败或人工复核分支,保留失败原因,必要时再由自动化%20job%20或新%20Hint%20推动下一步。
4.12%20这套源码里真正有用的部分
读完这一套实现后,可以发现%20r0re%20的重点并不是换了哪一个模型。真正可复用的是四个工程约束:事实写后不改,错误路线也保留;任务必须引用已知事实,不能拿%20Goal%20自证;模型输出必须落入受限协议;完成结论必须经过证据门控。它们让大模型的探索能力可以被接到%20APK%20静态分析、JNI、动态%20hook%20和算法复现上,又不会把每次模型回答都当作真相。
这也是本文能够展示%209%20题全通、同时保留错误候选和人工%20Hint%20过程的原因。成绩可以作为结果,但源码中的状态、产物和验证链,才是结果可被他人复查、复用甚至反驳的基础。
五、Origin、Goal%20该怎么写
一开始我只在任务里写“分析%20APK,找正确%20Flag”。这会让%20Worker%20很容易跑偏:找到一个可疑常量就停止,或者在没有证据时盲目猜测。
后来固定为两个部分。Origin%20说明样本来源、保护特征、允许工具和证据边界;Goal%20则只写验收条件。下面是适用于%20AliCrackme\_3%20的精简版本:
Origin
这是看雪与阿里%202015%20移动安全挑战赛的公开%20Android%20CTF%20样本。
已知可能包含%20dex%20壳、反调试、JNI/native%20校验和摩斯码变换。
APK%20是唯一可信输入;公开资料只能作为线索,不能单独作为最终证据。
允许使用静态分析、脱壳、ADB/Frida%20动态分析和本地算法复现。
Goal
找出能被程序接受的输入,并形成可复现报告:入口、校验链、关键产物、
重建脚本或真机验证结果。没有通过恢复逻辑或实际运行验证的候选,不得作为最终答案。
这段话不是为了让模型突然变聪明,而是为了把停下来的条件写死:看到一个字符串不算完成;找到比较点、复现变换,或者在%20App%20中跑通,才算。
六、实战记录:5%20道题和几次回退
1.%20AliCrackme\_1:从资源伪装中还原映射表
第一题用于验证最基础的流程。创建项目后,Worker%20先列出%20APK%20结构和入口,再把资源文件与%20Java%20层 bytesToAliSmsCode 的调用关系写到黑板。最终不是凭“看起来像密码”的字符串结束,而是从 assets/logo.png 中恢复 256 项 UTF-8 映射表,反推出输入 581026,并用正向映射回验。
这个题不难,但它定下了后面的规矩:资源文件也可能是数据表;答案必须能回到校验函数。
2. AliCrackme\_2:首个答案错误后,补 Hint 重新拆题
第二题在第一次分析结束后,将候选输入 App 验证,结果错误。
这时没有把项目推倒重来,也没有手动修改结论,而是补充了“候选必须沿完整校验链验证,并继续定位 native 比较/变换点”的 Hint。Dispatcher 在下一轮看到 Hint 和失败记录后重新派发更窄的 Intent,第二次结果通过了 App 验证。
人工hint在这里做的不是替 AI 解题,而是把“错误候选”转译成下一轮可以执行的约束。对复杂样本而言,这是比反复换 prompt 更可靠的协作方式。
3. AliCrackme\_3:壳、反调试与 Morse 线索不能直接等同于答案
第三题包含阿里 dex 壳、反调试和摩斯码变换。创建项目时我直接把这些已知信息放进 Origin,同时明确“公开 writeup 只能作线索”。随后由 Worker 分别推进壳/DEX、Java 调用链、JNI/native 和算法验证,避免一条任务同时承担所有不确定性。
这里特别强调一个容易被忽略的点:Morse 表、hashCode 条件或历史文章给出的候选,都只能提高优先级,不能跳过验证。r0re 会把“已观察到的表”和“已证明到达最终比较的逻辑”分开记录。这样的记录也能避免后面写报告时,把线索误写成结论。
4. AliCrackme\_4:静态分析走不通,就让动态证据接管
第四题的第一次候选同样没有通过验证。
这次的 Hint 指向连接设备并观察运行时校验。r0re 自动进入 Frida 动态分析路径:在触发校验时 hook native 比较相关调用,结合脱壳后 dex 中的 AES 常量和密文,复现出完整变换链,再回到设备验证。
这类题的经验很直接:当 native 代码按需解密、静态 dump 里仍然是高熵数据时,不要让 Worker 在静态工具里无限打转。将“运行时触发校验、抓取比较参数或注册表”声明成独立 Intent,反而更快收敛。
5. AliCrackme\_2\_3
七、这轮测试留下的三点经验
第一,把并行用在互相独立的假设上。例如一个 Worker 看 Java 入口,一个看 JNI 映射,一个准备 Frida hook;不要让三个人同时泛泛地“分析整个 APK”。swarm 模式会竞争认领 Intent,已经被验证的 Fact 直接给后续任务使用。
第二,让失败可见且可消费。runtime events 会记录 Worker 的阶段、进度、产物和失败原因;dead_end 用来标识已排除的路线。这样重试不是把同一个 prompt 再跑一次,而是带着“为什么上一条路不通”重新调度。
第三,把完成当作一个状态机,而不是一句话。候选答案先进入 completion candidate;独立 verifier 检查证据和产物;满足门控后才 finalize。对于逆向题,这一步虽然显得慢,却能明显减少“模型把猜测写得很自信”的问题。
八、写在最后
这轮看雪 Android CTF 测试之后,r0re 不再只是“能调模型的服务”。它能拆任务、保存过程、接收人工方向,也能把错误候选留在项目里继续处理,最终完成整个逆向分析过程的到结果。
如果想试,建议先挑一题 Java 层校验清楚的 CrackMe,Goal 里直接写验证条件。把 facts、intents、产物和完成门控走通,再上壳、JNI 和动态分析题。
源码已公开在 fyrlove/r0re。欢迎在公开 CTF、教学样本和已获授权的 App 安全测试中使用,也欢迎把新的样本特征、验证脚本和失败路径补进来。
看雪ID:fyrlove
https://bbs.kanxue.com/user-home-703941.htm
\*本文为看雪论坛精华文章,由 fyrlove 原创,转载请注明来自看雪社区
火热售票中!1.25折门票即将售罄
往期推荐
当高频观测不再经过异常路径:Shadow Cave 与常驻式插桩架构
D3CTF 2026 d3llvm.apk 反调试定位与加密 SO的Dump
一串反引号,十层突破:n1ctf‑2018‑easy\_harder\_php 完整利用链
实现一个EDR不可见的网络通信(将lwip移植到nt内核中)
球分享
球点赞
球在看
点击阅读原文查看更多
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论