WorkBuddy + REA 逆向框架:从一个不给导出的 App 里拿回 936 条打卡记录

文章目录

今天躺在沙发上玩手机的时候,忽然想起 REA 这个框架。之前一直是「知道有这么个东西」,没真用过,就想试试。

正好电脑上 WorkBuddy 开着,我琢磨了一下:是不是可以直接让它来试?

说试试就试试。

于是我打了句话过去——帮我把 REA 装起来,逆向一个 APK,我记在里面的日志想导出来。剩下的装环境、找工具、反编译、判断下一步,全交给它。

我原本以为这是个半小时的活。结果花了 94 分钟,中间走了四条死路。

这三张是手机上的原始对话,从左到右:开工、收到 APK、判断能不能抓包。

手机上的三段对话:设置 REA 环境、确认 APK 已收到、判断抓包可行性

截到这儿我就换到电脑前了,后面全程是桌面端在跑,所以下文再没有手机截图。

先说我要拿什么

我有个习惯,每天在手机上一个很素的 App 里打卡,断断续续两年半,攒了九百多条记录。想导出来做点统计,看看哪几个月断得最多,顺便留个备份。

翻了半天设置,只有一个「云备份与恢复」,点进去是这样:

云备份与恢复:只有一个上传到服务器的备份,没有导出成本地文件

一句话:数据在它服务器上,你可以恢复,但你不能拿走。

这其实挺常见。很多小厂 App 都是这个思路,备份是为了换手机不丢数据,不是为了让你导出。但数据是我自己写的,我总得有个办法。

第一步:REA 装好了,但它自己的后端跑不起来

REA 的仓库是 morluto/rea,npm 包叫 rea-agents,定位是给 AI 客户端提供逆向能力的 MCP 服务——153 个命令、207 个工具,APK 分析底层依赖 jadx 和 JDK。这个概念正好和 WorkBuddy 对得上:它不自己写分析代码,而是把逆向工具编排成一套可调用的能力。

WorkBuddy 接手之后先摸环境,摸出来两个前提都不满足:Node 22 有了,JDK 和 jadx 都没有。

装的过程两个坑都跟沙箱有关:

brew install jadx  →  失败
原因:需要写 /opt/homebrew/var/homebrew/locks,被拒绝

改成便携版,直接把 JDK 和 jadx 解压到工作目录,不写系统目录也不提权:

curl -fL -o jadx.zip https://github.com/skylot/jadx/releases/download/v1.5.6/jadx-1.5.6.zip
curl -fL -o jdk.tar.gz "https://api.adoptium.net/v3/binary/latest/21/ga/mac/aarch64/jdk/hotspot/normal/eclipse"
unzip -q jadx.zip -d jadx && tar xzf jdk.tar.gz -C jdk --strip-components=1

GitHub 直连超时,得走本地代理;Adoptium 反而能直连。

然后是第一个真正值得记下来的发现:REA 的 APK 分析并不调用系统里的 jadx 命令。 它要一个叫 jadx-headless-mcp 的 jar,通过环境变量 REA_JADX_MCP_JAR 指过去,官方文档明确写着「这个我不负责下载安装」。

jar 找到了,也指过去了,rea doctor 的状态从 missing_analysis_engine 变成了 config_drift——说明它认出来了。但再往下就卡住:

rea doctor  →  spawn EPERM

它启动 java 的时候要经过一个临时进程,这一步在受限环境里被拦掉了。

所以这次的实际分工是:REA 提供思路和工具清单,WorkBuddy 负责动手,真正跑的是 jadx 加一个自己写的侦察脚本。 这不是 REA 的问题,是环境限制;但它说明一个事——把「框架能用」当成「流程能跑」,中间还差一层验证。

第二步:先搞清楚对手长什么样

逆向 APK 的第一步不是反编译,是看清单。代码可以加固、可以混淆,但 AndroidManifest.xml 和 resources.arsc 是系统要读的,壳动不了它们。

jadx -d out/app --no-res sample-1.7.1.apk

结果很有意思:

反编译 Java 文件数:12

一个 8.5MB 的 App,正常怎么也得几百上千个类,只有 12 个文件就说明一件事:它加固了。

看目录也对得上,libshella-4.6.2.2.so、libshell-super、com.wrapper.proxyapplication,这是腾讯乐固的典型特征。业务代码被加密在 dex 里,运行时由 native 库解密再加载,静态分析拿不到。

但乐固只保护代码,不保护资源。所以清单和资源文件里的信息反而是最干净的:

