iOS应用安全6 -- fishhook破解系统C函数

2,857 阅读14分钟

前言

在上上篇文章iOS应用安全4 -- 代码注入,窃取微信登录密码中我们知道了如何hook App中的OC方法,即使用OC的运行时机制,在运行期间替换相应方法的实现。
那么有没有想过我们如何才能hook C语言写的函数呢?
这就要使用到我们今天所要讲的fishhook了,下载地址

fishhook简介

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

话不多说,先把它下载下来添加到工程。

file
下载完fishhook之后可以发现里面有这5个文件,我们只需要使用这俩.h和.c就可以了,新建工程,将这俩文件拖如工程目录下。
project

点击fishhook.h文件,我们可以查看到这个头文件里面主要有以下一些信息:

  1. 有一个结构体,添加了一些注释进行解释,如下:
// rebinding--->重新绑定
struct rebinding {
  const char *name;         // 要hook的函数名称,char* 是C语言的字符串
  void *replacement;        // 用来 替换原始函数 的函数的地址
  void **replaced;          // 被替换掉的原始函数的地址,注意是void**
};
  1. 还有两个函数,如下:
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);

很明显,第二个函数是对第一个函数的扩展,那么就先来分析一下第一个函数如何使用吧。

fishhook使用

为了简单起见,就不再重签名一个App再测试了,直接在新建的项目的ViewController类中hook系统的C函数NSLog。代码很少,就下面这些

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    // 就以hook系统的NSLog函数为例
    // 创建一个结构体变量
    struct rebinding rebind;
    rebind.name = "NSLog";                  // 注意是C语言的字符串不用加@
    rebind.replacement = cus_log;           // C语言函数名 即 函数指针
    // 这里传的是函数地址的地址,为的是在rebind_symbols函数内部附上NSLog的地址
    rebind.replaced = (void *)&sys_log;
    
    // 定义rebinding数组,里面只有一个元素。当然,也可以存多个
    struct rebinding rbs[] = {rebind};
    rebind_symbols(rbs, 1);
    
}
// 定义函数指针用来`接收`系统的NSLog函数的地址
static void (*sys_log)(NSString *format, ...);
// 自定义打印函数,用来替换NSLog
void cus_log(NSString *format, ...) {
    // 给打印的字符串后面添加一个标记
    format = [format stringByAppendingString:@"-------->cus_log的标记"];
    // 调用系统NSLog函数
    sys_log(format);
}

// 测试
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"点击屏幕了😂😂😂");
}

我认为上面的代码注释的很清晰了,这里不再重复解释。但有一点还是需要说明一下,rebinding结构体的replaced变量是void** 类型,如果对此不太了解的可以自己搜索一下C语言传值和传地址的区别,也可以看我前几天写的文章数据结构与算法2 -- 链表的最下面总结部分,有对这一块的详细解释。

运行效果如下:

运行gif

hook自定义的C函数?

上面我们实现了使用fishhook替换系统C函数NSLog的实现,那么在App中由开发者自己写的C函数能不能使用fishhook替换呢?
试试就知道了,测试代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];

    struct rebinding rb;
    rb.name = "printHelloWorld";
    rb.replacement = hook_printHelloWorld;
    rb.replaced = (void *)&app_printHelloWorld;
    
    struct rebinding rbs[] = {rb};
    rebind_symbols(rbs, 1);
}

// 一个简单的C语言函数, 假设这个函数是别人App中实现的C语言函数
void printHelloWorld() {
    NSLog(@"Hello World!");
}
// 接收printHelloWorld函数的实现地址
static void (*app_printHelloWorld)(void);

// 替换printHelloWorld函数的函数
void hook_printHelloWorld() {
    NSLog(@"在Hello World!之前打印一句话,代表了hook成功");
    // 调用原始的printHelloWorld函数
    app_printHelloWorld();
}
// 测试
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    printHelloWorld();
}

运行效果如下,可以看到hook失败了。
因为在Hello World!之前打印一句话,代表了hook成功这句话并没有在Hello World!打印前打印。

hook函数失败

分析失败原因

P1、为什么会失败呢?为什么不能hook这个自定义的C函数呢?

仔细想想我们会发现以下两件事!

  1. OC是动态语言,这个动态就 体现在 OC的方法 是 在运行时 才能确定 真正要执行的方法实现 是哪个?(这句话有点长,笔者加了几个空格断句)
    也因为OC是动态语言,我们才能使用Method Swizzling等方法来实现hook OC的方法。
  2. C是静态语言,静态 体现在 使用C语言写的函数,在程序编译期间 就能够确定 调用函数时要执行的代码 在哪里?
    也正是因此,我们无法像OC那样去hook C语言写的函数。

P2、为什么fishhook可以hook NSLog函数呢?NSLog难道不是C函数吗?

首先说明NSLog确实是C语言函数,这一点没问题。
之所以fishhook能够hook NSLog函数,这个我默认你已经看过上一篇文章iOS应用安全5 -- main函数调用之前做了些什么?
上一篇文章主要介绍了MachO文件dyld是如何加载应用程序的?这两部分,那么当然不会平白无故的来讲这两块内容,肯定是有用的。

