跳到正文
龚晨昊
返回

用 REA 逆向一款虚拟定位 App,把自己的免 root 定位工具做成

这篇记录 FakeGps 的开发过程,重点讲怎么用 REA(Reverse Engineer Anything)分析一个闭源 Android App,以及逆向结果能告诉你什么、不能告诉你什么。

先说结论:REA 帮我在半小时内把对方的核心逻辑看清楚了。照着它的行为对齐以后,我发现自己的实现里有几处多做的事,删掉以后,FakeGps 在小米 14 Pro 上可以正常使用了。

1. 背景

需求很简单:做一个免费、免 root 的 Android 模拟定位 App,在小米 14 Pro(HyperOS,Android 15)上能用。

Android 的做法是公开的:在开发者选项里把 App 选为“模拟位置信息应用”,然后调用 LocationManager.addTestProvider 注册测试定位来源,再用 setTestProviderLocation 不断推送坐标。核心代码几十行。

第一版很快就能用了,微信里的位置会变。但高德地图有个稳定复现的现象:打开时显示模拟位置,大约 5 秒后跳回真实位置。而市面上一款付费的虚拟定位 App(下文称 App X)在同一台手机上,用户反馈是正常的。

在没有真机日志的情况下,我改了好几版参数都没用。于是决定用 REA 看看 App X 到底做了什么。

2. 边界:逆向是为了理解,不是为了复制

动手之前先划清楚:

3. REA 是什么

REA 是一个面向 AI 编程助手的逆向工具集,以 MCP 服务器的形式接入 Claude Code、Codex、Cursor 这类 Agent,也可以直接在终端里用 CLI。它把 jadx、Ghidra、Hopper、apktool 这些工具包装成统一的命令,输出带证据(哈希、调用记录、原始结果)的 JSON,适合让 Agent 一步步追问。

对 Android 来说,REA 的 APK 分析走 jadx,常用命令有这几个:

命令作用
inspect-android-package解析 APK 的清单和声明,确认引擎能加载
search-android-classes按类名子串搜索(区分大小写)
inspect-android-class列出一个类的方法和字段,包括静态常量值
inspect-android-method反编译单个方法
trace-android-references找某个类或方法被哪里调用

4. 环境搭建

4.1 安装 REA 并接入 Agent

REA 要求 Node.js 22.x(≥22.23.4)、24.x(≥24.22.1)或 26.12.1 及以上。我的机器上是 26.12.0,差一个小版本,先升级:

brew upgrade node
node -v   # v26.12.1

然后运行 setup。建议先用 --dry-run 看它要改哪些配置文件:

npx rea-agents setup --all-detected --dry-run

它检测到了机器上的 11 个 Agent。我只给常用的几个配置:

npx rea-agents setup --yes \
  --client claude_code --client codex --client cursor --client gemini_cli --client pi

setup 会给每个被修改的配置文件生成 .rea.backup 备份,撤销起来很方便。验证一下:

claude mcp list | grep rea
# rea: npx -y rea-agents@6.8.0 mcp - ✔ Connected

4.2 准备 jadx 提供者

第一次跑 APK 分析会直接报错:

Set REA_JADX_MCP_JAR to the absolute path of a caller-supplied
jadx-headless-mcp 0.7.1 JAR. REA does not download or install it.

REA 不会自己下载 jadx 组件,需要手动准备两样东西:

  1. Java 17:brew install openjdk@17
  2. jadx-headless-mcp 0.7.1:下载地址可以在 REA 的 npm 包里找到,dist/android/JadxRelease.js 里写着固定的 release URL。
mkdir -p ~/.rea && cd ~/.rea
curl -L -o jadx-headless-mcp-0.7.1-all.jar \
  https://github.com/1013503897/jadx-headless-mcp/releases/download/v0.7.1/jadx-headless-mcp-0.7.1-all.jar
unzip -tq jadx-headless-mcp-0.7.1-all.jar && echo OK   # 一定要校验

这里踩了个坑:第一次下载走代理被截断,只下了 2.4 MB(完整的是 42 MB)。doctor 报的是 ZIP archive is malformed: End of central directory not found。大文件下完先 unzip -t 校验,APK 也一样。

设置环境变量,然后用 doctor 确认:

export JAVA_HOME=/opt/homebrew/opt/openjdk@17
export PATH=$JAVA_HOME/bin:$PATH
export REA_JADX_MCP_JAR=$HOME/.rea/jadx-headless-mcp-0.7.1-all.jar

npx rea-agents@6.8.0 doctor --format json | jq '.providerInspections[] | {id, available}'
# { "id": "jadx", "available": true }

healthy 仍然是 false,因为 Ghidra 和 IDA 没装,分析 APK 用不到它们,可以不管。

5. 分析过程

5.1 加载 APK

npx rea-agents@6.8.0 inspect-android-package /tmp/target.apk --format json > pkg.json

输出里有一段 loaded:

{ "state": "LOADED", "class_count": 8525, "resource_count": 1584 }

8525 个类,大部分是第三方 SDK(百度地图、广告、Bugly 等),自己写的代码只占很小一部分。

5.2 用类名缩小范围

search-android-classes 的第二个参数是位置参数,不是 --query(我第一次写成了 --query,报 Unknown flag)。查询区分大小写,所以大小写都要试:

