iOS逆向之深入解析Hook的原理方法和安全防护

举报
Serendipity·y 发表于 2022/02/16 23:15:03 2022/02/16
【摘要】 Hook 简介 Hook就是一种改变程序执行流程的一种技术的统称;一段程序的执行流程是 A --> B --> C,现在我们在 A 和 B 之间插入一段代码或者直接改变 B ,这样程序原有的...

Hook 简介

  • Hook就是一种改变程序执行流程的一种技术的统称;
  • 一段程序的执行流程是 A --> B --> C,现在我们在 A 和 B 之间插入一段代码或者直接改变 B ,这样程序原有的执行流程就发生了改变。如下图所示:

在这里插入图片描述

  • Hook的方式:Method Swizzle,fishhook,Cydia Substrate;

Hook 原理

一、Method Swizzle 原理

  • 利用OC的Runtime特性,动态去改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的主要用于OC方法。
  • Hook中主要用到的方法(参数: Class、SEL、IMP、Method):
// 1、方法交换
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

// 2、替换方法
OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
                    const char * _Nullable types)
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

// 3、setIMP & getIMP
OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT IMP _Nullable
class_getMethodImplementation(Class _Nullable cls, SEL _Nonnull name)
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • Class 一个 objc_class 类型的结构体指针,用于说明对象是哪个类;
  • Method 一个 objc_method 类型的结构体指针,用于定义一个方法,在objc源码中定义如下:
struct objc_method {
    SEL _Nonnull method_name       OBJC2_UNAVAILABLE;
   	char * _Nullable method_types  OBJC2_UNAVAILABLE;
   	IMP _Nonnull method_imp        OBJC2_UNAVAILABLE;
}                                  OBJC2_UNAVAILABLE;

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • SEL 可以发现在 Method 的定义中,也能看见SEL。在苹果官方文档中定义这种类型:
typedef struct objc_selector *SEL;

  
 
  • 1
  • IMP 同样在 Method 的定义中也能看到,在苹果官方文档的定义如下:
id (IMP *)(id, SEL, ...)

  
 
  • 1
  • SEL是一个C String,用于表示一个方法的名称,IMP是一个方法实现首地址,默认有两个参数 self 和 _cmd。其实SEL和IMP的关系可以类比一本书的目录,SEL就是目录中的内容标题,IMP是后面的页码。一个方法调用时,通过SEL找到对应的IMP,进而找到方法的实现。如下图:

在这里插入图片描述

  • 在Hook一个OC方法时,只需要改变其SEL所指向的IMP时,就可以实现方法的交换的目的,或者使用class_replaceMethod 和 method_setImplementation 改变一个类原有方法的实现,原理如下图:

在这里插入图片描述

  • 逆向中,Hook一个OC方法其实就是改变其SEL所指向的IMP,从而找到另一个实现地址,执行另一个方法实现。但这种方法的局限在于,其只能针对OC方法进行Hook,对于C函数则无法Hook。

二、fishHook 原理

原理说明
  • fishhook主要利用了共享缓存功能和PIC技术来实现hook功能。
  • fishHook是由faceBook开发的,是一个动态修改MachO文件的工具,主要是通过修改懒加载和非懒加载表里的指针的指向来达到hook的目的,因此一般用它来hook系统的C函数。
  • DATA区有两个section和动态符号链接相关:__nl_symbol_ptr 、__la_symbol_ptr;__nl_symbol_ptr为一个指针数组,直接对应non-lazy绑定数据。__la_symbol_ptr也是一个指针数组,通过dyld_stub_binder辅助链接。
  • fishhook实现:根据符号(字符)获取系统函数地址;替换符号指向的地址为用户声明的函数地址(符号绑定);对外部声明的指针进行系统函数地址赋值。
  • 示例说明
static void (* old_log)(NSString* str);
void newLog(NSString * str){
    str = [str stringByAppendingString:@"\n 勾住了"];
    old_log(str);
}
// hook
rebind_symbols((struct rebinding [1]){{"NSLog",newLog,(void *)&old_log}}, 1);

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • rebind_symbols的简单逻辑
    假设:
    A -> 原方法 ,B -> 新方法 ,Temp -> 中间变量
    Temp = A;
    A = B;
    hook后需要调用原方法就调用Temp
  • 具体如下:
    1⃣️ 首先,MachO文件是被dyld(dynamic load)动态加载的,也就是说,MachO文件被加载到内存的时候是不固定的,它有一个ASLR随机值;
    2⃣️ 同样,依赖的系统库的加载也是随机的,因此,当MachO文件加载到内存之前根本不知道系统库在哪;
    3⃣️ 因此 ,苹果采用了PIC技术(位置代码独立,位置和代码无关);
