談談 iOS 識別虛擬定位調(diào)研
本文轉(zhuǎn)載自微信公眾號「Swift社區(qū)」,作者靜楓靈雨。轉(zhuǎn)載本文請聯(lián)系Swift社區(qū)公眾號。
前言
最近業(yè)務開發(fā)中,有遇到我們的項目 app 定位被篡改的情況,在 android 端表現(xiàn)的尤為明顯。為了防止這種黑產(chǎn)使用虛擬定位薅羊毛,iOS 也不得不進行虛擬定位的規(guī)避。
在做技術調(diào)研后,發(fā)現(xiàn)在蘋果手機上,單憑一部手機,真正要實現(xiàn)虛擬定位,是比較難實現(xiàn)的,但還是有存在的可能性,公司的一個項目 app 的 bugly 記錄反饋用戶存在使用越獄蘋果手機,這就著實讓人這種行為實在有大嫌。
本人和公司伙伴的共同努力下,大致調(diào)研了以下使用虛擬定位的情況(使用 Xcode 虛擬定位的方式本文忽略):
第一種:使用越獄手機
一般 app 用戶存在使用越獄蘋果手機的情況,一般可以推斷用戶的行為存在薅羊毛的嫌疑(也有 app 被競品公司做逆向分析的可能),因為買一部越獄的手機比買一部正常的手機有難度,且在系統(tǒng)升級和 appstore 的使用上,均不如正常手機,本人曾經(jīng)淺淺的接觸皮毛知識通過越獄 iPhone5s 進行的 app 逆向。
識別方式
建議一刀切的方式進行,通過識別手機是否安裝了 Cydia.app,如果安裝了直接判定為越獄手機,并向后臺上報“設備異常”的信息。如果不使用這種方式的方式,請繼續(xù)看,后面會有其他方式解決。
專業(yè)的逆向人員是絕對可以避免 app 開發(fā)者對 Cydia 的安裝檢測的,當然這種情況是 app 在市場上有很大的份量,被競爭對手拿來進行逆向分析,對這種情況,虛擬的識別基本毫無意義。個人建議,直接鎖死停掉此手機 app 的接口服務。這里推薦一篇開發(fā)者如何識別蘋果手機已經(jīng)越獄[1]的文章。
代碼實現(xiàn)
- /// 判斷是否是越獄設備
- /// - Returns: true 表示設備越獄
- func isBrokenDevice() -> Bool {
- var isBroken = false
- let cydiaPath = "/Applications/Cydia.app"
- let aptPath = "/private/var/lib/apt"
- if FileManager.default.fileExists(atPath: cydiaPath) {
- isBroken = true
- }
- if FileManager.default.fileExists(atPath: aptPath) {
- isBroken = true
- }
- return isBroken
- }
第二種:使用愛思助手
對于使用虛擬定位的場景,大多應該是司機或?qū)尤藛T打卡了。而在這種場景下,就可能催生了一批專門以使用虛擬定位進行打卡薅羊毛的黑產(chǎn)。對于蘋果手機,目前而言,能夠很可以的實現(xiàn)的,當數(shù)愛思助手的虛擬定位功能了。
使用步驟: 下載愛思助手 mac 客戶端,連接蘋果手機,工具箱中點擊虛擬定位,即可在地圖上選定位,然后點擊修改虛擬定位即可實現(xiàn)修改地圖的定位信息。
原理: 在未越獄的設備上通過電腦和手機進行 USB 連接,電腦通過特殊協(xié)議向手機上的 DTSimulateLocation 服務發(fā)送模擬的坐標數(shù)據(jù)來實現(xiàn)虛假定位,目前 Xcode 上內(nèi)置位置模擬就是借助這個技術來實現(xiàn)的。(文章來源[2])
識別方式
一、通過多次記錄愛思助手的虛擬定位的數(shù)據(jù)發(fā)現(xiàn),其虛擬的定位信息的經(jīng)緯度的高度是為 0 且經(jīng)緯度的數(shù)據(jù)位數(shù)也是值得考究的。真實定位和虛擬定位數(shù)據(jù)如下圖:
真實定位
虛擬定位
仔細觀察數(shù)據(jù),不難發(fā)現(xiàn),如果我們比對獲取定位信息的高度,以及對經(jīng)緯度的 double 位數(shù)也進行校驗,虛擬定位的黑帽子就會輕易被破了。
那么如果我們比對虛擬定位的高度為 0 時,就認定為虛擬定位,那么就會產(chǎn)生一個疑問,真實海拔就是零的地點,如何解決?這里科普下中國的海拔零度位置,中國水準零點位于青島市東海中路銀海大世界內(nèi)的“中華人民共和國水準零點”,是國內(nèi)唯一的水準零點。唯一的水準零點。
同時,因為比對經(jīng)緯度的 double 位數(shù),發(fā)現(xiàn)虛擬定位的位數(shù)很明顯不對,核對 swift 的 float 和 double 的位數(shù)精度發(fā)現(xiàn),虛擬定位的經(jīng)緯度數(shù)據(jù)只是敷衍的滿足 double 精度位數(shù),swift 的 float 有效位數(shù)是 7,double 的有效位數(shù)是 15。
當然這個比較的權重是相對高度比較低的,筆者剛剛更新愛思助手版本發(fā)現(xiàn)新版本經(jīng)緯度有更詳細,但是還是達不到 double 的有效位數(shù)級別。相對于目前的愛思助手的高度比較識別為虛擬定位,已經(jīng)完全可以做到。
代碼實現(xiàn)
- if location.altitude == 0.0 {
- print("虛擬定位")
- }
- //位數(shù)作為判定的權重比,如果位數(shù)小于12(假定值,目前愛思助手的虛擬定位的此數(shù)據(jù)的位數(shù)是9),判斷為虛擬定位,
- //危險慎用,但是作為小權重的異常數(shù)據(jù)記錄還是可以的
- let longitude = location.coordinate.longitude
- let longitudeStr = "\(longitude)".components(separatedBy: ".").last ?? ""
- print("經(jīng)度的有效位數(shù):\(longitudeStr.count)")
- if longitudeStr.count < 12 {
- print("虛擬定位")
- }
二、把定位后的數(shù)據(jù)的經(jīng)緯度上傳給后臺,后臺再根據(jù)收到的經(jīng)緯度獲取詳細的經(jīng)緯度信息,對司機的除經(jīng)緯度以外的地理信息進行深度比較,優(yōu)先比較 altitude、horizontalAccuracy、verticalAccuracy 值,根據(jù)值是否相等進行權衡后,確定。
三、
(一)通過獲取公網(wǎng) ip,大概再通過接口根據(jù) ip 地址可獲取大概的位置,但誤差范圍有點大。
- //獲取公網(wǎng)ip地址
- var ipAddress: String? {
- let ipUrl = URL(string: "https://ipof.in/txt")!
- let ip = try? String.init(contentsOf: ipUrl, encoding: .utf8)
- return ip
- }
(二)通過 Wi-Fi 熱點來讀取 app 位置[3]
(三)利用 CLCircularRegion 設定區(qū)域中心的指定經(jīng)緯度和可設定半徑范圍,進行監(jiān)聽。
代碼簡略實現(xiàn):
- manager = CLLocationManager()
- //設置定位服務管理器代理
- manager?.delegate = self
- //設置定位模式
- manager?.desiredAccuracy = kCLLocationAccuracyBest
- //更新距離
- manager?.distanceFilter = 100
- //發(fā)送授權申請
- manager?.requestWhenInUseAuthorization()
- let latitude = 115.47560123242931
- let longitude = 29.9757535600194
- let centerCoordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
- let locationIDStr = ""
- let clRegion = CLCircularRegion(center: centerCoordinate, radius: 100, identifier: locationIDStr)
- manager?.startMonitoring(for: clRegion)
- 代理方法
- func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
- }
- func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
- }
(四)通過 IBeacon 技術,使用 CoreBluetooth 框架下的 CBPeripheralManager 建立一個藍牙基站。這種定位直接是端對端的直接定位,省去了 GPS 的衛(wèi)星和蜂窩數(shù)據(jù)的基站通信。
代碼簡略實現(xiàn):
- func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
- for beacon in beacons {
- var proximityStr: String = ""
- switch beacon.proximity {
- case .far:
- proximityStr = "Unknown"
- case .immediate:
- proximityStr = "Immediate"
- case .near:
- proximityStr = "Near"
- case .unknown:
- proximityStr = "Unknown"
- }
- var beaconStr = "信號:" + beacon.proximityUUID.uuidString + "major:" + beacon.major.stringValue + "minor:" + beacon.minor.stringValue + "距離:" + beacon.accuracy + "信號:" + "\(Int64(beacon.rssi))" + "接近度:" + proximityStr
- print("beacon信息: \(beaconStr)")
- }
- }
- func locationManager(_ manager: CLLocationManager, rangingBeaconsDidFailFor region: CLBeaconRegion, withError error: Error) {
- }
- ----------------------------------------------------------------------------------
- //不能單獨創(chuàng)建一個類遵守CBPeripheralManagerDelegate協(xié)議,需要先遵守NSObjectProtocol協(xié)議,這里直接繼承NSObject
- class CoreBluetoothManager:NSObject, CBPeripheralManagerDelegate {
- //建立一個藍牙基站。
- lazy var peripheralManager: CBPeripheralManager = CBPeripheralManager(delegate: self, queue: DispatchQueue.main, options: nil)
- lazy var region: CLBeaconRegion = {
- guard let uuid = UUID(uuidString: "xxx") else {
- return CLBeaconRegion()
- }
- let major: CLBeaconMajorValue = 1
- let minor: CLBeaconMajorValue = 1
- let id = "創(chuàng)建的藍牙基站的名稱"
- let region = CLBeaconRegion(proximityUUID: uuid, major: major, minor: minor, identifier: id)
- return region
- }()
- func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
- switch peripheral.state {
- case CBManagerState.poweredOn:
- if let data = self.region.peripheralData(withMeasuredPower: nil) as? [String : Any] {
- self.peripheralManager.startAdvertising(data)
- }
- case CBManagerState.poweredOff,
- CBManagerState.resetting,
- CBManagerState.unauthorized,
- CBManagerState.unsupported,
- CBManagerState.unknown:
- break
- }
- }
- func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
- }
- }
四(待完善)、 iOS防黑產(chǎn)虛假定位檢測技術 文章的末尾附的解法本人有嘗試過,一層一層通過 kvc 讀取 CLLocation 的 _internal 的 fLocation,只能讀取到到此。再通過 kvc 讀取會報以下錯誤:
- Expression can't be run, because there is no JIT compiled function
深入研究,在蘋果的官方開發(fā)文檔上發(fā)現(xiàn)了這個解釋[4],也有說設置 debug+ 優(yōu)化策略的,但 iOS 默認 bug 環(huán)境就是 -Onone 級別的。其實主要原因貌似因為 JIT 的設置是在開發(fā) mac 客戶端的時候,才能在 Signing&Capabilities 的 Hardened Runtime 中找到。關于 Allow Execution of JIT-compiled Code 的設置(官方文章[5])。最終只能卡到這里,若有大神能通過其他方式讀取 CLLocation 的真實定位(這是極其完美的解決方案),還請不吝賜教。
附:
CLLocation 對象私有變量 _internal 實例對象的官方定義[6]:
- @interface CLLocationInternal : NSObject {
- struct {
- int suitability;
- struct {
- double latitude;
- double longitude;
- } coordinate;
- double horizontalAccuracy;
- double altitude;
- double verticalAccuracy;
- double speed;
- double speedAccuracy;
- double course;
- double courseAccuracy;
- double timestamp;
- int confidence;
- double lifespan;
- int type;
- struct {
- double latitude;
- double longitude;
- } rawCoordinate;
- double rawCourse;
- int floor;
- unsigned int integrity;
- int referenceFrame;
- int rawReferenceFrame;
- } fLocation;
- CLLocationMatchInfo * fMatchInfo;
- double fTrustedTimestamp;
- }
- @class NSData;
- @interface CLLocationMatchInfo : NSObject <NSCopying, NSSecureCoding> {
- id _internal;
- }
- @property (nonatomic,readonly) long long matchQuality;
- @property (nonatomic,readonly) CLLocationCoordinate2D matchCoordinate;
- @property (nonatomic,readonly) double matchCourse;
- @property (nonatomic,readonly) int matchFormOfWay;
- @property (nonatomic,readonly) int matchRoadClass;
- @property (getter=isMatchShifted,nonatomic,readonly) BOOL matchShifted;
- @property (nonatomic,copy,readonly) NSData * matchDataArray;
參考資料
[1]
用代碼判斷 iOS 系統(tǒng)是否越獄的方法: https://www.huaweicloud.com/articles/7c6b8027253c4a97196d359840f638d9.html
[2]
iOS 防黑產(chǎn)虛假定位檢測技術: https://cloud.tencent.com/developer/article/1800531
[3]
Wifi 定位原理及 iOS Wifi 列表獲取: http://www.caojiarun.com/2017/01/iOS_Wifilist/
[4]
Allow Execution of JIT-compiled Code Entitlement: https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_allow-jit
[5]
Hardened Runtime: https://developer.apple.com/documentation/security/hardened_runtime
[6]
_internal 實例對象的官方定義: https://github.com/nst/iOS-Runtime-Headers/blob/master/Frameworks/CoreLocation.framework/CLLocationInternal.h