Files
apk-reverse/README.zh-CN.md
T
inliver233 6ab905b2c8 Four-state labels, and the negatives that used to look alike
Every claim carried one of three labels, and the third was doing two jobs. `unverified` meant
both "someone asserted it and nobody here paid for the counters" and "no evidence was gathered
at all" -- different statements with different next actions. This adds `unknown` for the second.

The distinction that pays for itself lives inside a *negative* result, because an empty probe has
three causes and only one of them is a finding:

  absent                  -- coverage reached the question, nothing is there
  present but unreadable  -- the thing is there and this route cannot read it
  unknown                 -- nothing covered the question

The middle one is the dangerous one: it produces byte-identical output to the first, and it is the
normal case on the input this skill is aimed at. A packed APK returning four classes is not an APK
with four classes, and "the string was not in the dex" is a statement about a probe. The rule now
says to name the probe's coverage and run a control that must hit before writing "not present", and
to record `unknown` about the probe rather than `absent` about the target.

Also in this pass:

  * P47 -- two tools disagree about the same artifact, and the disagreement is your parser. One
    extraction rule reused across outputs that do not share a format: an unescaped `$` dropped every
    descriptor containing one, and dotted names were compared against DEX descriptors so one side
    parsed to nothing. The comfortable failure is the one that confirms what you already expected,
    which is why it survived the sanity check it should not have.
  * dexsql: a cache is second-hand evidence and it ages silently. A stale cache answers for the old
    artifact with no error, and `--trusted-input` skips revalidating the recorded source inputs --
    so a cached run supports "true of the artifact the cache was built from", not "true of the
    artifact in front of me".
  * ete-diff-tracing: a divergence is a location, not a cause. The differential method produces a
    positional claim, and three readings it does not authorise are written down beside the step that
    produces it.
  * CONTRIBUTING.md -- the gates, what a green run does and does not establish, the sync rules a
    change has to keep (a new reference must be named in the inventory; renaming a heading breaks
    every section reference that pointed at it), and the two failure modes to watch when editing the
    gates: a limit that cannot fail, and a gate whose answer varies with the runner. README links it
    from both language editions.
2026-10-16 19:17:31 +08:00

52 KiB
Raw Blame History

apk-reverse

English · 简体中文

stars forks license python android ci

能力 · 结构 · 安装 · 环境要求 · 失败目录 · 范围 · 维护 · 免责声明

apk-reverse

面向 Android APK 逆向工程、去臃肿、去广告、外科式 dex 补丁、重打包,以及运行时/服务端分析的 Agent Skill。

它是一个 skill,不是教程:它是写给 agent(Claude Code、Codex,或任何支持 Agent Skills 格式的 harness)在工作过程中加载的,因此按 progressive disclosure(渐进披露)来组织——一份简短、面向决策的 SKILL.md,只在某一步真正需要时才加载的详细 references,以及可直接运行的参数化脚本。

Agent 应当如何使用它

SKILL.md 刻意写成一份带 gate 的流程,而不是建议,因为观察到的失败模式并不是无知——而是模型把整份文档读完、表示认同,然后照样从第一性原理重新推理一遍。

所以正文里有四样东西是用来执行的,不是用来读的:

  • 四条覆盖规则(R1–R4)。 当它们与当前计划冲突时,在证据推翻它们之前,以它们为准。
  • 一份症状索引。 每一行都是已经付过代价的失败。命中一行就是停止信号:在你跑下一条命令之前加载那个文件,而不是在又试了几次之后。绕过已经记录在案的已知症状继续推理,就是同一批小时被花掉两次的方式。
  • 四个 gate(G1–G4),每个都是一个动作加一条通过标准。「我理解这个思路」并不能通过 gate。它们存在的意义是让分类、环境真相和对照构建发生在第一次打补丁之前,而不是在第三次失败之后。
  • 一条两击规则和若干停止条件。 同一形态的失败出现两次,说明错的是模型,不是参数。一个已经失败过两次的假设再出第三个变体,就是回合葬身之处。

另外还有一样在结尾、应当被扣住的东西:**「done」**有定义(六项)。一份干净的日志不在其中。少于六项全部达成,就只是一个检查点,应当作为检查点汇报,并说明还剩什么。

如果你是正在读这段的 agent:成本最低的第一条命令是 python skills/apk-reverse/scripts/doctor.py。它会告诉你这里存在哪些工具、哪些脚本真的能跑,以及环境里是否已经有什么东西在污染你的测量结果。