使用示例:符号绑定分析
  • 假设要调用NSLog函数:首先会在映射表中增加一个间接指针,指向外部的NSLog函数;然后dyld会动态的去绑定,将指针指向外部的函数地址。
  • machO符号表中有懒加载表(_la_symbol_ptr)和非懒加载表(_nl_symbol_ptr)的_data段,在表中存放着与外部绑定的函数指针,在懒加载端有offset地址,如下:

在这里插入图片描述

  • 右边红色方框中的都是我们熟悉的名称,这些函数的使用是需要进行懒加载绑定的;
  • 左边的offset提供了相对于MachO起始地址的偏移量offset,实际地址即是系统函数所在的内存地址;
  • 我们观察NSLog函数,记住对应的offset=0x8018这个偏移量;
  • 使用fishHook的rebind_symbols库函数交换系统NSLog函数:
#pragma mark - 交换NSLog
- (void)exchangeLog {
	// 加载出来NSLog函数,生成内存地址(因为NSLog是Lazy Symbol Pointers)
     NSLog(@"我来了");
    // hook NSLog函数
    struct rebinding imp;
    imp.name = "NSLog";
    imp.replacement = my_NSLog;
    imp.replaced = &sys_nslog;
    
    //  存放rebinding结构体数组,一次可以交换多个函数
    struct rebinding rebs[1] = {imp};
    rebind_symbols(rebs, 1);
    NSLog(@"点击了屏幕");
}
// 实现一个函数来替换原有函数-函数名称即是函数的指针
void my_NSLog(NSString *format, ...) {
    printf("拦截打印\n");
    sys_nslog(format);
}
// 定义指针来接收原始函数的指针
static void (*sys_nslog)(NSString *format,...);


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 然后运行输出,点击屏幕,查看控制台打印输出,可以看到方法已被拦截,说明NSLog函数已被替换;
  • 所有的函数地址在编译后都是确定,在OC中能够交换方法是因为有sel和imp的连接过程,函数与用户之间有一个中间者,那么fishhook应该也是如此,修改了中间者的imp指向,否则直接调用函数,就没有交换的可能,在MachO中这个中间者叫符号。
  • 在rebind_symbols(rebs, 1);调用前打断点,在控制台执行image list获取模块列表(image就是一个个模块,一个个MachO文件),如下:

在这里插入图片描述

  • 上面的红色标志就是应用程序加载的起始地址(在程序运行期间是固定,重新启动该地址则会随机变化),然后根据这个地址去找NSLog函数的地址(偏移offset=0x8018),即在控制台继续输入:x 0x000000010453c000+0x8018

在这里插入图片描述

  • 上图中的74 39 59 84 01 00 00 00即为NSLog函数所对应的值,由于CPU的小端模式,因此需要从右往左取值: 0x0184593074就是NSLog的内存地址;然后dis -s 0x0184593074:

在这里插入图片描述

  • 继续让断点往下多执行一步,重复上面的操作,就可以看到fishhook对系统NSLog函数的替换:

在这里插入图片描述

通过符号表查找系统函数
  • fishHook官方说明示意图
    在这里插入图片描述
  • 回到MachO文件,来看看Lazy Symbol、Dynamic Symbol Table、Symbol Table、String Table表关系:
    ① Lazy Symbol和Dynamic Symbol Tabel一一对应(在数组的下标一致)这两个表包含了所有与动态库相关的符号;
    ② Dynamic Symbol Tabel和Symbol Table关联,Dynamic Symbol Table中的Data字段是Symbol Table数组的下标;
    ③ Symbol Table中的data字段地址 + String Table表的起始地址,就是目标函数对应字符的位置。
  • 首先在Lazy Symbol Pointers中找到NSLog函数,它处于表中第一个元素;

在这里插入图片描述

  • 找到indirect Symbols表中的NSLog项,它也处于表中第一个元素;

在这里插入图片描述

  • 在上图中找到NSLog的Data地址0x94,等于十进制148,即为Symbol Table表中NSLog对应的下标,如下:

