当你的手机说你在北京,但你明明在上海——Android 位置伪装检测实战
# 当你的手机说你在北京,但你明明在上海——Android 位置伪装检测实战
> 一部手机、一个 Xposed 模块,就能让微信以为你在故宫打卡、让钉钉以为你在工位上。位置伪装在灰产和安全研究中已不新鲜,但如何**检测**它?本文记录了一个从零到发布的 Android 位置伪装检测工具的完整开发过程。
## 一、问题背景:位置伪装是怎么做到的
Android 系统的位置服务架构可以简化为三层:
```
应用层 ← LocationManager / FusedLocationProviderClient
↑
框架层 ← LocationManagerService(系统服务)
↑
硬件层 ← GPS 芯片 / Wi-Fi 定位 / 基站定位
```
正常情况下,应用通过 `LocationManager` 或 Google 的 `FusedLocationProviderClient` 获取位置,数据源自 GPS 卫星信号。但 Xposed 框架允许在**框架层**插入 hook——拦截 `LocationManager.getLastKnownLocation()`、`LocationListener.onLocationChanged()` 等方法,返回伪造的 `Location` 对象。
更高级的伪装方案还会:
- Hook `Location.isFromMockProvider()` 强制返回 `false`
- Hook 高德/百度/腾讯地图 SDK 的内部回调
- Hook `WifiManager.getScanResults()` 伪造周边 Wi-Fi 列表(Wi-Fi 定位依赖扫描结果)
- 甚至 Hook `GnssStatus` 伪造卫星数据
这意味着**单一检测手段几乎必然被绕过**。我们需要多维度交叉验证。
## 二、检测思路:五道关卡
我设计了五项独立检查,全部通过才算位置可信。每项检查针对伪装方案的一个薄弱环节:
| # | 检查项 | 攻击面 | 防御原理 |
|---|--------|--------|----------|
| 1 | 经纬度一致性 | 单 provider 伪造 | GPS/NETWORK/FUSED 三源坐标交叉比对 |
| 2 | Mock 标记检测 | isFromMockProvider hook | 双 API 检测 + 反射兜底 |
| 3 | 卫星数校验 | GnssStatus hook | 真实卫星信号难以伪造 |
| 4 | 地址解析 | 坐标注入 | Geocoder 反查地址验证合理性 |
| 5 | 轨迹连续性 | 瞬移作弊 | Haversine 距离 + 速度限制 |
下面逐项展开。
### Check 1:经纬度一致性——三源交叉比对
Android 提供三种位置来源:
- **GPS_PROVIDER**:卫星定位,精度最高(3-10m),但室内不可用
- **NETWORK_PROVIDER**:Wi-Fi/基站定位,精度较低(50-200m),室内可用
- **FUSED_PROVIDER**:Google 融合定位,自动选择最优源
大多数位置伪装工具只 hook 了其中一个 provider(通常是 GPS),而忘了同步伪造另外两个。这就导致三源坐标出现明显偏差。
```kotlin
// 核心逻辑:两两比对所有 provider 的坐标
for (i in entries.indices) {
for (j in i + 1 until entries.size) {
val (p1, l1) = entries[i]
val (p2, l2) = entries[j]
val dLat = Math.abs(l1.latitude - l2.latitude)
val dLng = Math.abs(l1.longitude - l2.longitude)
val delta = Math.max(dLat, dLng)
if (delta > maxDelta) {
maxDelta = delta
deltaInfo = "Δ($p1↔$p2): lat=${dLat}, lng=${dLng}"
}
}
}
// 容差 0.0001°(约 11m),超过即判异常
result.status = if (maxDelta <= COORD_TOLERANCE) PASS else FAIL
```
容差设为 `0.0001°`(约 11 米)。正常情况下 GPS 和 NETWORK 的偏差在 50-200m 内,但伪装工具通常只改一个源,偏差会大到几度甚至几十度——一眼就能看出来。
> **局限**:高级伪装方案会同时 hook 三个 provider 返回一致坐标。所以这只是第一道关。
### Check 2:Mock 标记检测——双 API + 反射兜底
Android 系统本身提供了检测 mock 位置的 API,但有两个版本:
- `Location.isFromMockProvider()`:API 18+,**API 31 起已废弃**
- `Location.isMock()`:API 31+,替代方案
伪装工具通常会 hook 这两个方法返回 `false`。但问题是:
1. 不是所有伪装工具都覆盖了两个 API
2. 低版本 Android(< 31)只有 `isFromMockProvider`
3. 有些工具只 hook 了方法但忘了清理 `Location.extras` 中的 mock 标记
```kotlin
val isFromMock = location.isFromMockProvider
val isMock = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
location.isMock // API 31+ 原生方法
} else {
tryReflectiveIsMock(location) // 反射调用,兼容低版本
}
// 任何一个返回 true 即判定为 mock
val anyMock = isFromMock || isMock
```
反射兜底是为了应对某些混淆/定制 ROM 的情况:
```kotlin
private fun tryReflectiveIsMock(location: Location): Boolean {
return try {
val method = Location::class.java.getMethod("isMock")
method.invoke(location) as Boolean
} catch (_: Exception) { false }
}
```
同时还会检查 `location.extras`——系统在标记 mock 时会往 extras 里塞数据,有些 hook 工具忘了清理。
> **局限**:Xposed 可以 hook 这两个方法 + 清理 extras。这道关能挡住低级工具,挡不住高级方案。
### Check 3:卫星数校验——最难伪造的一关
这是我认为**最有价值的一项检测**。
GPS 定位依赖至少 4 颗卫星信号。`GnssStatus.Callback` 可以拿到卫星的实时状态:
```kotlin
gnssCallback = object : GnssStatus.Callback() {
override fun onSatelliteStatusChanged(status: GnssStatus) {
lastGnssStatus = status
checkSatelliteCount(status)
}
}
```
检测逻辑:统计 `usedInFix`(参与定位解算的卫星数),如果为 0 说明没有真实卫星信号——大概率是伪装的。
```kotlin
for (i in 0 until status.satelliteCount) {
val used = status.usedInFix(i)
val cn0 = status.getCn0DbHz(i) // 载噪比
val constellation = status.getConstellationType(i)
if (used) usedInFix++
}
result.status = if (usedInFix > 0) PASS else FAIL
```
同时识别卫星所属星座(GPS/GLONASS/北斗/伽利略),输出信号强度(C/N₀ dBHz):
```
Used in fix: 8 | Total visible: 12 | Count: 28
svid=2 cn0=32.5dBHz used=true [GPS]
svid=15 cn0=28.1dBHz used=true [BEIDOU]
...
```
为什么说这关难伪造?因为:
1. `GnssStatus.Callback` 是系统级回调,数据直接来自 GPS 硬件驱动
2. 伪造卫星数据需要 hook `GnssStatus` 类的几十个 native 方法(`getSvid`、`getCn0DbHz`、`usedInFix`、`getConstellationType`…),工作量巨大
3. 卫星信号有物理约束:卫星位置随时间变化,C/N₀ 有合理范围,多普勒频移要一致——伪造一套自洽的卫星数据极其困难
> **实践发现**:市面上绝大多数位置伪装工具(包括一些付费方案)都没有伪造 GnssStatus。这道关能挡住 90% 以上的伪装工具。
### Check 4:地址解析——语义合理性验证
拿到坐标后用 `Geocoder.getFromLocation()` 反查地址:
```kotlin
val addresses = geocoder.getFromLocation(lat, lng, 1)
if (addresses.isNullOrEmpty()) {
result.status = FAIL // 无法解析地址,可疑
} else {
val addr = addresses[0]
// 输出国家、省、市、邮编等
// 人工核对是否合理
result.status = PASS
}
```
这一关更多是辅助验证——如果坐标指向太平洋中间,Geocoder 返回空,就值得怀疑。但它依赖网络连接,且无法自动判断"地址是否合理"(需要人看),所以是五项中偏弱的一关。
### Check 5:轨迹连续性——抓瞬移
位置伪装的典型特征是**瞬移**:上一秒在上海,下一秒在北京。通过 Haversine 公式计算相邻两点间的球面距离,结合时间差算速度:
```kotlin
private fun haversine(lat1: Double, lng1: Double, lat2: Double, lng2: Double): Double {
val r = 6371000.0 // 地球半径(米)
val dLat = Math.toRadians(lat2 - lat1)
val dLng = Math.toRadians(lng2 - lng1)
val a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLng / 2) * Math.sin(dLng / 2)
val c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return r * c
}
```
两个判定条件:
- **瞬移检测**:距离 > 500m 且时间 < 1s → FAIL
- **速度限制**:速度 > 100 m/s(360 km/h)→ FAIL
```kotlin
if (curr.jumpDistance > TELEPORT_DISTANCE && timeDelta < TELEPORT_TIME) {
teleportDetected = true
}
if (curr.speed > SPEED_LIMIT) {
speedViolation = true
}
```
轨迹数据用 `ConcurrentLinkedDeque` 存储,最多保留 50 个采样点(滑动窗口)。同时输出一个 ASCII 轨迹图,直观展示移动路径:
```
--- Recent trajectory plot ---
lat↑ 39.9087
| ··························
| ··························
| ···············*·········
| ··············***········
| ·············**·*········
| ··························
| ··························
| ··························
+----------------------------------→ lng
116.3974 116.3978
```
> **局限**:高级伪装工具会模拟平滑移动(插值生成中间点),轨迹连续性就不会异常。但如果伪装的轨迹和真实轨迹不一致,Check 1 的多源比对可能抓到。
## 三、架构设计
### 整体结构
```
LocationVerifier(核心检测引擎)
├── start() / stop()
├── 三个 LocationListener(GPS / NETWORK / FUSED)
├── GnssStatus.Callback(卫星状态)
├── 周期检查协程(每 2s 执行 Check 1/4/5)
└── 回调:onCheckUpdated / onLog
MainActivity(UI 层)
├── 5 个状态指示灯(绿/红/灰)
├── 实时日志输出
└── 权限请求处理
```
### 关键设计决策
**为什么用协程而不是 RxJava?**
项目用 Kotlin Coroutines 管理异步任务。`SupervisorJob` 确保一个检查失败不会取消其他检查。周期检查用一个 `while (isActive) { delay(2000) }` 循环,简洁且可取消:
```kotlin
periodicJob = scope.launch {
while (isActive) {
delay(2000L)
checkCoordinateConsistency()
checkTrajectoryContinuity()
triggerAddressResolution()
}
}
```
**为什么 Check 2/3 是事件驱动,Check 1/4/5 是周期执行?**
- Check 2(Mock 标记):每次 Location 回调都检查,实时性最高
- Check 3(卫星数):卫星状态变化时触发,频率已足够高
- Check 1(坐标一致性):需要多源数据到齐,周期比对更合适
- Check 4(地址解析):Geocoder 是网络请求,不能太频繁
- Check 5(轨迹连续性):需要积累采样点,周期检查
**为什么用 `ConcurrentLinkedDeque` 存轨迹?**
Location 回调可能在多线程触发(GPS 和 FUSED 回调不同线程),`ConcurrentLinkedDeque` 线程安全,且 Deque 方便从头删旧数据、从尾加新数据。
## 四、UI 设计
界面简洁直接——五个状态指示灯 + 日志区:
- 🟢 绿色:检查通过
- 🔴 红色:检查异常(可能被伪装)
- ⚪ 灰色:等待数据
点击 "Start Checking" 开始检测,日志区实时输出每个 provider 的坐标、卫星信息、轨迹距离等详细数据。
## 五、效果与局限
### 能检测到的伪装方案
| 伪装方案 | 触发哪项检查 |
|----------|-------------|
| 只 hook GPS provider | Check 1(三源不一致) |
| 未 hook isFromMockProvider | Check 2(Mock 标记) |
| 未伪造 GnssStatus | Check 3(卫星数为 0) |
| 瞬移式伪装 | Check 5(轨迹不连续) |
| 坐标指向无效区域 | Check 4(Geocoder 返回空) |
### 无法检测的方案
一个**完美的**位置伪装工具需要同时做到:
1. ✅ 同步伪造 GPS/NETWORK/FUSED 三个 provider 的坐标
2. ✅ Hook `isFromMockProvider()` 和 `isMock()` 返回 false
3. ✅ 伪造完整的 GnssStatus(卫星数、信号强度、星座类型、多普勒频移…)
4. ✅ 模拟平滑移动轨迹(不是瞬移)
5. ✅ 伪造坐标指向合理地址
满足全部 5 条的方案,本工具确实检测不到。但实现难度极高——尤其是第 3 条,伪造一套物理自洽的卫星数据几乎是研究级别的课题。
**检测的本质是提高攻击成本。** 当伪造工具的开发成本远高于检测工具时,检测就是有效的。
## 六、发布到华为云开发者展览馆
项目已发布到华为云开发者作品展览馆,完整源码托管在 GitCode:
- **作品链接**:https://gallery.developer.huaweicloud.com/gallery/00672ca6e5c00000
- **源码仓库**:https://gitcode.com/tttttqqqqq/gps-spoof-verifier
### 技术栈
- Kotlin + Coroutines
- minSdk 24, targetSdk 34
- LocationManager + FusedLocationProviderClient
- GnssStatus.Callback
- Geocoder
### 使用方式
1. 用 Android Studio 打开项目
2. Sync Gradle 后编译运行到目标设备
3. 点击 "Start Checking" 开始检测
4. 观察五项检查的状态指示灯
## 七、后续改进方向
1. **C/B 层检测**:通过 native 代码直接读取 GPS 硬件抽象层(HAL)数据,绕过 Java 层 hook
2. **传感器交叉验证**:加速度计 + 陀螺仪积分推算位移,与 GPS 轨迹比对
3. **网络层验证**:检测 Wi-Fi BSSID 与坐标的对应关系(需要离线数据库)
4. **机器学习异常检测**:训练模型识别伪造轨迹的特征分布
5. **服务端验证**:将设备传感器数据上传服务端做综合判定,客户端不可信
## 写在最后
位置伪装是一个攻防不对称的领域——防守方需要覆盖所有维度,攻击方只需突破一个。但通过多层检测,我们可以将攻击成本提升到让大多数工具无法企及的水平。
这个项目本身也是一个学习 Android 位置服务架构的好例子。如果你对位置安全感兴趣,欢迎在 GitCode 上 Star 和交流。
---
*本文涉及的项目已开源:[GitCode 仓库](https://gitcode.com/tttttqqqqq/gps-spoof-verifier) | [华为云展览馆](https://gallery.developer.huaweicloud.com/gallery/00672ca6e5c00000)*
- 点赞
- 收藏
- 关注作者
评论(0)