1.Fishhook hook原理
在一节笔记中我们已经掌握了fishhook的基本使用,也详细探讨了dyld在加载应用程序的过程中绑定外部符号的流程,那么现在我们再来研究一下fishhook是如何通过符号的字符串来找到其在懒加载符号表中的指针的。
1.1 查找String table
首先通过传入的符号字符串去string table查找这个字符串所在的位置,例如:如果要查找NSLog函数的符号,那么就回去字符串表(函数名的符号就是在函数名前加上一个下划线,并且每个符号之间都用.隔开)中查找_NSLog这个符号,找到之后计算其在字符串表中的偏移值为0xE8(0x11458 - 0x11370),如下图所示:
1.2 查找Symbol Table
通过查找String table得到的偏移值0xE8查找这个符号在符号表中的索引值,如下图所示:
1.3 查找indrect symbols
在Symbol table中获取到这个符号在Symbol table中的索引值后,就去indrect symbols中查找这个符号在indrect symbols中的编号,如下图所示:
1.4 查找Lazy Symbol Pointers
外部符号在懒加载符号表中的位置与其在间接符号表中的位置是一一对应的,上一步查找到符号在间接符号表中的位置后,就可以在懒加载符号表中获取到指向这个符号的指针了,如下图所示:
2.应用程序脱符号以及恢复符号
2.1 脱符号
给应用程序脱符号的主要目的是为了减少MachO文件的体积,如果你想要为APP瘦身,可以在你的项目工程中设置如下的选项:
将Deployment Postprocessing设置为YES,脱去符号之后我们再来看看MachO文件中的符号,如下图所示:
首先,工程编译完成之后多了一个bc文件,这个文件是用来在线上崩溃的时候恢复符号用的。
间接符号表中的符号是没有任何变化的。
而应用程序中的本地符号以及全局符号都被脱去了,我们再在viewDidLoad函数中打上断点编译运行一下,结果发下根本就没有来到断点,如下图所示:
虽然我们添加的普通断点是不能执行的,那么我们就设置一个NSLog函数的符号断点试一下,如下图所示:
调试运行,来到断点之后,我们使用bt指令查看一下函数调用栈,如下图所示:
2.2 动态调试查看脱去符号之后的MachO运行时某个类所调用方法名
经过上面脱符号的操作,我们发现原本的本地符号都被替换成了___lldb_unnamed_symbolX的字符串,在APP上架的时候,是都会脱去所有的符号的,这样我们在调试的时候就无从得知到底是哪个类调用了哪个方法呢?但是我们还是可以动态调试获取到这个方法的信息的,具体操作如下所示:
我们现在viewController中编写如下代码
- (void)viewDidLoad {
[super viewDidLoad];
[self my_test];
}
- (void)my_test {
NSLog(@"调用了my_test方法");
}
运行到断点的时候,我们看一下函数调用汇编代码,如下图所示:
假设我们现在想要知道红框位置所调用的objc_msgSend是那个类的哪个方法,该怎么动态调试出来呢?
我们可以尝试计算出这个bl跳转的地址在MachO中的偏移量并在下次运行的时候获取到主应用程序的ASLR下一个符号断点,跳到这个objc_msgSend中的汇编代码中,打印一下x0以及x1寄存器的值,流程如下:
首先获取bl 0x104d8e4d8这行汇编代码在MachO中的偏移量,为(0x104d8ddb0 - 0x0000000104d88000)0x5db0,然后设置一个objc_msgSendSuper2函数符号断点,获取到刚刚那个方法还没执行之前的主程序的首地址,如下图所示:
然后计算出刚刚那行汇编代码在本次运行中的地址为(0x102e08000 + 0x5db0)0x102e0ddb0,然后设置一个地址断点,如下图所示:
紧接着跳到这个断点查看一下寄存器x0以及x1的值,如下图所示:
然后就可以查看到调用的地址了。
以上的操作步骤很复杂,因此就有人提出了为什么不直接打objc_msgSend这个符号断点,而是要通过获取对应bl指令地址去下断点查看方法信息呢?这是因为objc_msgSend这个函数其实在项目中调用次数非常的多,如果下这个断点,我们根本无从得知当前objc_msgSend中x0与x1寄存器上的值是不是我们想要查找的类和方法。
2.2 使用restore-symbol工具恢复machO文件中的符号(不能恢复静态函数符号)
我们知道OC语言中有很多库函数(例如:NSStringFromClass、NSClassFromString、NSSelectorFromString、NSStringFromSelector等),这些函数都是对类的方法或者类名字符串进行的一些操作,试想一下,脱符号有可能把这些类名符号以及类方法符号都完全脱去吗?如果脱去了,以上的这些函数调用就会出现问题了,所以这些符号一定还在其他MachO段中存在着,而脱符号仅仅是对Symbol Table、String Table中的本地以及全局符号进行了脱离,而这些必要的类名符号以及类方法符号其实还有所保留,如下图所示:
因此我们就可以根据这三个表中的信息对(除了函数之外的符号进行恢复)
使用restore-symbol工具对MachO文件的符号进行恢复
restore-symbol工具链接 密码:9xk6
打开终端,输入如下命令:
查看恢复之后的MachO文件的符号,如下图所示:
如果恢复完之后想要再次运行到真机上,就需要重新签名,重新安装。
3.使用fishhook做防护
可以使用fishhook做防护的原因:是因为在hook别的应用程序的时候我们使用最多的方式就是Method Swizzle,而Method Swizzle的方式使用的就是系统的函数,因此我们就可以使用fishhook去hook这些系统的函数以达到防护自己的APP目的。
3.1 防护代码示例
3.1.1 创建一个项目,在项目中创建一个framework,拖入fishhook两个代码文件,创建文件并暴露其头文件
3.1.2 在AntiHookCode.m文件中编写如下代码
#import "AntiHookCode.h"
#import "fishhook.h"
//在framework中做防护的原因,因为framework中load方法的调用要早于主程序以及后插入的其他动态库的load方法,在这里面做防护才能有效的防止别人在hook你的APP的时候,使用的objc方法是已经被你所改变了的。
@implementation AntiHookCode
+ (void)load {
//exchange
struct rebinding exchange;
exchange.name = "method_exchangeImplementations";
exchange.replacement = my_method_exchangeImps;
exchange.replaced = (void *)&exchangeP;
NSLog(@"来到了Load方法!!");
struct rebinding rebindings[] = {exchange};
rebind_symbols(rebindings, 1);
}
void (*exchangeP)(Method _Nonnull m1, Method _Nonnull m2);
void my_method_exchangeImps(Method _Nonnull m1, Method _Nonnull m2) {
NSLog(@"检测到了Hook!");
}
@end
在AntiHookCode.h文件中编写如下代码
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
NS_ASSUME_NONNULL_BEGIN
//暴露给本工程使用的method_exchangeImps指针
CF_EXPORT void (*exchangeP)(Method _Nonnull m1, Method _Nonnull m2);
@interface AntiHookCode : NSObject
@end
NS_ASSUME_NONNULL_END
3.1.3 在Main.storyboard中拖入两个按钮,如下图所示:
在ViewController.m中编写如下代码:
#import "ViewController.h"
#import <AntiHookLib/AntiHookLib.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Method originMethod = class_getInstanceMethod([self class], @selector(button2Click));
Method myHookMethod = class_getInstanceMethod([self class], @selector(myHookBtn2Click));
Method myHookMethod2 = class_getInstanceMethod([self class], @selector(myHookBtn2Click2));
method_exchangeImplementations(originMethod, myHookMethod);
if (exchangeP != NULL) {
exchangeP(originMethod, myHookMethod2);
}
}
- (void)myHookBtn2Click2 {
NSLog(@"自己的hook成功了2");
}
- (void)myHookBtn2Click {
NSLog(@"自己的hook成功了");
}
- (IBAction)button1Click {
NSLog(@"点击了按钮1");
}
- (IBAction)button2Click {
NSLog(@"点击了按钮2");
}
@end
运行程序,就会发现系统函数method_exchangeImplementations以及被hook了,但是可以使用exchangeP指针进行与method_exchangeImplementations一样的操作。
运行结果如下:
3.2 防护应用程序AntiHookDemo hook测试
3.2.1 新建一个HookDemo
首先新建一个HooDemo工程,主要创建完毕后一定先要在真机中运行一次。然后在应用程序根目录创建APP文件夹,拖入写好的应用重签名脚本文件以及yololib工具,压缩应用程序
inject.m文件中编写的注入代码
#import "inject.h"
#import <objc/runtime.h>
@implementation inject
+ (void)load {
Method originMethod = class_getInstanceMethod(NSClassFromString(@"ViewController"), @selector(button1Click));
Method hookMethod = class_getInstanceMethod([self class], @selector(myButton1Click));
method_exchangeImplementations(originMethod, hookMethod);
}
- (void)myButton1Click {
NSLog(@"hook成功!");
}
@end
修改appSign.sh中的第三方库名
。
最后运行程序,分别点击两个按钮,就可以看到如下的打印结果:
注意:遇到下面这个崩溃别慌,重新运行就好了