从哪来拿到了什么
Activity 名HabitTrackerActivity、TimingLogsActivity、SiginNotesActivity、GesturePwdActivity、WhiteAppListActivity
资源 idsignins、signinlist、note、signinpics、timingtype、tottime、todo、tags、createbackup、backupfile
字符串「云备份与恢复」「创建云备份防止数据丢失,您可以在任何时刻一键恢复数据」

拼起来其实已经把数据结构说清楚了:打卡项 → 每次打卡的时间 → 备注文字,另外还有计时和待办。

还有一个关键字段:

allowBackup = false

第一遍我把它当成 <manifest> 的属性去读,得到「未声明,默认 true」,还挺高兴,以为有戏。后来发现它是 <application> 的属性,实际值就是 false。这一条直接判了 adb backup 的死刑。

顺带说,这个 App 的 targetSdk 是 30,允许明文 HTTP。当时我以为「允许明文」是突破口,后来发现不是。

第三步:抓包,看起来最像技术流的那条路

我手机没 root,所以在「拿本地文件」这条路上基本是封死的:Android 从 4.0 起就不允许读 /data/data/<包名>/,除非 App 自己声明 debuggable,它没有。

那就只剩抓包。逻辑是:既然只有云备份,那我就抓备份时的请求,看能不能直接把数据截下来。

我:点过备份,我手机没有 root,如果抓包我应该怎么做?

WorkBuddy:你现在这种情况,抓包大概率不是最短路径。不过在给结论之前,我先做几个探测,结果直接决定抓包是否可行。

探测的结果不太好。服务器 80 和 443 都不通,8080 开着但响应不像 API。真正的接口地址,是后来从手机流量里才看到的。

抓包环境用 mitmproxy 官方 macOS 二进制。中途 pip 装依赖卡了六分钟没动静,换二进制包就快了——这和前面 brew 失败是同一类教训:受限环境里,能下载就不要用包管理器。

然后手机上设代理、点一次备份。日志刷出来是这样的:

[20:54:25][192.168.31.14:47384] Client TLS handshake failed.
The client does not trust the proxy's certificate for 203.0.113.10:9111
[20:54:28][192.168.31.14:47390] Client TLS handshake failed.
The client does not trust the proxy's certificate for 203.0.113.10:9111
[20:54:43][192.168.31.14:34038] Client TLS handshake failed.
The client does not trust the proxy's certificate for 203.0.113.10:9111

三十多次握手,一次都没成功。但日志漏了一个非常有用的信息:真实接口是一个裸 IP 加非标准端口 9111。不是域名,不是 443——这就是为什么前面扫端口什么都没扫到。

接下来要分清它到底是「不信任我的证书」,还是「只信任它自己的证书」。用 openssl 直连那个端口,把服务器返回的证书拉下来,和 APK 里自带的那张对比:

openssl s_client -connect 203.0.113.10:9111 < /dev/null \
  | openssl x509 -noout -subject -dates -fingerprint -sha256

结果:同一张证书。 主体和签发者是同一个缩写(O=XXX, CN=XXX),深圳,2026 年 8 月签发,有效期十年。这张自签证书就打包在 APK 的 assets/srca.crt 里。

也就是说,它不认系统的证书链,只认包里写死的那一张。这是标准的证书绑定。

我:我已经点击,提示请求失败

WorkBuddy:结论已经可以确定了:服务器返回的证书就是 APK 里那张证书。抓包这条路走不通,而且属于硬性走不通——就算把证书装进系统证书区(需要 root),它也只会认包里那一张。

到这里抓包彻底出局。这里有个容易被忽略的点:很多人以为「装上证书就能抓」,其实得先问三个问题——targetSdk 是不是 ≥ 24、有没有 network_security_config 放开 user 证书、有没有做证书绑定。三个里有一个不满足,装证书就是白装。

第四步:模拟器,唯一剩下的路

抓包不行,本地又没权限,剩下的方案其实只有一个:找一个我能拿到 root 的 Android 环境,在里面用我的账号把云备份恢复下来,然后直接把数据文件拷出来。

不需要解流量,不需要绕证书,因为数据最终一定会落到那个环境的私有目录里,而那里我能读。

这个环境就是模拟器。Android 11,google_apis 的 arm64 镜像,adb root 直接可用。

准备过程比想象中麻烦,说两个真正会卡住人的:

一是 AVD 建不出来。 模拟器默认把 AVD 放在 ~/.android,在受限环境里写不进去,报 Failed to save hardware-qemu.ini。解法是把三个环境变量全部指到工作目录:

export ANDROID_EMULATOR_HOME=$PWD/tools/android-home
export ANDROID_AVD_HOME=$PWD/tools/android-home/avd
export ANDROID_PREFS_ROOT=$PWD/tools/android-home

