关于 apk-reverse
apk-reverse 是用于分析和修补 Android APK 的流程技能,目标是产出经过验证且可安装的文件。它强调先识别代码所属层、控制实验、检查工具能力,并明确记录证据边界。
主要功能
- 01分类目标并识别所属代码层
- 02设定交付门槛并记录基线
- 03使用索引器和分层工具
- 04区分已观察、推断与未知结果
- 05安装并测试修补后的文件
适用场景
- 01修补前定位 APK 中的检查逻辑
- 02诊断补丁未生效的原因
- 03在设备上验证修改后的 APK
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ zzusec/ apk-reverse --skill apk-reverse安装后在 Claude Code 中输入/apk-reverse确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonevidence/capability-matrix.jsonknown-limitations.mdtested-tool-versions.jsonreferences/account-gates.mdad-removal.mdadvanced-unpacking.mdbyte-level-patching.mdcode-virtualization-and-custom-linkers.mdcoverage-and-limits.mddart-aot.mddesensitization-and-leak-scans.mddetection-and-anti-analysis.mddex-patching.mddexsql-sql-query-layer.mddynamic-frida.mdemulation-and-rpc.mdenvironment.mdete-diff-tracing.mdevidence-summary.mdframework-runtimes.mdhandoff-boundaries.mdhardware-sampling-and-crash.mdhardware-watchpoints.mdjava2c-and-jni-sinking.mdkernel-and-environment-hardening.mdkernel-event-injection.mdlong-task-discipline.mdlsposed-and-modules.mdreferences/coverage/route-inventory.md详细介绍
APK 逆向工程与补丁
目标:快速获得经过验证、可安装且仍能正常工作的产物,并避免那些会彻底毁掉 APK、却看起来完全正常的各类错误。
立即行动 — 先完成这四件事
本文档是一份带关卡的操作流程,而非背景阅读材料,而且篇幅很长,中间部分很容易被略读。因此,先列出四项行动;下面的内容都是对它们的说明。
1. 先分类,再选择路径。 回答 §从这里开始 中的十三个问题。R4 决定你是否在正确的层上操作;走错分支不会明显报错——只会生成一个能够构建、能够运行,却做错事情的产物。
2. 按顺序通过四道关卡,并满足各自的通过标准(§关卡)。G1 要求在开始任何工作前,用一句可测试的话写明交付物的形式;G2 确认这台机器实际上能做什么;G3 定位代码;G4 记录基线和对照组。“我理解这个想法”不算通过关卡。
3. 无法解释的症状是停止信号,不是谜题。 在进行下一次尝试之前,先在 §症状索引 中查找对应情况,并加载该行指定的文件。这些条目之所以存在,是因为每一种情况都曾耗费数小时——而且有好几次,答案明明早已写下,却又被重新推导了一遍。
4. 针对同一种尝试方式失败两次后,应回到分类步骤,而不是再试第三种变体(§停止条件)。而且,没有 §“完成”的含义 中的六项内容,绝不要声称已完成。
关于本文档本身的两条规则。 当失败情况不符合你的计划时,下一步应查阅症状索引,而不是继续推理。当本文档与你自己的判断不一致时,以本文档为准,直到你有足以推翻它的证据为止:这里的每条规则,都是曾耗费数小时的失败经验留下的教训;而你当前的直觉,属于尚未遭遇过这种情况的人。
覆盖其他一切的四条规则
R1 — 在接触目标之前,先用一句可测试的话写明交付物。
“能运行”不是目标;“在指定约束下能运行”才是。借助 Root、实时插桩、主机代理和已打补丁设备得到的结果,通常不符合“在普通手机上可运行的可安装产物”这一要求——而且很容易把这种结果当作已完成的交付物。写下这句话,在每个检查点重新阅读;如果无法满足,就坦率说明,并将特权方式的变通方案标记为备用方案,绝不能把它当成交付物。
→ references/long-task-discipline.md §最昂贵的偏离
R2 — 一次只更改一个变量,并保留对照构建。
如果一次实验同时改变两项内容,失败时就无法得出任何结论;无法归因的失败则会被归因于错误原因。每一项“应用拒绝 X”的说法都需要单独运行验证;每个补丁都需要一个采用相同流程的对照版本,且它仍会像以前一样失败。
→ references/long-task-discipline.md §单变量原则
R3 — 绝不交付或声称已验证未经验证的产物。
“已完成组装”不等于“能正常工作”;“进程已启动”不等于“功能正常”;“日志中没有错误”不等于“检查已被移除”。安装它、启动它、实际使用你修改的功能,并查看屏幕。证明设备运行的是你构建的版本——对其计算哈希,不要相信文件名。
→ references/verification.md
R4 — 打补丁前先确定所属层;实际情况不符时,重新分类。
广告、付费墙、功能门控、完整性检查和更新门控可能位于不同层(Java、dex、native、Dart/Unity、服务器)。在错误的层打补丁,要么毫无效果,要么会破坏应用。如果补丁“没有效果”,说明诊断有误——应返回分类步骤,而不是加大打补丁的力度。
→ references/recon.md,然后查阅症状索引指定的对应层文件
工具使用——选择合适的工具,并在断言工具不存在前先检查
在这个领域,最常见的时间浪费并非对目标的推理不当,而是把正确的问题交给了无法回答它的工具:在手动导出的 smali 树上用 grep 搜索一小时,而一次索引查询就能解决;或者手动遍历 ELF,而反编译器只需 pip install 一下就能用。问题从内部很难察觉,因为较弱的方案仍然会产出结果。
五项要求。这些是指令;违反即为缺陷,而非偏好。
| 要求 | 应当这样做 | 忽视的代价 |
|---|---|---|
| 先用索引器定位,而非导出代码 | 阅读代码前,先具备向制品提问的能力:使用 droidasc findrefs / ddc findrefs(字符串/类型/方法 → 每个引用位置,耗时不到一秒);当问题涉及联接或聚合——例如某方法的调用者、包含某个数值常量的方法、调试行号表——而非类列表时,使用 dexsql query。反编译用于阅读已经定位的类,不是用于定位类。jadx 是最后手段中的可读查看器,绝非事实来源,也绝非侦察的入口 | 每个问题都要在事先付费导出的代码上用 grep 搜索数小时 |
| 安装缺少的工具;这是任务的一部分 | 缺少 arm64 反编译器时,下一步就是安装它,而不是绕着这个限制走。只有确实无法安装时才求助他人 | 手动重新推导本可由反编译器在几分钟内输出的内容,白白耗费数小时 |
| 投入时间解决前,先明确差距 | 明确说明阻碍任务的能力是什么,以及这台机器是否具备该能力。这属于 G2 要求 | 围绕一个十分钟就能安装的工具设计替代方案 |
| 动手编写脚本前,先找现成脚本 | 解析、哈希、对齐和热插拔探测都已有现成脚本;用自制脚本替代 scripts/dex_find_insn.py,就会导致偏移量被猜出来,而非计算出来 | 错误的偏移量解码后看似正常,实际行为却不对 |
| 根据问题所在的层级选择工具 | 内存中的 dex 需要使用 dex_mem_scan.py(查找并截取)以及 dex_dump_validate.py(判定),而非让反编译器处理一个片段。只需调用的算法应使用 emulation-and-rpc.md。“这个类在运行时是否会加载”需要一个会触发或不触发的钩子 | 更重型的工具并不更安全——它会给出一个看似合理的答案,这比没有答案更糟 |
在断定任何功能不可用前,先运行 python skills/apk-reverse/scripts/doctor.py --json。
它会报告各项能力的闭合情况——缺少什么、要安装什么,以及大致需要多长时间——而未经检查的能力,不代表你不具备。**它有意允许报告 BLOCKED:**有 JVM 但没有 zipalign/apksigner 的机器无法重新签名,报告会如实说明这一点,而不会仅因某个工具存在就推断你具备该能力。详情见:
references/toolchain.md;它读取的注册表是 scripts/capabilities.py。
覆盖范围——本技能声称能做什么,以及不能做什么
本节要避免的失败并非不了解,而是**把最接近的可用流程套用到一个它从未针对过的目标上,从而给出自信却错误的答案。**几乎适用的文档化方法比完全没有方法更危险,因为它附带一套计划、一套术语和一组令人安心的数字。
**这些声明的可靠程度——这是关键,不是装饰。**本技能中的每项声明都带有以下四种标签之一,你也必须用同样的方式标记自己的结果:
| 标签 | 含义 | 能否据此做出决定? |
|---|---|---|
observed | 已运行命令,且有其输出 | 可以 |
inferred | 根据观察结果得出,但经过了推理 | 暂时可以 |
unverified | 基于假设,或由其他地方报告但未在此复现 | 只能作为待测试事项 |
unknown | 此处没有任何内容涵盖:未运行探测,或探测未覆盖到该范围 | 不可以——更不能据此认定不存在 |
否定结果有三种情况,把它们混为一谈正是本节要避免的失败。unverified 表示有人提出了某种说法,但此处无人承担核验的成本;unknown 表示完全没有收集证据。但如果探测确实运行了,却没有返回结果,那就有两种不同情况,而只有一种能算作发现:
- 不存在——探测覆盖到了这个问题,且该事物确实不在那里;
- 存在但无法读取——该事物确实存在,只是这条路径无法读取它:例如 shell 内的加密负载、运行中的 ABI 永远不会加载的库、在钩子建立之前就触发的检查,或解析器拒绝的某种类型下存在的字段。
第二种情况更危险,因为它产生的输出与第一种情况逐字节相同,而且在本技能针对的加固目标上,这才是常见情况。一个打包后的 APK 返回 4 个类,并不意味着它只有 4 个类。因此,在把“并不存在”写入记录之前,先说清楚是哪种探测覆盖了这个问题,并运行一个必定能命中的对照测试。如果对照测试也没有结果,诚实的说法应是关于你的探测的 unknown,而非关于目标的“不存在”;接下来应换一种探测,而不是得出更强的结论。
**用来替代猜测的清单:**按路径列出哪些内容已经覆盖、哪些尚未覆盖,并区分已验证的机制与有文档记录但大多基于推断的路径。清单位于 references/coverage-and-limits.md。当问题是“这项技能真的能做到 X 吗”时,请加载该文件。
容易被过度解读的部分覆盖范围——以下路径虽有文档记录,但本技能并未提供其依赖项;同一文件中也为每项内容标明了边界:Dart AOT 分析需要你从其他地方获取快照转储,而且前端名称很重要 · 提取 shell 的恢复止于 VMP 边界 · 并非所有设备都保证能进行 Stalker 跟踪 · 要从内核侧得出结论,需要一个本技能只能提供脚手架的模块。
**未覆盖的内容——请如实说明,不要临场发挥:**Unity / IL2CPP · React Native / Hermes / Cordova · iOS · 绕过服务器端权威机制 · 现成解包器或反检测军备竞赛 · 构建并发布内核模块。原因和证据边界见 references/coverage-and-limits.md;该记录中没有提及,不代表本技能支持。
后备方案,作为明确指令——这是规则,不是建议。如果目标不符合覆盖清单,或没有任何症状索引行与之匹配,那么先停下来进行分类,再选择分支:回答 13 个问题(§Start here)。如果目标特征仍不匹配——例如运行时未知,或你无法说清机制——**就准确说明这一点,并提出成本最低、能识别它的实验。**不要选用最接近的文档化路径,硬套到目标上。错误的分支不会明显失败:它会产出一个能够构建、能够运行,却会做错事的成果。
交接点——本技能的终点与另一个视图的起点
有四个边界,很容易在不知不觉中跨过。每项都说明另一侧负责什么,而不是重复说明,因为同一建议的两个副本会逐渐产生偏差。JNI 形式表和逐形式验证表位于 references/handoff-boundaries.md。
- JNI — Java
native声明及其实现是同一函数的两个视图,而本技能会用不同工具分别读取它们。符号搜索遇到动态注册时会静默失败。权威说明:java2c-and-jni-sinking.md§The JNI boundary — why a symbol search fails silently。 - 加固 — 从 dex 侧观察到的加壳器,是一个原生侧实现问题。
packers.md负责 dex 侧的识别;深入分析应在另一侧进行。 - 原生异常 — 从 APK 侧 读取,因为要判断的是补丁是否导致了崩溃。恢复符号或逆向 OLLVM 函数是另一项活动,使用不同的工具链:应指向另一侧,而不是把
native-and-so.md的范围扩展到这类内容。 - 交付物形式 — 当产物不再是 APK 时,验证问题也会随之改变。G1 的其他三种形式各自会将证据转移到别处,而故障不易察觉:以已完成构建的说法报告特权结果。
症状索引——匹配到某一行即表示应停止
你遇到的是某种症状,而不是某个文件名。下方每一行都是已经付出代价的故障。如果任何一行符合你观察到的情况,请在运行下一条命令之前加载对应文件——不要等到再尝试三次之后。此时从第一原理开始推理,只会让同样的时间再耗费一次;这里不止一项经验教训,都是在答案明明就在仓库里、却无人阅读时手动重新推导出来的。
| 观察到的现象 | 首先阅读 |
|---|---|
重新打包/重新签名的构建在你的代码运行之前就崩溃;SIGSEGV、所有寄存器均为零、pc=0、fault addr 接近 0x0 | native-tamper-and-suicide.md(故意崩溃),然后阅读 code-virtualization-and-custom-linkers.md |
| 没有加壳(Application 是应用自身的,dex 可读),但仍然崩溃 | code-virtualization-and-custom-linkers.md §加载器仍有可能;但如果同一构建在另一台无关设备上也会崩溃,那么这是普通启动故障,而不是加固导致的问题 |
应用在所有设备上启动时都会崩溃,无论是否加壳;没有 tombstone,但 crash_dump 显示 already traced,logcat 显示 exited cleanly (0) | 内置的崩溃报告器接管了信号处理程序,因此平台自身的线索已消失。恢复途径是 Frida spawn-gating |
Flutter 应用在主线程上、首帧完成前,因 FORTIFY: pthread_mutex_lock called on a destroyed mutex 而中止 | dart-aot.md — 检查 libapp.so 是否确实已加载;原生生命周期故障通常会在 Flutter 引擎启动时显现 |
| 日志显示Java 层签名/完整性检查已通过,但进程仍然崩溃 | code-virtualization-and-custom-linkers.md §Java 层的“签名杀手”是个诱饵 |
删除某个库后验证通过,却出现 UnsatisfiedLinkError: dlopen failed: library "X" not found | code-virtualization-and-custom-linkers.md §耗时数小时的死锁 |
整个类都显示为没有方法体的裸 native 声明 | java2c-and-jni-sinking.md — 在转储内存之前先读此文:如果这是 Java2C,那么在进程生命周期的任何时刻都找不到 DEX。普通 dex 中只有少数 native 方法属于 JNI sinking,不是这种情况 |
在加固库中搜索 Java_*,结果完全为空 | java2c-and-jni-sinking.md §JNI 边界 — 为什么符号搜索会静默失败 — 动态注册,或 -fvisibility=hidden。有效的检查方法是“导出 JNI_OnLoad 且没有 Java_*” |
| 你准备发布引用了真实工作的证据文件、记录或 README | references/desensitization-and-leak-scans.md — 在提交之前运行 scripts/scan_leaks.py;命中列表列出的是需要检查的行,而 --show-exempt 会显示错误的豁免项 |
| 一个 Hook 模块似乎运行过,但其日志标签没有输出,而你正准备记录“它从未加载” | references/precedents/logd-broken-module-never-ran-case-3.md — 损坏的 logd 不会向 logcat 输出任何内容,而模块的完整运行记录却保存在 LSPosed 的文件日志中;两种渠道都要检查 |
| 某个库的SONAME 与其文件名不匹配 | code-virtualization-and-custom-linkers.md、native-and-so.md |
| 你的修改完全没有效果,也没有报错 | server-config-and-updates.md §3(该值可能由服务端下发),然后阅读 packers.md §确定验证边界 |
进程没有崩溃记录却卡住,或被 uid 0 的终止程序杀死 | native-tamper-and-suicide.md §规则(你可能让某条终止路径不再返回) |
| 在加固库中,程序以普通的空指针解引用方式崩溃 | native-tamper-and-suicide.md §故意崩溃桩 |
| 应用只在你已附加进程/已获取 root 时崩溃 | detection-and-anti-analysis.md;首先在相同条件下运行未经修改的原始版本 |
安装因 [-124] 失败,并提及 resources.arsc/对齐 | repack-and-sign.md §2a — 必须同时满足 STORED 和 4 字节对齐 |
安装失败,只有裸数字代码(例如 [-99]),没有 INSTALL_FAILED_* | repack-and-sign.md §厂商安装拦截 — 这是设备侧拦截器造成的,不是你的构建有问题。使用 root 下的 pm install 路径 |
安装后,am start 没有任何反应/截图显示的是另一个应用/am start -W 一直卡住 | repack-and-sign.md §安装器可能仍占据屏幕 |
日志显示 Failure to verify dex file ...: Bad checksum,并且启动时普通类出现 ClassNotFoundException | byte-level-patching.md §dex 头部有两个完整性字段 — 顺序很重要 |
| 安装“成功”了,但没有任何变化,或版本没有更新 | long-task-discipline.md §保持观察窗口干净 |
| 证据相互矛盾,或一次捕获看起来混合了两种状态 | long-task-discipline.md §保持观察窗口干净 |
| 你截了图,却根据日志或补丁本身得出结论 | long-task-discipline.md §从未查看过的捕获不算证据 |
| 你准备重跑一个结果已经记录过的实验 | long-task-discipline.md §长上下文衰减 |
| 脚本无法启动,或某个工具“缺失” | scripts/doctor.py,然后阅读 toolchain.md §“不在 PATH 中”不等于“未安装” |
| Hook 或探针报告完全没有事件,而你正准备将其归因于检测 | 先用 scripts/anti_detect_probe.js 获取环境自检报告,然后阅读 detection-and-anti-analysis.md §步骤 3:定位检查 — 从阶段 0 开始的搜索顺序 |
attach 一直卡住,随后失败,但进程仍在 ps 中 | detection-and-anti-analysis.md §步骤 3,阶段 0 — 检查 /proc/<pid>/stat 中是否有 D,并先附加一个不同的 pid 作为单行对照,再将问题归咎于目标进程 |
| 应用退出时没有 tombstone、崩溃记录或 ANR 记录 | detection-and-anti-analysis.md §步骤 3 — 干净地自行退出意味着检查在你的 Hook 生效前就已运行;该节的分支条件会指出对应阶段 |
转储区域验证结果不对,或 base.apk 的 r--s 映射看起来像 dex | advanced-unpacking.md §此方法做不到什么,以及如何在耗尽时间前判断 |
| 特定功能的网络请求失败(登录/注册/支付),但其他功能正常 | tls-and-cert.md — 不要假定这是你的补丁导致的 |
| 一切正常,但重新打包后所有签名请求都失败 | signature-derived-keys.md |
重新签名后的构建运行正常,完整 UI 也能渲染,日志中没有错误 — 但某个功能始终悄然无法加载,而且 dumpsys/DNS/logcat 完全没有显示该功能的请求(不是请求被拒绝:而是没有请求) | code-virtualization-and-custom-linkers.md §原生检查实际读取的内容 — 在构造请求之前就已拒绝。不要看上面那一行:“已发送但被拒绝”和“从未发送”由不同部分负责 |
| 你无法判断缺失的功能是你的补丁造成的,还是目标本身的行为 | long-task-discipline.md §单变量原则。让零改动对照版本走完相同流程,并运行决定性的变体:在相同设备和网络上,仅在内存中为未经修改的原始版本应用补丁 |
在 Frida spawn 模式下,UI 始终不出现 — mCurrentFocus 一直为 null,截图为空白,Activity 栈始终无法建立 | dynamic-frida.md §spawn 会阻止 Activity 栈建立:将补丁写入内存,分离,然后正常启动 Activity |
实验过程中 frida-server 不断消失,或设备自行重启 | dynamic-frida.md §ROM 如何追踪并清除你的插桩 |
| 应用仍显示广告,尽管你应用的补丁本应将其移除 | server-config-and-updates.md §6(缓存配置/远程重新启用),然后阅读 ad-removal.md §步骤 4(统计 SDK 自身的日志行数;n -> 0,而不是“我没看到”) |
| 强制更新或“必须更新”提示阻止了构建 | updates-and-forced-upgrade.md §步骤 6 |
| 对话框消失了,但功能仍被锁定 | membership-and-limits.md/account-gates.md — 再次打补丁前,先判断权限由服务端还是客户端控制 |
| 你准备将某条路径判定为“受阻”而放弃 | packers.md — 放弃任何路径之前都要重新阅读;错误归因的失败曾让可行路径白白搁置数小时 |
| 任务已经持续很久,而你不确定哪些内容已经得到证实 | long-task-discipline.md §保持实时记录 |
转储的 dex 可以完整解析,所有类都在,但大多数方法体都是 return-void 桩或 nop 填充 | references/advanced-unpacking.md — 这是一个提取外壳:先用 scripts/dex_dump_validate.py 测量 stub%,再决定是否可信,并注意恢复方法体需要采用另一条路径 |
frida 转储在写入过程中崩溃(script has been destroyed),或被转储的进程 pid 不断变化 | advanced-unpacking.md §frida 被拒时如何转储 — 先排除内存压力;回收内存并重新启动无需插桩 |
| 重新打包遭到多个独立检查拒绝,或构建必须能持续适应应用商店更新 | references/lsposed-and-modules.md — 应交付模块,而不是 APK;G1 的表格说明了适用时机 |
| Hook 模块已安装、启用并设置了作用域,但其日志标签始终没有出现 — 而你正准备认定它从未运行 | lsposed-and-modules.md §部署、启用和验证 — 单凭 logcat 得出的判断在这里已经出错过:某个 ROM 上的 logd 已损坏,输出只会进入 /data/adb/lspd/log/modules_<ts>.log |
| 模块的入口类不存在于其自身 dex 中(因此它永远无法加载),但包仍能安装、启用,且看起来一切正常 | references/lsposed-and-modules.md — 对照 dex 中的实际类检查 assets/xposed_init;安装不代表任何东西 |
| 你只需要调用目标自身的例程(签名、令牌、加密),而不是修改应用 | references/emulation-and-rpc.md — 对其进行模拟,或通过 Frida RPC 将实时函数封装为服务 |
某个原生函数是数千行的 switch 状态机,或反编译器的输出毫无意义 | references/native-dbi-and-deobfuscation.md — OLLVM 结构、Stalker 跟踪,以及跟踪实际上能帮你追到什么程度 |
Stalker.follow 已安装但没有事件到达,或跟踪一个高频 libc 导出函数时进程崩溃 | references/native-dbi-and-deobfuscation.md §6 失败模式 — 本仓库实测了这两种情况 |
你必须在不改代码、不注入线程且 TracerPid = 0 的情况下观察或修改目标 — 页面校验和能检测到每个 Hook | hardware-watchpoints.md — 使用 ptrace 设置硬件调试槽位,并了解其真实的隐蔽性记录;然后阅读 kernel-event-injection.md,了解完全无需跟踪器的内核设防路径 |
| 崩溃回溯不可信 — 栈帧属于错误的模块或匿名区域,代码已扁平化/虚拟化,或栈已损坏 | hardware-sampling-and-crash.md — 使用 BRBE 分支记录/ETE 尾部信息还原真实控制流,然后用 scripts/trace_addr2sym.py 符号化每个 PC |
| 你手上有一份运行时地址列表(命中点、跟踪 PC、分歧点),需要的是符号/静态偏移,而不是十六进制 | hardware-sampling-and-crash.md §3 崩溃循环 — 使用 scripts/trace_addr2sym.py,并遵循其 vaddr/文件偏移规则 |
| 虚拟化函数的跟踪结果只是一个不断重复的解释器循环,读起来像噪声 | scripts/vm_handler_slice.py — 从分派器入口切分出处理程序片段,对签名进行聚类,并还原来宾操作码轨迹(经 xvmp 校准) |
你无法判断眼前的 table[index]; br 是 VM 分派器还是 OLLVM 分派器 — 或者 VM 根本没有可读的静态结构 | references/native-vm-recovery.md — 区分这两个堆叠层的行为表、VM 存在性信号、经三重验证的寄存器锚定、嵌套 VM 片段栈,以及 STORE 反向操作数切片 |
| 跟踪片段看起来本身就已损坏 — 处理程序头部缺失,尾部粘进了错误的片段 | references/native-vm-recovery.md §7 嵌套 VM — 扁平片段累加器会丢弃外层;嵌套切片需要使用片段栈(vm_handler_slice.py 会标记这一症状) |
| 你需要知道同一操作的两次运行在哪里发生分歧(密码/许可证/密钥),而不仅是知道执行了什么 | ete-diff-tracing.md — ETE 硬件跟踪门控与差分方法;scripts/trace_diff.py 也可以对比 Stalker 日志 |
| 你面前有生成的 KPM/LKM 模板,或者内核路径看起来像是可行方案 | kernel-event-injection.md — 基本操作映射、组合方案,以及所有内核侧文件在成为武器之前必须通过的门槛 |
| 流量使用 protobuf/gRPC/QUIC,或者代理能看到 TLS,但 Flutter 应用的请求仍然失败 | references/protocol-reverse.md — 无 schema 的 protobuf、帧捕获,以及原生侧固定 |
用户空间 Hook 已生效,但应用仍然崩溃:检查通过原始 svc 读取 /proc/self/status,或在 JNI_OnLoad 之前运行 | references/kernel-and-environment-hardening.md — 上下层能实际做什么,以及何时应该停止 |
| 你必须直接在手机上编辑、重新打包、签名或检查 APK | references/on-device-tooling.md、scripts/mt_mcp_probe.py |
| 捕获的消息体无法解码为可读内容,或你无法判断长度限定字段是字符串、嵌套消息还是打包数组 | protocol-reverse.md。线上的 protobuf(实测)— 运行 scripts/protobuf_decode_raw.py;候选列表及其中的 tie: 行就是答案 |
| 方法体存在,但解码后是私有操作码;你需要的是映射,而不是为什么 VMP 难以处理的解释 | vmp-differential-analysis.md,然后阅读 advanced-unpacking.md 进行结构诊断 |
应用商店构建以 base.apk + split_config.*.apk 的形式提供,或重建后的构建作为一组文件遭到拒绝,尽管每个文件单独验证都通过 | split-apk.md — 使用 pm install-multiple 时,所有成员都必须使用同一个密钥库;信任合并后的单一 APK 之前,先确认合并是否合法 |
闸门 — 按顺序通过这些关卡,再开始修改
每个关卡都是一项行动及其通过标准。未通过某个关卡,就不要继续;也不要把“我理解了”当作通过。跳过关卡不是捷径,只会导致返工。
G1 · 交付物形式 — 以及成本上限。 用一句可以转述给他人的话,说明最终必须产出什么,以及有哪些约束(是否需要 root、能否安装在原厂设备上、是否必须经受更新、是否需要联网)。通过: 这句话明确了一项可测试的约束,而不是一项活动。未通过: 你正在一个交付物永远不会运行的环境中解决问题。
然后确定这句话所暗示的形式。“重新构建的独立 APK”只是四种形式之一,应该在这里做出选择,而不是等重新打包失败后再选:
| 形式 | 适用情况 | 代价 |
|---|---|---|
| 重新构建、可安装的 APK | 客户端掌控行为;没有多点完整性检查;不存在提取壳 | 重新打包、重新签名,以及一台用于验证的设备 |
| LSPosed / Xposed 模块 | 逻辑在客户端,但应用会对抗重新打包(多点签名检查、壳自检),或者结果只需在你控制的已 root 设备上运行 | 一台已 root 的设备、模块脚手架,以及一个不得检测到 Hook 框架的应用 → references/lsposed-and-modules.md |
| 本地 RPC / 仿真服务 | 你不需要修改应用,而是需要调用它:签名例程、令牌、加密函数 | 一台运行中的设备或仿真加载器,以及一个调用脚手架 → references/emulation-and-rpc.md |
| 附有明确边界的分析报告 | 权限在服务端,或者目标是真正的 VMP / 提取壳,恢复成本超过任务价值 | 不交付任何东西——这才是诚实的答案,并非失败 → references/advanced-unpacking.md、references/server-api.md |
离开第一列的决策触发条件: 一旦证据表明存在以下任一情况,就应停止选择“重新构建 APK”:(a) 必须全部通过的独立完整性检查不止一项;(b) 提取壳中的方法体仅在调用时存在;或 (c) 有方法体解码后属于私有操作码。此时,重新打包路线不只是成本高,而是行不通;交付物说明应指出替代形式。在此处选定形式,并在每个检查点重新审视,是防止此技能中代价最高的方向偏移的保障(references/long-task-discipline.md)。
G2 · 环境实况与能力清单。 运行 scripts/doctor.py(若涉及设备,也运行 scripts/preflight.py)。通过: 你清楚这里实际能运行哪些工具链和脚本,也已查看环境警告——时钟偏差、遗留的 adb forward/代理、设备上已有 frida 进程在运行、工具安装在 PATH 之外——并且写下了目标所需的能力与本机现有能力之间的对照。 指出任务几乎肯定会涉及的两三个层面(例如“arm64 原生反编译”“Dart AOT 快照转储”“设备端 TLS 检查”“全 dex 交叉引用”),并将每项标记为可用/缺失但可安装/确实无法实现。未通过: 你即将把自身设置导致的失败归咎于目标,或花一天时间绕过一个十分钟就能装好的工具。缺少工具的层面属于任务项,不应被当作设计时要绕开的约束。→ references/toolchain.md §关闭能力缺口
G3 · 代码位置。 根据清单和 dex,回答:是否存在加壳?应用自身的代码位于何处(dex / 原生代码 / Dart / Unity / 服务端)?其中是否有任何部分被虚拟化为原生代码?*通过:*你能指出拥有计划修改行为的类,或者已有明确的查找方案。*未通过:*你即将修改自己尚未定位的层。如果逆向分析得出“没有加壳”,仍需检查虚拟化形态——见上面的索引行。
G4 · 基线与对照。 *通过:*你已进行对照运行——使用未修改的原始版本,或通过同一流水线进行零改动重打包,并记录观察到的故障(包括崩溃时的存活时间)。*未通过:*补丁版本出现异常时,你将无从对比,后续每次测量都无法证伪。
从这里开始:通过十三个问题对目标进行分类
**在接触任何工具之前,先按顺序回答这十三个问题。每个问题都会改变整个计划,而这里答错不会明显报错:它会产出一个能够构建、运行,却做错事的产物。**答案栏会同时指出要执行的操作和负责说明相关细节的文件;在回答问题前,先加载该文件。
| # | 问题 | 答案会如何影响后续工作 | 加载 |
|---|---|---|---|
| 1 | **应用是否经过打包/加固?**读取清单中的 application android:name | 如果是第三方壳类而非应用自身的 Application,就说明应用使用了加固器,必须首先处理 | recon.md |
| 2 | 实际行为由什么决定?——广告 SDK · 服务端下发、由客户端渲染的 UI 配置 · 会员/VIP · 功能开关或调试开关 · 任何由 API 响应决定的内容 | 客户端侧内容,可移除;服务端提供数据、由客户端据此决定;或者由服务端权威决定,客户端补丁只是表面功夫。尽早判断这一点:花几个小时寻找一个根本不存在的广告 SDK,代价很大;而服务端配置的模式中根本没有 SDK 可找 | ad-removal.md、server-config-and-updates.md、membership-and-limits.md、server-api.md |
| 3 | 应用自身的代码是在普通 dex 中,还是移到了 native/Flutter/Unity? | 这决定了整个任务使用哪种工具链。在投入前先做低成本运行时检查:对你关心的 UI 对应的显而易见的 Java 类进行挂钩,并重现该 UI。挂钩触发说明它由 Java 控制;如果 UI 明明显示在屏幕上,挂钩却一次也没有触发,就说明该 UI 由运行时或 native 代码绘制,只针对 dex 的方案会卡住。探测结果为零后,不要继续在 dex 中寻找——这是本技能历史上代价最高的错误路线 | recon.md §应用自身的代码在哪里,framework-runtimes.md |
| 4 | **交付成果必须具备什么能力?**将其写成可测试的句子,并在每个检查点重新阅读 | 本技能中代价最高的偏差:仅运行时生效的结果(数据编辑、实时挂钩、屏蔽主机名、主机代理)看起来像成功,却不符合要求。考量维度:权限(是否未 root?)、修改形式(重建产物还是实时插桩)、ABI/设备类别、网络、持久性、分发(是否自包含?)。如果证据已表明存在深度提取壳、VMP,或多个独立的完整性检查,请根据 G1 的四种形式重新作答——重打包这一栏可能是行不通,而非只是代价高 | long-task-discipline.md §代价最高的偏差 |
| 5 | 应用自身还是服务端会验证应用签名? | 如果由应用侧验证,你必须绕过验证;如果由服务端验证,重新签名会在之后悄无声息地破坏应用 | repack-and-sign.md、server-api.md |
| 6 | 你的设备情况如何?——已 root 的真机、已 root 的模拟器,还是没有设备 | 这决定了 Frida 是否可用,以及交付成果能否在实际运行环境中测试。请在首次实验前运行 scripts/preflight.py,并在遇到意外故障时再次运行——设备状态、已停止的设备服务端、遗留代理和时钟偏差都可能伪装成补丁损坏 | environment.md、pitfalls.md P9 |
| 7 | 实际运行的是哪种架构? | getprop 报告的是设备声称的架构,primaryCpuAbi 报告的是包管理器选择的架构——两者都不能说明实际运行的是什么。只有实时映射才是确凿依据。库未映射、转换器正在运行,或 ABI 与假设不同,都会比任何补丁更大地改变方案 | native-and-so.md §跨架构,scripts/lib_map.py |
| 8 | 是否只有某个特定功能在运行时失效(登录、注册、付款、由 API 支持的屏幕),而其他功能正常? | 特定功能的网络故障通常是某条代码路径上的 TLS/证书问题,而不是补丁导致的。应用可能有两条独立的信任链,所以“其他请求都能用”说明不了任何问题。在寻找签名检查之前,先花几分钟排除这种情况 | tls-and-cert.md |
| 9 | 输入是否是你没有构建的版本(流传的“破解”/“修改版” APK)? | 这类构建版本经常会被重新加固——有时甚至比原版多出更多层——还可能包含注入的组件或端点。切勿用它作为补丁测试版本 | third-party-builds.md |
| 10 | 客户端是否使用自己的签名证书对请求签名? | 首次重打包前,搜索 toCharsString() / signatures[0] / getPackageInfo(..., 64)。如果该值被传入 native HMAC/DES 例程,重建后的 APK 必须在每个读取位置硬编码原始证书值,否则应用虽然仍能启动且看似正常,但所有签名请求都会失败。这是重打包中代价最高的静默故障;花 15 分钟搜索就能避免 | signature-derived-keys.md |
| 11 | 应用是否会自行运行一段时间后退出——没有 Java 堆栈,或出现看似程序缺陷的 native 崩溃? | 加固库很少直接调用 kill;它们更常制造故障(加载一个小常量,再将其用作指针),让退出看起来像普通缺陷。在动手处理前,请遵守两条规则:列出实际触发的机制(通过信号和 tombstone 区分),并且通过返回来中和,而不是让它无法返回——忙等的桩函数会冻结进程,产生完全不像根因的症状 | native-tamper-and-suicide.md |
| 12 | 这个构建版本一周后还能用吗? | 任何版本检查、升级提示或自更新路径,都意味着未打补丁的构建版本可能被远程停用或替换。这只需一两处修改,却决定了成果是否持久——它是构建的一部分,不是后续工作。还要检查是否存在热更新/远程配置通道,因为即使版本未变,它也能恢复已移除的行为 | updates-and-forced-upgrade.md |
| 13 | 请求是否涉及登录或手机绑定(“无需登录”“跳过绑定”“允许访客”)? | 区分客户端侧的门槛(可打补丁)和账户范围的资源(屏幕为空是因为服务端没有可响应的账户——无法通过打补丁解决)。先进行分类;并且切勿伪造会话来绕过门槛,否则产生的状态会比未登录更糟 | account-gates.md |
| 长任务规则(这是条件,不是建议):如果任务可能耗时较长,请现在打开 | |||
references/long-task-discipline.md,并在执行过程中持续更新记录。开始任何实验前,请重新阅读其中的 | |||
| 已证伪结论和无效路径部分。丢失先前的发现是这项技能中代价最高的失败,而且完全可以避免。 |
完整工作流程
步骤按顺序排列。仅当满足注明的跳过条件时,才跳过某个步骤——“看起来没必要”不算条件,而这正是 pitfalls.md 中大多数失败的原因。
两次失败规则。如果同一类型的尝试失败两次,就停止并返回分类步骤。不要对已经失败两次的假设再尝试第三种变体。相同模式的两次失败意味着模型有误,而不是参数需要调整——第三次尝试只会耗掉一整轮,去确认前两次已经说明的情况。此时请重新阅读症状索引;它正是为这种情况准备的。
- 先做预检,再侦察 —
scripts/doctor.py是成本最低的第一条命令:它会报告哪些工具链和脚本在当前环境中确实可以运行,并揭示可能干扰实验的环境因素(时钟偏差、遗留的adb forward/代理、设备端已有 frida 进程在运行、工具安装在 PATH 之外)。然后,在涉及设备时,先运行scripts/preflight.py,再做其他任何事(只需几秒,就能避免一整类错误结论),接着查看references/recon.md。检查清单、包名、版本、ABI、dex 数量、加壳器、内嵌 SDK,以及应用自身代码的位置。在这里花十分钟,可以省下几小时。如果应用已加壳,先脱壳,再做其他任何事(references/recon.md§unpacking):你无法修补看不到的代码;加密载荷的长度能告诉你哪个转储的 dex 是原始 dex;内存转储必须先按哈希去重并进行结构验证,之后才能信任其中的任何内容。 如果侦察结果表明应用没有加壳,但重新签名后的构建仍然崩溃,那么问题属于references/code-virtualization-and-custom-linkers.md涵盖的层面——不要因为“没有加壳”就假定应用“可以编辑”。 如果应用本身已经会崩溃——特别是启动后大约在固定时间崩溃,或发生原生崩溃——请先找出机制,再规划任何修补(references/native-tamper-and-suicide.md、scripts/native_crash.py)。记录观察到的崩溃时间:这是衡量后续每次尝试的基准;没有这个基准,就无法判断一次成功运行是因为问题解决了,还是崩溃时间表发生了变化。 跳过条件: 永远不跳过。G2/G3 必须在这里通过 §Gates 中的检查,否则就无法通过。 - 提取字符串和端点 — 通过 dex 字符串表,梳理应用的 API 表面和 SDK 清单。无需反编译器,速度也很快。脚本:
scripts/dex_strings.py。 - 追踪到所属类 — 找出封装该行为的类(应用几乎总会用一个辅助类封装第三方 SDK)。反向查找指令见:
references/dex-patching.md§finding-the-call-site。 - 确定修补层级 — 客户端 SDK 调用/客户端渲染/客户端数据使用/服务端契约。参见
references/ad-removal.md中的表格。 - 精准修补 — 参见
references/dex-patching.md和references/byte-level-patching.md。 有两种技术,选择哪一种是一个决策,而非偏好: 当修改可以放入现有指令槽或常量时,使用等长字节编辑(scripts/dex_patch_bytes.py,通过scripts/dex_find_insn.py定位)——没有任何内容移动,因此不会使偏移量、try/catch 块或调试指针失效。只有在确实需要新增指令时,才使用 dexlib2 方法重写(scripts/dexpatch/)。对整个目录树进行 smali 往返转换会损坏经过 R8 优化的 dex,而且只有在运行时才会显现;重建方法也会让文件变大(实测:某个样本的debug_info从 924 B 增至 22.8 KB,dex 从 4.32 MB 增至 7.73 MB)。无论使用哪种方式,都要重新计算 dex 头部的完整性字段(先计算签名,最后计算校验和)——参见references/byte-level-patching.md§the dex header has two integrity fields。 - 重新打包并签名 — 参见
references/repack-and-sign.md。不要删除整个META-INF/。 这个错误会毁掉原本正确的构建。 6b. 中和更新路径——在宣布构建完成之前。 如果应用会检查更新,就添加双层修补(references/updates-and-forced-upgrade.md):将更新例程入口设为空操作,并强制版本比较走“无更新”分支。能够被远程关闭或替换的构建不算交付成果;现在花几分钟处理,可以避免之后重新构建。任何可能恢复已移除行为的远程配置或热更新渠道也要同样处理。 6c. 对账户门槛进行分类后再处理 — 如果请求提到登录或绑定,请应用references/account-gates.md,并明确说明哪些受限界面将变得可用,哪些界面仍为空,因为其内容与账户绑定。 - 在设备上验证 — 参见
references/environment.md和references/verification.md。检查:应用能启动,目标行为确实发生了变化,没有其他无关功能受损,并且应用能正常进入界面,且没有阻塞对话框。首先确认设备上的构建产物确实发生了变化——包管理器报告成功,并不能证明拦截的确认操作已被接受(P18)。启动后的前约 20 秒要连续录制,并查看录制内容——采样间隙可能导致阻塞模态框被漏掉(P20);一批从未检查过的图像不算证据。如果无障碍树为空,图像就是主要证据,而非备用手段。 - 记录经验 — 如果某次故障耗费了你超过三十分钟,请将其添加到
references/pitfalls.md。该文件是这项技能中最有价值的产物。
“完成”意味着什么——不要过早宣称完成
在报告完成之前,以下各项必须全部成立。否则都只是一个检查点,必须明确称其为检查点,并说明还有哪些事项未完成。过早宣称“完成”是你能报告的最糟糕的事,因为这会在用户相信问题已解决时终止调查。
- 产物确实存在,且已记录其身份信息——记录路径和哈希值,而不只是文件名。
- 产物已在交付说明中指定的环境中安装并启动(G1/R1)。如果你无法访问该环境,请如实说明,并相应标注结果。
- 已通过直接观察功能,而非仅凭没有错误消息,验证你所做的行为更改确实生效。“日志没有问题”不算证据;“屏幕显示了 X”才算。
- 受影响的功能仍然正常。你已实际测试过这些功能。程序能够启动,但受影响的功能无法使用,这不算交付结果。
- 如果原有限制仍然存在,必须说明,并指出造成该限制的耦合关系,以便后续人员决定是否接受。
- 你所做的操作没有导致目标或设备处于损坏状态,除非这本来就是目标;任何特权变通方案都必须标注为备用方案,而非交付物。
如果第 1–4 项均成立,但环境不符,你得到的是一个原型,而非交付物。请明确称其为“原型”,并说明差距。
停止条件——停止并重新分类,不要重试
遇到以下情况时,继续推进是错误的做法。有些地方已经因此耗费了数小时。
- **同一类型的尝试已失败两次。**参见上文的两次失败规则。
- **补丁没有效果,而你正准备尝试第三种变体。**没有效果说明诊断有误,而不是补丁运气不好。请重新判断问题所在的层级。
- **出现的新故障无法纳入你当前的模型。**这正是症状索引的触发条件。
- **你正准备将某条路径认定为“受阻”,却没有通过对照构建证明阻碍来自应用本身,而非你的流水线。**错误归因曾导致可行路径被放弃。
- **你正准备仅凭没有错误就宣称成功。**参见 §“完成”意味着什么。
- **测量结果与已经记录为定论的结论不符。**重新审视该结论;不要对测量结果作牵强解释。
不可妥协的约束
违反这些约束属于缺陷,而不是偏好。 每一行都说明了用于检测问题的检查方式,因此“我是否遵守了”可以通过命令检查,而不是靠自我判断。
| 约束 | 绝对执行的原因 | 检查方式 |
|---|---|---|
| 输入为只读。 对副本进行操作;保留已知良好的基线 | 编辑原件会毁掉唯一可供比较的参考 | 首次编辑前使用 git/哈希检查原件;使用 scripts/apk_diff.py 提供条目级证据 |
| 一次只改一个变量,并运行对照构建(使用相同流水线,不做任何补丁) | 复合改动失败时毫无收获,还会把失败归咎于错误的原因 | 存在对照运行,并已记录其结果 |
| 每次编辑 dex 后都要验证结构 | 能成功汇编、却在加载时崩溃的补丁,看起来就像成功的补丁 | 使用 scripts/dex_classdiff.py:对于非预期修改的类,类集合和访问标志必须零差异 |
| 绝不修改被广泛共用的方法。 先统计调用方 | 一个有 30 个调用方、形似 Long.valueOf 的辅助方法,并不是广告专用钩子 | 对即将修改的方法运行 scripts/find_refs.py |
| 绝不通过让 API 失败来抑制 UI 元素 | 其他功能依赖的端点一旦返回 404/400,整个屏幕都会受影响;表现为构建始终停留在启动屏幕 | 应在数据消费层或渲染层进行抑制;如果请求路径发生变化,那就是问题 |
| 通过返回来中和原生终止路径,绝不能让它无法返回 | 自旋桩会让调用方在持有锁时冻结;无关线程会卡死,症状(挂起、被外部终止、重启循环)与原因毫不相似,而且没有崩溃记录 | 返回无害值,优先返回成功值(0),而不是失败值(-1);绝不接触 pthread_exit/exit/abort/snprintf/closedir。参见 native-tamper-and-suicide.md |
| 下结论前先观察,等待时也要观察 | 实际查看屏幕,一步就能回答坐标猜测五步也无法回答的问题 | 捕获并检查;字节完全相同的样本意味着不会有变化。参见 scripts/snap.py、environment.md §查看屏幕 |
| 为每条命令设置时限,并根据测量结果校准时限 | 不限时的调用会把停滞误认为“仍在运行”,无法与取得进展区分 | 对操作计时一次并记录,根据记录设定截止时间。参见 long-task-discipline.md §为每次等待设置时限 |
每项结论都必须标注为 observed、inferred、unverified 或 unknown | “可能”“应该是”和“理论上”都不是发现;没有标注的猜测会被当作事实传播 | observed 必须附带命令及其输出;其他内容都不得写成已证实的结论。未能触及问题的探测结果应标为 unknown,绝不能标为 absent |
| “完成”意味着用户可见的结果 | 阻塞对话框仍显示在屏幕上,就表示任务尚未完成,无论日志有多干净 | §“完成”的含义中的六项检查,缺少日志行绝不能作为成功证据 |
| 绝不因复合证据而放弃一条路径 | 如果失败发生在同时进行两项改动之后,归因就只是一个假设;错误归因的失败曾让可行路径被搁置数小时 | 记录放弃路径之前,先重新运行单变量放弃测试 |
| 开始测量前先证明设备已发生变化 | 安装成功描述的是请求,而不是磁盘上的应用 | 将设备上的产物与构建产物进行哈希比对;否则,后续所有观察描述的都是上一版构建 |
| 再次打补丁前,先将失败归因到正确的层 | 特定功能的网络故障往往是应用自身的 TLS/证书问题;追查并不存在的签名检查会浪费数小时 | 先在同一设备和网络上运行未经修改的原始版本(tls-and-cert.md) |
参考文件索引和脚本索引
这两张表都位于 references/routing.md——加载一次,就能查到每个参考文件及其加载时机,以及每个脚本的用途。这里特意不重复列出这两张表:每次技能激活时,都会完整加载此文件;而这两张表约有 140 行查找数据,只有在已经决定要做什么之后才会用到。
上方的症状索引会保留,因为从症状到文件必须一步到位:出现故障时,你不是在选择文件,而是在识别故障;此时需要两步才能查到文件,正是导致忽略停止信号的原因。
references/evidence-summary.md 是唯一一个能在已安装副本中回答“主张可信度”问题的参考文件:能力 → 一句话结论 → observed/inferred/unverified → 随技能附带的证据。当某项主张的可信度会影响你是否相信它,而运行记录又不在手边时,请加载此文件。
python check_routing.py 会检查此文件与 references/routing.md 是否仍保持一致、其中是否列出了每个参考文件,以及是否列出了每个脚本。CI 会运行此检查。