iOS逆向工程之fishhook

一、概述

峰哥在上一篇文章整理了在插件开发中如何HOOK 动态语言 Objective-C 中的方法,实际上静态语言 C 语言中的函数也是有办法 HOOK 的,这也说明了绝对的静态语言是不存在的。

为了实现HOOK C 语言中的函数,我们需要用到 Facebook 的一个开源框架 fishhook,通过 fishhook 我们可以很轻松的 HOOK C 语言中的函数,从而达到修改函数功能的目的。

我在参考了 fishhook 官方 demo 和 Draveness 的文章后,发现对 C 函数的 HOOK 也是非常简单的。

在开始之前需要先简单了解两个概念:

Mach-O:对于每个操作系统中的可执行程序都是有格式的,如 ELF 是 Linux 下可执行文件的格式,PE32/PE32+ 是 windows 的可执行文件的格式,那么对于 OS X 和 iOS 来说 Mach-O 是其可执行文件的格式。 OS X 和 iOS 开发中的可执行文件、库文件、Dsym文件、动态库、动态连接器都是这种格式的。

镜像:在 Mach-O 文件系统中,所有的可执行文件、dylib 以及 Bundle 都是镜像。

二、fishhook的使用

我们先通过一个简单的 demo 去了解一下 fishhook 的使用,fishhook GitHub链接:https://github.com/facebook/fishhook

下载下来 fishhook 后你会发现这个框架非常简单,只有两个文件“fishhook.h”和“fishhook.c”。

我们打开文件“fishhook.h”会发现只有一个结构体和两个方法:

struct rebinding {
  const char *name;
  void *replacement;
  void **replaced;
};
 
FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
 
FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
                         intptr_t slide,
                         struct rebinding rebindings[],
                         size_t rebindings_nel);

我们先看 rebinding 结构体,结构体中 name 是一个原始函数(要被替换的函数)名字符串,replacement 是替换后的新的函数指针,replaced 是我们自己创建的一个与原始函数签名相同(参数的个数、类型、顺序相同)的函数的指针的指针。关于 rebinding 暂且先不要纠结,后面看过代码就知道如何使用了。

rebind_symbols 函数和 rebind_symbols_image 函数是用来 HOOK 函数的两个方法,只不过参数不同而已,前者比较简单,两个参数一个是 rebinding 数组,一个是数组中 rebinding 个数。后者就稍微复杂点,根据源码中的注释说明,该函数是在仅指定镜像的时候使用。所以,我们这里直接使用 rebind_symbols 函数就可以了。

C 语言中有个 strlen 函数,用来获取字符串的长度,如下:

//
//  main.m
//  FishHookDemo
//
//  Created by 李峰峰 on 2017/7/2.
//  Copyright ? 2017年 李峰峰. All rights reserved.
//
 
#import  
int main(int argc, const char * argv[]) {
    @autoreleasepool {
 
        char *str = "imlifengfeng";
        long result = strlen(str);
        printf("结果:%ld/n",result);
 
    }
    return 0;
}

运行结果:

iOS逆向工程之fishhook

接下来我们就修改 strlen 函数的返回值,使无论字符串真实长度是什么,都返回 666。我们使用前面说到的 rebind_symbols 函数去实现。

首先我们要声明一个与 strlen 函数签名相同的函数,方法名任意,我们定义为 original_strlen,如下:

static int (*original_strlen)(const char *__s);

然后再定义一个替换后的函数,使其不管参数是什么直接返回 666,方法名也任意,我们定义为 new_strlen,如下:

int new_strlen(const char *__s) {
    return 666;
}

接着我们就使用 rebind_symbols 函数进行绑定:

struct rebinding strlen_rebinding = { "strlen", new_strlen, (void *)&original_strlen };
rebind_symbols((struct rebinding[1]){strlen_rebinding}, 1);

上面这些操作完成之后再调用 strlen 函数无论字符串真实长度是什么都会直接返回 666。完整代码如下:   

