mcpField
A
apk-reverse

apk-reverse

github.com/zzusec安全21954 次安装2 周前更新
代码仓库

关于 apk-reverse

apk-reverse 是用于分析和修补 Android APK 的流程技能,目标是产出经过验证且可安装的文件。它强调先识别代码所属层、控制实验、检查工具能力,并明确记录证据边界。

主要功能

  1. 01分类目标并识别所属代码层
  2. 02设定交付门槛并记录基线
  3. 03使用索引器和分层工具
  4. 04区分已观察、推断与未知结果
  5. 05安装并测试修补后的文件

适用场景

  1. 01修补前定位 APK 中的检查逻辑
  2. 02诊断补丁未生效的原因
  3. 03在设备上验证修改后的 APK

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/zzusec/apk-reverse --skill apk-reverse

安装后在 Claude Code 中输入/apk-reverse确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
evidence/
capability-matrix.json
known-limitations.md
tested-tool-versions.json
references/
account-gates.md
ad-removal.md
advanced-unpacking.md
byte-level-patching.md
code-virtualization-and-custom-linkers.md
coverage-and-limits.md
dart-aot.md
desensitization-and-leak-scans.md
detection-and-anti-analysis.md
dex-patching.md
dexsql-sql-query-layer.md
dynamic-frida.md
emulation-and-rpc.md
environment.md
ete-diff-tracing.md
evidence-summary.md
framework-runtimes.md
handoff-boundaries.md
hardware-sampling-and-crash.md
hardware-watchpoints.md
java2c-and-jni-sinking.md
kernel-and-environment-hardening.md
kernel-event-injection.md
long-task-discipline.md
lsposed-and-modules.md
references/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。

  1. JNI — Java native 声明及其实现是同一函数的两个视图,而本技能会用不同工具分别读取它们。符号搜索遇到动态注册时会静默失败。权威说明:java2c-and-jni-sinking.md §The JNI boundary — why a symbol search fails silently。
  2. 加固 — 从 dex 侧观察到的加壳器,是一个原生侧实现问题。packers.md 负责 dex 侧的识别;深入分析应在另一侧进行。
  3. 原生异常 — 从 APK 侧 读取,因为要判断的是补丁是否导致了崩溃。恢复符号或逆向 OLLVM 函数是另一项活动,使用不同的工具链:应指向另一侧,而不是把 native-and-so.md 的范围扩展到这类内容。
  4. 交付物形式 — 当产物不再是 APK 时,验证问题也会随之改变。G1 的其他三种形式各自会将证据转移到别处,而故障不易察觉:以已完成构建的说法报告特权结果。

症状索引——匹配到某一行即表示应停止

你遇到的是某种症状,而不是某个文件名。下方每一行都是已经付出代价的故障。如果任何一行符合你观察到的情况,请在运行下一条命令之前加载对应文件——不要等到再尝试三次之后。此时从第一原理开始推理,只会让同样的时间再耗费一次;这里不止一项经验教训,都是在答案明明就在仓库里、却无人阅读时手动重新推导出来的。

观察到的现象首先阅读
重新打包/重新签名的构建在你的代码运行之前就崩溃;SIGSEGV、所有寄存器均为零、pc=0、fault addr 接近 0x0native-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 foundcode-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_*”
你准备发布引用了真实工作的证据文件、记录或 READMEreferences/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,并且启动时普通类出现 ClassNotFoundExceptionbyte-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 映射看起来像 dexadvanced-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 的情况下观察或修改目标 — 页面校验和能检测到每个 Hookhardware-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 — 上下层能实际做什么,以及何时应该停止
你必须直接在手机上编辑、重新打包、签名或检查 APKreferences/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 中大多数失败的原因。

两次失败规则。如果同一类型的尝试失败两次,就停止并返回分类步骤。不要对已经失败两次的假设再尝试第三种变体。相同模式的两次失败意味着模型有误,而不是参数需要调整——第三次尝试只会耗掉一整轮,去确认前两次已经说明的情况。此时请重新阅读症状索引;它正是为这种情况准备的。

  1. 先做预检,再侦察 — 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 中的检查,否则就无法通过。
  2. 提取字符串和端点 — 通过 dex 字符串表,梳理应用的 API 表面和 SDK 清单。无需反编译器,速度也很快。脚本:scripts/dex_strings.py。
  3. 追踪到所属类 — 找出封装该行为的类(应用几乎总会用一个辅助类封装第三方 SDK)。反向查找指令见:references/dex-patching.md §finding-the-call-site。
  4. 确定修补层级 — 客户端 SDK 调用/客户端渲染/客户端数据使用/服务端契约。参见 references/ad-removal.md 中的表格。
  5. 精准修补 — 参见 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。
  6. 重新打包并签名 — 参见 references/repack-and-sign.md。不要删除整个 META-INF/。 这个错误会毁掉原本正确的构建。 6b. 中和更新路径——在宣布构建完成之前。 如果应用会检查更新,就添加双层修补(references/updates-and-forced-upgrade.md):将更新例程入口设为空操作,并强制版本比较走“无更新”分支。能够被远程关闭或替换的构建不算交付成果;现在花几分钟处理,可以避免之后重新构建。任何可能恢复已移除行为的远程配置或热更新渠道也要同样处理。 6c. 对账户门槛进行分类后再处理 — 如果请求提到登录或绑定,请应用 references/account-gates.md,并明确说明哪些受限界面将变得可用,哪些界面仍为空,因为其内容与账户绑定。
  7. 在设备上验证 — 参见 references/environment.md 和 references/verification.md。检查:应用能启动,目标行为确实发生了变化,没有其他无关功能受损,并且应用能正常进入界面,且没有阻塞对话框。首先确认设备上的构建产物确实发生了变化——包管理器报告成功,并不能证明拦截的确认操作已被接受(P18)。启动后的前约 20 秒要连续录制,并查看录制内容——采样间隙可能导致阻塞模态框被漏掉(P20);一批从未检查过的图像不算证据。如果无障碍树为空,图像就是主要证据,而非备用手段。
  8. 记录经验 — 如果某次故障耗费了你超过三十分钟,请将其添加到 references/pitfalls.md。该文件是这项技能中最有价值的产物。

“完成”意味着什么——不要过早宣称完成

在报告完成之前,以下各项必须全部成立。否则都只是一个检查点,必须明确称其为检查点,并说明还有哪些事项未完成。过早宣称“完成”是你能报告的最糟糕的事,因为这会在用户相信问题已解决时终止调查。

  1. 产物确实存在,且已记录其身份信息——记录路径和哈希值,而不只是文件名。
  2. 产物已在交付说明中指定的环境中安装并启动(G1/R1)。如果你无法访问该环境,请如实说明,并相应标注结果。
  3. 已通过直接观察功能,而非仅凭没有错误消息,验证你所做的行为更改确实生效。“日志没有问题”不算证据;“屏幕显示了 X”才算。
  4. 受影响的功能仍然正常。你已实际测试过这些功能。程序能够启动,但受影响的功能无法使用,这不算交付结果。
  5. 如果原有限制仍然存在,必须说明,并指出造成该限制的耦合关系,以便后续人员决定是否接受。
  6. 你所做的操作没有导致目标或设备处于损坏状态,除非这本来就是目标;任何特权变通方案都必须标注为备用方案,而非交付物。

如果第 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 会运行此检查。

评论

支持 Markdown