共享缓存

在dyld加载应用程序的过程中,有一个步骤叫加载共享缓存,相信大家应该都还记得这一步是做什么的?

我们都知道NSLog是系统库Foundation框架中的一个C函数,而这个Foundation框架本身就是一个真正意义上的动态库,即它具有在多个进程中共享的特性。而我们平时说的共享缓存就是指这一类系统库。

自定义的C函数和系统共享库中的C函数的区别

系统C函数

系统定义的C函数,由于具体的函数实现是在系统共享库中,因此在程序编译期间是无法获取到这个C函数的实现地址,只能通过一种叫符号绑定的方法动态链接到函数名。
这样直接说有点抽象,下面我列举个场景:

1、新建一个工程,在ViewController.mviewDidLoad方法中写一句NSLog(@"哈哈哈");
2、然后command+B编译程序,那么在编译到NSLog(@"哈哈哈");这一句代码时,xcode会判断NSLog函数是不是系统库里面的函数。
3、发现NSLogFoundation框架里的函数,那么就会将NSLog(@"哈哈哈");这句代码和符号表中的一个char*类型的C字符串"NSLog"进行绑定。
4、当command+R运行应用程序时,就会在dyld加载应用程序时将iOS系统共享缓存区Foundation库加载到内存中。
5、程序运行过程中,当代码第一次执行到NSLog函数调用时,就会到Foundation库的MachO文件中查询NSLog函数的实现地址。然后将函数的实现地址和符号表中的"NSLog"字符串进行绑定,这样后面再调用到NSLog函数时就能够直接找到函数的实现地址。(有点懒加载的意思,所以这种方法也被称为懒绑定)

自定义的C函数

到这,想必不用我说,也都知道了自定义的C函数是怎么回事了吧?
自定义的C函数,由于函数的实现和函数的调用是在同一个MachO文件(App本身的MachO文件)中,因此在编译链接期间,xcode就直接将函数调用语句和函数的实现地址进行了链接,也就不会有系统C函数的那些步骤了。

fishhook原理

理解了上面系统C函数的工作原理,我们应该也能猜到一些fishhook hook系统C函数的原理了。下面仍然以NSLog函数为例:

从上面我们知道,当第一次调用NSLog函数的时候,系统就会到Foundation框架中找NSLog的函数实现地址,与App的MachO文件中的符号表中的"NSLog"字符串进行绑定。

fishhook就是利用这一点,回过头看fishhook中的那个函数名rebind_symbols,翻译为重新绑定符号,再看看那个rebinding结构体里面有哪些东西?

// rebinding--->重新绑定
struct rebinding {
  const char *name;         // 要hook的函数名称,char* 是C语言的字符串
  void *replacement;        // 用来 替换原始函数 的函数的地址
  void **replaced;          // 被替换掉的原始函数的地址,注意是void**
};

是不是一切都是那么的恰到好处?
我们给rebind_symbols函数传进去一个rebinding结构体,包括了符号表中对应NSLog函数的符号"NSLog",然后rebind_symbols函数通过符号"NSLog"找到Foundation框架中NSLog函数的实现地址,将这个函数地址赋值给replaced变量返回给我们,再将符号"NSLog"绑定的函数地址替换为我们传进去的函数地址replacement。(注意:fishhook为了保证返回给我们的replaced是正确有值的,必要时它会在rebind_symbols函数内部先默认调用一次对应的函数)

空口无凭?

说了那么大一堆东西,谁知道是真的还是框我的?

  1. 虽然fishhook为了保证返回给我们的原函数地址是正确的,必要时会在重绑定之前先默认调用一次对于的函数,但是为了更清晰的测试,我们可以在rebind_symbols函数调用之前手动先调用一次NSLog函数。如图,添加一句NSLog(@"123456");,并在调用前打上断点。
    断点
  2. 运行代码,断点断到20行。使用lldbimage list命令查看主程序MachO在内存中的偏移值(即ASLR)。
    ASLR
  3. 使用MachOView工具打开App的MachO文件。
    绑定地址
  4. 通过上图中说的计算方法,计算符号NSLog当前绑定的地址在内存中的真实地址。
    即真实地址 = 0x00000001005b0000 + 0xc000 = 0x00000001005bc000。
  5. 通过lldb命令memory read查看我们计算出来的那块内存。
    内存
  6. 此时可以看到符号NSLog绑定的内存地址保存的值,这个值是一个指针,因为iOS默认是小端模式,高位字节保存在低位地址中,所以这个指针指向的真实地址应该是0x00000001005b6904。
  7. 通过lldbdis -s指令反汇编这个地址。可以看到这个地址现在就是一块无意义的地址,也就是符号NSLog默认绑定的地址。
    反汇编
  8. 断点往下走,执行完NSLog(@"123456");断点停在rebind_symbols函数之前。
    走断点
  9. 此时重复步骤5,通过lldb命令memory read查看内存,可以发现,同一块内存地址,里面保存的地址已经发生了改变。
    查看内存
  10. 同样,使用lldbdis -s指令反汇编这个地址(注意小端)。
    NSLog地址
  11. 此时可以发现,符号NSLog绑定的那个指针保存的地址已经由最开始的那个无意义的地址变成了Foundation框架里面的NSLog函数的地址了。
  12. 别急,还没有结束,断点继续往下走一步,让rebind_symbols函数执行。再次使用lldb命令memory read查看那块内存。
    再次查看
  13. 发现绑定的指针指向的地址又变了,老规矩,反汇编这个地址。
    自定义的log函数
  14. 可以发现符号NSLog绑定指针保存的Foundation里面的NSLog函数地址已经被fishhook修改成了fishhook-test里面的cus_log函数

