当你的手机说你在北京,但你明明在上海——Android 位置伪装检测实战

举报
yd_291797886 发表于 2026/10/20 11:36:50 2026/10/20
【摘要】 # 当你的手机说你在北京,但你明明在上海——Android 位置伪装检测实战> 一部手机、一个 Xposed 模块,就能让微信以为你在故宫打卡、让钉钉以为你在工位上。位置伪装在灰产和安全研究中已不新鲜,但如何**检测**它?本文记录了一个从零到发布的 Android 位置伪装检测工具的完整开发过程。## 一、问题背景:位置伪装是怎么做到的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)*

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。