它擅长什么

  • 快速判断一个需求在客户端是否本来就做不到,而不是把几小时烧在一个由服务端强制执行的付费墙(paywall)上。
  • 在任何工作开始之前先确定交付物必须是什么形态——一个免 root、自包含的产物与「让它在你这台机器上能跑」是两个不同的问题,混淆这两者是这个领域里最昂贵的漂移。
  • 为给定改动选择最安全的补丁层,并避开那些会把 app 弄坏的层。
  • 抓住那种「看起来成功」的重打包失败:app 安装、启动、渲染都完美,但每一个签名请求都被拒绝,因为客户端是从自己的签名证书推导请求签名密钥的。
  • 把你自己的错误与 app 或服务端的问题区分开——限定在某个功能上的失败(登录、注册、支付)常常是某一条代码路径上的 TLS/证书问题,而不是你刚做出来的补丁的后果。设备状态、失联的 device server 和时钟漂移会以同样的方式伪装成同类问题。
  • 确认实际执行的是哪个架构、哪个库,而不是相信 manifest 里带了什么或设备自称什么。
  • 处理加壳/加固目标:识别壳、脱壳,并把内存 dump 还原成一个打过补丁、可安装的 APK。
  • 处理一个故意终止进程的加固库——包括那种刻意崩溃的形态(fault addr 0x4),它看起来和普通的空指针解引用 bug 一模一样;以及「把它失效化,但绝不要让它不返回」这条规则,它决定修法是生效,还是以完全看不出与原因有关的方式冻住整个 app。
  • 知道该伸手去拿哪些工具,以及每个工具在哪里说谎——包括那些只以 GUI 形式存在的工具,此时应当请求人类介入,而不是悄悄换用一个更弱的方法。
  • 让打过补丁的构建保持打过补丁:失效化版本检查、强制升级弹窗和自更新安装器,使这份成果无法被远端一键关掉——同时认出那个能在版本号毫无变化的情况下悄悄把它撤销的热更新/远程配置通道。
  • 把客户端侧的登录闸门(可打补丁)与账号维度的资源(为空,因为服务端没有东西可答)区分开;并且知道伪造一个 session 所产生的状态比保持未登录更糟。
  • 让长时间任务保持诚实:一份实时记录、分级结论、校准过的超时和有界等待,使进展不会丢失、同一个错误不会被犯两次。
  • 避开那些会产出「构建完美、运行即死」的 APK 的具体错误。
  • 当 APK 不是可选项时,决定交付物应该是什么——被多个独立检查拒绝的重打包是受阻,不是昂贵;回落阶梯是系统级模块、本地 RPC 服务,或一份明确写出边界的诚实报告。
  • 分辨真实内存 dump 与抽取壳骨架,并知道哪条恢复路线适用——包括当 frida 本身被拒绝时走 root 侧 dump。这次测量能看见什么、看不见什么,写在 skills/apk-reverse/references/advanced-unpacking.md。
  • 当逆向的成本高于调用时,调用例程而不是逆向它:在宿主上做模拟执行,或把一个活函数通过 Frida RPC 服务化。
  • 当一个 native 函数被 OLLVM 碾平成状态机时,去读指令级执行证据——包括在真实设备上实测到的 Stalker 反咬的两种方式。
  • 当每一个软件级 tracer 都在修改它所观察的东西时,转向硬件层:arm64 硬件观察点(零代码改动,但带诚实的 TracerPid 账本)、带五道闸门的 ETE/TRBE 差分追踪,以及在拥有匹配闸门的设备把它们打响之前始终标注为未核实的内核注入模板。
  • 认出用户态 hooking 根本够不到那个检查的情况(裸 svc 系统调用、init_array 早期检测)、上下相邻层实际能做什么,以及什么时候继续升级是错误答案。
  • 处理非 REST 的协议——无 schema 的 protobuf、gRPC、QUIC/HTTP3——以及无视系统信任库的 native 侧证书固定(pinning)。
  • 直接在手机上工作:MT Manager 的编辑/重打包/签名流程及其 APK MCP 接口、LSPosed Manager、设备上的数据检查,与 PC 工具链并用,而不是用它来替代 PC 工具链。
  • 在花几小时去追一个在进程生命周期内任何时刻都不存在的「解密 DEX」之前,先把 Java2C 与抽取壳区分开——代码已经被编译进 .so。
  • 处理以 split APK / App Bundle 集合形式到来的构建:从设备上取出该集合、用一个 keystore 为每个成员签名以便 pm install-multiple,或在合法时把 code/native 成员合并成一个独立 APK。
  • 用已知明文差分处理一个真实的 Dex VMP——哪些环节可以自动化、哪些不能,一个编译出来的 fixture 能触达什么、不能触达什么,以及如何证明一张推导出的私有 opcode 表,而不是断言它。
  • 在发表所学的同时不发表目标——一个带上下文报告目标身份形态的扫描器、一份明确列出不得脱敏内容(工具、库、协议字段、CVE、加固产品、公开 crackme)的清单,因为把这些脱敏会毁掉可复用的那部分;以及能 gate 住一次提交的退出码。
  • 在重复本仓库已经收敛过的劳动之前先读先例:记录中正向的那一半,含路线及其死路、对每条断言的分级,以及该案例指明要回写的文件。

结构

SKILL.md、references/ 与 scripts/ 都在 skill 目录 skills/apk-reverse/ 内。仓库根目录下的一切是跨 skill 共享的维护工具,不属于已安装的 skill。

SKILL.md                  带 gate 的流程,不是背景阅读材料:
                          how-to-use  -> 四条覆盖规则(R1-R4)
                          症状索引(命中一行即为停止信号)
                          四个 gate(G1-G4,带通过标准的动作)
                          十三个分类问题
                          工作流,带每一步的跳过条件和一条两击规则
                          「done」的含义  ->  停止条件  ->  约束  ->  索引
