《iOS底层原理文章汇总》
上一篇文章《iOS-逆向13-Dyld》介绍了Dyld的加载过程,本文介绍Hook原理
1.HOOK
HOOK,中文译为“挂钩”或“钩子”。在iOS逆向中是指改变程序运行流程的一种技术。通过hook可以让别人的程序执行自己所写的代码。在逆向中经常使用这种技术。所以在学习过程中,我们重点要了解其原理,这样能够对恶意代码进行有效的防护。
2.iOS中HOOK技术的几种方式
1、Method Swizzle
利用OC的Runtime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法。
2、fishhook
它是Facebook提供的一个动态修改链接mach-O文件的工具。利用MachO文件加载原理,通过修改懒加载和非懒加载两个表的指针达到C函数HOOK的目的。
3、Cydia Substrate
Cydia Substrate 原名为 Mobile Substrate ,它的主要作用是针对OC方法、C函数以及函数地址进行HOOK操作。当然它并不是仅仅针对iOS而设计的,安卓一样可以用。官方地址:http://www.cydiasubstrate.com/
MobileHooker
顾名思义用于HOOK。它定义一系列的宏和函数,底层调用objc的runtime和fishhook来替换系统或者目标应用的函数.其中有两个函数:
MSHookMessageEx 主要作用于Objective-C方法
void MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP result)
MSHookFunction 主要作用于C和C++函数
void MSHookFunction(voidfunction,void* replacement,void** p_original) , Logos语法的%hook 就是对此函数做了一层封装
MobileLoader
MobileLoader用于加载第三方dylib在运行的应用程序中。启动时MobileLoader会根据规则把指定目录的第三方的动态库加载进去,第三方的动态库也就是我们写的破解程序.
safe mode
破解程序本质是dylib,寄生在别人进程里。 系统进程一旦出错,可能导致整个进程崩溃,崩溃后就会造成iOS瘫痪。所以CydiaSubstrate引入了安全模式,在安全模 式下所有基于CydiaSubstratede 的三方dylib都会被禁用,便于查错与修复。
3.fishhook的简单实用
fishHOOK可以HOOK我们的C函数,但是我们知道函数是静态的,也就是说在编译的时候,编译器就知道了它的实现地址,这也是为什么C函数只写函数声明调用时会报错。那么为什么fishhook还能够改变C函数的调用呢?难道函数也有动态的特性存在?我们一起来探究它的原理…
I.Hook系统函数NSLog
OC的动态特性:不是直接调用方法实现的地址,是通过SEL和IMP之间的对应关系,通过SEL寻找IMP,存在一张映射关系的表,若将此表改掉,达到hook目的
C静态语言:直接通过地址访问,汇编bl 0x123456,编译之后会存在内存中
将fishhook.h和fishhook.c文件拖入工程中
fishhook.h中的三个属性
struct rebinding {
const char *name;//需要HOOK的函数名称,C字符串
void *replacement;//新函数的地址
void **replaced;//原始函数地址的指针!
};
name是需要HOOK的函数名称,replacement是新函数的地址,replaced是原始函数地址的指针
- (void)viewDidLoad {
[super viewDidLoad];
//创建rebinding结构体
struct rebinding nslog;
nslog.name = "NSLog";
nslog.replacement = my_NSLog;
nslog.replaced = (void *)&sys_nslog;
//需求:HOOK NSLog
struct rebinding bds[] = {nslog};
rebind_symbols(bds, 1);
}
//函数指针void NSLog(NSString *format, ...)
static void (*sys_nslog)(NSString *format, ...);
//新函数
void my_NSLog(NSString *format, ...){
format = [format stringByAppendingString:@"\n我hook到了!"];
//走到系统的NSLog里面去
sys_nslog(format);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"NSLog");//指向了my_NSLog
}
如上系统的NSLog被成功hook到。
II.Hook C函数
C是静态语言,直接通过地址访问
同样的方式hook C函数
void func(const char *str){
NSLog(@"%s",str);
}
- (void)viewDidLoad {
[super viewDidLoad];
//创建rebinding 结构体
struct rebinding func;
func.name = "func";
func.replacement = my_func;
//保存C函数func地址的指针!
func.replaced = (void *)&func_p;
//需求:HOOK NSLog
struct rebinding bds[] = {func};
rebind_symbols(bds, 1);
}
static void (*func_p)(const char *str);
void my_func(const char *str){
NSLog(@"Hook了!!");
func_p(str);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
func("hello");
}
并不能hook成功,为什么呢???
4.FishHook的原理
系统的函数NSLog能hook到,自定义的函数func无法hook到,编译的时候无法知道NSLog的真实地址
App通过LLVM编译成可执行文件MachO时,其中有调用NSLog,此时的NSLog并没有地址,预留占位符
待程序运行到内存中的时候,加载共享缓存UIKit、Foundation等,dyld用Foudation中NSLog的真实地址将其替换,
从而能准确调用,MachO文件中存在Text段,即代码段可读可执行,Data段可读可写可执行,
NSLog的符号放入Data段中,存在一张映射表,方便运行时dyld修改绑定真实的NSLog地址,
绑定真实地址后才能运行调用
MachO文件中的NSLog
符号,dyld告知其在
共享缓存Foundation中
的地址进行符号绑定,
在内存中执行时,能准确
找到NSLog的地址
I.符号和地址存在一张映射表符号(SEL)--->地址(IMP)
HOOK时,替换的是MachO文件中符号表中NSLog的符号地址,将其地址绑定为了自定义的my_nslog的符号地址
rebind_symbols(bds, 1)重新绑定符号,通过NSLog字符串找到符号表,修改符号表中对应地址的映射关系,将NSLog符号的真实地址修改为了my_nslog的地址
II.符号绑定地址替换过程
前文探究了符号地址绑定替换的过程,现在真实运行程序验证符号地址替换过程
dyld应用程序加载,链接主程序后会进行非懒加载符号的绑定,程序启动会绑定非懒加载的符号,懒加载是运行调用的时候采取绑定,NSLog只有调用的时候采取绑定符号,并不是程序启动的时候就绑定了
A.绑定前的地址
NSLog的符号地址在懒加载(运行调用到了才去绑定符号)Lazy Symbol Pointers符号表中,将可执行文件MachO拖入MachOView中查看
NSLog符号绑定的地址值是00000001000064D4
相对于程序入口起始地址的偏移值00008010
上图中的NSLog的地址是未绑定的地址,是一个占位符实质也是一段代码的地址,我们在程序中断点调试,得到系统随机分配的主程序的起始地址也就是ASLR0x00000001041e0000,前面的1是PAGEZERO,加上偏移值offset00008010
读取ASLR+offset内存地址中的值,也是地址值
此地址值减去ASLR(程序的起始地址)等于MachOView中
NSLog绑定的地址值,此地址为NSLog在没有被调用前的地址
MachOView中的是没有ASLR偏移地址的地址,但是有PAGEZERO,所以内存中的地址0x00000001041e64d4减去ASLR0x00000000041e0000得到编译后NSLog符号绑定的地址0x00000001000064d4
NSLog符号绑定的地址为0x00000001000064d4,此地址是NSLog没有被调用前,编译器LLVM绑定的地址
B.绑定后的地址
NSLog执行后,符号NSLog绑定的地址发生变化,变为NSLog的真实地址,Foundation中的NSLog在进程内存中的地址
懒加载符号表里面绑定的地址已经变了
C.FishHook后的符号地址
执行rebind_symbols(bds,1)后,符号NSLog绑定的地址再次发生变化,变为自定义的my_NSLog的地址
懒加载符号表里面绑定的地址再次变了
5.符号绑定过程
iOS中函数名、变量名、方法名、编译完成后会生成一张符号表
符号与符号之间存在差别,一个是内部符号,一个是外部符号
I.内部符号:内部函数,方法名称,如ViewDidLoad
内部符号又分为本地和全局
本地:我自己内部使用的
全局:外部也可以使用
定义函数时,全局符号,如自己写一个动态库
//全局符号,可以暴露给外界
void test(){
}
本地符号:App在上架时会去符号,去的是本地符号
//本地符号 作用域相当于本文件
static void test1(){
NSLog(@"test1");
}
编译后通过命令行查看符号objdump --macho -t Symbol
II.外部符号(间接符号表):MachO文件中调用外部方法名称,如NSLog,LLVM编译时期并不知道外部(MachO文件以外)方法的地址
符号表Symbols:包含所有的符号,本地符号,全局符号,间接符号
间接符号有个专门的符号表Indirect Symbols,用到了外部的NSLog,编译时会生成一个符号
汇编中跳入的地址0x100caa4e4减去ASLR0x0000000100ca4000得到的地址0x64e4,在MachOView中查询0x64e4的地址
编译时,会先让NSLog跳入桩0x64e4中,桩的值1F2003D510D9005800021FD6,是一串二进制指令,代码
前文bl -> 占位符(符号表里面保存的地址),其实不是这样的。实际是bl -> 桩(一块代码,去符号表里面的一块代码执行) -> 符号表
依据此,此时应该跳入懒加载符号表中的地址
所以桩的代码就是去执行符号表里面所指向的地址,接下来在MachO中寻找000000010000658C地址,发现会跳向00006574,Lazy Symbols中的其他符号如NSStringFromClass符号表中对应地址为0000000100006598,故开头的这一段代码共用,用作符号绑定,Lazy Symbols中的所有符号都会执行本地的一段代码,也就是开头的共用代码来进行符号绑定
加上ASLR0xca4000即和运行时的地址一模一样,0x100006574+0xca400=0x100caa574,b跳转的地址一致
此时我的电脑屏幕黑掉了,关机一会重启,重新运行ASLR地址变为0x0000000104f8c000,所以ASLR是系统分配的随机值,每次都不一样
断点继续调试,进入符号绑定函数dyld_stub_binder,懒加载符号表里面执行的都是符号绑定函数0x100006594+0x4f8c000=0x104f92594,b跳转的地址为0x100006574+0x4f8c000=0x104f92574
dyld_stub_binder此函数是内部的还是外部的呢???外部的
既然是外部的,那什么时候进行的dyld的绑定呢?去找dyld的dyld_stub_binder函数,通过找非懒加载的符号表,br0x100008000,在0x100008000的位置指向的地址去执行
对于dyld中的符号dyld_stub_binder也有一个符号,在非懒加载里面,在程序运行的时候就绑定了
dyld_stub_binder的地址为ASLR+0x8000,验证如下x16寄存器里面的值就是非懒加载符号表里面所指向的值,默认全是0,程序启动就绑定了dyld_stub_binder
(屏幕又黑掉,重新运行接着调试,ASLR再次变化为0x0000000102bbc000)程序继续往下执行进入dyld_stub_binder函数,此间接路径有点长,bind完毕后,此时点击step up 跳出,第一次外部符号就调用了,此时懒加载符号表里面的地址就变了
第二次执行NSLog,此时还是先找桩,桩里面的代码没有变化,去Lazy Symbols Pointers里面取Data执行,而此时NSLog对应的Lazy Symbols Pointers符号表里面的Data已经变化了,不会再走绑定,符号表里面的内容变化了,指向了NSLog的真实地址,此时执行跳入Foundation`NSLog中
此时直接通过桩就跳入了真实地址了!!!外部函数的执行都是先找桩,桩里面的代码就是去符号表里面执行地址,符号表里面的地址只要变化,执行就发生变化
为什么要有桩的存在呢?符号表里面是数据段,数据段是保存数据,地址,从一个数据段找到拿出一个地址执行,需要代码,这段代码放哪里呢?这段代码就放在桩里面Text段,和直接执行没什么两样,只不过在编译成汇编时需要有三行代码,这三句代码是去查表,叫桩
总结:符号绑定过程
PIC技术,外部符号不确定,编译时搞一个符号表专门去保存外部符号的地址,运行时刻得到
1.外部函数调用是执行桩里面的代码!(TEXT,stubs)
2.通过懒加载符号表里面的代码去执行
3.懒加载符号表里面默认保存的是寻找binder的代码,绑定成功改变了懒加载符号表里面的值
4.binder函数在非懒加载符号表里面(程序运行就绑定好了)
第一次:懒加载符号表中查找地址执行进入绑定过程
第二次:懒加载符号表中地址已经变为NSLog真实地址,直接执行
动态库和静态库,编译静态库体积更小,因为动态库还会保留一些外部符号,给外部调用,静态库合并成一个文件,不需要符号了,静态库没有符号更小一点
符号绑定只改懒加载表和非懒加载符号表不会改符号表Symbols
Symbols,Indirect Symbols,String Table,Lazy Symbols Pointers这几个表之间会有关联,并不是Symbols包含了所有,是几张表之间的关联,fishhook通过什么找到NSLog的,通过字符串“NSLog”,通过字符表能找到NSLog,怎么通过NSLog找到Lazy Symbols的呢?
内部函数跳转没有查找符号流程,直接变成地址bl了