iOS逆向-- fishhook原理分析

2,200 阅读13分钟

一、 hook分类

runtime

利用OC的Runtime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法

fishHook

是Facebook提供的一个动态修改链接mach-O文件的工具,利用MachO文件加载原理,通过修改懒加载和非懒加载两个表的指针达到C函数HOOK的目的

Cydia Substrate

原名是Mobile Substrate,它的主要作用是针对OC方法、C函数已经函数地址进行HOOK操作,并不是仅仅针对iOS设计的,安卓一样可以用

二、fishHook

  • 静态:我们知道c语言是静态,在编译时就确定了函数的地址,
  • 动态:而OC是运行时,动态的,只有当真正执行函数时候才回去找函数的地址

我们先来了解一下fishHook,首先有一个结构体

struct rebinding {
  const char *name;//需要HOOK的函数名称,C字符串
  void *replacement;//新函数的地址,C函数的名称就是函数的指针
  void **replaced;//原始函数地址的指针!
};

有一个函数,这个函数是用来hook所有C函数的

  • 参数1:一个rebinding结构体的数组,一次可以交换多个函数,并且通过结构体就知道要交换函数的名称,以及新老函数的地址
  • 参数2:数组的长度
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);

基本使用方式--hook系统函数NSLog

//---------------NSLog--------------------

+(void)handlNSLog{

    struct rebinding nslog;
    nslog.name = "NSLog";
    nslog.replacement = myNSLog;
    //fishhook 运行的时刻,动态的获取到NSLog的地址。
    nslog.replaced = (void *)&sys_nslog;

    //结构体数组
    struct rebinding rebs[1] = {nslog};
    /** 用于交换C函数
     * arg1:存放rebinding结构体的数组
     * arg2:
     */
    rebind_symbols(rebs, 1);
}
//函数指针,用来保存原始的函数地址
static void(*sys_nslog)(NSString * format,...);
//C函数的名称,就是函数的指针!
//定义一个新的函数
void myNSLog(NSString * format,...){

    format = [format stringByAppendingString:@"\n勾上了!"];

    //调用系统的函数
    sys_nslog(format);
}

为什么replaced是一个二级指针,并且赋予的值是(void *)&sys_nslog?

  • 因为NSLog在funcation框架里面,不同手机funcation的内存位置是不一样的,所以编译期我们没法确定函数的真实地址
  • 但是fishHook能在运行的时候可以动态获取NSLog的地址
  • 这个时候fishHook就需要一个指针来保存它获取到的函数地址,这就是为啥replaced是一个二级指针,是用来保存fishHook获取到的函数地址,
  • 并且当我们HOOK函数后,有可能执行完新函数后还是需要执行原来的函数,这个时候自定义的函数指针就用到了;
  • 对于OC是面向对象的,传递数据都是通过对象,而对象本身就是一个指针,所以我们可以修改对象的值,
  • 但是对于c函数,都是通过值传递,值传递是没法改变本身的值,这个时候就需要拿到保存函数地址的指针,
  • 而NSLog就是一个函数地址,我们没法去改变它,只能去拿到保存NSLog这个函数地址的指针,然后,这样才能去改变它,这也就是replaced需要用(void *)&NSLog的原因

hook自定义函数


void func(const char * str){
    NSLog(@"%s",str);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    //rebinding结构体
    struct rebinding nslog;
    nslog.name = "func";
    nslog.replacement = new_func;
    nslog.replaced = (void *)&old_func;
    //rebinding结构体数组
    struct rebinding rebs[1] = {nslog};
    /**
     * 存放rebinding结构体的数组
     * 数组的长度
     */
    rebind_symbols(rebs, 1);
}
//---------------------------------更改NSLog-----------
//函数指针
static void(*old_func)(const char * str);
//定义一个新的函数
void new_func(const char * str){
      NSLog(@"%s + 1",str);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//    func("哈哈");
    NSLog(@"哈哈!");
}

打印:


2020-11-05 20:59:25.202349+0800 001--fishHookDemo[33081:7724521] 哈哈
2020-11-05 20:59:26.343317+0800 001--fishHookDemo[33081:7724521] 哈哈

我们发现并没有hook成功,这是为什么呢?

  • 因为运行时只能交换动态的函数,而自定义的函数是静态的,在编译期就确定了内存地址,在MachO文件里面
  • 而系统函数存在动态的部分,这是因为iOS系统为了节省内存,将系统的库都放在动态库共享缓存(dyld shared cache)中了
  • 而每个手机的动态库的内存地址是不一样的,这样就导致编译器在编译的时候无法地位系统函数的真实内存地址,也就无法访问系统函数的真实地址
  • 这个时候就用到dyld来进行链接MachO文件

PIC技术

  • 编译器在编译程序的时候是把程序都编译成二进制文件,并且所有C函数的代码都变成二进制,
  • 因为C语言的特性,这些二进制都要变成内存地址,而系统函数例如NSLog是没法确定,苹果为了解决这个问题就使用了PIC技术(位置代码独立),这个技术就导致调用系统函数变成动态的了,

