Hook原理

什么是hook

HOOK,中文译为“挂钩”或“钩子”。在iOS逆向中是指改变程序运行流程的一种技术。 例如,一个正常的程序运行流程是A->B->C,经过hook技术可让程序的执行变成A->咱们本身的代码->B->C。在这个过程当中,咱们的代码能够获取到A传递B的数据,对其进行修改或利用再传递给B,而A,B是不会感知到这个过程的。因此,经过hook可让别人的程序执行本身所写的代码。在逆向中常用这种技术。在学习过程当中,咱们重点要了解其原理,这样可以对恶意代码进行有效的防御。在iOS系统中有如下三种方式能够实现hook,这篇文章主要讲究fishhook的使用及其原理。git

iOS中hook的三种方式

rumtime

利用OC的runtime api,动态改变SEL和IMP的对应关系,达到方法调用流程改变的目的。主要用于OC方法github

fishhook

它是Facebook开源的一个动态修改连接Mach-O文件的工具。利用dyld加载Mach-O文件的原理,经过修改懒加载和非懒加载两个表的指针达到hook系统动态库函数的目的。主要用于系统库的C函数。api

Cydia Substrate

Cydia Substrate原名为Mobile Substrate,它的主要做用是针对OC方法,C函数以及函数地址进行Hook操做。而且它并非仅仅针对iOS设计的,在安卓平台同样可使用。官方地址www.cydiasubstrate.com数组

rumtime hook

不少文章里介绍说是使用Method Swizzling来进行方法交换的,但其实吧,这两单词翻译过来就是方法交换的意思。代入原句就很别扭了,明明就是使用runtime提供的api来达到Mehtod Swizzling方法交换的目的。其实runtime里面除了使用method_exchangeImplementations()来实现方法的交换之外,还可使用class_replaceMethod()方法替换实现,也可使用method_getImplementation()method_setImplementation搭配使用实现。这个部分的内容在我前面的文章代码注入中已经讲过了,感兴趣的读者能够自行前往查看。缓存

方法交换在正向开发中可用于埋点,数据监控统计,防止崩溃等,在iOS逆向工程中能够经过对某个方法进行拦截和修改达到修改逻辑和数据的目的,在后面的实战中会大量使用该技术。安全

fishhook

fishhook利用了dyld加载Mach-O文件的原理,dyld从iOS13开始,从dyld2更新到了dyld3,目前看来,对fishhook的影响不是很大,依然能够正常使用。这里贴上github对这个问题的讨论 fishhook with dyld 3.0 #43markdown

fishhook的使用

fishhook交换NSLog()

从github下载fishhook源码,能够看到fishhook源码就一个.c和.h加起来不到300行代码。新建一个工程,并添加如下代码: image.png 这里须要讲一下fishhook提供的api就两个,都是c语言的函数。rebing_symbols函数须要两个参数,第一个参数是一个rebinding结构体的数组,第二个参数是数组的个数。架构

rebinding结构体是fishhook提供的
ide

  • name字段表示须要hook的函数的名称。
  • replaced字段这里须要传入一个函数指针,用来保存被hook的函数的原始实现。
  • replacement传入我们本身实现,用来替换的的函数。

点击屏幕,发现咱们的输出带上了后缀,表示hook成功了!!! image.png函数

fishhook交换自定义的函数

image.png 没有交换成功,为何fishhook能够交换系统的C函数,而没法交换咱们自定义的C函数呢,请看下面的原理

fishhook 原理

iOS工程师们常常会听到说Objective-C是一门动态语言,而C是一门静态语言,这里说的动态和静态,具体是指什么呢?主要区别在于编译时肯定,仍是运行时肯定。那么这个肯定,是指肯定什么呢,好比变量的具体类型,函数的具体实现等...下面举个例子,在工程中声明一个OC方法,不写定义代码,和声明一个C方法,一样不写定义代码。编译一下,查看编译器是否经过? image.png 虽然Xcode给出了一个警告⚠️test方法定义未找到,但仍是编译经过了。 image.png 而C语言声明的一个函数func,Xcode提示编译未经过,报错Undefined symbol:_func未定义的符号_func,这里系统自动给func加上下划线。Objective-C的动态特性使咱们可使用runtime来hook,而C的静态特性决定了它的函数实如今编译期就肯定了,是没法进行hook的,那么为何咱们iOS系统的C函数可以被fishhook交换呢?