整个流程就是这样,没有框你吧?

如何通过C字符串"NSLog"找到符号NSLog?

从上面我们知道了如何通过MachO文件中懒加载符号表中的符号找到函数在内存中的地址,进而hook这个函数。而这整个过程都有一个前提,那就是要找到符号表中的NSLog,那么如何找呢?

别急,仔细看fishhook的rebinding结构体,我们出来要传入hook函数的地址之外还需要提供一个和系统C函数同名的C字符串。那么肯定就是通过这个C字符串来找对应函数的符号了。

  1. 使用MachOView打开App的MachO文件,里面有一项叫做String Table,听名字就知道这是一个字符串表,既然要通过字符串"NSLog"找函数符号,那么当然就要先从字符串表开始了。String Table如下:
    string table
  2. 这些字符串使用.进行分割,使用_表示后面的是一个函数,仔细看上图中的"._NSLog"对应的二进制值,可以发现二进制值其实就是每个字符的ascii码。
  3. 这也就是说,我们可以将C字符串"NSLog"中的字符取出来,转换成对应的ascii码,再到MachOString Table表中查询,就可以找到"NSLog"字符串在String Table表中的偏移量。
    偏移量
  4. 上图的偏移量有点问题,应该计算"_NSLog"的偏移量而不是"NSLog"的偏移量(所以上图计算的结果应该是0x00010F5B,而非0x00010F5C)。
  5. 用计算出的"_NSLog"相对于MachO文件的偏移量 减去 String Table相对于MachO文件的偏移量即可得到"_NSLog"相对于String Table的偏移量。
    计算结果 = 0x00010F5B - 0x00010EC0 = 0x9B。
  6. 接下来查看符号表Symbol Table可以发现,每一个符号都对应的有一个String Table Index的东西,这个东西就是上一步计算出来的。
    符号表
  7. 可以查找String Table Index等于0x9B的,发现它对应的字符串值Value就是_NSLog,说明没有找错。
  8. 接下来可以发现,这个"_NSLog"在符号中对应的位置是105,换算成16进制是0x69。再换到Dynamic Symbol Table目录下,查找值是0x69的行。
    Dynamic Symbol Table
  9. 可以发现,值0x69对应的Symbol符号也正是_NSLog。再次印证了没错。并且关键点就在于下面的那个Indirect Address行,0x10000C000。
  10. 0x10000C000中0x100000000表示的是Load Commands里面的_PAGEZERO的大小,这个大小其实是一个虚拟大小
    pagezero
  11. 也就是说Indirect Address真正的偏移量是0xC000,这个0xC000有没有觉得很熟悉?好,再看懒加载符号表--->Lazy Symbol Pointers
    懒加载符号表
  12. 其中偏移量0xC000对应的符号就是_NSLog了,至此终于和上一个标题接上了。

总结

通过这篇文章,我们学到了以下几点:

  • fishhook是什么?
    • 它是用来hook系统C函数的一种工具。
  • fishhook怎么用?
    • 代码很简单,上面有。
  • fishhook为什么能hook系统C函数?
    • 因为系统的C函数的函数实现地址在共享库里面,无法在编译期间直接获取到。
  • fishhook实现的原理?
    • 通过函数符号字符串在MachO文件中经过一系列的查找,最终找到函数的实现地址,将函数的实现地址替换为我们自定义的函数地址。

题外篇---简单防护

之前就讲过,hook OC的方法,可以通过3种方式交换方法的实现地址,使得App调用方法1的时候执行交换后的方法2,我们再调用方法2的时候执行我们添加的代码和方法1原本的实现。具体可以回头再看看iOS应用安全4 -- 代码注入,窃取微信登录密码这篇文章。

那么面对这种破解,我们作为开发者要如何防护呢?
很简单,可以利用本篇文章讲述的fishhook,把runtime里面的那几个方法交换的C函数全部替换成无法使用的C函数,这样,别人再使用方法交换方法的时候就会调用到我们替换之后的无法使用的C函数。哈哈😄。

但是这样就完全可以保证安全了吗?
还差得远呢,别人仍然可以使用其他方法来破坏或者绕过你的保护。

攻防永远是对立的,没有绝对的安全,只有相对的安全。黑客若真的铁了心的要破解你的App,你再怎么防护也是没用的。我们要做的就是要让黑客花费很长很长的时间才能破解出来,让他知难而退即可。

本文地址https://juejin.cn/post/6844904128158629902