谈谈 iOS 识别虚拟定位调研

时间:2021-09-01 13:54:58

谈谈 iOS 识别虚拟定位调研

前言

最近业务开发中,有遇到我们的项目 app 定位被篡改的情况,在 android 端表现的尤为明显。为了防止这种黑产使用虚拟定位薅羊毛,iOS 也不得不进行虚拟定位的规避。

在做技术调研后,发现在苹果手机上,单凭一部手机,真正要实现虚拟定位,是比较难实现的,但还是有存在的可能性,公司的一个项目 app 的 bugly 记录反馈用户存在使用越狱苹果手机,这就着实让人这种行为实在有大嫌。

本人和公司伙伴的共同努力下,大致调研了以下使用虚拟定位的情况(使用 Xcode 虚拟定位的方式本文忽略):

第一种:使用越狱手机

一般 app 用户存在使用越狱苹果手机的情况,一般可以推断用户的行为存在薅羊毛的嫌疑(也有 app 被竞品公司做逆向分析的可能),因为买一部越狱的手机比买一部正常的手机有难度,且在系统升级和 appstore 的使用上,均不如正常手机,本人曾经浅浅的接触皮毛知识通过越狱 iPhone5s 进行的 app 逆向。

识别方式

建议一刀切的方式进行,通过识别手机是否安装了 Cydia.app,如果安装了直接判定为越狱手机,并向后台上报“设备异常”的信息。如果不使用这种方式的方式,请继续看,后面会有其他方式解决。

专业的逆向人员是绝对可以避免 app 开发者对 Cydia 的安装检测的,当然这种情况是 app 在市场上有很大的份量,被竞争对手拿来进行逆向分析,对这种情况,虚拟的识别基本毫无意义。个人建议,直接锁死停掉此手机 app 的接口服务。这里推荐一篇开发者如何识别苹果手机已经越狱[1]的文章。

