夜雨聆风学习资料网

ARTICLE · 1190452

安卓应用虚拟定位实现

安卓应用虚拟定位实现

LSPosed 定位 Hook 实现记录

前言

本项目是为方便我妈办公写的,未做多机型/系统适配,只在我自己的小米 9 上验证过。本文仅作开发过程记录,项目仅供学习交流,请勿用于非法用途。

功能

一个 APK 两个身份:安装后是控制端;被 LSPosed 勾选后是注入模块。

配置四条通道,谁新用谁:

system bridge  >  root 镜像  >  Provider 轮询(3s)  >  XSharedPreferences(任意进程)       (/data/system)     (binder对账)        (直读prefs文件)

改写两层:

  • • 系统层:作用域勾“系统框架”。在 system_server 改 LastKnown 和投递点,驱动 Pump,并掐掉 Wi-Fi 扫描、BSSID、基站事件、GNSS 原始数据,全机生效。
  • • App 层:勾谁装谁。改进程内 getter,给监听器推合成定位,堵 getCurrentLocation,并再堵网络定位源。

两层是逼出来的,见下文。

做法

核心:不逐个 hook 接口,找所有定位必经点。Android 8.1–11,LocationManagerService 向 App 投递定位,无论 requestLocationUpdates 还是 PendingIntent,最终都到 Receiver.callLocationChangedLocked。替换 args[0] 为合成对象,一次覆盖所有投递方式。Receiver 是内部类,类名需拼接:

val recv = XposedHelpers.findClassIfExists(lms.name + "\$Receiver", cl)deliverHooks = if (recv != null) HookUtil.hookAll(recv, "callLocationChangedLocked", DeliverHook(st)) else 0
SystemHooks 里的 DeliverHook

只改投递点不够:系统只有真定位结果才投递,室内、地下车库、GPS 未锁定时不送,被动 hook 不触发。因此用 HandlerThread 主动推(Pump),定时把合成定位投给所有非豁免 App 的注册。投递时镜像系统记账:间隔/距离过滤、appops 记账、单次计数递减、过期回收,否则 App 认为定位流不合法。投递线程挂 ThreadLocal 标记,避免自己的注入再次经过自己的 Hook。

Pump 的注入逻辑

关键取舍:只改“系统源”对象。高德 AMapLocation 继承 Location,内部换算大量 new Location 作中间量;无差别改 getLatitude 会破坏换算。因此只认 provider 为 gps/network/fused/passive。hasAltitude、hasSpeed、hasBearing、hasAccuracy 都不动:Parcel 按这些标志决定字段布局,只改一边会错位,高德读出乱码。

getter 与来源判断

光堵 GPS 不够。高德、微信还会读 Wi-Fi 扫描结果和当前 BSSID,连同基站信息传回服务器,用 Wi-Fi 定位库反查,绕开 android.location。因此:

  • • getScanResults 返回空表;
  • • getConnectionInfo 和 NetworkCapabilities.getTransportInfo() 中 BSSID 打码;
  • • TelephonyRegistry 摘掉基站监听位;
  • • GNSS 原始报文拒非系统 uid;
  • • ConnectivityService 中所有 NetworkCapabilities 分发都会过 location-sanitize 复制方法,在那打码即可全路径覆盖。方法名随版本变,按形状匹配兜底。
ConnectivityService 形状匹配

踩坑

微信、高德最初无反应。1.3.0 查下来四件事叠加:

  1. 1. LSPosed 作用域只勾了 Demo,模块没进微信/高德;
  2. 2. 当时是被动替换,系统不送定位就不动;
  3. 3. 网络定位源没断;
  4. 4. 无差别改所有 Location,破坏高德换算器。

四条一起错,光加接口没用,最后推倒重写,才有双层架构。

坐标用一阵卡死在旧值,改配置无反应。根因:SharedPreferences 落盘先写 xxx.xml.tmp 再 rename,system_server 的 XSharedPreferences 早持有旧文件描述符,rename 后仍读旧 inode,表现为永远读到“自洽的旧数据”——版本号、经纬度全对,就是陈旧。后加 root 镜像通道:su 原子写 /data/system/momclock_config.json,版本号单调拒旧,解决。

镜像通道加了仍不生效,su 弹窗都没有。原因:Magisk 静默拒 su;XSharedPreferences 那条“旧但自洽”能过版本检查,把 Provider 实时值挡在外面。于是做 state bridge:调 getLastKnownLocation("momclock.state"),system_server 识别特殊 provider 名,直接返回 Location,把配置快照塞进 extras。不用 root、不用签名、不用包可见性,微信这类第三方进程也能拿。注意 App 层 Hook 要豁免它,否则快照被改写逻辑吃掉。

最磨人的一次:前面都改完,用户仍说不行。拉全 logcat,全链路无 spoof active——每个进程读到的配置都是 enabled=false。根因是 Magisk 没给“打卡助手”root 授权,镜像通道没写进去;之前 su 被拒被当成“不影响”,实际致命。授权后立刻通。顺手加定案日志 config: enabled=true lat=.. lng=.. (pid),以后先看它,一眼知道各进程读到的到底是不是 enabled。

logcat 里冒出 app hooks installed in com.android.providers.settings 等包名。SettingsProvider、Telecom、NetworkStack 等“系统内部包”跑在 system_server,LSPosed 按包名发加载事件,代码就在里面装 App 层钩子,等于在 system_server 挂 App 逻辑,干扰系统。后按 processName 判断:android/system 直接跳,电话、蓝牙、Shell、NetworkStack 拉黑。

高德真假位置来回跳、微信一直真,1.6.0 才揪出:NetworkCapabilities.getTransportInfo() 带当前 Wi-Fi BSSID,App 拿去服务器反查;另外 getCurrentLocation 一次性定位进程内没堵,微信按需定位主路径走它。这两处补上才消停。

另两个零碎:

  • • Android 11 的 WifiServiceImpl 挪进 APEX,用独立 classloader,主 classloader 找不到。需 hook SystemServiceManager.startService,从加载 WifiService 的参数摸出 classloader。
  • • 系统层 Hook 全写防御式:找不到类、字段、方法就记日志跳过,绝不外抛;抛了崩的是 system_server。兜底写在帮助页。
帮助页里的救砖说明

安装与使用

  1. 1. 装 app-debug.apk,一个即可。
  2. 2. LSPosed → 模块 → 「打卡助手」→ 启用 → 作用域勾 「系统框架 (Android 系统)」,再勾目标 App(高德 com.autonavi.minimap / 微信 com.tencent.mm / Demo com.mom.locationdemo)。
  3. 3. 保存后 重启手机(改的是 system_server 代码,必须重启;日常改坐标不用再重启)。
  4. 4. Magisk → 超级用户 → 给「打卡助手」root 授权(镜像通道要用)。
  5. 5. 控制端选中地点 → 「开始模拟」。每次在目标点周围 20–40m 随机取点,整段会话固定不变。
  6. 6. 打开目标 App 看定位;「停止模拟」后系统层几秒内跟随,全机恢复。微信有内存缓存,偶尔不变就强停重开。
LSPosed 模块已启用
LSPosed 作用域勾选
Magisk root 授权
控制端写入后的提示

跑通后wx和高德都落在目标点:

高德定位被改写
wx定位被改写

相关学习资料