references/               按需加载,每个文件一个主题
  recon.md                    识别壳、SDK、代码位置、篡改检查;脱壳
  server-config-and-updates.md
                              「广告」最常见的那种形态,也是通常被误诊的那种:
                              服务端提供 UI,客户端负责渲染(启动屏、弹窗、
                              公告、tab 集合)。证明它的两层抓取、如何按
                              data class 保留的字段名找到配置 DTO、为什么
                              你要补的是决策而不是数据、「移除」的范围如何界定,
                              以及远端重新启用 / 配置缓存持久性
  byte-level-patching.md      等长字节编辑:为什么它优于方法重建(实测)、
                              如何在不刮 listing 的情况下定位一条指令的精确偏移、
                              会让解码失步的指令宽度陷阱、把分支失效化还是重定向它、
                              dex 头完整性字段的顺序,以及校验器的 move-result 规则
  packers.md                  加固目标:拒绝信号、用单变量测试测量校验边界、
                              选择 native 宿主
  code-virtualization-and-custom-linkers.md
                              「已加壳」与「干净」之间的那一层:整个类被变成
                              `native` 声明、SONAME 与文件名不匹配的私有加载器、
                              内嵌的自解密 payload、一个 Java 层「签名杀手」在日志里
                              报告成功而 native 检查却在杀你。留还是删的死锁、
                              如何把*检查器*与*实现*分开,以及那条不做任何
                              失效化就终结它的字符串重定向技术
  framework-runtimes.md       Flutter / React Native / Unity:UI 归哪一层所有,
                              以及在没有符号时怎么找逻辑(字符串编码陷阱)
  dart-aot.md                 Dart AOT 深入:版本钉定与构建一个匹配的反编译器、
                              对象池与引用索引、寄存器/布尔约定、
                              识别业务逻辑的三种签名、定位、打补丁。
                              开篇先讲它所依赖的 snapshot 解码前端(aotopsy 或
                              blutter),因为 pool 列表是输入,不是本 skill 自己产出的东西
  native-and-so.md            .so 宿主、DT_NEEDED vs JNI_OnLoad、重定位限制、
                              免重定位的 bootstrap、用 native 替换 Java 方法,
                              以及*实际加载并执行*的是哪个 ABI/库
  native-tamper-and-suicide.md  加固库如何杀掉自己的进程:可见的机制、
                              如何判断实际触发的是哪一个、如何定位现场、
                              伪造的 section header、由 PT_GNU_EH_FRAME 推出的
                              函数边界、扫描器陷阱,以及安全地失效化
  detection-and-anti-analysis.md  当 app 反击,或工具在这里跑不起来时:把检测与
                              环境损坏区分开、按成本而不是按升级来决策、认出
                              动态分析根本无效的环境,并把「挡住我的分析」这个问题
                              与「挡住交付物」分开
  toolchain.md                该装什么、如何非交互地调用它、哪些工具只有 GUI、
                              版本对齐陷阱、离线工作、**「不在 PATH 上」不等于「没装」**,
                              以及该用哪个签名器
  long-task-discipline.md     实时记录、结论分级、漂移控制、超时与等待校准、
                              交付物形态漂移、你从未看过的截图、
                              长上下文衰减、交接
  ad-removal.md               广告分类学、包装层映射、回调陷阱、全局闸门、验证
  updates-and-forced-upgrade.md  让打过补丁的构建活下去:定位版本检查、
                              两层补丁(把例程 no-op、把比较失效化)、不能碰的东西
                              (manifest 版本、安装器权限、host 阻断)、
                              自更新与热更新/远程配置通道、验证根本没有发出任何版本请求
  account-gates.md            登录墙、强制手机绑定、游客模式:把客户端侧闸门
                              (可补)与账号维度资源(不可补)区分开、为什么
                              伪造 session 比保持未登录更糟,以及重装后
                              无法避免的登录态丢失
  signature-derived-keys.md   当 app 自己的签名证书被当作密钥材料时:
                              检测用 grep、为什么离线提取不可靠、
                              先硬编码再验证的流程
  membership-and-limits.md    服务端 vs 客户端权威;什么可补、什么不可补
  server-api.md               探测 app 的 API;证明闸门归谁所有
  tls-and-cert.md             功能维度的网络失败:过期证书、双信任链
  third-party-builds.md       在信任一个「破解版」/「魔改版」APK 之前审计它
  dex-patching.md             补丁层对照表 + dexlib2 技术深入
  patch-audit.md              证明一个补丁*落地*了并且*合法*:长度 vs 字节
                              比较、等长替换的盲区、校验器层面的合法性
                              (move-result 相邻性)静态检查、文本匹配式
                              补丁陷阱,以及如何报告一个缺失的补丁
  repack-and-sign.md          重打包规则、解包与重打包、签名、安装后隐患
  runtime-data.md             DataStore / SharedPreferences / SQLite / protobuf;app 何时
                              会改写你的编辑,以及解码一个看起来像加密的值
  dynamic-frida.md            Frida 配置、版本钉定、四层探测、hook 策略
  environment.md              设备/模拟器配置、root、ADB、离线设备、日志信号、
                              模拟器 console 控制与恢复、preflight、看一眼屏幕
  verification.md             主张阶梯;「done」的含义
  desensitization-and-leak-scans.md
                              发表纪律:什么必须脱敏、什么必须保留、
                              不得匿名化的清单、泄漏扫描器及其退出状态,以及
                              入口文件本身就是一处提示词暴露面
  precedents/                 正向案例库:含死路的路线、每条断言的分级、
                              实测到的坑,以及回写清单
  routing.md                  按需清单:每个 reference 及其加载时机、每个
                              script 及其用途,以及症状索引的镜像
  rasc-and-droidsaw.md        ASC 索引器的 Rust 重实现:实测加速比与
                              完全一致的类集合、它会静默丢掉方法体的 enum 形态,
                              以及如何构建与验证它
  evidence-summary.md         随 skill 一起发布的浓缩版:能力、一行结论、
                              强度,以及你在已安装副本里实际能打开的证据

  ../evals/                   也不是 spec 目录,而是 Agent Skills 指南推荐的
                              位置:`evals.json` 存放本 skill **尚未**运行过的
                              带 skill / 不带 skill 用例,运行方法写在文件里
  ../evidence/                不是 spec 目录:上面 evidence summary reference 的
                              机器可读配套文件 —— `capability-matrix.json`(同样的行、
                              更多字段)、`tested-tool-versions.json`(版本及每个版本背后的
                              探测)、`known-limitations.md`(面向安装者的限制清单)。随 skill
                              一起发布,使已安装的副本不需要仓库就能回答「这一条验证过吗、
                              强度如何」
  pitfalls.md                 失败目录 —— 构建之前先读
  advanced-unpacking.md       dump 拿到了但方法体是空的:按平凡方法体比例做
                              抽取壳诊断、FART 式主动调用以及为什么它的经典 hook
                              在 Android 12-16 上失效、code_item 拼接、当 frida
                              本身被拒绝时的 root 侧 dump,以及诚实的 VMP 边界
  lsposed-and-modules.md      重打包被拒绝,那就交付一个系统级 hook 模块:模块解剖、
                              无 gradle 的构建链、作用域配置与如何验证注入,
                              以及 Java 模块够不到的那一层
  emulation-and-rpc.md        调用例程而不是读它:Unidbg/Unicorn 模拟及其
                              环境填充成本,对比把活函数通过 Frida RPC 服务化
  native-dbi-and-deobfuscation.md
                              OLLVM 形态、Frida-Stalker trace、trace 到 CFG 的路线、
                              Stalker/QBDI/模拟的取舍,以及两条实测边界(一次
                              不产生任何事件的 follow,和一次因 follow 热 libc 导出而崩溃)
  protocol-reverse.md         无 schema 的 protobuf、从反编译代码恢复 schema、gRPC 帧
                              抓取、QUIC/HTTP3 的限制,以及 native 侧证书固定
  kernel-and-environment-hardening.md
                              用户态 hooking 可证明够不到检查:裸 `svc`、init_array 早期
                              检测、每种 root 方案隐藏了什么、带版本闸门的
                              内核路线图,以及什么时候停止升级
  hardware-watchpoints.md     arm64 上的隐形内存观察点:硬件调试槽位、带着诚实
                              `TracerPid` 账本的 ptrace 交付路线、8 字节窗口对齐
                              规则、各个陷阱(槽位按线程生效、信号转发、断点的
                              步越舞步),以及每次命中前都欠着的那次对照运行
  ete-diff-tracing.md         硬件执行流追踪(CoreSight ETE/TRBE)加差分法:五道
                              闸门与它们各自如何静默失败、按线程的 `cs_etm` 采集,
                              以及把两次只差一个输入位的运行还原成依赖输入的块集合
                              ——另附从源码读出的两个开源平坦化家族的状态机差异,
                              以及 xvmp IR 虚拟机的结构
  hardware-sampling-and-crash.md
                              无需预知地址的数据流(SPE:每采样带 PC + 数据地址)、
                              栈不可信时的崩溃取证(BRBE 分支记录 / ETE 尾部),
                              以及把运行时地址变成符号与静态偏移
                              (`trace_addr2sym.py`,PIE vaddr/文件偏移规则)
  native-vm-recovery.md       native VM 还原:VM 存在信号、区分叠在一起的 CFF/VM 双层
                              dispatcher 的行为判据表、用 relocation 恢复分发表、三重
                              验证的寄存器锚定、嵌套 VM 的段栈、从 STORE 反向切片加
                              字节道的操作数恢复,以及干净重实现 VM 的验证闭环 —— 吸收自
                              两篇公开实战文
  kernel-event-injection.md   把驱动当伸进目标里的手:从内核布置、无 tracer 关系的
                              观察点、陷阱处的寄存器改写、按路线(KPM/LKM/eBPF/
                              Zygisk)的能力对照表,以及把这一层与另外两层叠加的
                              组合配方
  on-device-tooling.md        直接在手机上工作:MT Manager 的编辑/重打包/签名及其 APK MCP、
                              LSPosed Manager、Termux+frida、设备上数据检查
  java2c-and-jni-sinking.md   Java2C 与 JNI sinking,最容易与抽取壳混淆的两种加固
                              形态:区分落地壳 / 抽取壳 / VMP / Java2C / JNI sinking
                              的那张表、为什么代码在 `.so` 里而*永远*不在 dump 出的 dex 里,
                              以及为什么 `Java_*` 符号搜索会一无所获(动态注册、
                              `-fvisibility=hidden`)
  split-apk.md                App Bundle / split APK 集合:集合是什么、如何从设备拉取、
                              合并成一个 APK 还是把整个集合作为一个单元签名、安装拒绝
                              及其各自含义,以及如何从拉取的集合做出一个可安装的 fixture
  vmp-differential-analysis.md
                              针对真实 Dex VMP 的已知明文差分:哪些环节可以
                              自动化、哪些不能(上传是瓶颈)、一个编译出的
                              fixture 能覆盖到什么程度、如何*证明*一张推导出的私有
                              opcode 表、smali 生成,以及这条路线何时关闭
  coverage-and-limits.md      把主张阶梯用在 skill 自己身上:每个已覆盖条目背后的
                              证据、本 skill 不随附的依赖,以及从未实际演练过的东西
  handoff-boundaries.md       本 skill 在哪里结束、另一门学科在哪里开始:JNI 形态
                              对照表、壳与加载器的分界,以及「已核实」对四种
                              交付物形态各自意味着什么