for q in mock Mock Location Gps; do
  npx rea-agents@6.8.0 search-android-classes /tmp/target.apk "$q" --format json \
    | jq -c '.normalized_result.matches'
done

结果很说明问题:

服务类只有一个,范围一下子就缩小了。

5.3 看类的“目录”

npx rea-agents@6.8.0 inspect-android-class /tmp/target.apk \
  "com.example.service.MockGpsService" --format json \
  | jq -r '.normalized_result.methods[] | select(.name | startswith("access$") | not) | .reported_signature'

过滤掉编译器生成的 access$xxx 以后,剩下的方法名基本就是一份设计说明:

configureProvider(String, boolean, boolean): void
injectProvider(String): boolean
generateLocation(String): Location
recordProviderReadback(String): void
acquireWakeLock(): void
removeProvider(String): void
onStartCommand(Intent, int, int): int
onDestroy(): void
...

字段的静态常量值更有用,REA 会直接读出 dex 里的常量:

... | jq -r '.normalized_result.fields[] | select(.dex_static_value) | "\(.name) = \(.dex_static_value.value)"'
UPDATE_INTERVAL_MS = 10
READBACK_INTERVAL_MS = 5000
RETRY_INTERVAL_MS = 5000
HEARTBEAT_INTERVAL_MS = 60000

推送间隔是 10 毫秒,而我当时用的是 200 毫秒。这是第一个具体的差异。

5.4 反编译关键方法

inspect-android-method 的类名和方法名都是位置参数:

npx rea-agents@6.8.0 inspect-android-method /tmp/target.apk \
  "com.example.service.MockGpsService" configureProvider --format json \
  | jq -r '.normalized_result.source.text'

几个关键方法的结论如下(不贴原代码,只写行为):

5.5 REA 拆不开的地方,换 jadx CLI

推送循环在一个匿名内部类 MockGpsService$b 里。REA 的 inspect-android-class 对这个内部类返回的是空结果,search-android-classes 也搜不到它。

这时直接用 jadx CLI,按单个类导出(比全量反编译快得多):

brew install jadx
jadx -d out --single-class "com.example.service.MockGpsService" --show-bad-code target.apk

jadx 对这个方法的控制流还原得也不完整(Code decompiled incorrectly),但加上 --show-bad-code 后,主干还是能看懂的:

  1. 每次循环先检查 isLocationEnabled()
  2. 拿 wake lock
  3. 依次 injectProvider("gps")、injectProvider("network")
  4. 每 5 秒做一次 readback
  5. 成功的话 10ms 后再跑一次;全部失败就退到 5 秒

5.6 用全量反编译排除“暗道”

为了确认它有没有用到别的手段(root、Shizuku、VPN、无障碍),我做了一次全量反编译,然后用 grep 来排除:

jadx -d full --no-res --threads-count 8 target.apk     # 约 4 分钟,8232 个源文件
grep -rl "addTestProvider" full/sources                   # 只有 MockGpsService 一处
grep -rln "VpnService\|shizuku\|Runtime.getRuntime().exec" full/sources
# 只命中第三方 SDK 的工具类,App 自己的代码里没有

再用 aapt2 看清单,确认它没有声明 VPN 或无障碍服务:

aapt2 dump badging target.apk | grep uses-permission
aapt2 dump xmltree --file AndroidManifest.xml target.apk | grep -A3 "E: service"

结论:App X 只用了 Android 公开的测试定位接口,只接管 gps 和 network,不接管 fused,10ms 推送一次。没有任何隐藏通道。

6. 和自己的实现对比

项目App XFakeGps 早期FakeGps 1.4.0
推送间隔10ms200ms10ms
接管来源gps、networkgps、network、fusedgps、network
提供者属性都是低功耗 + 高精度gps 高功耗、network 低精度都是低功耗 + 高精度
位置字段accuracy=2、alt=55、bearing=1、satellites=7多设了 speed 和各种 accuracy对齐
wake lock有没有有
周期性重新注册没有1.3.0 加过,后来删了没有

照着这张表,我把行为全部对齐,代码是自己重写的。顺带发现自己 1.3.0 引入过两个问题:一是“每 2 秒删掉再重新注册来源”,删掉的那一下会留出空档;二是“自己去监听定位”,这会让系统打开真实的定位硬件。两者都可能把真实位置放进来,已经删掉了。

7. 对齐之后

对齐后的 1.4.0 第一次在手机上测试,高德仍然会在 5 秒后跳回真实位置。这时候我手里最有用的,是 App 自己导出的读回数据:

推送成功 gps=1453 network=1453 失败=0
推送中断: 次数=0
passive: mock=true 距目标=0m age=16ms
fused:   mock=true 距目标=0m age=1668496ms

passive 是全系统的“旁听”来源,系统发给任何 App 的定位它都能看到。这份数据至少证明了两件事:注入一直是成功的,推送一次都没断;App 也没有被系统冻结。所以问题不在“推不进去”,排查的方向就收窄了。

继续在真机上调试之后,现在 FakeGps 在小米 14 Pro 上可以正常使用。

8. 经验

关于 REA

关于逆向本身

关于工程


FakeGps 代码:https://github.com/yanqing7914/FakeGps。只用于开发测试,请不要用于考勤打卡、签到等需要真实位置的场景。



上一篇
从零搭建个人网站:域名、简历站与博客
下一篇
力扣 1. 两数之和:从暴力到单遍哈希的 C++ 演进