chundev
日期:2026-04-22 技術棧:pymobiledevice3 / CoreLocation / CMPedometer / Niantic Real-World Engine
iOS GPS 偽造不是「假造衛星訊號」,而是利用 Apple 留給 Xcode 的開發者通道直接 override CoreLocation 的輸出。但對 Pikmin Bloom 這種 Niantic 遊戲來說,光改 GPS 還不夠 — 遊戲會交叉比對 CMPedometer 步數、GPS 速度合理性、horizontalAccuracy 值、軌跡連續性。這篇筆記拆解:(1)pymobiledevice3 是怎麼把 (lat, lon) 送進 iOS 深處的;(2)Niantic 遊戲拿什麼資料來判斷你是不是在真的走路;(3)為什麼一個「像真人」的隨機漫步器要用 correlated random walk + trail repulsion + home tether 這種物理模型,以及每個參數背後的 trade-off。
前置閱讀:pymobiledevice3 Wi-Fi GPS Spoofing 完整攻略(iOS 16 / 17+ 三條通道的設定流程、pair record 匯出、AP isolation 踩坑)。這篇不重複,直接從原理開始。
當一個 App 呼叫 CLLocationManager.startUpdatingLocation(),系統內部走的是:
App (你的程式)
↓ XPC
CoreLocation.framework
↓ XPC
locationd (/usr/libexec/locationd)
↓ 融合多個 source
┌────────┬────────┬──────────┬────────────┐
│ GPS HW │ Wi-Fi │ Cellular │ Bluetooth │
│ 衛星 │ BSSID │ tower │ beacon │
│ 晶片 │ lookup │ lookup │ │
└────────┴────────┴──────────┴────────────┘
關鍵:locationd 是全系統唯一跟位置硬體對話的 process,所有 App 拿到的 CLLocation 都是它融合後分發的。
Simulate-location 做的事,是在 locationd 的 source 融合層塞一個 override input。一旦生效:
horizontalAccuracy 直接填固定值 65.0altitude 填 0、verticalAccuracy 填 -1、floor 為 nilspeed 和 course 由 locationd 從連續 tick 的 delta 自己推算這不是 hook、不是 swizzle、更不是 jailbreak — 是 Apple 合法留在 locationd 裡的開發者通道。啟動它需要:
horizontalAccuracy = 65 是最大漏洞真實 GPS 的 accuracy 是連續變動的:
| 環境 | 真實 accuracy |
|---|---|
| 空曠戶外、7+ 顆衛星 | 3-8 m |
| 都市峽谷 | 15-30 m |
| 室內、窗邊 | 30-100 m(靠 Wi-Fi positioning) |
| 地下室 | 100-500 m(純 BSSID lookup) |
Simulate-location 永遠回報 65.0(一個 float 常數,不管你在哪)。這是 anti-cheat 最便宜的鉤子:連續看到 accuracy == 65.0 或 accuracy 永遠不變 就能標記整個 session。有些商業 spoofer 會在中間層加「假的 accuracy 抖動」再丟給 App,但 pymobiledevice3 沒做這層 — 直接走開發者通道,吃 locationd 原生的 65.0。
pymobiledevice3 是 libimobiledevice(Apple 私有的 MobileDevice.framework 跨平台重寫)的純 Python 版本。它實作了從 Mac 對 iPhone 發開發者指令的整套堆疊。Location 只是它支援的上百個 service 之一。
完整通訊堆疊有四層:
/var/run/usbmuxd 是 macOS 隨 iTunes / Mobile Device Framework 裝上去的常駐 daemon。它做兩件事:
_apple-mobdev2._tcp.)pymobiledevice3 的 create_using_usbmux() 背後就是往這個 socket 丟:
{"MessageType": "Connect", "DeviceID": ..., "PortNumber": 62078}
然後拿回一條已經 proxy 到裝置內部的 socket — 寫進去的 bytes 會直達 iPhone 上的 lockdownd。Wi-Fi 連線則是繞過 usbmuxd,用 create_using_tcp(hostname, port=62078) 直接連 iPhone 的 IP。
iPhone 內部監聽 TCP 62078 的 daemon。它是 iOS 對外的開發者總閘門,處理三種請求:
| MessageType | 用途 |
|---|---|
QueryType |
取得裝置資訊(ProductVersion、UDID、SerialNumber) |
Pair / ValidatePair |
首次配對與信任驗證 |
StartService |
啟動一個 service,回傳新 port 讓你直接連 |
Pairing 的本質:Mac 生成一對公私鑰,把公鑰存到 iPhone 的 keychain。之後每次連 lockdownd,Mac 用私鑰過 TLS handshake 證明自己被信任過。Pair record(那個 .plist 檔)就是 Mac 這邊的「私鑰 + iPhone 回存的 device certificate」。
StartService 能啟動的 service 超過 50 種,每個有自己獨立的二進位協議。跟位置相關的兩個:
com.apple.dt.simulatelocation(iOS ≤16 限定)— 這個 service 不存在於 stock iOS,要先掛 DeveloperDiskImage。DDI 是 Apple 打包的一個 disk image,裡面有各種開發者工具的執行檔和 ServiceAgents/*.plist,掛載後 lockdownd 會把這些 service 加到可用清單。DTX(Distributed eXchange Protocol)是 Xcode Instruments 跟裝置溝通用的協議,格式類似精簡版的 ObjC remote invocation:
DTXMessage {
channel_id: 42,
method: "setSimulatedLocationWithLatitude:longitude:",
args: [37.7749, -122.4194],
expects_reply: true
}
iOS 17 之後,LocationSimulation 變成 DTServiceHub 上的一條 DTX channel。pymobiledevice3.services.dvt.instruments.location_simulation.LocationSimulation 就是這條 channel 的 Python wrapper。
iOS 17 把 DTX 藏在 RemoteXPC 後面 — RemoteXPC = Apple 用 HTTP/2 frames 包裝 XPC messages 的混合協議。要開 RemoteXPC 連線需要:
sudo pymobiledevice3 remote tunneld 是跑在 Mac 上的 daemon,做三件事:
utun 虛擬介面(所以需要 sudo)127.0.0.1:49152 讓應用程式透過它取得 tunnelled device handleApp 要位置
↓
CoreLocation.framework
↓ (XPC)
locationd ←────────────────────┐
↑ │ override
│ ≤ iOS 16 │
│ com.apple.dt.simulatelocation
│ (需要 DDI 掛載)
│
│ iOS 17+
│ DTServiceHub ←─ DTX channel: LocationSimulation
│ ↑
│ RemoteXPC
│ ↑
│ QUIC/TCP tunnel (tunneld)
│ ↑
└── lockdownd (TCP 62078) ←──USB/WiFi── usbmuxd (Mac)
↑
pymobiledevice3
pymobiledevice3 的價值,就是把這整條堆疊從 C 框架 port 到 Python,而且還逆向補完了 Apple 沒公開的 RemoteXPC 細節。
Pikmin Bloom 是 Niantic 在 Pokémon GO 之後的另一款步行遊戲,共用底層的 Real-World Engine。它收兩條獨立的資料流:
CLLocation {
coordinate: (lat, lon) // 位置
altitude: Double // 高度
horizontalAccuracy: Double // ★ 水平精度
speed: Double // locationd 推算的速度
course: Double // locationd 推算的航向
timestamp: Date
}
用途:判斷玩家在哪、走多遠、速度合不合理。
CMPedometer {
numberOfSteps: Int
distance: Double
currentPace: Double
currentCadence: Double
}
資料來源是加速度計 + 陀螺儀,由 iPhone 的 M 系列 motion coprocessor 即時處理。不經過 locationd,不受 simulate-location 影響。 這點常常被 spoofing 教學忽略。
| 偽造 GPS | CMPedometer | Pikmin Bloom 表現 |
|---|---|---|
| ✓ 已偽造 | ✓ 手機在口袋真的走 | 步數 ✓ / 小花 ✓ / 新地區 ✓(完美) |
| ✓ 已偽造 | ✗ 手機放桌上 | 小花 ✓(GPS 距離)/ 步數 = 0 |
| ✗ 未偽造 | ✓ 手機真搖晃 | 步數 ✓ / 小花卡在原地(沒移動) |
所以「真・偽造」= 偽造 GPS + 搖晃手機(夾在擺錘上、腳踏車把手、電扇震動台)。光跑 pymobiledevice3 不帶著手機走,步數那條永遠是死的。這也是為什麼 iAnyGo 類工具會建議使用者把手機放進口袋走一小段再開 spoofer。
Niantic 的反偵測由三個元素組成:
根據 Pokémon GO 社群多年實測(Pikmin Bloom 未公開但表現類似):
| 速度 | 行為 |
|---|---|
| < 10.5 km/h | 所有資料正常計入 |
| 10.5 – 20 km/h | 部分功能關閉 |
| 20 – 35 km/h | 開車判定,距離不累加 |
| > 35 km/h | 完全凍結,可能觸發 cooldown |
Pikmin Bloom 社群共識:< 15 km/h 種花穩定、< 10 km/h 最保險。
上面第一部分講過的 65.0 問題。遊戲可以簡單過一個 filter:只收 accuracy 有波動且 < 某閾值的 sample,spoofed 訊號連續 65.0 就被整批丟掉。
等速直線走路是最明顯的紅旗。真人走路有:
對應上面三條偵測點,一個「像真人」的漫步器需要:
每 tick 的新 heading 是「上一個 heading + 高斯雜訊」,不是白雜訊:
heading_new = heading_prev + random.gauss(0, sigma=22°)
σ=22° 是經驗值:太小軌跡太直、太大會抖到像癲癇。產出的軌跡是自然的 S 形曲線,跟 Brownian motion 的鋸齒狀很不一樣。
無邊界的 random walk 會一路 diffusion 飄走,這對 Pikmin Bloom 不合理(連續半小時不會跑到三條街外)。解法是超出 max_radius_m 時把 heading 往中心拉:
dist_from_home = haversine(position, home)
if dist_from_home > max_radius:
pull_bearing = bearing_toward(home, from=position)
pull_strength = min(1.0, (dist_from_home - max_radius) / max_radius)
heading = lerp(heading, pull_bearing, pull_strength)
避免一分鐘內踩同一塊地。把最近 N 個位置當成斥力源,加上 inverse-square 力場:
for recent in trail[-300:]:
dx, dy = position - recent
dist_sq = dx*dx + dy*dy + epsilon
force += (dx, dy) / (dist_sq ** 1.5) # inverse-square
heading = add_force(heading, force, weight=0.1)
選 inverse-square 而不是 inverse-distance 的原因:遠的點影響趨近 0,只有剛踩過的點會把你推開。Pikmin Bloom 的 fog-of-war 會記錄走過的格子,重踩不產小花 — 用斥力場強制探索新區域。
每 tick 在乾淨座標上加小抖動當 output,但不把抖動寫回 state:
current_clean = advance_by_step(current_clean, heading, step_m)
output = current_clean + gauss_noise(sigma_m=1.0) # yield 給 iOS
# current_clean 保持乾淨
如果把 jittered 座標回寫,N tick 後累積誤差會 O(√N) 放大,random walk 整個漂到銀河系去。這是隨機漫步模擬的一個關鍵 invariant,寫錯不容易看出來但結果會越跑越歪。
原本想用 15 km/h 當 nominal,實測發現 position jitter σ=1m 在 step=5.3m 下每步的實際距離期望值會膨脹:
E[|step_real|] = E[|step_nom + noise|]
≈ step_nom × (1 + σ²/(2·step_nom²))
= 5.3 × 1.018
≈ 5.4 m
加上速度 jitter σ=12%,effective speed ≈ 19.8 km/h。與其在 UI 寫 15 卻跑出 19,不如把 profile 直接標 19 讓數字跟實際一致。如果 Pikmin Bloom 後續把 filter 改嚴(只認 < 15 km/h),就把 nominal 降到 13-14 看表現。
locationd daemon 接受開發者 channel 的 override,所有 App 拿到的 CLLocation 都被替換。horizontalAccuracy = 65.0 是這條通道的指紋,anti-cheat 只要檢查這一個欄位就能打下大部分 spoofer。usbmuxd → lockdownd → lockdown services(iOS ≤16)或 RemoteXPC → DTX channel(iOS 17+)整條 Apple 私有協議堆疊。Location 只是它支援的上百個 service 之一,檔案共享、debug proxy、syslog、crash report、AFC 都是同一套堆疊。O(√N) 累積偏差會讓 random walk 中心點整個漂掉。MobileDevice.framework 的跨平台 FOSS 重寫,pymobiledevice3 的協議來源