共享缓存

iOS中使用了共享缓存技术,每一个APP进程都会用到的系统库,好比UIKit,Foundation...都会被放到共享缓存库中,在个人上篇文章dyld中讲到过,感兴趣的同窗能够前往查看

位置无关代码 (position-independent code,PIC)

由于C函数是静态的,在编译的时刻,就须要有一个C函数的实现地址。而iOS因为有了共享缓存机制,使得咱们APP内调用的系统函数,经过dyld加载进内存的时候,才会绑定系统函数在共享缓存中的地址。这里存在一个矛盾,编译器在编译C函数的时候,必需要一个地址,但共享缓存的存在,让咱们实际的地址只有在运行的时刻才能知道。因此苹果使用PIC技术。

根据当前APP中调用到了的系统库函数的符号(好比NSLog),在Mach-O的Data段(Data段可读写)创建了了懒加载表和非懒加载表,在编译的时候,就使用对应的符号地址,这个时候的地址是内存中的随机值,仅仅是为了经过编译。在程序启动dyld执行完绑定时,这个时候才将共享缓存中真正的实现地址找到并赋值给咱们的符号。

理论讲了那么多,怎么验证咱们讲的是否是对的?

根据经验咱们知道NSLog符号是懒加载的,那咱们就以NSLog符号举例。咱们能够新建一个崭新的工程,什么代码也不写,直接查看Mach-O文件的懒加载符号指针以下图,是找不到NSLog的。 image.png 而后咱们在任何位置,添加一句NSLog打印代码: image.png 以后再次查看Mach-O文件的懒加载符号指针: image.png 发现多了一个地址,这个就是咱们NSLog符号在咱们Mach-O文件中的地址。这里证实了系统确实是根据咱们项目里面用到的库函数,来创建的符号表的。

咱们回到交换NSLog函数的代码,在调用fishhook重绑定前,添加一行NSLog输出代码,再分别在NSLog打印前,打印后,和fishhook重绑定后打上三个断点: Snip20210708_66.png 再次运行,到第一个断点,使用LLDB的image list命令查看咱们当前程序加载的镜像以及地址,咱们只须要第一个镜像,也就是咱们当前APP本身的Mach-O的内存地址 image.png 再打开Mach-O文件查看NSLog符号的地址 image.png 须要注意的是,Mach-O文件中NSLog符号的地址是至关于当前文件的偏移,再加上当前Mach-O文件在内存中的地址,就是上一步获得的。相加的值才是NSLog符号在内存中的地址。使用LLDB命令memory read 地址能够查看内存中的值,我这里加起来是0x1024F0000,再使用dis -s 地址查看反汇编,发现如今啥也不是。 image.png 这个时候,放掉第一个断点,来到第二个断点,此时NSLog就已经打印一次了,那么咱们NSLog符号的地址里面存放的应该就是Foundation中NSLog的实现。验证一下: image.png 没有问题,咱们来到最后一个断点,这个时候fishhook执行了重绑定代码,那么咱们看看如今NSLog符号是否是指向了咱们的实现: image.png

能够看到,fishhook其实就是修改懒加载符号表,非懒加载符号表中符号指向的地址,从而达到hook的目的。

fishhook 源码分析

fishhook的实现代码不过200多行,分析这200多行代码,须要对Mach-O文件有必定的理解。若是有兴趣的能够查看我以前的文章Mach-O文件,若是不了解Mach-O文件的话,那这200多行的代码就有点像天书...

先看一下fishhook的头文件 image.png 头文件很是简单,一个rebinding结构体,两个C函数,都是用来重绑定的,其中一个不须要指定镜像和ASLR的偏移,另外一个须要。通常推荐仍是使用不须要指定镜像和偏移的,毕竟这两个参数咱们也不太好弄到...(可使用dyld提供的一些api获取,但不是很方便,dyld的api在mach-o/loader.h,使用Xcode快捷键command + shift + o打开),并且我在使用指定镜像的这个api的时候,会出现只绑定成功一次的状况...