scripts/                  参数化、与路径无关
  doctor.py                   先跑这个:能力报告 + 每个脚本的可运行性,发现
                              安装在 PATH 之外或作为可运行 jar 存在的工具,并暴露
                              会污染实验的环境事实(时钟偏移、残留的
                              adb forward / proxy、设备侧已在运行的 frida 进程)
  dexutil.py                  无依赖的 dex 读取器:结构遍历 + 精确指令
                              解码、dex 头重算/校验(正确的 checksum/signature
                              顺序)、分支目标与操作数辅助函数。被
                              dex 脚本共享的库,也可独立运行以带偏移 dump 单个方法
  dex_find_insn.py            按解码语义定位一条指令,并打印其精确字节
                              偏移,附上下文与任一分支的两侧 —— 你靠这个找到
                              补丁点,而不是猜偏移
  dex_patch_bytes.py          按 JSON 规格做等长字节补丁:语义匹配、通过
                              expect_next 钉定极性、等长强制、校验器检查、dex
                              头重算、重新解码以证明它已落地(先 --dry-run)
  dex_check_verifier.py       三级检查:是否有任何条件分支以 move-result 为目标
                              (绕过它的生产者)?比较两个构建,并把
                              既存发现与你补丁引入的回归分开
  coldstart.py                冷启动捕获:定时截图连拍 + logcat 信号 +
                              已安装构建事实 + 启动计时,并在前台
                              activity 不是你的 app 时发出警告
  so_constpatch.py            对一个孤立的字符串常量做等长原地改写,用于
                              重定向一次库加载,而不是击败一个检查
  smtool.py                   带可配置 classpath 的 baksmali/smali 包装器
  dexpatch/                   dexlib2 方法级重写器(用于需要新指令的改动)
  patch_smali.py              smali 树中的方法体替换
  dex_strpatch.py             带 string_ids 顺序守卫的字节级字符串补丁
  dex_classdiff.py            证明一次 dex 编辑是外科式的
  dex_strings.py              不需要反编译器的 strings/URL/SDK 标记提取
  dart_pool_strings.py        从 Dart AOT snapshot 恢复字面量(带帧条目、
                              单字节 vs UTF-16 的分岔、文件偏移、游程噪声过滤)
  dart_pprefs.py              为 Dart snapshot 构建/查询 对象池 -> 代码位置 索引
  dart_disasm.py              Dart AOT 代码的带注释窗口式反汇编 + B/BL 调用者索引
  find_refs.py                在给一个方法打补丁之前统计它的调用者
  repack.py                   重建 APK、只剥离签名、保留 META-INF/services/、写出
                              4 字节对齐的归档(resources.arsc STORED+对齐)、签名、校验;
                              也处理 split APK / App Bundle 集合:清点、用一个
                              keystore 为每个成员签名,或把 code/native 成员合并成独立 APK
  devsh.py                    引号安全的 ADB shell 辅助工具
  usb_net_proxy.py            通过 USB 给离线设备提供网络
  datastore_inject.py         安全地编码/注入 AndroidX DataStore preferences
  probe_api.py                带正确请求头探测一个 HTTP API
  grab_crash.py               恢复被崩溃上报 SDK 藏起来的栈
  install_test.py             安装 + 启动健康检查,带 logcat 信号扫描
  frida_probe.js              四层运行时探测(app 网络层 + OkHttp + java.net + 异常)
  run_probe.py                注入探测、把它流式写入日志文件、保持常驻
  tls_check.py                对一个或多个 host 做严格证书检查
  preflight.py                每个实验块之前的环境检查(设备、root、
                              ABI/翻译层、时钟偏移、残留 proxy/forward、失联的 server)
  lib_map.py                  一个活进程里*实际映射*了什么:每库路径、
                              基址、架构,以及它来自 APK 还是
                              运行时物化出来的
  elf_plt.py                  从重定位表把 PLT stub 解析到它导入的符号(x86_64 + aarch64);
                              列出某个符号的调用者;对两个
                              库做字节级 diff,并指出每个变更 stub 属于哪个符号
  apk_diff.py                 两个构建的条目级 diff:变更 / 新增 / 移除,按
                              内容哈希,使等长替换也能被抓住
  native_crash.py             从日志或 tombstone 定位一次 native 死亡:signal、故障
                              地址、寄存器、把帧拆成 app 与 system、故障
                              指令本身,以及当故障看起来是*被安排好的*时给出标记
  blob_decode.py              搜索、而不是猜测一个存储值的分帧
                              (base64/hex x 旋转 x deflate);把编辑后的 payload 重新编码
  snap.py                     有界连拍截图 + 带停滞检测的控件树捕获,
                              并对这棵控件树是否可用给出结论
  sig_probe.py                找到确切的 signatures[0].toCharsString() 值 —— 从 APK 得到的
                              离线候选,或从设备读取的权威值
  spawn_patch_detach.py       在 Frida 探测下 spawn、detach,然后启动并捕获:在
                              spawn 模式下 Activity 栈常常起不来,而内存
                              写入能在 detach 后存活,hook 不能
  hook_patch_only.js          spawn_patch_detach.py 的最小探测 —— 按偏移把一个 native
                              死亡点失效化并报告 PATCHED
  dex_dump_validate.py        对一目录的 dump dex 镜像去重、校验并排序:sha256
                              分组、头完整性、区分真实 dump 与抽取壳骨架的
                              平凡方法体比例,以及一个最可能原件的排序
                              (对页对齐的 /proc/<pid>/mem 捕获用 --trim)
  dex_mem_scan.py             在内存捕获中搜索内嵌的 dex 镜像,并按每个镜像自己头部
                              声明的尺寸抽取出来 —— 用于躺在匿名映射里、
                              没有任何 maps 条目为它命名的解密 dex
  lsposed_scaffold.py         生成一个最小 LSPosed/Xposed 模块工程(带
                              xposed meta-data 的 manifest、assets/xposed_init、hook 类、无 gradle 的构建说明)
  frida_rpc_serve.py          把 Frida 脚本的 rpc.exports 桥接到本地调用方,带断线重连
                              处理,从而可以调用一个活的 native 函数,而不是逆向它
  rpc_template.js             frida_rpc_serve.py 的可编辑配套脚本
  stalker_trace.js            用 Frida Stalker 做指令级追踪:可配置目标、
                              触发选择、事件流,以及输出体积规则
  stalker_report.py           把 stalker_trace.js 的日志归约为基本块直方图与调用序列,
                              并对实测到的零事件情形给出明确诊断
  mt_mcp_probe.py             探测 MT Manager 的设备端 APK MCP(Streamable HTTP,端口 8787):
                              JSON-RPC 握手加上分组后的工具清单
  java2c_probe.py             收集能区分 Java2C 与抽取壳、VMP 及普通 JNI sinking 的
                              证据:从 dex 得到 native 密度与 stub 比例、
                              从 `.so` 得到 JNI_OnLoad / 动态注册 / 工具链字符串,
                              每一项都标注为 强/中/弱
  protobuf_decode_raw.py      无 schema 的 protobuf 解码:hex / 文件 / stdin 到 JSON 树,每个
                              长度定界字段都保留为一个候选集合、并列项予以标注而
                              不是猜测,另含字节级精确重编码以检查一次往返
  vmp_diff_harness.py         构建带标注的 opcode 覆盖 fixture、从原始/加固 dex 对
                              推导候选私有 opcode 映射、在闭环中验证该比较,
                              并把还原出的指令流渲染成 smali 骨架
  kernelsu_syscall_mask.py    生成 KernelSU/APatch 系统调用遮蔽脚手架:一个可安装的
                              用户态模块骨架加上 KPM/LKM/eBPF 内核侧模板,每个
                              都带自己的版本闸门和明确的未核实标注
  hw_watch.py                 arm64 上的隐形内存观察点/执行断点:plan(带 8 字节窗口
                              规则的 WCR/BCR 编码器)、emit(自包含的设备端 C 助手 ——
                              可干净编译为静态 aarch64 ELF)、gates、run(构建+推送+
                              流式采集命中)与 decode(每条命中的模块+偏移)
  trace_diff.py               差分轨迹归约:把一条轨迹归约为 游程+直方图,或把两条
                              轨迹对齐出 首次分歧 / 分歧区段 / 各自独有块 —— 与生产者
                              无关(Stalker 日志或任何每行一个地址的流,含解码后的 cs_etm)
  ete_capture.py              先过闸门的 CoreSight ETE 采集:五道闸门(内核年代、
                              coresight sysfs、cs_etm PMU、带 CORESIGHT 的 perf、权限),
                              perf/OpenCSD 构建配方,以及 `--control` 验证运行必须先
                              通过的按线程记录封装
  kernel_instrument_scaffold.py
                              生成内核侧插桩脚手架:KPM/LKM 观察点+寄存器改写模板与一个
                              诚实的用户态载体,各带自己的闸门和由 `verify` 强制执行的
                              `NOT COMPILED AND NOT LOADED` 横幅
  trace_addr2sym.py           运行时地址 → 符号 + 静态偏移,零依赖 ELF64
                              (PT_LOAD vaddr/文件偏移换算、.dynsym/.symtab、
                              strip 后降级):接受 hw_watch 命中、trace 的 mod+off 键
                              或裸地址列表;用真实交叉编译的 aarch64 .so 验证过
  vm_handler_slice.py         把解释器轨迹去虚拟化:dispatcher 识别、按分发入口切成
                              handler 段、签名聚类,以及通过可选 handler→opcode 映射
                              得到的 guest opcode 轨迹 —— 按开源 xvmp IR 虚拟机校准,
                              并带 VM 嵌套调用的循环诊断
  rasc_build.py               构建并验证 rasc,即 ASC 索引器的 Rust 重实现:
                              --check 检查已有什么、--build 克隆并 cargo、--verify 用
                              droidasc 校验一个 APK,并在任何类集合差异上失败
  scan_leaks.py               在发布一个仓库之前扫描其中的目标身份:manifest / `pm` / `ps`
                              上下文里的 bundle id、序列号形态的 token、PAT、内联
                              appkey 赋值、字面 endpoint、宿主用户路径。对
                              必须保留的一切给予豁免(工具、库、CVE、加固产品、
                              公开 crackme、占位符),发现项带上下文,
                              `--show-exempt` 打印某个命中项为什么被抑制,退出码 0/1/2
  svc_scan.py                 指出某条内联 `svc` 背后的系统调用以及它所在的段,
                              这决定 libc 层面的 hook 究竟能否观察到该调用;
                              `--context` 展示邻近字节,因为字节扫描也会匹配到数据
  anti_detect_probe.js        仅观察的 Frida 探测(不打任何补丁):path/loader/thread/kill
                              hook 附调用者模块 + 偏移、一份环境自述
                              (`TracerPid`、以 frida 命名的映射),以及实时流式输出,使一个
                              亚秒级自毁目标仍能产出证据

