有一类技术问题,很容易在源码阶段给人一种“已经解决”的错觉:我们找到了一个导入方法,确认了它接收什么格式的文件,离线也成功造出了一个结构正确的包。接下来只要把包放进手机,再打开应用,似乎就应该能看到一个新草稿。
这次我就是沿着这条思路往下走的。目标是批量、程序化地生成或修改草稿,同时尽量不碰 root,不改应用私有目录。源码里确实存在一个可以吃草稿压缩包的导入能力,包也确实被推送到了手机的外部下载目录。可是最后的真机结论不是“登录失败”,也不是“包格式不对”,而是更基础的一件事:移动端没有把这个能力暴露成用户可以触发的入口。
这件事让我重新意识到,逆向工作里最容易混淆的,是“系统内部存在某种能力”和“用户能够通过产品路径使用这种能力”。它们之间,隔着不止一个按钮。
源码里的能力,只能算候选入口
最初看到导入类和方法时,我的判断确实偏乐观。代码显示,导入器可以读取一个普通压缩包,里面是一个新 UUID 目录,目录下放同名 JSON 文件。应用进程会负责解压,把内容写进自己的草稿区域,再建立索引。这个设计看上去非常适合跨设备制作草稿:外部只负责准备文件,应用自己负责落盘,不需要我们直接写入受保护的目录。
从静态证据看,路线很干净。导入器没有显示出必须登录、服务器签名或额外完整性校验的门槛;源码还处理了桌面系统压缩时可能产生的额外文件,说明这种包本来就可能来自应用之外。只要不混入模板相关文件,只保留草稿目录和 JSON,就能走到可编辑草稿的分支。
但这里有一个必须压住的冲动:把 exported 方法直接当成用户功能。一个方法能被调用,不等于产品已经提供了调用它的方式。它可能只被桌面端、迁移流程、实验功能或尚未接上的界面使用,也可能只在另一个进程或协议中被间接触发。
源码里找到一个可调用方法,证明的是能力存在,不是用户已经拥有一条可走的产品路径。
如果在这里就宣布成功,后面会把大量时间花在包字段、压缩参数和登录状态上,甚至会误以为每次失败都是格式问题。真正缺失的入口,反而会一直被当成“还没调对”。
离线造包成功,仍然没有完成导入
接下来我做了最小化的离线验证。包只包含一个全新的 UUID 草稿,不拿真实草稿改写,也不复用现有项目的目录。结构自检通过后,再把这个包推送到手机外部下载目录,确认文件确实在位。这样做的目的,是把“包有没有造对”和“应用能不能消费”拆开。
这一步的结果是明确的:包本身没有因为路径结构而被判死,推送也没有失败。它说明文件通道成立,至少可以把一个待消费的输入交给手机。可是文件在下载目录里,并不意味着应用会主动扫描它。操作系统看得到文件,也不代表目标应用知道应该在什么时候、以什么方式打开它。
这件事表面上是在验证一个压缩包,实际上是在验证一条完整的消费链。文件需要先到达正确位置,然后必须有某个界面、系统回调或应用内协议把它交给导入器,导入器才有机会执行。少了中间这一步,前面的静态能力和离线包都只是悬空的准备工作。
我选择先做新 UUID 的一次性包,是因为这能把风险压到最低。如果后面真的找到入口,最多只会增加一个可识别的测试草稿;如果找不到,也不需要清理原有内容。验证结束后,测试文件可以移除,整个过程不触碰真实草稿,不删除应用数据。
真机遍历后,卡点变成了产品入口
有了包之后,真正关键的工作不是继续猜字段,而是逐屏确认应用有没有消费它的入口。我按草稿箱、首页菜单、单个草稿的更多操作、批量管理等路径检查,重点寻找“从本地导入草稿”“打开外部草稿”或类似语义的动作。
遍历结果很干脆:移动端没有本地 zip 导入入口。现有的草稿迁移动作是“草稿箱 → 长按选择 → 快传”,它要求目标设备打开应用、使用同一个账号并处在同一网络中,作用是把发送端自己的草稿传给另一台设备,而不是从外部下载目录读取一个我们手工制作的压缩包。系统层面的文件关联也只覆盖图片和视频,没有把草稿压缩包交给应用的处理器。
所以,这次没有进入“包被应用拒绝”的阶段。包没有机会被喂给导入器,导入方法也没有机会执行。最终阻塞点不是权限、登录或包格式,而是产品没有暴露能触发这段能力的入口。
这里最容易误判的错误捷径,就是看到源码方法名和文件结构都吻合,就直接把文件推到手机,然后把“应用没有反应”解释为还有某个参数没补齐。这个解释听起来技术味很足,却跳过了最应该先问的问题:用户到底从哪里发起这次操作?
如果不处理这个问题,后果不只是多试几次。我们可能继续修改已经验证通过的包,反复切换登录状态,甚至为了绕过没有入口的问题去尝试直接写私有目录。最后得到的不是更可靠的路线,而是一条越来越重、越来越接近越权操作的旁路。
把路线拆成 5 层,失败才会落得准确
这次验证后,我把类似路线固定拆成 5 层。第 1 层是源码能力:目标函数是否存在,输入结构和分支条件是什么。第 2 层是文件通道:输入能否在不破坏数据的前提下到达设备或应用可见的位置。第 3 层是用户入口:产品有没有按钮、文件选择器、系统回调或协议触发动作。第 4 层是应用消费:应用是否真的调用导入逻辑,能否把内容写入自己的数据结构。第 5 层是结果验收:草稿是否出现、能否打开、是否保留为可编辑状态。
这 5 层不是为了把流程写复杂,而是为了防止不同类型的证据互相冒充。源码证据不能替代界面证据,文件在位不能替代应用消费,应用消费成功也不能替代最终结果验收。每层都要留下一个最小实证;只要某一层缺失,就把路线标成“不可用”或“待验证”,不要用“理论上可以”把空白遮住。
我会把每层的通过条件写得很具体。源码层要能指出输入和落点;文件层要能确认文件在位且没有污染真实数据;入口层要能在真实界面走到触发动作;消费层要看到应用产生了对应记录;结果层要打开新草稿并确认它不是一个空壳。这样,即使最后失败,也能说清楚是卡在入口、协议还是结果,而不是笼统地说“导入不行”。
这套拆法也改变了决策顺序。我判断这条移动端本地导入路线应该停止,是因为前 2 层已经通过,第 3 层被真机证伪,继续优化前两层不会改变结论。后续若要继续,只能另开一个问题:研究快传协议,或者取得直接写入私有区域的授权。那已经不是“把这个 zip 导进去”的同一个任务了。
代码可行和产品可用要分开验收
很多技术路线之所以拖得久,是因为验收标准只有一句“能不能做”。这句话至少应该拆成 2 个问题:代码是否具备完成动作的能力,用户是否拥有一条稳定、允许、可重复的路径完成动作。前者适合用源码、日志和离线构造验证,后者必须回到真实设备和真实界面。
我更愿意把产品入口看成能力的一部分,而不是能力之外的包装。没有入口,用户就无法触发;没有合法文件通道,入口就无法拿到输入;没有结果验收,所谓成功也可能只是写入了一条孤立记录。技术路线只有穿过这几层,才配得上“可用”这个判断。
这件事还提醒我,保守不是降低效率。先用一次性草稿、全新 UUID 和外部目录做验证,看起来比直接改真实数据慢一点,但它让失败可恢复,也让结论更可信。真正浪费时间的,是在没有入口的前提下继续打磨一个已经不可能被消费的包。
一个流程成熟的标志,不是它能在源码里跑通一次,而是它能在真实产品路径中被触发、被消费,并被安全地验收。
下次我会怎么做
下次再遇到“非 root 批量改草稿”这类问题,我会先把路线画成 5 层,再决定要不要深入逆向。先确认源码能力,但不会把函数名当成用户功能;再构造一个不碰真实数据的最小输入,确认文件通道;紧接着立刻做真机入口遍历,而不是先投入更多时间完善包格式。
如果入口存在,我再验证应用是否消费以及最终草稿是否可编辑。如果入口不存在,我会直接把结论写成架构性阻塞,并说明已通过的层、缺失的层和可能的替代路线。这样,停止不是“什么都没做”,而是知道继续投入不会改变当前判断。
我也会把“代码可行”和“产品可用”放在验收表的两列里。前一列记录静态和离线证据,后一列记录真实界面、权限条件和结果证据。只有两列都满足,才会把路线交给后续批量化。技术工作最有价值的时刻,有时不是找到一条能绕过去的路,而是尽早确认这扇门根本没有开,并把时间留给真正存在的入口。