我们在分析MachO文件时候,发现MachO文件的data层和Load Commands直接有一块空余的内存空间,而这一段空间放的都是一个又一个符号,他是在data数据段,因为数据段是可读可写的,

这样在编译期,既然系统函数需要一个函数内存地址,编译期就会把系统函数编译成一个又一个的符号放在MachO文件里面,然后把函数变成指向MachO的符号

在应用被dyld加载到内存中的时候,加载的时候是要读取MachO文件,并且会从Load Commands里面读取依赖的系统的动态库,在后再做一个链接绑定动作,也就是会把系统动态库里面的函数的真实地址赋给MacO文件的符号上,这就是绑定

绑定之后,我们程序在调用系统函数的时候就会先去找符号,再从符号里面找到真实的函数地址,这时候就会调动系统的函数

而自定的函数因为在编译器就在MachO文件里面,所以不需要链接,所以也用不到PIC技术

而fishHook能hook系统函数,其实就是重新绑定符号,也就是原来MachO中的符号指向了系统函数,现在fishHook把符号改成指向了自定义函数,然后将系统的函数地址复制到函数指针变量上去,

我们可以看一下MachO文件的,这些都是MachO里面的符号

我们可以看到,符号其实就函数名,所以对于fishHook我们把函数名传进去,他就能通过函数名找到对应的符号

符号绑定过程分析

首先我们在Demo中写上下面代码

然后编译,编译完成后拿到可执行文件用MachOView打开

我们发现符号表分为懒加载表(Non-Lazy Symbol Pointers)和非懒加载表(Lazy Symbol Pointers),并且我们发现NSLog是在懒加载表里的,我们猜测在NSLog调用之前对应的符号并没有和系统库的NSLog函数进行链接,试验一下:

  • 我们发现NSLog的绑定符号的内存偏移Offset是00008010,也就是从MachO文件的第00008010位置
  • 我们通过image list打印出所有镜像文件,并且找到我们项目内存的首地址,也就是:0x0000000102960000

  • 我们知道我们MachO文件的首地址,并且知道NSLog符号的偏移量大小,相加就知道NSLog符号的内存地址0x0000000102960000 + 00008010,十六进制相加得到的是0x102968010

  • 然后用xcode读内存指令memory read 0x102968010,查看这个符号里面保存的内存地址值

(lldb) memory read 0x102968010
0x102968010: 14 69 96 02 01 00 00 00 78 83 ac 99 01 00 00 00  .i......x.......
0x102968020: 68 1d 0d 9b 01 00 00 00 98 69 96 02 01 00 00 00  h........i......
(lldb) 
  • 因为指针都是8个字节,并且iOS是小端模式,读内存是从右往左,所以符号里面保存的地址0x0000000102966914
  • 我们通过汇编指令dis -s 看一下这个内存地址里面的值

很明显不是NSLog函数,因为在MachO文件里面NSLog在懒加载符号表里,说明在NSLog没调用之前真实的NSLog函数没有和符号进行链接

  • 我们将第一个断点跳过去,这时候NSLog已经执行了,我们再来查看一下符号里面保存的值
(lldb) memory read 0x102968010
0x102968010: bc 46 ad 99 01 00 00 00 78 83 ac 99 01 00 00 00  .F......x.......
0x102968020: 68 1d 0d 9b 01 00 00 00 98 69 96 02 01 00 00 00  h........i......
(lldb) 

发现里面值变了,再通过dis -s查看一下汇编

