iOS上虚拟定位检测的探究

最近收到了好多莫名奇妙的crash反馈,仔细分析后发现这些crash发生在越狱机的虚拟定位插件上,一般这种crash发生在虚拟定位插件的dylib内,根本无法去调试。想要避免这种crash是不可能了,只能针对使用虚拟定位的情况进行一些检测,然后去给出风险提示,所以对这部分内容进行了一些研究和探索。

目前遇到的情况

首先是去查询下crash所在的xxx.dylib,基本都是一些虚拟定位插件的dylib,因为发生在第三方的dylib内,根本无法去进行Debug。还有一些是通过将ipa砸壳进行动态库注入,然后重签名安装到非越狱机上,也可以实现修改位置的效果。最近“阴阳师”的流行也催生了另外一种方法,借助于iOS系统本身的模拟位置功能。

因为我本身也不是专业做移动安全的,思路和方法基本上来自于各种Google,也看了很多比如 iosre.com 等一些逆向相关的网站和教程,所以思路和方法不一定完全正确,如有问题还希望懂安全的大神们指出。

常见的iOS端虚拟定位方式

首先介绍下目前见到过的虚拟定位的方式,主要有以下三种方式:

  1. 通过各种方式找到CLLocationManager的delegate的locationManager:didUpdateLocations:回调方法实现并进行hook,然后仅修改回调的参数CLLocation的coordiante数值,其他数值保持不变。

  2. 通过CLLocation类coordinate属性的get方法,对返回的coordinate数值进行修改。既可以通过hook coordinate方法实现,也可以为CLLocation类新增category并对coordinate方法进行override。

  3. 借助iOS系统Map应用的模拟位置功能,这种方式需要对iPhone的备份进行修改,然后用修改过的备份对iPhone进行恢复。(可自行百度:iOS不越狱修改定位)

另外:

  • 其中前两种方式,无论是在越狱环境还是非越狱环境都有对应的实现方式,但本质都是借助于OC语言的runtime特性实现。

  • 越狱与非越狱的区别仅在执行hook的途径不同,越狱环境下可以通过编写Tweak或者其他方式执行,非越狱环境下可以通过对ipa砸壳、做动态库注入、重签名安装等流程实现。

常见的虚拟定位的实现原理

然后逐个简单的说一下每种方式的基本原理吧:

  1. 第一种方式是最常见的虚拟定位的方式,是大多数越狱插件选择的方式,某些越狱插件会找到CLLocationManager类的setDelegate:方法进行hook,某些会对CLLocationManager的startUpdateLocation方法进行hook,但最终的目的都是要顺藤摸瓜找到locationManager:didUpdateLocations:方法并进行hook,达到修改回调参数CLLocation的coordiante数值的目的。

  2. 第二种方式,可以通过对coordiante的get方法进行hook,进而修改返回值。更简单的一个实现方式是,可以为CLLocation类增加新的category,然后直接重写coordiante属性的get方法,返回特定的位置值。但这种方式的缺点也比较明显,修改后会影响所有的CLLocation类实例,所有用到CLLocation类的地方都会受到影响,很容易造成误伤。

  3. 第三种方式,目前还没找到特别明显的特征,通过这种方式实现的话,locationManager:didUpdateLocations:回调会每一秒调用一次,每次返回的定位信息除时间戳外完全相同。看到这些特征的时候,会想到是否可以通过返回值的这些特征进行识别,但后来发现有个坑是,如果在xcode中对程序进行debug模拟位置的时候,locationManager:didUpdateLocations:回调的返回值特征和这种方式一样,所以如果用返回值的特征进行识别,则存在影响xcode进行debug模拟位置的可能性。

检测虚拟定位的基本原理

