iOS逆向工程之fishhook

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


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 <Foundation/Foundation.h>


int main(int argc, const char * argv[]) {

    @autoreleasepool {


        char *str = "imlifengfeng";

        long result = strlen(str);

        printf("结果:%ld ",result);


    }

    return 0;

}


运行结果:



接下来我们就修改 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 <Foundation/Foundation.h>

#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 ",test);

    }

    return 0;

}


运行结果:



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


fishhook的原理


1、Mach-O


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


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



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 <stdio.h>

 

int main(int argc, const char * argv[]) {

    // insert code here...

    printf("Hello, World! ");

    return 0;

}


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



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


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


//

//  main.c

//  test

//

//  Created by 李峰峰 on 2017/7/4.

//  Copyright © 2017年 李峰峰. All rights reserved.

//


#include <stdio.h>


void test(){


}


int main(int argc, const char * argv[]) {

    // insert code here...

    printf("Hello, World! ");

    return 0;

}


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



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


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



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



可以发现 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 动态绑定符号:



3、fishhook 的原理


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


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



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



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


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

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

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


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

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

  • iOS开发整理发布,转载请联系作者授权

↙点击“阅读原文”,加入 

『程序员大咖』

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值