inline hook之fishhook objc_msgSend

常见的Hook方案

  • 基于 Objective-C Runtime 的 Method Swizzling:也就是我们经常使用的 class_replaceMethod 方法;
  • 基于 fishhook 的 Hook:由于在 Mach-O 当中,有 Bind 和 Lazy Bind 的两个概念,所以 Facebook 通过修改 __la_symbol 和 __nl_symbol 两个表的指针,在二次调用的时候,直接通过 __la_symbol_ptr 找到函数地址直接调用,从而不用多次繁琐的进行函数寻址;
  • 基于 Dobby 的 Inline Hook:Dobby 是通过插入 __zDATA 段和 __zTEXT 段到 Mach-O 中。__zDATA 用来记录 Hook 信息(Hook 数量、每个 Hook 方法的地址)、每个 Hook 方法的信息(函数地址、跳转指令地址、写 Hook 函数的接口地址)、每个 Hook 的接口(指针)。__zText 用来记录每个 Hook 函数的跳转指令。Dobby 通过 mmap 把整个 Mach-O 文件映射到用户的内存空间,写入完成保存本地。所以 Dobby 并不是在原 Mach-O 上进行操作,而是重新生成并替换。

Inline Hook

Inline Hook 就是在运行的流程中插入跳转指令来抢夺运行流程的一个方法。大体分为三步:

  1. 将原函数的前 N 个字节搬运到 Hook 函数的前 N 个字节;
  2. 然后将原函数的前 N 个字节填充跳转到 Hook 函数的跳转指令;
  3. 在 Hook 函数末尾几个字节填充跳转回原函数 +N 的跳转指令;

以上的 N 有多大,取决于你的跳转指令写得有多大(占用了多少指令)。
在这里插入图片描述
相较与 Inline Hook, fishhook 使用的是很 Trick 的方式,通过劫持 stub 从而达到替换的目的。

fishhook 跳转

通过查看 objc_msgSend ,我们知道 Runtime 的 Method Swizzling 并不适用,因为它并不是 Objective-C 方法,调用时并不会有我们经常说的“消息转发”;

通过查看 Runtime 源码,我们发现 objc_msgSend 是使用纯汇编实现函数,通过汇编文件我们可以看到以下定义:

ENTRY  _objc_msgSend

在文件中搜索 ENTRY 我们找到了这么一个宏:

.macro  ENTRY /* name */
	.text
	.align 5
	.globl    $0
$0:
.endmacro

这里定义了一个汇编宏,表示在 text 段定义一个 global 的 _objc_msgSend ,$0 其实就是这个宏传入的参数,也就是一个方法入口。我们可以手动将这个宏来展开:

.text
.align 5
.globl _objc_msgSend

; ...

这里我们发现,在第三行的位置通过 C 的 name mangling 命名规则,将符号 _objc_msgSend 映射为 C 的全局方法符号。

也就是说,这段汇编可以通过头文件声明,便已完成了 C 的函数定义。

我们在后续处理的时候可以将其视为 C 方法。

当然我们也可以使用 MachOView 来验证这个符号名:
在这里插入图片描述

fishhook实现

既然 objc_msgSend 已经视作 C 方法,那么我就可以使用 fishhook 来完成 Inline Hook 的第一步:跳到 Hook 方法。
在这里插入图片描述
Apple 自身的共享缓存库其实不会编译进我们自己的 Mach-O 中的,而是在 App 启动后的动态链接才会去做重绑定操作。
这里我们以某个APP举例,通过nm -n命令查看所有方法的符号以及对应地址:

> nm -n xxxxx
		...
     	U _NSLocalizedRecoverySuggestionErrorKey
		U _NSLog
		U _NSLogv
		U _NSMallocException
		...
		U _objc_msgSend
		U _objc_msgSendSuper
		U _objc_msgSendSuper2
		...
0000000100000000 T __mh_execute_header
0000000100af3cb8 T __ZN5folly6detail15str_to_floatingIdEENS_8ExpectedIT_NS_14ConversionCodeEEEPNS_5RangeIPKcEE
...

