关于 apk-reverse
apk-reverse 是用于 Android APK 逆向分析和补丁处理的流程技能,重点是产出经过验证且可安装的制品。它指导开发者分类目标、选择工具、控制实验,并明确覆盖范围限制。
主要功能
- 01补丁前分类目标和交付要求
- 02检查工具能力并选择适用工具
- 03使用对照构建并一次只改一个变量
- 04要求安装并验证具体功能
- 05说明覆盖边界和停止条件
适用场景
- 01识别应用行为所属的代码层
- 02制定带对照构建的补丁方案
- 03在设备上验证已修改的 APK
- 04判断目标是否属于文档覆盖范围
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ newliver666/ 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 | 属于假设,或由其他地方报告但未在此复现 | 只能作为待测试事项 |
用来取代猜测的清单: 按路由列出哪些内容已覆盖、哪些未覆盖——分别通过已验证机制和有文档记录但大多依赖推断的路由实现——详见 references/coverage-and-limits.md。如果问题是“本技能实际上能否做到 X”,请加载此文件。
容易被过度解读的部分覆盖范围——这些是有文档记录的路由,但本技能不提供其依赖项,且同一文件中为每项都划定了边界:Dart AOT 分析需要你从其他地方获取快照转储,而且前端的命名很重要 · 提取壳层恢复到 VMP 边界为止 · 无法保证每台设备都能进行 Stalker 跟踪 · 内核侧的答案需要一个本技能只提供脚手架的模块。
未覆盖的内容——请明确说明,不要临场发挥: Unity / IL2CPP · React Native / Hermes / Cordova · iOS · 绕过服务端权威校验 · 现成的解包工具或反检测军备竞赛 · 构建并发布内核模块。原因和证据边界见 references/coverage-and-limits.md;记录中的沉默并不代表支持。
后备方案,作为指令——这是规则,不是建议。 如果目标不符合已覆盖清单,或者没有症状索引条目相匹配,那么先停止并分类,再选择分支:回答十三个问题(§从这里开始)。如果问题形态仍不匹配——例如运行时未知,或你无法说出其机制——就明确说明这一点,并提出成本最低、能够识别该机制的实验。 不要选取最接近的文档化路由,然后照搬应用。这里选错分支不会明显失败:它会产出一个能够构建、能够运行、却做错事情的产物。
交接点——本技能到此为止,另一种视角由此开始
有四处边界很容易在不知不觉中跨越。每处都说明另一方负责什么,而不是重复说明,以免同一建议的两份副本逐渐出现偏差。JNI 形式表和按形式划分的验证表见 references/handoff-boundaries.md。
- JNI——Java
native声明及其实现是同一函数的两种视图,本技能会用不同工具分别读取它们。对于动态注册,符号搜索会静默失败。权威说明:java2c-and-jni-sinking.md§JNI 边界——为什么符号搜索会静默失败。 - 加固——dex 侧对加壳器的观察属于 native 侧实现问题。
packers.md负责 dex 侧的识别;深入分析则属于另一侧。 - Native 异常——应当从 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 层的“signature killer”是个幌子 |
删除某个库后验证通过,但出现 UnsatisfiedLinkError: dlopen failed: library "X" not found | code-virtualization-and-custom-linkers.md §耗费数小时的死锁 |
整个类都表现为没有方法体的裸 native 声明 | java2c-and-jni-sinking.md——在转储内存前先阅读此文:如果这是 Java2C,那么在进程生命周期的任何时候都找不到 DEX。普通 dex 中只有少数 native 方法的情况属于 JNI 下沉,不是这种情况 |
在加固库中搜索 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——检查 assets/xposed_init 是否与 dex 中的实际类相符;安装成功不能证明任何事 |
| 你只需要调用目标自身的例程(签名、令牌、加密),而不是修改应用 | 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 校准) |
| 你需要知道同一操作的两次运行在何处发生分歧(密码/许可证/密钥),而不只是知道执行了什么 | ete-diff-tracing.md——使用 ETE 硬件跟踪门控和差分方法;scripts/trace_diff.py 也能比较 Stalker 日志 |
| 你面前有一个生成的 KPM/LKM 模板,或内核路径看起来像是可行方案 | kernel-event-injection.md——查看原语地图、组合方案,以及所有内核端文件在成为武器前必须通过的门槛 |
| 流量使用 protobuf/gRPC/QUIC,或在 Flutter 应用上代理能看到 TLS 流量但请求仍然失败 | references/protocol-reverse.md——无模式 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:对于你无意修改的类,类集合和访问标志差异为零 |
| 绝不修改被广泛共用的方法。 先统计调用方 | 一个类似 Long.valueOf 的辅助方法若有 30 个调用方,就不是广告专用钩子 | 对你准备修改的方法运行 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 | “可能”“应该是”和“理论上”都不算发现;未标注的猜测会被当作事实传播 | observed 必须附有命令及其输出;其他任何内容都不得写成已证实的事实 |
| “完成”意味着用户可见的结果 | 阻塞对话框仍在屏幕上,就说明任务尚未完成,无论日志多么干净 | §“完成”的含义,全部六项;缺少日志行绝不能作为成功的证据 |
| 绝不只凭复合证据放弃一条路线 | 如果失败发生在两项变更同时进行之后,归因就只是一个假设——错误归因曾让可行路线白白搁置数小时 | 写下放弃结论前,重新进行单变量实验以验证该放弃决定 |
| 测量任何内容前,先证明设备上的内容已改变 | 安装成功描述的是请求,而非磁盘上的应用 | 将设备上的产物与构建产物进行哈希比对;否则,之后的每项观察描述的都是先前的构建 |
| 再次打补丁前,先将失败归因到正确的层 | 特定功能的网络故障通常是应用自身的 TLS/证书问题;追查并不存在的签名检查会浪费数小时 | 先在同一设备和网络上运行未修改的原始版本(tls-and-cert.md) |
参考文件索引和脚本索引
这两张表都位于 references/routing.md——加载一次,就能查到每个参考文件及其加载时机,以及每个脚本的用途。这里特意不重复列出这两张表:每次技能激活时,都会完整加载此文件;而这两张表约有 140 行查找数据,只有在已经决定要做什么之后才会用到。
上方的症状索引会保留,因为从症状到文件必须一步到位:出现故障时,你不是在选择文件,而是在识别故障;此时需要两步才能查到文件,正是导致忽略停止信号的原因。
references/evidence-summary.md 是唯一一个能在已安装副本中回答“主张可信度”问题的参考文件:能力 → 一句话结论 → observed/inferred/unverified → 随技能附带的证据。当某项主张的可信度会影响你是否相信它,而运行记录又不在手边时,请加载此文件。
python check_routing.py 会检查此文件与 references/routing.md 是否仍保持一致、其中是否列出了每个参考文件,以及是否列出了每个脚本。CI 会运行此检查。