//
//  main.m
//  FishHookDemo2
//
//  Created by 李峰峰 on 2017/7/2.
//  Copyright ? 2017年 李峰峰. All rights reserved.
//
 
#import #import "fishhook.h"
 
static int (*original_strlen)(const char *__s);
 
int new_strlen(const char *__s) {
    return 666;
}
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct rebinding strlen_rebinding = { "strlen", new_strlen, (void *)&original_strlen };
        rebind_symbols((struct rebinding[1]){strlen_rebinding}, 1);
        char *str = "imlifengfeng";
        long test = strlen(str);
        printf("结果:%ld/n",test);
    }
    return 0;
}

运行结果:

iOS逆向工程之fishhook

可以看到我们已经达到了 HOOK C函数的目的,已经理解的可以自己尝试 HOOK 一些其他的函数去实现一些更复杂的功能。

三、fishhook的原理

1、Mach-O

前面峰哥也说了 Mach-O 是 OS X 和 iOS 可执行文件的格式,我们这里再来简单看下 Mach-O 文件格式的结构,无需深究。

每一个 Mach-O 文件都会被分为不同的 Segments,比如 __TEXT, __DATA, __LINKEDIT:

iOS逆向工程之fishhook

Mach-O 中的 segment_command(32 位与 64 位不同):

struct segment_command_64 { /* for 64-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT_64 */
    uint32_t    cmdsize;    /* includes sizeof section_64 structs */
    char        segname[16];    /* segment name */
    uint64_t    vmaddr;     /* memory address of this segment */
    uint64_t    vmsize;     /* memory size of this segment */
    uint64_t    fileoff;    /* file offset of this segment */
    uint64_t    filesize;   /* amount to map from the file */
    vm_prot_t   maxprot;    /* maximum VM protection */
    vm_prot_t   initprot;   /* initial VM protection */
    uint32_t    nsects;     /* number of sections in segment */
    uint32_t    flags;      /* flags */
};

每一个 segment_command 中又包含了不同的 section:

struct section_64 { /* for 64-bit architectures */
    char        sectname[16];   /* name of this section */
    char        segname[16];    /* segment this section goes in */
    uint64_t    addr;       /* memory address of this section */
    uint64_t    size;       /* size in bytes of this section */
    uint32_t    offset;     /* file offset of this section */
    uint32_t    align;      /* section alignment (power of 2) */
    uint32_t    reloff;     /* file offset of relocation entries */
    uint32_t    nreloc;     /* number of relocation entries */
    uint32_t    flags;      /* flags (section type and attributes)*/
    uint32_t    reserved1;  /* reserved (for offset or index) */
    uint32_t    reserved2;  /* reserved (for count or sizeof) */
    uint32_t    reserved3;  /* reserved */
};

2、dyld 与动态链接

dyld(the dynamic link editor)是 Apple 的动态链接器(GitHub地址:dyld),系统 kernel 做好启动程序的初始准备后,交给 dyld 负责,关于其作用顺序,可参考文章《dyld: Dynamic Linking On OS X》,相关部分翻译内容如下:
(1)从kernel留下的原始调用栈引导和启动自己
(2)将程序依赖的动态链接库递归加载进内存,当然这里有缓存机制
(3)non-lazy符号立即link到可执行文件,lazy的存表里
(4)运行可执行文件的静态初始化程序
(5)找到可执行文件的main函数,准备参数并调用
(6)程序执行中负责绑定lazy符号、提供runtime dynamic loading services、提供调试器接口
(7)程序main函数return后执行static terminator
(8)某些场景下main函数结束后调libSystem的_exit函数

一句话总结就是:负责将各种各样程序需要的镜像加载到程序运行的内存空间中!

其作用的时间是 OC 运行时初始化之前!

dyld 加载镜像后会执行相关回调函数,当一个镜像被动态链接时,都会执行回调 void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide),传入文件的 mach_header 以及一个虚拟内存地址 intptr_t。