在这里我们就可以发现,其实 NSLog 方法其实并没有地址,这些系统库函数并不会打入到我们的 App 包中;当我们使用它们时,dyld 就要从共享的动态库中查找对应方法,然后将具体的函数地址绑定到之前声明的地方,从而实现系统库方法的调用。
对于这种可在主存中任意位置正确地执行,并且不受其绝对地址影响的技术,在计算机领域称之为 PIC(Position Independent Code)技术。

fishhook 对于 Mach-O 利用

Mach-O 中 __DATA 段有两个 Section 与动态符号绑定有关系:

  • __nl_symbol_ptr :存储了 non-lazily 绑定的符号,这些符号在 Mach-O 加载的时候绑定完成;
  • __la_symbol_ptr :存储了 lazy 绑定的方法,这些方法在第一次调用时,由 dyld_stub_binder 进行绑定;

既然 __la_symbol_ptr 存储了所有 lazy 绑定的方法,那也就是说在这些位置应该存储了对应方法的地址。
写一段代码验证一下:

#import "ViewController.h"
#import "fishhook.h"

@implementation ViewController

static void (*ori_nslog)(NSString * format, ...);

void new_nslog(NSString * format, ...) {
    //自定义的替换函数
    format = [format stringByAppendingFormat:@" FISHHOOK "];
    ori_nslog(format);
}


- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"hello world");
    
    struct rebinding nslog;
    
    nslog.name = "NSLog";
    nslog.replacement = new_nslog;
    nslog.replaced = (void *)&ori_nslog;
    rebind_symbols((struct rebinding[1]){nslog}, 1);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"hello world");
}

@end

控制台输出如下:

2020-08-05 16:42:32.698782+0800 Fish[7569:336213] hello world
2020-08-05 16:42:34.032334+0800 Fish[7569:336213] hello world FISHHOOK 
2020-08-05 16:42:34.776257+0800 Fish[7569:336213] hello world FISHHOOK 

我们在第一个 hello world 的位置增加断点:
在这里插入图片描述
使用 image list 命令来获取 App 的基地址:
在这里插入图片描述
可以看到App 基地址为 0x0000000106f9a000。
然后我们使用 MachOView 来查看NSLog在 __la_symbol_str 中的偏移量,偏移量为0x5000。
在这里插入图片描述
我们使用lldb的x命令来查看一下 基地址+偏移量 位置的数据,发现在此位置的数据是 0x0106f9c3f0 (这一步看不懂的请看上一篇博文里面的大端序和小端序)。
使用反汇编 dis 命令, 来看对应地址所指向的代码段:
在这里插入图片描述
那么这段代码到底是我们的 NSLog 的代码吗?我们可以直接看断点的汇编代码:
在这里插入图片描述
可以看到汇编中 callq 命令对应对地址是 0x106f9c354 。
对这个地址再次进行 dis -s 反汇编来查看:
在这里插入图片描述
可以看到上图中方框圈起来的地址就是上面 基地址+偏移量 这个位置存储的地址。
在这之后,我们再对点击事件中的 NSLog 方法下一个断点,并且点击一下模拟器屏幕来触发一下。
在这里插入图片描述

我们再使用 x 和 dis -s 两个命令来查看一下 基地址+偏移量 中的新数据:
在这里插入图片描述
可以很清楚的看到此时的NSLog函数的地址已经被替换了:
在这里插入图片描述
至此,fishhook Hook C 方法已经完成。

fishhook 总结

fishhook 的 Hook 思路,也就是我们上述所描述的,当第一次调用系统动态库中 C 方法时,去替换掉 __la_symbol_str 的指针。
但是它的逻辑要比这个思路还是要复杂一些,比如 fishhook 要解决以下问题:

  • 使用数据结构来描述所有 Hook 方法
  • 通过方法名来找到对应的 Lazy 指针
  • 计算对应方法的地址
  • 非 Lazy 表中要如何处理
  • 查找到对应符号名称

如果想看具体的实现,推荐去阅读源码(不到两百行)。

归纳一下 fishhook 来修改 C 方法的本质,dyld 更新 Mach-O 二进制的 __DATA segment 的 __la_symbol_str 中的指针,使用 rebind_symbol 方法更新两个符号位置来进行符号的重新绑定。
无fishhook:
在这里插入图片描述
有fishhook:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/youshaoduo/article/details/107817405