仓库另外带有一个可执行的测试层,它与证据记录是两回事:tests/ 断言脚本的行为(单元、CLI 契约、无设备集成),tests/benchmark.md 记录某条路线在真实目标上的表现。tests/README.md 说明了这个划分,.github/workflows/ci.yml 运行各闸门加测试套件。

安装

这个仓库是一个 skills 仓库:skill 位于 skills/apk-reverse/,这正是 skills CLI 解析的布局;安装是按名字进行的,而不是复制一个目录:

npx skills add newliver666/apk-reverse              # install every skill in the repo
npx skills add newliver666/apk-reverse --list       # list what is here, install nothing
npx skills add newliver666/apk-reverse --skill apk-reverse -y
npx skills use  newliver666/apk-reverse@apk-reverse # use it once, without installing

CLI 默认把 skill 软链进你的 agent 的 skills 目录(--copy 则生成独立副本),-g 为所有项目安装而不是仅当前项目。仓库里只有一个 skill 时,--skill apk-reverse 今天是多余的;这里把它写出来,是因为等第二个 skill 存在时,正是它用来选中单个 skill。

安装后,agent 在任务匹配其描述时加载 SKILL.md,并且只在需要时拉入 references/*。没有全局状态,没有与机器绑定的路径,也没有构建步骤。

环境要求

没有什么是强制的;每个脚本都自己检查所需。skills/apk-reverse/scripts/doctor.py 报告这里哪些工具存在、因此哪些脚本能跑,以及——这很有用——哪些工具存在于 PATH 之外。

如果你的工具链不在 PATH 上(项目本地的 tools/ 目录、带版本号的 SDK 目录、可运行的 .jar 而不是一条命令),把 APKREV_TOOLS 设为一个或多个目录,doctor.py 就会找到它们:

set APKREV_TOOLS=<dir>;<dir>                     # Windows, e.g. an SDK or project-local tools dir
export APKREV_TOOLS=<dir>:<dir>                  # POSIX

脚本本身是纯 python3,并且意图在 Windows、macOS 和 Linux 上行为一致;凡有片段仅限 POSIX,都会标注出来。这里没有任何东西假定你用的是 Unix shell。

工具 用途
Python 3.9+ 所有脚本
droidasc(ASC)(可选,但强烈建议——先装这个) 整 APK 交叉引用索引:findrefs / listclass / getclass / getmanifest。一句 pip install droidasc,无 JVM、无 SDK、无需建索引。把「这几千个类里哪一个提到过这个字符串」变成亚秒级查询,也是找到名字被 R8 混淆过的类的唯一路线。这是 agent 在做任何完整反编译之前应当伸手去拿的工具 —— 见 skills/apk-reverse/references/toolchain.md §droidasc (ASC) —— 一次查询就能问一个 APK「谁引用了这个?」
ddc(可选,但强烈建议) 单文件 dex→Java 反编译器,带查询子命令(info、findrefs、strings --with-locations、按类反编译)。无 JVM。读 ASC 所定位的东西;也能可靠地报告包身份 —— 见 skills/apk-reverse/references/toolchain.md §ddc —— 带查询子命令的 dex 转 Java(值得采用)
dexsql(可选,但强烈建议) 单文件,用 SQL 查询整个 dex,外加 APK manifest 与 resources.arsc,并内置 MCP server。它的类定义集合在四个归档上与 rasc/droidasc/ddc 完全一致,而且能回答另两个表达不了的问题:某被调方法的调用者、字段访问、数值常量、调试行表、注解、hiddenapi 标志。当问题是连接或聚合时用它 —— 见 skills/apk-reverse/references/dexsql-sql-query-layer.md
baksmali / smali + dexlib2 jars 反汇编、汇编、外科式打补丁
JDK(javac、java) 构建/运行 dexlib2 补丁器;同时提供 keytool/jarsigner
Android SDK build-tools(aapt、zipalign、apksigner) manifest 信息、对齐、签名。apksigner 才是该用的签名器 —— jarsigner 会重写归档并破坏 Android R+ 所要求的对齐
uber-apk-signer(可选) 一步完成对齐 + 签名
ADB 设备工作
Frida(宿主包 + 与之匹配的设备端 server) 动态分析
一台已 root 的设备或模拟器 静态分析之外的任何事

这些都不需要在 PATH 上:每个脚本都为它外壳调用的工具接受显式路径,skills/apk-reverse/references/toolchain.md 覆盖了如何找到一个 PATH 不认识的安装(对 apksigner 和 keytool 来说这是常见情况)。

先读这个

本项目仅为学习、研究和经授权的安全测试而发布。 它不附带任何 exploit payload、任何目标数据、任何第三方二进制 —— 它是一套方法、一组脚本和一份证据记录。你有责任确保自己对所分析的对象拥有相应权利;见文末免责声明。

skills/apk-reverse/references/pitfalls.md。这是这里最有价值的文件 —— 每一条都是在看起来完全健康的情况下产出了损坏产物的失败。

最痛的四个:

  1. 重打包时把整个 META-INF/ 剥离,会删掉 ServiceLoader 注册,app 在启动时死掉,而报错信息指向一个毫不相干的库。
  2. 对一个字节级字符串打补丁却不保持 string_ids 顺序,整个 dex 会被拒绝,而 checksum 和签名却完美通过校验。
  3. 用整棵 smali 树往返来重建 dex,会不可见地损坏 R8 的输出 —— 类表对比是干净的,它只在运行时炸开。
  4. 通过让它不返回来失效化一条 native 终止路径。一个自旋 stub 并不能抑制检查;它冻住调用者和它身后的每一个线程。app 挂死,且完全没有崩溃记录,最终的死亡会被归咎于那个杀掉这个冻结进程的任意东西。

范围

为处理你自己的应用、你被授权分析的样本,以及 CTF/竞赛沙箱而构建。它不含任何内置的第三方二进制,也不含任何目标特定数据。

它覆盖什么、刻意不覆盖什么,写在 SKILL.md 顶部 Coverage 一节。简版:仅 Android(不含 iOS),并且在经过真实打磨的层上深入 —— dex 补丁、重打包、壳与自定义加载器、native 篡改响应,以及 Flutter/Dart AOT。一次扩展迭代加入了第二梯队的已记录路线:重打包受阻时的模块侧交付、抽取壳恢复及其 VMP 边界、用于「调用而不是阅读」的模拟执行与实时 RPC、针对 OLLVM 的指令级追踪、REST 之外的协议逆向、当用户态 hooking 可证明够不到时用的内核路线图,以及设备上工具。随后一次基准迭代把公开目标放到这些路线之下(tests/benchmark.md):它加入了 Java2C 判别(那种会让 agent 去追一个从不存在的解密 DEX 的误诊)、split APK / App Bundle 处理、无 schema 的 protobuf 解码、一个 Dex-VMP 差分测试台,以及带版本闸门的内核模块模板 —— 并纠正了先前两条与自身测量结果不一致的说法。随后一次硬件迭代加入了软件工具之下的那一层:带诚实 TracerPid 账本的硬件观察点、带五道闸门的 ETE/TRBE 差分追踪,以及内核侧注入模板 —— 凡是宿主可验证的部分都实测过,凡是设备绑定的部分在闸门打开前一律标注 unverified。Unity/IL2CPP 逻辑恢复、React Native/Hermes 字节码内部,以及击败服务端权威不在覆盖范围内,skill 的写法就是明说这一点并停下,而不是把最近的一条已记录流程套用到一个它并非为之编写的目标上。

Coverage 一节完整陈述、同时也应当放在这里的四条限定:

  • Flutter/Dart AOT 分析有一个依赖。 工作流始于一份 pool 列表(pp.txt 一类输出)。产出它需要一个 snapshot 解码反编译器 —— aotopsy(静态二进制,不需要工具链)或 blutter(从源码构建,约 80 秒)—— 而本仓库不含其中之一。它被点名为前置条件,而不是留在那里让人自己推。
  • 本仓库里并非每条主张背后都有一次运行。 docs/tool-verification/ 记录了实际测量了什么、在哪个目标上、用哪个独立交叉检查确认;那里未覆盖的内容都是凭经验记录的,按本 skill 自己的主张阶梯,应当读作推断。
  • 扩展迭代被单独记录,并且大部分是推断。 其证据在 docs/tool-verification/EXTENSION-*.md,一个主题一个文件,各自带强度说明。那里常见的形态是工具被测量过,路线没有 —— 所以在把任何较新的文档当作已验证路径之前,先读那些文件。
  • 基准迭代按行记录,并带有该行自己的强度。 tests/benchmark.md 列出每个公开目标、该行演练的脚本、实际发生了什么(包括失败的行和没人跑的行),以及证据有多强。标为 unverified 的行是在陈述本仓库证据的状况,不是在陈述机制本身。

仓库维护

要改动这里的东西?先读 CONTRIBUTING.md。 它列出各道门禁、一次全绿意味着什么又不意味着什么、改动必须同步的规则(新增一个 reference 必须在清单里被点名;重命名一个标题会让所有指向它的章节引用断掉),以及修改门禁脚本自身时要留意的两类失效模式。

根目录下住着四个工具,它们不属于已安装的 skill:

check_repo.py      发现的每个 skill、frontmatter 有效、脚本可运行、
                   文档中的路径可解析、README 路径显式且存在,
                   并且 —— 仅在受跟踪的表面上 —— 没有目标身份
                   (把规则委派给 skills/apk-reverse/scripts/scan_leaks.py,
                   这样要争论豁免清单时就只有一个地方)
check_refs.py      每一处指向另一文档某节的交叉引用,
                   都能到达该文档里真实存在的标题
check_routing.py   按需清单仍与入口点一致:症状镜像与 SKILL.md 一致、
                   每个 reference 文件都在 skills/apk-reverse/references/routing.md 中
                   被点名,每个脚本也是
check_commands.py  文档让你运行的每条命令都会与脚本自己的 argparse 表
                   核对 —— 一个不存在却被写进文档的 flag,是锚点检查
                   看不见的漂移
check_budget.py    阻止常驻加载的部分悄悄膨胀:按行和 token 测量 SKILL.md
                   的整个主体(含索引行,因为它们也会被加载)、索引行长度、
                   没有可导航头部的长文件,并把含糊其辞的规则作为趋势报告
build_scripts.py   审计机器特定残留物(绝对路径、凭据)

一致性有一种天然的反向压力 —— 一条断掉的路径会大声失败,然后有人修它。膨胀没有,这正是第三个工具存在的原因:每次迭代都会加一个 reference、一行索引和一条覆盖主张,而没有测量的话,仓库里没有任何东西会注意到。

全绿的门禁证明了什么,又不证明什么

上面的检查证明的是一致性,不是真相。check_repo.py、check_refs.py、check_routing.py、check_commands.py、check_budget.py 全绿,意味着路径可解析、清单完整、文档里的 flag 真实存在、入口点没有膨胀、受跟踪表面上没有目标身份。它们对一个主张是否正确什么也没说:一个引用可以指向真实存在的标题,却描述一个没人跑过的机制;预算门禁也分不清一个文件是承重还是只是相邻。而这两件事恰恰是读者真正要问的,能回答它们的只有强度标注和证据记录。

接纳外部贡献

失败关闭。 如果一项贡献与仓库宣称的覆盖范围的契合度、它的副作用、或确立这两者所需的验证仍有实质未决,就不要合并、不要发布:让它挂起、缩小它,或者拒绝它。把未决问题记录下来是有效的结果;把它变成「无」或「大概没问题」则不是。

有四样东西不是批准,无论它们看起来多好:全绿的 CI、完成的实现、上游的流行度、以及没有人提出异议。 它们证明的是代码能跑、还没人抱怨,这与「这属于这里、而且它的边界已知」是两回事。

两个决定多数此类判断的推论:

  • 标注优先于体量。 一份每一项主张都带着正确 observed / inferred / unverified / unknown 标注的大改动,比一份把假设当成结论的小改动更容易被接受,因为前者可以被核对。要问的是实际跑了什么;把「没有哪个主张是靠读资料升级成 observed 的」看作好迹象,而不是弱点。
  • 未解决就是 unknown,绝不是不存在。 一个在这里没有被演练过的能力是 unknown:不是被排除,不是被取消资格,也不是因为「大概能行」就合并。当一项贡献含有一半绑定设备或工具链的内容,可合并的是被测量过的那一半,加上为其余部分明确标注的模板,以及说明「另一半要变成 observed 需要什么存在」的那道门。

tests/benchmark.md 保存回归矩阵:维度 -> 公开目标 -> 该行演练的脚本 -> 实测结果 -> 强度标注。它是你在信任 docs/tool-verification/ 下任何主张之前要重跑的检查表。样本会下载进 tools/_work/ 且从不提交,因此每一行都点名它的公开来源并记录它所针对的哈希。

docs/tool-verification/ 也不属于已安装的 skill。它是针对一个真实目标、一次测量迭代的证据记录:每个脚本实际做了什么、哪个独立方法确认了它、发现了哪些缺陷,以及该目标未能演练哪些场景。它存在的意义,是让 SKILL.md 里的 Coverage 主张可以被对照运行来检查,而不是被信任;也让缺口被写在下一个人能找到它们的地方。


Proudly supported by the LINUX DO community.

免责声明

仅为学习、研究和经授权的安全测试。 本仓库中的每个脚本、reference 和已记录结果,都是为了解释 Android 应用分析如何工作,使从业者能对自己已经拥有的工具进行推理。这里没有任何东西是一项服务、一个产品,也不构成对任何特定用途的背书。

  • 仅限经授权的目标。 请把它用在你自己拥有的应用、你被明确许可分析的应用、公开的 CTF/挑战材料,或你掌控的沙箱中。分析你无权分析的软件,在你所在的地区可能违法,而这个判断要由你作出,不是由本仓库作出。
  • 不提供担保,不保证适用于任何目的。 材料按现状提供,不附带任何形式的担保。结果是某台机器在某个时刻的测量记录;这里没有任何东西承诺某条路线会在你的目标、你的设备、你的工具链或今天的 app 版本上生效。
  • 先验证再信任;先备份再行动。 多个脚本会修改产物(dex、APK、.so、已存储的 app 数据),有些会在已 root 的设备上操作。保留你自己的副本、在副本上作业,并在对你在意的东西跑任何东西之前先读 SKILL.md 的 gate。
  • 你的使用由你自己负责。 作者与贡献者不对因使用或误用本仓库而产生的任何损失、损害、法律后果或服务中断承担任何责任,也不与任何可能被用于检查的应用、厂商或平台存在隶属、背书或代理关系。
  • 测试数据不在此分发。 样本、dump 和设备产物被刻意排除在这棵树之外(.gitignore 排除了它们),只存在于本地被忽略的工作区中。你为了跟做而取得的任何东西,都由你自己负责保管,并在用完后删除 —— 遵守你本地的规则以及样本自带的条款。本仓库确实发表的是方法与证据,且已移除全部目标身份。
  • 无隶属关系。 工具、库、加固产品和公开挑战目标的名字出现,只是为了让材料可复用;它们属于各自的拥有者,本项目与它们没有关联。