我们先使用 Xcode 新建一个简单的 C 项目,项目名为 test ,项目新建后默认 main.c 文件内容如下:

//
//  main.c
//  test
//
//  Created by 李峰峰 on 2017/7/4.
//  Copyright ? 2017年 李峰峰. All rights reserved.
//
 
#include  
int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!/n");
    return 0;
}

我们打开终端 cd 到 main.c 文件目录,使用 gcc 命令编译  main.c 源文件生成可执行文件,执行完成后会生成名为 a.out 的可执行文件。之后通过 nm 命令查看可执行文件中的符号:

iOS逆向工程之fishhook

从上图可以看出,_printf 这个符号是未定义(undefined)的,换句话说,编译器还不知道这个符号对应什么东西。

那如果我们自己增加一个函数:

//
//  main.c
//  test
//
//  Created by 李峰峰 on 2017/7/4.
//  Copyright ? 2017年 李峰峰. All rights reserved.
//
 
#include  
void test(){
 
}
 
int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!/n");
    return 0;
}

那结果是什么样的呢?如下:

iOS逆向工程之fishhook

可见我们手动添加的 test 函数所对应的符号 _test 并不是为定义的,它包含一个内存地址以及 __TEXT 段。

为了更深入理解,我们需要用到一个神器 Hopper Disassembler ,这是一个类似于 IDA 的反汇编工具,个人感觉它比 IDA 好用的多,感兴趣的可以自己从网上下载,它最新图标是下面这样的:

iOS逆向工程之fishhook

我们使用该工具分析一下之前的 a.out 的可执行文件:

iOS逆向工程之fishhook可以发现 nm 打印出的另一个符号 dyld_stub_binder 对应另一个同名函数。dyld_stub_binder 会在目标符号(例如 printf)被调用时,将其链接到指定的动态链接库 libSystem,再执行 printf 的实现(printf 符号位于 __DATA 端中的 lazy 符号表中)。

每一个镜像中的 __DATA 端都包含两个与动态链接有关的表,其中一个是 __nl_symbol_ptr,另一个是 __la_symbol_ptr:

  • __nl_symbol_ptr 中的 non-lazy 符号是在动态链接库绑定的时候进行加载的

  • __la_symbol_ptr 中的符号会在该符号被第一次调用时,通过 dyld 中的 dyld_stub_binder 过程来进行加载

在上述代码调用 printf 时,由于符号是没有被加载的,就会通过 dyld_stub_binder 动态绑定符号:

 iOS逆向工程之fishhook

3、fishhook 的原理

dyld 通过更新 Mach-O 二进制文件 __DATA 段中的一些指针来绑定 lazy 和 non-lazy 的符号;而 fishhook 先确定某一个符号在 __DATA 段中的位置,然后保存原符号对应的函数指针,并使用新的函数指针覆盖原有符号的函数指针,实现重绑定。

对于前面我们 HOOK strlen 函数的例子,过程如下图示:

iOS逆向工程之fishhook

其中最复杂的部分就是从二进制文件中寻找某个符号的位置,在 fishhook 的 README 中,有这样一张图:

iOS逆向工程之fishhook

这张图初看很复杂,不过它演示的是寻找符号的过程,我们根据这张图来分析一下这个过程:

  1. 从 __DATA 段中的 lazy 符号指针表中查找某个符号,获得这个符号的偏移量 1061,然后在每一个 section_64 中查找 reserved1,通过这两个值找到 Indirect Symbol Table 中符号对应的条目

  2. 在 Indirect Symbol Table 找到符号表指针以及对应的索引 16343 之后,就需要访问符号表

  3. 然后通过符号表中的偏移量,获取字符串表中相关函数的符号

原创文章,转载请注明: 转载自李峰峰博客

本文链接地址: iOS逆向工程之fishhook

PS:如果您想和业内技术大牛交流的话,请加qq群(527933790)或者关注微信公众 号(AskHarries),谢谢!

转载请注明原文出处:Harries Blog™ » iOS逆向工程之fishhook

赞 (0)

分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址