二是模拟器会被后台任务回收。 一开始写成脚本里 emulator & 然后返回,结果父 shell 一退出,qemu 进程跟着没了。改成让模拟器本身作为常驻前台任务跑,才稳。

起来之后:

adb root
adb install sample-1.7.1.apk
adb shell monkey -p $PKG -c android.intent.category.LAUNCHER 1

App 是加固过的,我本来担心它检测模拟器或者直接闪退,结果并没有,乐固在 API 30 的 arm64 镜像里正常工作。打开之后是登录页,我用自己的账号登进去,回到设置页,还是那个「云备份与恢复」。

第五步:先看清楚数据在哪,再动手

在点任何按钮之前,先看它的私有目录:

adb shell ls -la /data/data/$PKG/files/
adb shell ls -la /data/data/$PKG/databases/

结果如下:

路径是什么
files/signin.dat主数据,671 字节(此时是空状态)
databases/Account.db账号库,User(uid, mobile, nickname, vipexpire)

没有 SQLite 主库,没有 SQLCipher,没有加密的 blob。主数据就是一个 signin.dat。

把这 671 字节拉下来看了眼文件头,是 zlib:

import zlib, json
data = json.loads(zlib.decompress(open("signin.dat", "rb").read()))
print(list(data.keys()))   # ['signins', 'timing', 'timing2', 'todos', 'ver']

zlib 压缩的 JSON,完全没加密。 这一下就轻松了,不用管什么密钥派生、什么自定义格式。

第六步:点「一键恢复」,然后别碰「一键备份」

这一步有风险。

云端那份备份是 2026-10-21 21:39:15 的,这是我手机里唯一一份真实数据。模拟器里现在是空的,点「一键恢复」只会把云端的覆盖到模拟器的空数据上,安全。

但反过来,如果在模拟器里点「一键备份」,就会用这份空数据把云端覆盖掉,那就真没了。

所以整个操作我只做了一半:只恢复,不备份。恢复完之后立刻验证:

adb pull /data/data/$PKG/files/signin.dat ./signin_restored.dat
压缩后:6656 字节
解压后:64523 字节
文件 mtime:2026-10-21 21:39

文件时间戳和云端备份时间对上了,说明拿到的就是从云端恢复下来的那份,没有串。

拿到了什么

九百多条记录,五个打卡项,七十九条带文字备注,跨度从 2024 年 5 月到 2026 年 10 月:

指标数值
打卡记录936 条
打卡项5 个
带文字备注79 条
时间跨度2024-05 ~ 2026-10
最长连续(单项)126 天

单条记录的结构很简单:

{
  "date": 20261021,
  "finish": true,
  "amount": 10,
  "star": 0,
  "note": "今天状态不错"
}

有两个意外发现:

一是计时和待办是空的。 我平时基本只用打卡,这两个模块一直没用过,导出来自然是空的。这也说明一件事:备份是全量字段,只是你没填的字段本来就空着。

二是打卡配图不在备份里。 备份只传文字,图片没有一起上传。所以如果你也用过这类 App,别指望云备份能帮你保住图片。

数据长什么样

九百多条记录,光看表格没感觉。我把其中一项单独拎出来画了张图——不是次数最多的那项,最长连续也不是它,一共 275 次:

单个打卡项的两年半:日历热力图、每月次数、累计曲线

三块图是同一件事的三个切面:

  • 上面是日历热力图,一行是一周,一列是一天。颜色从青到橙只表示时间从左往右走,不代表完成质量——每条记录都是「完成」,没有强弱之分。
  • 左下是每月次数,2024 年 5 月到 12 月几乎没断,最长连续 48 天。
  • 右下是累计曲线,斜率就是速度。中间那些平掉的地方,就是停下来的日子。

数据自己会说话。2025 年一整年只有 33 次,其中 24 次挤在头一个月,之后断了大半年,5 月回来 7 次,然后一直到 2026 年 3 月才重新开始。这种断层,光看「总共 275 次」是完全看不出来的,看平均值更看不出来。

这也是我坚持要拿原始数据、而不是截几张图的原因:截图只能证明「我做过」,原始记录才能告诉你「什么时候没做」。

导出成文件

拿到 signin.dat 之后,剩下的就是写脚本。核心就几行:

import zlib, json

raw = zlib.decompress(open("signin_restored.dat", "rb").read())
data = json.loads(raw)

for habit in data["signins"]:
    for item in habit.get("items") or []:
        print(habit["name"], item["date"], item.get("note", ""))

