iOS

iOS中的几种Hook实现方案

Posted by lingjye on June 26, 2018

Hook的原理就是改变程序的执行流程,采用面向切面(AOP)编程思想。

Runtime–Method Swizzling

得益于Objective-C这门语言的动态性,我们可以让程序在运行时做出一些改变,进而调用我们自己定义的方法。使用Runtime交换方法的核心就是Method Swizzling。

需要调用的方法为:method_exchangeImplementations,它实际上将两个方法的实现进行交换:

1
2
3
4
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);

在iOS中,每一个选择子(Selector, SEL类型),实际上是以C的字符串(const char *)来标识的,可以参考Swift,它主要用作方法的名字。而每一个Selector会对应一个实现,否则编译器会有警告,如果执行未实现的调用,怎会发生崩溃,而实现了的方法则用IMP(Implementation)来标识,即方法的实现。选择子(SEL)与实现(IMP)组成一个方法(Method)。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static inline void swizzleMethod(Class cls, SEL originSelector, SEL swizzledSelector) {
    Method originMethod = class_getInstanceMethod(cls, originSelector);
    Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
    // 添加原Selector是为了做一层保护,如果这个类没有实现原始方法"originalSel" ,但其父类实现了,那么class_getInstanceMethod会返回父类的方法,使用 method_exchangeImplementations实际上替换的是父类的方法。之所以这里的SEL是orginSelector,是因为将交换后方法的实现映射到原方法调用中
    BOOL result = class_addMethod(cls, originSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (result) {
        class_replaceMethod(cls, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    } else {
        method_exchangeImplementations(originMethod, swizzledMethod);
    }
};

+ (void)load {
    //
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originSel = @selector(viewWillAppear:);
        SEL swizzledSel = @selector(lj_viewWillAppear:);
        swizzleMethod(self, originSel, swizzledSel);
    });
}

- (void)lj_viewWillAppear:(BOOL)animated {
    [self lj_viewWillAppear:animated];
    NSLog(@"%s", __func__);
}

RAC

本质还是通过Method Swizzle实现,只不过是通过关联方式返回了一个RACSubject的信号类。

示例:

1
2
3
4
5
6
7
8
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    ViewController *viewController = [super allocWithZone:zone];
    [[viewController rac_signalForSelector:@selector(viewDidLoad)] subscribeNext:^(id x) {
        NSLog(@"调用ViewDidLoad");
    }];
    return viewController;
}

fishhook

fishhook是FaceBook开源的可以动态修改Mach-O符号表的工具,仓库地址:https://github.com/facebook/fishhook

借助它,我们可以实现C函数的Hook,fishhook的实现依赖于PIC技术:

  • 先在Mach-O文件_DATA段的符号表中为每一个被引用的系统C函数建立一个指针(8字节的数据,放的全是0),用于动态绑定时重定位到共享库中的函数实现。
  • 在运行时当系统C函数被第一次调用时会动态绑定一次,然后将Mach-O文件_DATA段符号表中对应的指针,指向外部函数(其在共享库中的实际内存地址)。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static int (*orig_close)(int);
static int (*orig_open)(const char *, int, ...);

int my_close(int fd) {
    printf("Calling real close(%d)\n", fd);
    // 调用原函数
    return orig_close(fd);
}

int my_open(const char *path, int oflag, ...) {
    va_list ap = {0};
    mode_t mode = 0;
    // O_CREAT 如果不存在则创建
    if ((oflag & O_CREAT) != 0) {
        // mode 只适用于 O_CREAT
        va_start(ap, oflag);
        mode = va_arg(ap, int);
        va_end(ap);
        printf("Calling real open(%s, %d, %d)\n", path, oflag, mode);
        return orig_open(path, oflag, mode);
    } else {
        printf("Calling real open(%s, %d)\n", path, oflag);
        return orig_open(path, oflag, mode);
    }
}

- (void)test {
	// hook C函数,第一个参数是一个rebinding类型结构体数组,第二个参数是第一个参数结构体数组的长度
    struct rebinding rebindings[2] = {
            {"close", my_close, (void *)&orig_close},
            {"open", my_open, (void *)&orig_open}
        };
    rebind_symbols(rebindings, 2);
    // 获取二进制文件进行读取
    NSString *binaryPath = [[NSBundle mainBundle] pathForResource:@"Hooks" ofType:@""];
    const char *path = [binaryPath cStringUsingEncoding:NSUTF8StringEncoding];
    int fd = open(path, O_CREAT);
    uint32_t magic_number = 0;
    // 读取4个字节
    read(fd, &magic_number, 4);
    printf("Mach-O Magic Number: %x \n", magic_number);
    close(fd);
}

libffi

通过约定好参数的传递顺序、传递方式,栈维护的方式,名字修饰,动态地调用C函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int rectangleArea(int length, int width) {
    printf("Rectangle length is %d, width is %d\n", length, width);
    return length * width;
}

void *testArea() {
    // dlsym 返回rectangleAred函数指针
    void *dlsymFuncPtr = dlsym(RTLD_DEFAULT, "rectangleArea");
    return dlsymFuncPtr;
}

void testFFICallCFunc() {
    // libffi调用c函数
    ffi_cif cif;
    //参数类型指针数组, 根据被调用的函数入参的类型来制定
    ffi_type *argumentTypes[] = {
        &ffi_type_pointer,
        &ffi_type_pointer,
        &ffi_type_sint32,
        &ffi_type_sint32
    };
    //用过ffi_prep_cif 内ffi_prep_cif_core 来设置ffi_cif结构体所需要的数据, 包括ABI
    // 参数个数,参数类型等
    ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 4, &ffi_type_pointer, argumentTypes);
    // 函数参数的设置
    int bar = 123;
    int baz = 456;
    void *args[] = {&bar, &baz};
    // 返回值声明
    int returnValue;
    
    ffi_call(&cif, testArea(), &returnValue, args);
    NSLog(@"ffi_call: %d", returnValue);
}

Cydia Substrate

主要用作逆向开发,制作Tweak插件。

使用 theos Tweak示例:

1
2
3
4
5
6
7
8
9
10
11
%hook ViewController

- (void)clickButton {
	UIAlertController *alerView = [UIAlertController alertControllerWithTitle:@"测试" message:@"Hook成功,鼓励一下吧" preferredStyle:UIAlertControllerStyleAlert];
  	UIAlertAction *cancelAction =[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:nil];
  	[alerView addAction:cancelAction];
  	[self presentViewController:alerView animated:YES completion:nil];
	%orig;
}

%end

格式为:

1
2
3
4
5
6
7
8
9
10
%hook 后对应需要 Hook 的 Class

- (void)needHookMethod {
	// 是否有返回值取决于该函数
	
	// 调用原函数
	%orig;
}

%end

Tweak示例

本文Demo