Hook原理

什么是hook

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

iOS中hook的三种方式

rumtime

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

fishhook

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

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 #43

fishhook的使用

fishhook交换NSLog()

从github下载fishhook源码,可以看到fishhook源码就一个.c和.h加起来不到300行代码。新建一个工程,并添加以下代码:

rebinding结构体是fishhook提供的* name字段表示需要hook的函数的名称。* replaced字段这里需要传入一个函数指针,用来保存被hook的函数的原始实现。* replacement传入咱们自己实现,用来替换的的函数。点击屏幕,发现我们的输出带上了后缀,表示hook成功了!!!

fishhook交换自定义的函数

fishhook 原理

iOS工程师们经常会听到说Objective-C是一门动态语言,而C是一门静态语言,这里说的动态和静态,具体是指什么呢?主要区别在于编译时确定,还是运行时确定。那么这个确定,是指确定什么呢,比如变量的具体类型,函数的具体实现等...下面举个例子,在工程中声明一个OC方法,不写定义代码,和声明一个C方法,同样不写定义代码。编译一下,查看编译器是否通过?

共享缓存

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的。

我们回到交换NSLog函数的代码,在调用fishhook重绑定前,添加一行NSLog输出代码,再分别在NSLog打印前,打印后,和fishhook重绑定后打上三个断点:

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

fishhook 源码分析

fishhook的实现代码不过200多行,分析这200多行代码,需要对Mach-O文件有一定的理解。如果有兴趣的可以查看我之前的文章Mach-O文件,如果不了解Mach-O文件的话,那这200多行的代码就有点像天书...

先看一下fishhook的头文件

再来看实现代码,这里就只分析int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel)这个不需要指定镜像的。

接下来看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 Commandfor (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的segmentfor (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的segmentif (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的sectionfor (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起始的indexuint32_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 tableuint32_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];//遍历最初的链表,来进行hookstruct 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肯定在前面,这样就可以屏蔽恶意代码注入了。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力的Kiko君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值