前面主要介绍了虚拟定位的主要方式和与其对应的基本实现原理,那怎么去检测是否存在虚拟定位呢?因为除了第三种方式外,其他两种方式本质上都是借助于OC的runtime特性,hook相关的回调函数或者接口去实现的,这里主要介绍下如何去检测一个方法是否被hook。

  1. 可以检测将要被hook的目标method的implemention是否在method原始的module中,只要method被hook并且替换了imp,method的真实imp往会在攻击module中,至少不会在method原始的module中。

  2. 如果是app开发中,可以检测将要被hook的目标方法的imp是否在app的原始可执行mach-o文件中,一般修改定位信息做虚拟定位的都是第三方,越狱机的插件dylib和非越狱环境下的动态库注入,hook目标方法后替换的新imp都不会存在于app原始的可执行mach-o文件中。

  3. 如果是framework的开发的话,要分情况去介绍:

    • 如果你开发的是staticLibrary,则和上面的app开发原理一致,当其他app引入你的framework打包ipa后,最终会将你的framework的mach-o文件链接到app的可执行mach-o文件中。
    • 如果你开发的是dynamicLibrary,或者开发者将你的staticLibrary包含到一个动态库并作为resource放到mainBundle中的话,则需要检测hook的目标方法的imp是否在dynamicLibrary的module中。

呃。。。总结性描述太干了,来详细说一下吧:

  • 熟悉OC的runtime的或者使用过MethodSwizzling话应该知道,OC中method和methodImplemention的对应关系是在运行时确定的,OC中调用一个实例对象的方法,其本质是向其发送消息,然后根据消息(SEL)去Class的DispatchTable中查找method对应的implemention,这也是对方法进行hook的基本原理。

  • 前面一直说到的模块(module),是指你的methodImplemention所在mach-o文件:

    • 如果你是app开发,你的代码,包括你的方法和方法实现,最终会被编译链接到与你app同名的一个executable mach-o文件中;
    • 如果你开发的是dynamicLibrary,那最终是在一个dylib类型的mach-o文件中;
    • 如果你开发的是staticLibrary,那最终是在一个intermediate mach-o文件中,当你的staticLibrary被其他app或者另外的dynamicLibrary所引入后,最终还是会被链接到对应的mach-o文件中。

      (具体详细内容可以参考”OS X ABI Mach-O File Format Reference.pdf”)

  • 大多数的虚拟定位会利用MethodSwizzling原理,去hook相应的目标函数,method被替换的新implemention所在的module往往不会与原始implemention所在的module一致。越狱插件的方式新implemention所在的module通常是插件本身的dylib;对ipa砸壳做动态库注入的方式,新implemention所在的module通常是被注入的dylib。

  • 检测implemention的方式还可以避免掉一个坑,因为都是借助于runtime去hook目标函数,开发过程中在源代码中进行类似的处理,也可以造成虚拟定位的效果。但此时无论你如何利用MethodSwizzling,最终替换的新implemention还是与原始implemention所在的module一致,这样就避免掉自己给CLLocation加了个分类,然后override了coordiante的get方法,被错误识别成存在虚拟定位的尴尬。

检测虚拟定位用到的方法

dladdr函数,去获取方法的实现所在的模块,可以参考Apple的官方文档中对dladdr函数的介绍: dladdr函数

检测方法的代码示例

引入头文件

1
2
#import <objc/runtime.h>
#import <dlfcn.h>

获取一个方法所在的模块信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void logMethodInfo(const char *className, const char *sel)
{
Dl_info info;
IMP imp = class_getMethodImplementation(objc_getClass(className),sel_registerName(sel));
if(dladdr(imp,&info)) {
NSLog(@"method %s %s:", className, sel);
NSLog(@"dli_fname:%s",info.dli_fname);
NSLog(@"dli_sname:%s",info.dli_sname);
NSLog(@"dli_fbase:%p",info.dli_fbase);
NSLog(@"dli_saddr:%p",info.dli_saddr);
} else {
NSLog(@"error: can't find that symbol.");
}
}

其他

  • 应该注意对检测方法的保护,比如做混淆、使用函数指针、裁剪符号表、避免使用常量字符串等等方式去保护,增加被逆向研究的难度。

  • 目前这种检测方式依赖于dladdr函数,如果这个函数本身被修改了,并对上面的检测进行了过滤,就会使检测失效,需要研究和学习的就是对dladdr函数本身进行保护。