代码实现

  1. /// 判断是否是越狱设备 
  2. /// - Returnstrue 表示设备越狱 
  3. func isBrokenDevice() -> Bool { 
  4.      
  5.     var isBroken = false 
  6.      
  7.     let cydiaPath = "/Applications/Cydia.app" 
  8.      
  9.     let aptPath = "/private/var/lib/apt" 
  10.      
  11.     if FileManager.default.fileExists(atPath: cydiaPath) { 
  12.         isBroken = true 
  13.     } 
  14.      
  15.     if FileManager.default.fileExists(atPath: aptPath) { 
  16.         isBroken = true 
  17.     } 
  18.      
  19.     return isBroken 

第二种:使用爱思助手

对于使用虚拟定位的场景,大多应该是司机或对接人员打卡了。而在这种场景下,就可能催生了一批专门以使用虚拟定位进行打卡薅羊毛的黑产。对于苹果手机,目前而言,能够很可以的实现的,当数爱思助手的虚拟定位功能了。

使用步骤: 下载爱思助手 mac 客户端,连接苹果手机,工具箱中点击虚拟定位,即可在地图上选定位,然后点击修改虚拟定位即可实现修改地图的定位信息。

原理: 在未越狱的设备上通过电脑和手机进行 USB 连接,电脑通过特殊协议向手机上的 DTSimulateLocation 服务发送模拟的坐标数据来实现虚假定位,目前 Xcode 上内置位置模拟就是借助这个技术来实现的。(文章来源[2])

识别方式

一、通过多次记录爱思助手的虚拟定位的数据发现,其虚拟的定位信息的经纬度的高度是为 0 且经纬度的数据位数也是值得考究的。真实定位和虚拟定位数据如下图:

真实定位

谈谈 iOS 识别虚拟定位调研

虚拟定位

谈谈 iOS 识别虚拟定位调研

仔细观察数据,不难发现,如果我们比对获取定位信息的高度,以及对经纬度的 double 位数也进行校验,虚拟定位的黑帽子就会轻易被破了。

那么如果我们比对虚拟定位的高度为 0 时,就认定为虚拟定位,那么就会产生一个疑问,真实海拔就是零的地点,如何解决?这里科普下中国的海拔零度位置,中国水准零点位于青岛市东海中路银海大世界内的“*水准零点”,是国内唯一的水准零点。唯一的水准零点。

同时,因为比对经纬度的 double 位数,发现虚拟定位的位数很明显不对,核对 swift 的 float 和 double 的位数精度发现,虚拟定位的经纬度数据只是敷衍的满足 double 精度位数,swift 的 float 有效位数是 7,double 的有效位数是 15。

当然这个比较的权重是相对高度比较低的,笔者刚刚更新爱思助手版本发现新版本经纬度有更详细,但是还是达不到 double 的有效位数级别。相对于目前的爱思助手的高度比较识别为虚拟定位,已经完全可以做到。

谈谈 iOS 识别虚拟定位调研

代码实现

  1. if location.altitude == 0.0 { 
  2.     print("虚拟定位"
  3.  
  4. //位数作为判定的权重比,如果位数小于12(假定值,目前爱思助手的虚拟定位的此数据的位数是9),判断为虚拟定位, 
  5. //危险慎用,但是作为小权重的异常数据记录还是可以的 
  6. let longitude = location.coordinate.longitude 
  7. let longitudeStr = "\(longitude)".components(separatedBy: ".").last ?? "" 
  8.  
  9. print("经度的有效位数:\(longitudeStr.count)"
  10. if longitudeStr.count < 12 { 
  11.  
  12.     print("虚拟定位"

二、把定位后的数据的经纬度上传给后台,后台再根据收到的经纬度获取详细的经纬度信息,对司机的除经纬度以外的地理信息进行深度比较,优先比较 altitude、horizontalAccuracy、verticalAccuracy 值,根据值是否相等进行权衡后,确定。

三、

(一)通过获取公网 ip,大概再通过接口根据 ip 地址可获取大概的位置,但误差范围有点大。

  1. //获取公网ip地址 
  2. var ipAddress: String? { 
  3.  
  4.     let ipUrl = URL(string: "https://ipof.in/txt")! 
  5.     let ip = try? String.init(contentsOf: ipUrl, encoding: .utf8) 
  6.  
  7.     return ip 

(二)通过 Wi-Fi 热点来读取 app 位置[3]

(三)利用 CLCircularRegion 设定区域中心的指定经纬度和可设定半径范围,进行监听。

代码简略实现:

  1. manager = CLLocationManager() 
  2. //设置定位服务管理器代理 
  3. manager?.delegate = self 
  4. //设置定位模式 
  5. manager?.desiredAccuracy = kCLLocationAccuracyBest 
  6. //更新距离 
  7. manager?.distanceFilter = 100 
  8. //发送授权申请 
  9. manager?.requestWhenInUseAuthorization() 
  10.  
  11. let latitude = 115.47560123242931 
  12. let longitude = 29.9757535600194 
  13. let centerCoordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) 
  14. let locationIDStr = "" 
  15. let clRegion = CLCircularRegion(center: centerCoordinate, radius: 100, identifier: locationIDStr) 
  16. manager?.startMonitoring(for: clRegion) 
  17.  
  18. 代理方法 
  19.  
  20.  func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { 
  21.  
  22.  
  23. func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { 
  24.  

(四)通过 IBeacon 技术,使用 CoreBluetooth 框架下的 CBPeripheralManager 建立一个蓝牙基站。这种定位直接是端对端的直接定位,省去了 GPS 的卫星和蜂窝数据的基站通信。

代码简略实现:

  1. func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) { 
  2.  
  3.     for beacon in beacons { 
  4.         var proximityStr: String = "" 
  5.         switch beacon.proximity { 
  6.         case .far: 
  7.             proximityStr = "Unknown" 
  8.         case .immediate: 
  9.             proximityStr = "Immediate" 
  10.         case .near: 
  11.             proximityStr = "Near" 
  12.         case .unknown: 
  13.             proximityStr = "Unknown" 
  14.         } 
  15.  
  16.         var beaconStr = "信号:" + beacon.proximityUUID.uuidString + "major:" + beacon.major.stringValue + "minor:" + beacon.minor.stringValue + "距离:" + beacon.accuracy + "信号:" + "\(Int64(beacon.rssi))" + "接近度:" + proximityStr 
  17.  
  18.         print("beacon信息: \(beaconStr)"
  19.     } 
  20.  
  21.  
  22. func locationManager(_ manager: CLLocationManager, rangingBeaconsDidFailFor region: CLBeaconRegion, withError error: Error) { 
  23.  
  24.      
  25. ---------------------------------------------------------------------------------- 
  26.  
  27. //不能单独创建一个类遵守CBPeripheralManagerDelegate协议,需要先遵守NSObjectProtocol协议,这里直接继承NSObject 
  28. class CoreBluetoothManager:NSObject, CBPeripheralManagerDelegate {  
  29.      
  30.     //建立一个蓝牙基站。 
  31.     lazy var peripheralManager: CBPeripheralManager =  CBPeripheralManager(delegate: self, queue: DispatchQueue.main, options: nil) 
  32.              
  33.     lazy var region: CLBeaconRegion = { 
  34.          
  35.         guard let uuid = UUID(uuidString: "xxx"else { 
  36.             return CLBeaconRegion() 
  37.         } 
  38.         let major: CLBeaconMajorValue = 1 
  39.         let minor: CLBeaconMajorValue = 1 
  40.         let id = "创建的蓝牙基站的名称" 
  41.         let region = CLBeaconRegion(proximityUUID: uuid, major: major, minor: minor, identifier: id) 
  42.         return region 
  43.     }() 
  44.      
  45.     func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { 
  46.          
  47.         switch peripheral.state { 
  48.         case CBManagerState.poweredOn: 
  49.              
  50.             if let data = self.region.peripheralData(withMeasuredPower: nil) as? [String : Any] { 
  51.                  
  52.                 self.peripheralManager.startAdvertising(data) 
  53.             } 
  54.              
  55.         case CBManagerState.poweredOff, 
  56.              CBManagerState.resetting, 
  57.              CBManagerState.unauthorized, 
  58.              CBManagerState.unsupported, 
  59.              CBManagerState.unknown: 
  60.              
  61.             break 
  62.         } 
  63.     } 
  64.     
  65.     func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) { 
  66.          
  67.     } 
  68.          

四(待完善)、 iOS防黑产虚假定位检测技术 文章的末尾附的解法本人有尝试过,一层一层通过 kvc 读取 CLLocation 的 _internal 的 fLocation,只能读取到到此。再通过 kvc 读取会报以下错误:

  1. Expression can't be run, because there is no JIT compiled function 

深入研究,在苹果的官方开发文档上发现了这个解释[4],也有说设置 debug+ 优化策略的,但 iOS 默认 bug 环境就是 -Onone 级别的。其实主要原因貌似因为 JIT 的设置是在开发 mac 客户端的时候,才能在 Signing&Capabilities 的 Hardened Runtime 中找到。关于 Allow Execution of JIT-compiled Code 的设置(官方文章[5])。最终只能卡到这里,若有大神能通过其他方式读取 CLLocation 的真实定位(这是极其完美的解决方案),还请不吝赐教。

附:

CLLocation 对象私有变量 _internal 实例对象的官方定义[6]:

  1. @interface CLLocationInternal : NSObject { 
  2.     struct { 
  3.         int suitability; 
  4.         struct { 
  5.             double latitude; 
  6.             double longitude; 
  7.         } coordinate; 
  8.         double horizontalAccuracy; 
  9.         double altitude; 
  10.         double verticalAccuracy; 
  11.         double speed; 
  12.         double speedAccuracy; 
  13.         double course; 
  14.         double courseAccuracy; 
  15.         double timestamp
  16.         int confidence; 
  17.         double lifespan; 
  18.         int type; 
  19.         struct { 
  20.             double latitude; 
  21.             double longitude; 
  22.         } rawCoordinate; 
  23.         double rawCourse; 
  24.         int floor; 
  25.         unsigned int integrity; 
  26.         int referenceFrame; 
  27.         int rawReferenceFrame; 
  28.     }  fLocation; 
  29.     CLLocationMatchInfo * fMatchInfo; 
  30.     double  fTrustedTimestamp; 
  1. @class NSData; 
  2.  
  3. @interface CLLocationMatchInfo : NSObject <NSCopying, NSSecureCoding> { 
  4.  
  5.     id _internal; 
  6. @property (nonatomic,readonly) long long matchQuality; 
  7. @property (nonatomic,readonly) CLLocationCoordinate2D matchCoordinate; 
  8. @property (nonatomic,readonly) double matchCourse; 
  9. @property (nonatomic,readonly) int matchFormOfWay; 
  10. @property (nonatomic,readonly) int matchRoadClass; 
  11. @property (getter=isMatchShifted,nonatomic,readonly) BOOL matchShifted; 
  12. @property (nonatomic,copy,readonly) NSData * matchDataArray; 

参考资料

[1]用代码判断 iOS 系统是否越狱的方法: https://www.huaweicloud.com/articles/7c6b8027253c4a97196d359840f638d9.html

[2]iOS 防黑产虚假定位检测技术: 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

原文地址:https://mp.weixin.qq.com/s/ZbZ4pFzzyfrQifmLewrxsw