在这里插入图片描述

  • 通过下标找到了对应的符号,主要上面的标注0xBA为String Table中的偏移量,表的起始地址加上偏移量即是函数名所在的位置;

在这里插入图片描述

  • 通过首地址获取最终函数名,0xBA + 0x6180 = 0x623A,找到0x623A地址处,如下:

在这里插入图片描述

  • 最终找到系统的函数NSLog。

三、Cydia Substrate原理

① 原理
  • Cydia Substrate 原名为 Mobile Substrate ,它的主要作用是针对OC方法、C函数以及函数地址进行HOOK操作。
  • 跟method Swizzle类似,也是用的OC的动态性,知识Method Swizzle是用的方法交换,Cydia Substrate 是用的method_getImplementation和 method_setImplementation这两个方法,获取方法实现和设置方法实现。
② 函数组成

Cydia Substrate主要由3部分组成:

  • MobileHooker
    它定义一系列的宏和函数,底层调用objc的runtime和fishhook来替换系统或者目标应用的函数。主要有两个函数:MSHookMessageEx 主要作用于Objective-C方法,MSHookFunction 主要作用于C和C++函数;
    MSHookMessageEx函数的作用对象是Objective-C函数,其原理是调用Objective-C中高等级的运行时函数API:class_getInstanceMethod、method_setImplementation、method_exchangeImplementations等来替换原函数的逻辑,其实MobileHooker就是对fishhook和runtime做了封装,就是和AFNetworking和NSURLSession的关系是一样的;
// MSHookMessageEx 
void MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP result)

// MSHookFunction
void MSHookFunction(voidfunction,void* replacement,void** p_original)


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • MobileLoader
    MobileLoader的作用就是去加载第三方dylib,在ios启动的时候,会由launchd将MobileLoader载入内存,然后MobileLoader会根据同名的plist文件指定的作用范围,有选择地在不同的进程当中去通过dlopen函数打开目录/Library/MobileSubstrate/DynamicLibraries/下的所有的dylib。
  • safe mode
    因为APP程序质量参差不齐崩溃再所难免,破解程序本质是dylib,寄生在别人进程里。 一旦出错,就可能导致整个进程崩溃,而一旦崩溃的是SpringBoard等系统进程,崩溃后就会造成iOS瘫痪。所以CydiaSubstrate引入了安全模式,在安全模式下所有基于CydiaSubstratede 的三方dylib都会被禁用,便于查错与修复。

Hook 流程(以Objective-C为例)

在 Objective-C 中,所有的 [receiver message] 都会转换为 objc_msgSend(receiver, @selector(message))。

一、寻找 IMP 过程
  • 在当前 class 的方法缓存里寻找(cache methodLists)
    找到了跳到对应的方法实现,没找到继续往下执行;
  • 从当前 class 的 方法列表里查找(methodLists),找到了添加到缓存列表里,然后跳转到对应的方法实现;没找到继续往下执行;
  • 从 superClass 的缓存列表和方法列表里查找,直到找到基类为止;
  • 以上步骤还找不到 IMP,则用_objc_msgForward函数指针代替 IMP ,最后执行这个 IMP;
// objc-runtime-new.mm 文件里与 _objc_msgForward 有关的三个函数使用伪代码展示,这也是 obj_msgSend 的实现过程

id objc_msgSend(id self, SEL op, ...) {
    if (!self) return nil;
    IMP imp = class_getMethodImplementation(self->isa, SEL op);
    // 调用这个函数,伪代码...
    imp(self, op, ...); }

//查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
    if (!cls || !sel) return nil;
    IMP imp = lookUpImpOrNil(cls, sel);
    // _objc_msgForward 用于消息转发
    if (!imp) return _objc_msgForward; 
    return imp;
}