(lldb) memory read 0x102968010
0x102968010: bc 46 ad 99 01 00 00 00 78 83 ac 99 01 00 00 00  .F......x.......
0x102968020: 68 1d 0d 9b 01 00 00 00 98 69 96 02 01 00 00 00  h........i......
(lldb) dis -s 0x0199ad46bc
Foundation`NSLog:
    0x199ad46bc <+0>:  pacibsp 
    0x199ad46c0 <+4>:  sub    sp, sp, #0x20             ; =0x20 
    0x199ad46c4 <+8>:  stp    x29, x30, [sp, #0x10]
    0x199ad46c8 <+12>: add    x29, sp, #0x10            ; =0x10 
    0x199ad46cc <+16>: adrp   x8, 321987
    0x199ad46d0 <+20>: ldr    x8, [x8, #0x80]
    0x199ad46d4 <+24>: ldr    x8, [x8]
    0x199ad46d8 <+28>: str    x8, [sp, #0x8]
(lldb) 

我们发现符号里面保存的内存地址内容变成了Foundation里面NSLog函数,当NSLog执行之后,符号就和NSLog真实的函数地址绑定了

  • 我们再来看一下fishHook重绑定过程,我们将断点再过一下,再来查看一下符号里面保存的内存地址:
(lldb) memory read 0x102968010
0x102968010: a8 5b 96 02 01 00 00 00 78 83 ac 99 01 00 00 00  .[......x.......
0x102968020: 68 1d 0d 9b 01 00 00 00 a4 2d 8b a1 01 00 00 00  h........-......
(lldb) dis -s 0102965ba8
fishHookDemo`myNSLog:
    0x102965ba8 <+0>:  sub    sp, sp, #0x30             ; =0x30 
    0x102965bac <+4>:  stp    x29, x30, [sp, #0x20]
    0x102965bb0 <+8>:  add    x29, sp, #0x20            ; =0x20 
    0x102965bb4 <+12>: sub    x8, x29, #0x8             ; =0x8 
    0x102965bb8 <+16>: mov    x9, #0x0
    0x102965bbc <+20>: stur   x9, [x29, #-0x8]
    0x102965bc0 <+24>: str    x0, [sp, #0x10]
    0x102965bc4 <+28>: mov    x0, x8
(lldb) 

我们发现符号里面保存的内存地址变了,变成了myNSLog,这样就完成了符号重绑定,这个就是fishHook 只能hook系统函数,不能hook自定义函数的原因

fishHooK如何通过字符串找到符号

但是fishHook是怎么找到对应函数的符号呢,我们仅仅给fishHook一个函数名而已,github里的fishHook框架有一个图

其实这个图就是fishHook通过字符串找到符号的流程,但是毕竟抽象,我们通过MachOView来进行形象的分析一下:

  • 我们在machO文件里面看到NSLog在Lazy Symbol Pointers表的最上面

  • 其实Lazy Symbol Pointers表是跟 Dynamic Symbol Table表是相对于的,我们在这个表里面看到第一个也是NSLog

  • 而Dynamic Symbol Table表也跟另一个表Symbol Table表是一一对应的,在Dynamic Symbol Table表里面我们发现NSLog对应的Data是8a,换成10进制也就是138,我们到Symbol Table表的第138项看一下:

  • 发现第138项对应的就是NSLog,并且在Symbol Table表对应的Data是A1,而且描述写的string table Index,说明这个A1就是在string table表里的偏移值,我们再找一下sting Table。

  • sting table表里的都是字符串常量,并且合格表起始值是0000CEE8,我们知道NSLog的偏移值是A1,相加得到0xcf89,因为每一行16个自己,我们找到CF88这一行,往前近一个就是NSLog的码5F 4E 53 4C 6F,然后就找到NSLog字符串

  • 所以当我们传进去了字符串时候,fish如何找到对应符号的过程

关于hook的,初探防护

  • 我们在之前动态注入里面了解到,我们可以通过动态注入framework方式对ipa包里面的函数进行hook
  • 并且hook函数都是通过runtime的一些函数进行hook,而我们知道fishHook可以hook系统的函数
  • 所以我们可以hook系统库runtime相关的函数,这样我们就能监控到谁在hook我们的函数,从而做到一定的防护

代码:

+(void)load{
    //防护代码!
    struct rebinding bd;
    bd.name = "method_exchangeImplementations";
    bd.replacement = myExchange;
    bd.replaced = (void *)&exchangeP;
    struct rebinding rebs[1] = {bd};
    rebind_symbols(rebs, 1);

    //进攻的代码!
    // getIMP  setIMP
    Method old = class_getInstanceMethod(self, @selector(btnClick1:));
    Method newMethod = class_getInstanceMethod(self, @selector(click1Hook:));
    method_exchangeImplementations(old, newMethod);
}

//防护代码
//函数指针变量
void
(*exchangeP)(Method _Nonnull m1, Method _Nonnull m2);
void myExchange(Method _Nonnull m1, Method _Nonnull m2){
    NSLog(@"检测到HOOK!!");
}


-(void)click1Hook:(id)sender{
    NSLog(@"HOOK成功!!");
}

注意点:

  • 首先我们要保证我们的防护代码要在hook代码之前执行,因为只有在防护代码执行之后,我们才能监控到系统函数的执行,例如我们可以把防护代码写到framework里面,而通过注入的framework都是在macho本身framework的后面,所以可保证防护代码执行时机比较靠前,
  • 如果我们用fishHook工具hook了系统函数,那么原来系统函数就不能用了,如果想继续用,只能用我们hook后的函数。例如上面的myExchange
  • 因为是系统函数,一些系统函数,系统框架自己也会调用,如果我们hook了,可能会导致app不稳定,所以我们不能随便就把系统函数的hook完禁掉,例如微信并没有做一些源码的保护,只是做了检测,如果检测被hook了,就会把当前账号封号
  • 其实我们可以通过修改machO文件,来导致防护代码失效。例如我们hook系统函数是通过函数名来hook的,如果我们拿到可执行文件修改掉相应的位置的字符,就会让防护代码试下。

我们来操纵一下:

用MachO打开,我们再到字符区相应函数的函数名字符串,例如我们可以找到method_exchangeImplementations字符

然后修改对应符号的ASCII码,将6e改成6f,会使n变成o

这样就让method_exchange变成method_exchaoge,了,这样防护代码不能通过method_exchangeImplementations来找到相应的符号,从而破解了防护,类似的方式很多,但是本质上我们都需要属性相应的原理