一个 LSPosed 模块,面向高校「校园跑 / 阳光晨跑」类 App(以 运动世界校园 com.zjwh.android_wh_physicalfitness 为主要适配目标)提供可调的位置与步态模拟,并附完整的地图选点界面。
项目本身是一份踩坑记录。核心价值不在代码量,而在 docs/ 里那些「为什么不能那么写」——它们每一条都是实际跑崩过、被判定无效过、或者静默失效过之后才定下来的。
合规提示 本仓库以 GPL-3.0 发布,参考了 LocationSpoofer(GPL-3.0, Copyright (C) 2026 SuseOAA)的实现思路,详见 LICENSE 与 NOTICE。 仓库不包含任何第三方 APK、LSPosed 管理器/守护进程二进制,也不包含任何签名密钥。 使用本模块修改运动数据可能违反所在学校的学术诚信规定与目标 App 的服务条款。仓库作者不对由此产生的任何后果负责。请仅在自己拥有或已获授权的设备与账号上、以技术研究与自测为目的使用。
学生的实际需求通常是「不想在操场上绕圈,但希望记录正常」。直接改 GPS 是不够的——校园跑类 App 的判定链远不止坐标:
| 判定维度 | App 实际拿什么 | 纯改坐标的结果 |
|---|---|---|
| 位置 | 系统 LocationManager + GNSS(App 里的高德 SDK 只用来画地图,不用来定位) |
只改一次坐标不管用,它会持续回调 |
| 里程 | Location.distanceTo() 逐点累加 |
两侧取到同一时刻坐标 → 恒为 0 |
| 步数 | 加速度计波形数峰(采样 ≈15 Hz) | 1 Hz 推送还原不出 2.75 Hz 步态 → 步频 0 |
| 配速 | 服务端用 SpeedPerTenSec 序列独立重算 |
速度与轨迹不自洽就判异常 |
| 有效性 | libswsport.so 的 isValidPoint 四点评分 |
只伪造坐标,评分维度缺项 |
| 环境 | GNSS 卫星状态 / WiFi / 基站 / mock 标记 | 任一维度露馅即风控 |
所以本模块做的是一个自洽的整套输入:轨迹连续推进、速度与步频互相反推、加速度波形与步数对齐、GNSS/WiFi/基站环境补齐、反作弊标记清理。
- Android 13+(targetSdk 34),已 root
- KernelSU / Magisk + Zygisk Next(或 ReZygisk)
- LSPosed 2.2.1 及以上
⚠️ LSPosed 2.2.1 不认 legacy Xposed API 82。本模块使用 libxposed 新格式(META-INF/xposed/,API 101+),详见 docs/01-环境准备.md。
完整手把手流程见 docs/06-完整使用教程,这里只是最短路径。
-
从 Releases 下载
RunSpoof-v*.apk,安装 -
LSPosed 管理器 → 模块 → 启用 RunSpoof
-
作用域只勾选目标 App(
com.zjwh.android_wh_physicalfitness) -
★ 建配置文件(不做这步后面全白费):
adb shell "touch /data/local/tmp/runspoof_route.txt; chmod 666 /data/local/tmp/runspoof_route.txt" -
强制停止目标 App 后重新打开(
autoHotReload=true通常无需重启手机)
第 4 步为什么必须做:编辑器跑在模块自己 uid 下、注入代码跑在目标 App 的 uid 下,而 Android 10+ 的 per-app data isolation 让注入进程根本看不到模块的数据目录。
/data/local/tmp是唯一两边都可见的地方,但它的权限是drwxrwx--x且属主 shell,普通 app uid 建不了文件——只能写一个已经存在的文件。所以必须由你先建好并放开权限。漏了这步,模块界面会弹框提示「配置没能生效」并给出这条命令。
打开 RunSpoof 路线(模块自带的地图界面)。这是一个路线编辑器,不是只能填数字的表单:
| 操作 | 效果 |
|---|---|
| 点地图空白处 | 追加一个路径点 |
| 拖动已有顶点 | 微调该点位置 |
| 长按顶点 | 删除该点 |
| 拖动地图 | 只移动视野,不动路线中心 |
| 定中心 → 点地图 | 把该点设为路线中心(红色三角) |
| 跑道圈 | 以当前中心 + 当前半径一键生成 24 点标准跑道形状(两条直道 + 两个半圆) |
| 撤销 | 回退一步(保留 50 步) |
| 清空 | 清掉路径 |
| 保存配置 | 才真正写盘生效 |
画布上会实时显示:实心折线(你的路径)、虚线(自动闭合回起点)、编号顶点(起点为绿色)、红色三角(路线中心),以及顶部「路径 N 点 | 绕一圈 X 米」。
只有按「保存配置」才会写盘。 编辑过程中的所有改动都只在内存里,误触不会改掉已保存的配置。
坐标系说明(重要):编辑器底图是高德瓦片,所以你点出来的经纬度是 GCJ-02(火星坐标);而 Android 的
Location和 App 的判区用 WGS-84。模块在读配置时会自动做 GCJ-02 → WGS-84 转换(Geo类),高德那一侧的 hook 再转回 GCJ-02。你不需要手动换算,直接在地图上点就行。 实测这一步差约 370 米,不做的话轨迹会整体偏出跑道。详见 docs/00 的 B0。
四个参数可随时改,改完立刻显示换算结果:
| 参数 | 可调范围 | 说明 |
|---|---|---|
| 半径 | 5 – 200 m | 用「跑道圈」时的半圆半径;直道长度取 2×半径 |
| 速度 | 1.9 – 5.4 m/s | 实时显示对应配速,须落在你校的配速区间 |
| 步频 | 45 – 290 SPM | 实时显示对应步幅 |
| 海拔 | −500 – 9000 m | 建议填。留空会用兜底值,但「所有人上报同一个海拔」本身就是可被聚合的特征 |
为什么海拔要自己填:App 记录的每公里爬升(
elevationGain)与海拔 (endAltAbs)都取自Location.getAltitude()。如果这个值是个常量, 那么每一圈的爬升都恰好是 0 m、海拔一个数都不变 —— 真人跑步不会这样, 而且这类「恒定值」不需要任何外部参照就能被单字段判出来。 填成自己所在地的大致海拔即可(手机自带指南针/地图 App 都能看到)。 详见 docs/07。
超出范围的值在保存时会被自动钳制到区间内 —— 手填一个出界值跑完也是白跑,静默修正比让成绩作废好。
| 参数 | 默认 | 说明 |
|---|---|---|
| 中心点 | 0.0, 0.0 |
没有任何内置坐标。0,0 = 「还没设过」,打开编辑器画一遍才会变成真实位置 |
| 半径 | 39 m | 生成一圈约 398 米,接近标准 400 米跑道 |
| 速度 | 2.4 m/s | ≈ 6'57"/km,慢跑姿态,不贴任何区间上下限 |
| 步频 | 172 SPM | 与 2.4 m/s 自洽的慢跑步频 |
这些值怎么换算、怎么按自己学校的规则调整,见 docs/03-判定规则怎么摸.md。
为什么不留内置坐标:每个学校的有效跑区都不一样,内置任何一处真实坐标都只对那一所学校有意义,对别人反而是错的。而且 0,0 一眼就能认出「配置没读到」,比留一个看起来像真坐标的占位值安全。
关于本文档的数据口径:本仓库不记录任何一次真实跑步的实测数据(场次 ID、里程、配速、步频、时间戳、日志数值等),也不记录设备标识、账号信息或坐标。下面只描述功能是否走通与机制是否成立,具体数值一律不给。这样做既是为了脱敏,也是为了避免把一次偶然的观测当成结论——本仓库此前吃过几次「拿单场数据下普遍结论」的亏,详见 docs/00。
以下环节均已在 LSPosed + 目标 App 的真实运行环境下走通:
| 环节 | 判定依据(定性) |
|---|---|
| 模块注入 | logcat 出现完整的 hook 安装日志,各 hook 无异常 |
| 配置读取 | 文件通道读到配置,路径点数与编辑器一致 |
| 坐标系转换 | 转换前后轨迹在跑道上的位置从整体偏移变为贴合;GCJ-02 → WGS-84 的偏移在数百米量级 |
| 热重载 | 改配置文件后无需重启 App 即可生效 |
| 进入计分跑 | 点 GO 能进入计分跑步 Activity |
| 轨迹位置 | 界面地图上轨迹贴合操场跑道轮廓,「不在有效区域内」提示消失,打卡点全部落在跑道上 |
| 打卡点通过 | 进度能完整推进到「已打卡全部点位」 |
| 配速/步频被采信 | App 界面实时显示由模块生成的配速与步频 |
| 里程累计 | 里程随运行时间正常累加 |
也就是说:位置、步态、打卡、上传四件事都通了。
坐标系这一条是最后修掉的:早先版本配置读对了、日志也对,但轨迹整体偏移、App 判「不在有效区域内」。原因是编辑器取的是高德瓦片(GCJ-02),而
Location要 WGS-84,代码里没有转换。修完后同一份配置立刻就对齐了,见 docs/00 的 B0。
曾有一场计分跑:打卡全过、里程与配速步频全部达标,结果页只报一条「分段不达标」。
从 App 自己记录的 segment_json 定位到根因:分段边界在两条相差数百秒的
时间线之间来回跳,算出远超人类速度的「鬼魂分段」,
并被打了 state:256 异常标记。污染源是模拟器假 GPS 塞进来的
Location 自带 getTime(),而模块按 getTime() 反查坐标。
完整推导见 docs/00 B6。
⚠️ 一处更正:这一节早先写过「客户端直接拒绝上传(upload_success=0)」。 复查发现那只是上传过程中的中间态。判定为「分段不达标」的依据是结果页那条 原因 +segment_json里的state:256标记,不是上传失败。这个仓库此前 栽过一次「把不同场次的结果页混成一场」(docs/00 G3),所以这里把话收窄。
用仓库里真实的 RouteEngine + GaitEngine + 同一份 19 点配置(经真实
Config.parse() 解析),同一段时间、同一组被污染的时间戳,只改「位置按什么
时间反查」:
| 异常段 | 最大速度 | |
|---|---|---|
旧版(位置取被污染的 getTime()) |
出现 | 远超人类速度,约 20+ m/s 量级 |
| 修复后(位置取干净递进时间) | 0 | 落在设定速度附近,正常 |
修复后同一窗口的总距离与「设定速度 × 窗口时长」在路线曲率的误差内吻合。
| 项 | 状态 |
|---|---|
Clock 不变式 |
8 条断言全过:纯函数性(同一入参任何时刻同一输出)、单调、1s 间隔精确、60s 无漂移、历史 fix 不被钳、垃圾值可重入 |
| 旧实现缺陷 | 已实证:同一 fix 两次读得到相差极大的时间,会破坏 distanceTo() |
| 派发节拍(真机) | TIMELINE 自检连续多周期判定为 CLEAN:无负间隔、无大间隔、minGap/maxGap 贴近派发周期 |
| 无递归崩溃 | StackOverflow 0、FATAL 0(getTime() 已接管,timeOf() 内不再调它) |
| 设备墙钟 | 与宿主机同源,绝对时间正确,不会触发 G1 |
Clock兜底不纯:旧写法遇到「未来值」会钳到当前时刻,导致同一Location在不同时刻读到不同时间 → 坐标漂移 →distanceTo()算错。 改为返回固定anchorWall,仍是纯函数。- 一次 tick 造多条 fix:位置派发里
buildLocation()曾在 listener 循环内, 同一秒 N 个 listener 各拿一条时间戳不同的 fix(本该是同一次定位)。 已提到循环外,一次 tick 只造一条。
顺带澄清一个自己造的假警报:曾在
timeOf()里统计「相邻两次时间戳是否 递增」,打出大量SUSPECT。那是指标错——timeOf()每秒被 调用极多次(App 反复读属性),跨对象比较顺序天然会乱。改成在派发点 统计后即为 CLEAN。负面结论前先确认探测手段本身可用(同 docs/00 §10.5 纪律)。
修复后连续多场真实计分跑,全部 upload_success=1,分段数据无异常。
判据全部满足:分段边界严格递增(startTime < 上次 end 恒成立)、
无超出人类速度的鬼魂段、无 state:256、打卡点 isPass 全 true。
修复前存在问题的场次同样保留在记录中作为对照,因此可以说: 「分段不达标」已消失。
注:期间有一场曾被误操作中断(用
monkey -c ...LAUNCHER测触摸注入, 那条命令会以 LAUNCHER intent 重启 Activity,把进行中的跑步打回首页)。 中断后 App 仍落盘了该记录,尾段带一条畸形补写。这条不是模块缺陷, 是中断会话的产物。 教训:不要用monkey带-c LAUNCHER做交互探测。
Location(Parcel)构造路径未接管:nemu 的 Location 经 binder 进 App 时, 其字段(nativedistanceTo直读)仍是 nemu 的坐标。 目前靠 getter 接管兜住;实测未出现异常,暂不构成问题。
.\build.ps1 `
-Sdk 'D:\toolchain\android-sdk' `
-Jdk 'D:\toolchain\jdk17' `
-ApiJar 'D:\path\to\libxposed-api102-classes.jar'脚本会走完 javac → d8 → aapt2 link → jar uf → zipalign → apksigner 并自校验 APK 条目。
依赖(需自行准备,仓库不含):
- JDK 17
- Android SDK,
build-tools;34.0.0+platforms;android-34 - libxposed API 102 的
classes.jar(从 LSPosed 发布包内提取,或io.github.libxposed:api)
构建细节与坑见 docs/02-构建与打包.md。其中最要命的一条:META-INF/xposed/ 必须用 jar uf 塞进 APK 根目录,用 aapt2 -A assets 会因 Windows 反斜杠路径导致 LSPosed 完全不识别。
.
├── AndroidManifest.xml # 模块 manifest(含 xposed meta-data)
├── assets/META-INF/xposed/ # ★ LSPosed 模块声明(必须进 APK 根目录)
│ ├── module.prop # API 版本声明
│ ├── scope.list # 默认作用域
│ └── java_init.list # 模块入口类
├── src/com/qf/runspoof/
│ ├── RunSpoofModule.java # 主入口:全部 hook
│ ├── RouteEngine.java # 轨迹引擎:时间 → 连续坐标
│ ├── GaitEngine.java # 步态引擎:加速度波形合成
│ ├── Factory.java # 反射构造 Location/SensorEvent/GnssStatus/...
│ ├── ConfigStore.java # 配置读写(/data/local/tmp 文件通道)
│ └── MapPickerActivity.java # 地图选点 UI
├── build.ps1 # 一键构建
├── docs/ # ★ 踩坑与流程文档(本项目最有价值的部分)
├── LICENSE # GPL-3.0
└── NOTICE # 上游归属声明
| 文档 | 内容 |
|---|---|
| 00-踩坑总览 | 先读这个。全部已踩过的坑与结论索引 |
| 01-环境准备 | KernelSU + LSPosed + Zygisk 配置,LSPosed 起不来的排查 |
| 02-构建与打包 | 手搓构建链、APK 打包、签名 |
| 03-判定规则怎么摸 | 怎么摸清本校规则,以及规则怎么映射成参数 |
| 04-模块实现原理 | 每一类 hook 为什么这么挂 |
| 05-模拟器与虚拟化 | MuMu / Hyper-V / HVCI 踩坑,以及为什么建议真机 |
| 06-完整使用教程 | ★ 从零到跑起来,按步骤走。第一次用先看这篇 |
| 07-第三轮-恒定值与跨通道一致性 | 为什么「恒定值」和「跨通道不一致」比数值偏差更危险;海拔/精度/计步双通道的改法;以及一个必须避免的统计陷阱 |
-
设备时间必须是准确的北京时间,否则成绩传不上去
⚠️ 最容易忽略的一条 曾观察到:整场计分跑跑完、打卡全过、里程与步频都正常,界面底部显示「正在上传跑步数据…」,但 App 弹出:为了您跑步记录的有效性,请修改为准确的北京时间后再上传记录
模拟器默认时钟和真实时间差了几个小时,App 拿它跟服务端下发的跑区时间段比对,对不上就拒绝上传。把设备时钟校准到北京时间后重跑,成绩正常上传成功。
adb shell "settings put global auto_time 0" adb shell "date MMMDDhhmmYYYY.ss" # 例:date 101909012026.21 = 10/19 09:01 adb shell date # 确认
顺带纠正一个曾经的错误结论:早期版本这里写过「打卡点要求按 1→2→3→4→5 顺序通过,本仓库做不到」。那是把一次 0.00 公里的旧记录(配置还没通,根本没进跑区)的结果页,当成了后来那场完整跑步的结论,属于张冠李戴。闭环路线实际能正常打卡、正常上传,不需要按编号重排路径。
-
只做端侧输入伪造,不保证服务端采信 静态分析(见目标 App 的 native 层)表明服务端会用
SpeedPerTenSec序列独立重算成绩。本模块把轨迹平滑度、配速合理性、步频-速度自洽度都按生理参数做到了自洽,但没有实机验证过判定阈值。 -
反射字段名按 Android 14/15 AOSP 编写
CellIdentityLte.mCi、CellSignalStrengthLte.mRssi、ScanResult.SSID等字段若在某版本改名,对应 hook 会静默失效(代码里全部 try-catch 包住,不会崩)。logcat 搜RunSpoof:可看到每条 hook 的成功/失败。 -
模拟器体验差 MuMu 等模拟器在宿主机开了 Hyper-V/HVCI 后会退化到 VirtualBox 的 Snail 纯软件模拟模式,LSPosed native 桥接会崩。详见 docs/05。强烈建议真机。
-
反作弊不止这些 目标 App 还有极验设备校验、网易易盾、天穹风控、人脸核身、姿态识别等。本模块只处理了「输入数据」这一层。
本项目仅供技术研究与学习,用于理解 Android 定位链路、传感器数据流、以及 LSPosed 模块机制。
- 不要用于代替真实体育锻炼
- 不要在未获授权的设备上使用
- 使用可能导致目标 App 账号被封禁、体育成绩被判无效、甚至触发学校纪律处分
- 作者不提供任何形式的担保,不对使用后果负责
如果你真的只是想要体育学分——去跑步,比调这个快。
- LocationSpoofer — hook 实现思路、
createSensorEvent反射技巧、GNSSBuilder用法(GPL-3.0) - LSPosed / libxposed — 模块框架与 API
- libxposed/api — 新格式模块声明规范