再来看实现代码,这里就只分析int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel)这个不须要指定镜像的。 image.png 首先调用prepend_rebindings()来作一些准备工做,rebind_symbols使用链表来存储每一次调用本身时,传入的参数,每次调用的时候和参数一块儿构成新的一张表,新表的next指向旧的表,这样每次调用的参数都保存下来了... image.png 根据_rebindings_head->next是否有值,就能够判断出是不是第一次调用rebind_symbols,若是是第一次调用,就调用注册监听函数_dyld_register_func_for_add_image(),已经被dyld加载的镜像会马上执行回调,以后加载的镜像,会在dyld加载的时候触发回调。若是不是第一次调用了,就不须要重复注册回调了,直接遍历全部镜像进行重绑定。 image.png 回调函数_rebind_symbols_for_image里面调用本身的重绑定函数。

接下来看rebind_symbols_for_image的实现

Dl_info info;
    if (dladdr(header, &info) == 0) {
        return;
    }
复制代码

首先是一段判断逻辑,不太理解是作什么的,但不影响对后面总体流程的理解,就放过。。。

//定义好几个变量,准备从MachO里面去找!
    segment_command_t *cur_seg_cmd;
    segment_command_t *linkedit_segment = NULL;
    struct symtab_command* symtab_cmd = NULL;
    struct dysymtab_command* dysymtab_cmd = NULL;
    
    uintptr_t size = sizeof(mach_header_t);
    uintptr_t cur = (uintptr_t)header + size;
    // 循环遍历Mach-O文件的Load Commands,找到上面3个须要的Load Command
    for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
        cur_seg_cmd = (segment_command_t *)cur;
        if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
            if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
                linkedit_segment = cur_seg_cmd;
            }
        } else if (cur_seg_cmd->cmd == LC_SYMTAB) {
            symtab_cmd = (struct symtab_command*)cur_seg_cmd;
        } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
            dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
        }
    }
    //若是刚才获取的,有一项为空就直接返回,dysymtab_cmd->nindirectsyms意思LC_DYSYMTAB加载命令中 间接符号表个数的意思 小于0意思没有就不执行后面的代码了
    if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment || !dysymtab_cmd->nindirectsyms) {
        return;
    }
复制代码

这一步就是从Mach-O文件中的Load Commands中找到想要的加载命令,分别是LC_SYMTAB,LC_DYSYMTAB和__LINKEDIT段,下一步根据这几个Load Command分别找到符号表,字符串表和间接符号表的地址

// 镜像文件头在内存中的地址简称基址 = __LINKEDIT.VM_Address -__LINKEDIT.File_Offset + silde的改变值
    uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
    
    //符号表的地址 = 基址 + 符号表偏移量
    nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
   
    //字符串表的地址 = 基址 + 字符串表偏移量
    char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
    
    //动态符号表地址 = 基址 + 动态符号表偏移量
    uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
复制代码

接着,又是遍历一遍Load Commands,找到咱们的非懒加载符号表,和懒加载符号表,这个过程判断比较多,由于非懒加载符号表和懒加载符号表在__DATA_CONST段的__got节和__DATA段的__la_symbol_ptr节中

cur = (uintptr_t)header + sizeof(mach_header_t);
    // 又是遍历一遍Load Commands,若是是LC_SEGMENT_64或LC_SEGMENT加载命令,那么找到名字为__DATA_CONST或__DATA的segment
    for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
        cur_seg_cmd = (segment_command_t *)cur;
        if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
            //找到名字为__DATA_CONST或__DATA的segment
            if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 && strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
                continue;
            }
            
            // 找到section为S_LAZY_SYMBOL_POINTERS或者S_NON_LAZY_SYMBOL_POINTERS的section
            for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
                section_t *sect = (section_t *)(cur + sizeof(segment_command_t)) + j;
                //找懒加载表
                if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
                    perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
                }
                //非懒加载表
                if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
                    perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
                }
            }
        }
    }
复制代码

LC_SEGMENT_ARCH_DEPENDENT是一个针对不一样架构的宏,对应的是普通的段或者64位架构的段

#ifdef __LP64__
typedef struct mach_header_64 mach_header_t;
typedef struct segment_command_64 segment_command_t;
typedef struct section_64 section_t;
typedef struct nlist_64 nlist_t;
#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64
#else
typedef struct mach_header mach_header_t;
typedef struct segment_command segment_command_t;
typedef struct section section_t;
typedef struct nlist nlist_t;
#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT
#endif
复制代码