三种格式一起生成:Markdown 按打卡项分组、备注按日期倒序汇总,适合直接读;CSV 带 BOM,Excel 打开不乱码;JSON 保留原始结构,以后想换格式从这儿改。

其中 Markdown 那份读起来最像「日志」,因为备注都按时间排好了。备注大多是当天做了多久,偶尔会写两句身体感受。这些细节是打卡数据里唯一带情绪的部分,反而比数字有价值。

几条我自己的判断

反编译只能告诉你数据存在哪,不能帮你把数据拿出来。 这次静态分析的全部价值,是让我提前知道「数据在 files/signin.dat、是明文的、只有云备份没有本地导出」。但没有一步能替代真正去触达设备。

加固不是最难的,证书绑定才是。 乐固挡住了代码,但清单和资源照样把数据结构泄露了。真正让整条路封死的,是那张打包在 assets/ 里的自签证书。

数据格式往往比你想的朴素。 两年半的打卡史,64KB,没有 SQLCipher,没有 AES,就是 zlib 加 JSON。小厂 App 大多如此,别一上来就假设有加密。

风险最大的不是技术操作,是那个「一键备份」按钮。 云端是唯一一份真实数据,恢复可以随便试,备份不能乱点。

这些都不是知识题,是环境题。 94 分钟里真正花在「技术」上的时间不多,大部分花在试错上:brew 能不能用、证书为什么被拒、模拟器为什么被回收。这类问题没有标准答案,只能一边撞一边改。

如果你也想导出自己的数据

按这个顺序判断,能省不少时间:

  1. 先看 allowBackup。true 就有机会走 adb backup,false 直接跳过
  2. 看有没有自带的「导出/分享」功能。有就别折腾了,直接用它
  3. 看 App 里有没有「云备份」。有的话,模拟器 + 恢复是最省事的路子
  4. 只有上面都不行,才考虑抓包。而且要先确认 targetSdk、network_security_config、证书绑定这三件事
  5. 手机有 root 的话,前四条都不用看,直接 adb pull 就完了

最后说一句边界:这篇记录的是导出我自己账号里的数据,不涉及绕过付费、破解授权,也不针对任何人的数据。技术上能做的事,和应该做的事,是两回事。

数据一直在你手机里,只是被放在了你看不见的地方。找它不需要破解,需要的是知道它在哪。

最后说件事:这篇里大部分活不是我干的

得把话说清楚:在这次之前,我没有任何逆向经验。 我知道的大概就两件事——数据可能在手机本地、抓包和模拟器这两个词。真要问我 jadx 怎么用、证书绑定怎么绕、AVD 建不出来怎么办,一句都答不上来。

这件事从头到尾,动手的是 AI。我做的事只有一件:在它给的选项里选一个。

它说「adb backup 这条路封了,要不要试抓包」,我说试;它说「抓包也行不通,只剩模拟器」,我说装;它要我打开手机 Wi-Fi 设代理,我就去设;它要我在模拟器里登录账号,我就登录。连最后那下「一键恢复」都是它替我点的——我只负责在旁边看着,确认它没点错隔壁那个「一键备份」。

跑完之后我确实有点被震到。不是因为它会写代码,写代码这件事早就不稀奇了;是因为它能自己发现路走不通,然后换一条。brew 装不上就换便携版,pip 卡住就换二进制,证书被拒就去比对指纹,模拟器被回收就改成常驻。这些判断没有一个是我提醒它的。

当然它也有判断错的时候,allowBackup 那次就是——它把属性读错了层级,还高高兴兴地告诉我「默认 true」。所以更准确的说法是:它负责试,我负责验收。 一篇稿子写完总要有人签字,我签的就是这个字。

至于你现在读的这篇,素材整理、结构、代码片段、那张可视化图,也几乎都是 AI 做的。我改了几处措辞,删了几句口水话,把 App 名字和具体习惯拿掉了,剩下的部分是它写的。

参考链接

  • REA——把逆向能力编排成 MCP 工具的开源项目,本文的主角
  • rea-agents——REA 的 npm 包,npx -y rea-agents@latest 即可运行
  • jadx——dex 反编译工具,本文真正干活的那个
  • jadx-headless-mcp——REA 分析 APK 时依赖的 jar,要单独下载
  • mitmproxy——抓包代理,本文用它确认了证书绑定
  • Frida——绕过证书绑定和 root 检测的常规手段,需 root,本文没用上
  • Android 网络安全配置——解释了为什么 targetSdk ≥ 24 时用户证书不生效
  • Android 模拟器命令行——AVD、启动参数、环境变量都在这里查
  • sdkmanager——下载系统镜像和 platform-tools