IMP lookUpImpOrNil(Class cls, SEL sel) {
    if (!cls->initialize()) {
        _class_initialize(cls);
    }

    Class curClass = cls;
    IMP imp = nil;
    // 先查缓存,缓存没有时重建,仍旧没有则向父类查询
    do { 
        if (!curClass) break;
        if (!curClass->cache) fill_cache(cls, curClass);
        imp = cache_getImp(curClass, sel);
        if (imp) break;
    } while (curClass = curClass->superclass);

    return imp;
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
二、 消息转发流程
  • 第一个阶段的具体方法是+(BOOL)resolveInstanceMethod:(SEL)sel 和+(BOOL)resolveClassMethod:(SEL)sel,当方法是实例方法时调用前者,当方法为类方法时,调用后者。这个方法是为了给类利用 class_addMethod 添加方法的机会;
  • 第二个阶段是备援接收者阶段,对象的具体方法是-(id)forwardingTargetForSelector:(SEL)aSelector ,此时,运行时询问能否把消息转给其他接收者处理,也就是此时系统给了个将这个 SEL 转给其他对象的机会;
  • 第三个阶段是完整消息转发阶段,对应方法-(void)forwardInvocation:(NSInvocation *)anInvocation,这是消息转发流程的最后一个环节(hook方案的核心)。
    在这里插入图片描述
三、主要步骤
  • 直接替换原方法的实现为_objc_msgForward。当原来的函数被调用时,就不会在类方法,父类方法列表里查找实现了,直接表示找不到,进入转发流程。用_objc_msgForward来代替原来的函数,代码如下:
class_replaceMethod(class, selector, _objc_msgForward, method_getTypeEncoding(targetMethod));

  
 
  • 1
  • 替换forwardInvocation:的实现,当进入转发流程时,阶段一和阶段二都不接手,在阶段三forwardInvocation:里会接手;这里会替换forwardInvocation:的实现,用newForwardInvocation代替,这样就可以hook,完成自己的逻辑后,还要调用被hook的函数原来的逻辑。
    id newForwardInvocation = ^(id self, NSInvocation *invocation) {
        // hook时要添加的代码

        if (originalForwardInvocation == NULL) {
            [self doesNotRecognizeSelector:invocation.selector];
        } else {
            originalForwardInvocation(self, forwardInvocationSEL, invocation);
        }
    };

    class_replaceMethod(class, @selector(forwardInvocation:), imp_implementationWithBlock(newForwardInvocation), "v@:@");

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

简单防护

一、防护Method Swizzle
  • 我们知道Method Swizzle原理是方法交换,那么可以使用fishHook 修改method_exchangeImplementations函数的指向,这个修改指向要在我们的方法交换之后进行(保证自己能改,别人不能改)。
  • 我们的方法交换要在别人hook之前执行,这个地方就需要将我们的方法封装到静态库中,静态库中的load方法会先加载;
二、防护Cydia Substrate
  • 它的原理是修改imp的set和get方法,因此我们也可以通过fishHook修改method_getImplementation和method_setImplementation方法。
三、防护fishHook
  • 示例
+ (void)load {
    // 先交换,防护之前要将所有的交换都写完
    Method oldOne =  class_getInstanceMethod(objc_getClass("ViewController"), @selector(btnClickOne:));
    Method newOne =  class_getInstanceMethod(self, @selector(ClickOneHook:));
    method_exchangeImplementations(oldOne, newOne);
    
    // 基本防护 Method Swizzle
    struct rebinding bd ;
    // 原函数名(字符串) A函数
    bd.name = "method_exchangeImplementations"; 
    // 交换后的函数 B函数
    bd.replacement = myExchange; 
    // 暂存原函数的地址 中间变量temp函数
    bd.replaced = (void *)&old_exchage; 
    
    // 升级防护,用于防护logos(cydia substrate)
    // method_setImplementation
    struct rebinding bd1 ;
    bd1.name = "method_getImplementation";
    bd1.replacement = myExchange;
    bd1.replaced = (void *)&getImp;
    
    struct rebinding bd2 ;
    bd2.name = "method_setImplementation";
    bd2.replacement = myExchange;
    bd2.replaced = (void *)&setImp;
    
    // fishhook方法交换
    struct rebinding rebind[] = {bd,bd1,bd2};
    
    /*
     arg1:数组,元素必须是rebinding这个结构体
     arg2:数组个数
     */
    rebind_symbols(rebind, 3);
}


// 用于存放method_exchangeImplementations涵数 也就是Temp函数
void (* old_exchage)(Class _Nullable cls, SEL _Nonnull name);

// 用于存放method_getImplementation涵数
IMP _Nonnull(*getImp)(Method _Nonnull m) ;

// 用于存放method_setImplementation涵数
IMP _Nonnull(*setImp)(Method _Nonnull m, IMP _Nonnull imp) ;


// 新的交换函数 B函数
void myExchange(Class _Nullable cls, SEL _Nonnull name){
    NSLog(@"检测到hook");
    exit(1);
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

文章来源: blog.csdn.net,作者:Serendipity·y,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/Forever_wj/article/details/107777503

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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