找到了懒加载表或者非懒加载表以后,就开始执行真正的重绑定逻辑了perform_rebinding_with_section()

//__got和__la_symbol_ptr section中的reserved1字段指明对应的indirect_symbol table起始的index
    uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
    
    //slide + section->addr 就是符号对应的存放函数实现的数组
    //也就是我相应的__got和__la_symbol_ptr相应的函数指针都在这里面了,因此能够去寻找到函数的地址
    void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
复制代码

上面两个变量,一个用来寻找符号,一个用来寻找符号对应的函数实现地址,而后是遍历两个表里面的每个符号,每个符号都跟链表里面的咱们传入的name匹配,若是一致就说明找到了要hook的符号,而后将符号对应的原始函数实现地址,赋值给咱们用来保存的变量replaced,再将咱们自定义函数的地址赋值给符号保存;这样,咱们APP代码调用符号函数的时候,就先来到了咱们自定义函数的逻辑,若是咱们在自定义的函数逻辑里,调用保存的原始函数实现,就实现了hook,代码以下

//遍历section里面的每个符号
    unsigned long long count = section->size / sizeof(void *);
    for (uint i = 0; i < count; i++) {
        //找到符号在Indrect Symbol Table表中的值
        //读取indirect table中的数据
        uint32_t symtab_index = indirect_symbol_indices[i];
        if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
            symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
            continue;
        }
        //以symtab_index做为下标,访问symbol table
        uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
        //获取到symbol_name,能够打印出每一个符号
        char *symbol_name = strtab + strtab_offset;
        //判断是否函数的名称是否有两个字符,为啥是两个,由于C函数前面有个_,因此函数的名称最少要1个
        bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
        //遍历最初的链表,来进行hook
        struct rebindings_entry *cur = rebindings;
        while (cur) {
            for (uint j = 0; j < cur->rebindings_nel; j++) {
                struct rebinding one = cur->rebindings[j];
                //这里if的条件就是判断从symbol_name[1],从1开始去掉了_,两个函数的名字是否都是一致的
                if (symbol_name_longer_than_1 && strcmp(&symbol_name[1], one.name) == 0) {
                    //判断replaced的地址不为NULL以及原始方法的实现和rebindings[j].replacement的方法不一致
                    if (one.replaced != NULL && indirect_symbol_bindings[i] != one.replacement) {
                        //让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址
                        *(one.replaced) = indirect_symbol_bindings[i];
                    }
                    //将替换后的方法给原先的方法,也就是替换内容为自定义函数地址,
                    indirect_symbol_bindings[i] = one.replacement;
                    goto symbol_loop;
                }
            }
            cur = cur->next;
        }
    symbol_loop:;
    }
复制代码

Cydia Substrate

MobileHooker

顾名思义用于HOOK。它定义一系列的宏和函数,底层调用objc的runtime和fishhook来替换系统或者目标应用的函数。其中有两个函数:

  • MSHookMessageEx 主要做用于Objective-C方法
  • MSHookFunction 主要做用于C和C++函数,Logos语法的%hook就是对此函数作了一层封装。

MobileLoader

MobileLoader用于加载第三方dylib在运行的应用程序中。启动时MobileLoader会根据规则把指定目录的第三方的动态库加载进去,第三方的动态库也就是咱们写的破解程序。

safe mode

破解程序本质是dylib,寄生在别人进程里。系统进程一旦出错,可能致使整个进程崩溃,崩溃后就会形成iOS瘫痪。因此CydiaSubstrate引入了安全模式,在安全模式下全部基于Cydia Substratede的三方dylib都会被禁用,便于查错与修复。

反hook初探

利用fishhook修改runtime的相关api,好比上面所讲的method_exchangeImplementations等等,但须要最早加载,不然无效,放在工程的Framework中最好,这样别人没法使用第三方Framework插入的方式进行代码注入了,使用过yololib工具注入的同窗会发现,插入的Framework只能放在Load Commands的最后一条,那样咱们本身的Framework确定在前面,这样就能够屏蔽恶意代码注入了。