在上篇文章不知MachO怎敢说本身懂DYLD中已经详细介绍了MachO,而且由MachO引出了dyld
,再由dyld
讲述了App的启动流程,而在App的启动流程中又说到了一些关键的名称如:LC_LOAD_DYLINKER
、LC_LOAD_DYLIB
以及objc
的回调函数_dyld_objc_notify_register
等等。而且在末尾提出了MachO中还有一些符号表,而有哪些符号表,这些符号表又有些什么用呢?笔者在这篇文章就将一一道来。git
老规矩,片头先上福利:点击下载demo,demo中有笔者给fishhook每句代码加的详细注释!!! 这篇文章会用到的工具备:github
在开始正文以前,假设面试官问了一个问题:
都知道Objective-C最大的特性就是runtime,你们能够用使用runtime对OC的方法进行hook,那么C函数能不能hook?面试
有兴趣回答的朋友能够先行在评论区回答,答完以后再继续阅读或者预先偷窥一下文末的答案,看看这被炒了无数次冷饭的runtime本身是否真的了然于胸。缓存
本将从如下几方面回答上面所提的问题:bash
Runtime,从名称上就知道是运行时,也是它造就了OC运行时的特性,而要想完全明白什么是运行时,那么就须要将之与C语言有相比较。
今天我们就从汇编的角度看一看OC和C在调用方法(函数)上有什么区别。app
注:笔者使用的是iPhone 7征集调试,全部一下汇编都是基于arm64,因此如下全部汇编默认为基于arm64。框架
新建一个工程取名为:FishhookDemo
敲入两个OC方法mylog
和mylog2
,挂上断点,如图:
ide
开启汇编断点,如图:
函数
运行工程,会跳转到以下图的汇编断点: 工具
从上图能够看的出来调用了两个objc_msgSend
,这两个很像是 咱们的mylog
和mylog2
,但如今还不能肯定。
想想objc_msgSend
的定义:
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
复制代码
第一个参数是self
,第二个参数是SEL
,因此能够知道SEL是放在x1的寄存器里面(什么是x1?继续关注做者,以后的文章会有相关的汇编的专门篇章)。
快马加鞭,挂上两个汇编断点,查看一下两个x1中存放的究竟是什么,如图:
这也就验证了我们OC方法都是消息转发(objc_msgSend)。而同一个C函数的地址又都是同样的(笔者此次运行的地址就是0x1026ce130
) 。
因此在每次调用OC方法的时候就让咱们有了一次改变消息转发「目标」的机会。
这里稍微提一下runtime的源码分析流程:
Step 一、方法查找
① 汇编快速查找缓存
② C/C++慢速查找:self
->super
->NSObject
->找到换缓存起来
Step 二、动态方法解析: _class_resolveMethod
① _class_resolveInstanceMethod
② _class_resolveClassMethod
Step 三、消息转发
① _forwardingTargetForSelector
② _methodSignatureForSelector
③ _forwardInvocation
④ _doesNotRecognizeSelector
一样咱们从汇编的角度切入。
敲入代码一些C函数,挂上断点,如图:
运行工程:
会看到断点断到以下汇编:
能够看到每一个NSLog
对应跳转的地址都是0x10000a010
,每一个printf
对应跳转的地址都是0x10000a184
,也就是说每一个C的函数都是一一对应着一个真实的地址空间。每次在调用一个C函数的时候都是执行一句汇编bl 0xXXXXXXXX
。
因此上面讲述到的消息转发的机会没有了,也就是没有了利用runtime来Hook的机会了。
既然如此,那么是否C函数就真的那么牢不可破,没法对他进行Hook呢?
答案确定是否认的!
想要从根上理解这个问题,首先要了解:咱们的C函数分为系统C函数和咱们自定义的C函数。
在上面的步骤中咱们已经了解到全部C函数的调用都是跳转到一个「固定的地址」,那么就能够推断得出这个「固定的地址」实际上是在编译期已经被生成好了,因此才能快速、直接的跳转到这个地址,实现函数调用。
C语言被称之为是静态语言也就是这么个理。
在上篇文章不知MachO怎敢说本身懂DYLD已经提到了在dyld启动app的第二个步骤就是加载共享缓存库,共享缓存库包括Foundation框架,NSLog
是被包含在Foundation框架的。那么就能够肯定一件事情,在咱们将本身工程打包出的MachO文件中是不可能预先肯定NSLog
的地址的。
可是又由于C语言是静态的特性,无法在运行的时候实时获取共享缓存库中NSLog
的地址。而共享缓存库的存在好处太大,既能节省大量内存,又能加快启动速度提高性能,不能弃之而不用。
为了解决这个问题,Apple使用了PIC(Position-independent code)技术,在第一次使用对应函数(NSLog
)的时候,从系统内存中将对函数(NSLog
)的内存地址取出,绑定到APP中对应函数(NSLog
)上,就能够实现正常的C函数(NSLog
)调用了。
既然有这么个过程,iOS系统能够动态的绑定系统C函数的地址,那么我们就也能。
Facebook的开源库fishhook就能够完美的实现这个任务。
先上一张官网原理图:
整体来讲,步骤是这样的:
下面就来验证一下在NSLog的地址是否是真的就存在Indirect Symbol Table中。 一样在NSLog处下好断点,打开汇编断点,运行代码。会发现断点断在以下入位置:
能够发现NSLog的地址是0x104d36010
,先记住这个值。
而后查看咱们APP在内存中的偏移值。
利用image list
命令列出全部image,第一个image就是咱们APP的偏移值,也就是内存地址。
能够看到APP在内存中的偏移值为0x104d30000
。
接着打开MachOView查看MachO中的Indirect Symbol Table中的value,如图:
其值为0x100006010
,去除最高位获得的0x6010
就是NSLog
在MachO中的偏移值。 最后将NSLog
在MachO中的偏移值于APP在内存中的偏移值相加就获得NSLog
真实的内存地址:
0x6010
+0x104d30000
=0x104d36010
最终证实,在Indirect Symbol Table的value中的值就是其对应的函数的地址!!!
我们仍是用NSLog
来距离查找。
取出其data值0000010A
,用10进制表示,结果为266
,如图:
在Symbol Table中找到下标(offset)为266的的对象,取出其data0x124
,如图:
将在Symbols中获得的偏移值0x124
加上String Table的首个地址DC6C
,获得值DD90
,而后找到pFile为DD90
的值,以下两图:
上述就是根据MachO的表查找对应的函数名和函数地址全过程了。
fishhook的源码总共只有250行左右,因此结合MachO慢慢看,其实一点也不费劲,在笔者的demo中有对其每一句函数的详细注释。固然也有对fishhook使用的demo。
因此笔者就不在此处对fishhook作太过详细的介绍了。只对其中一些关键参数和关键函数作介绍。
// 给须要rebinding的方法结构体开辟出对应的空间
// 生成对应的链表结构(rebindings_entry),并将新的entry插入头部
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel)
复制代码
// 找到linkedit的头地址
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
// 获取symbol_table的真实地址
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
// 获取string_table的真实地址
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// Get indirect symbol table (array of uint32_t indices into symbol table)
// 获取indirect_symtab的真实地址
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
复制代码
// 在四张表(section,symtab,strtab,indirect_symtab)中循环查找
// 直到找到对应的rebindings->name,将原先的函数复制给新的地址,将新的函数地址赋值给原先的函数
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
section_t *section,
intptr_t slide,
nlist_t *symtab,
char *strtab,
uint32_t *indirect_symtab)
复制代码
上面说了这么多,那么我们来验证一下系统C函数是否是真的会这样被绑定起来,而且看一看,是在何时绑定的。
一样,在第一次敲入NSLog
函数的地方加上断点,在第二个NSLog
处也加上断点:
运行工程后,使用dis -s
命令查看该函数的汇编代码,而且继续查看其中第一次b
指令,也就是函数调用的汇编,如图:
从上图就能够看到,在咱们第一次调用NSLog
的时候,系统确实会默认的调用dyld_stub_binder
函数对NSLog
进行绑定。
继续跳过这个断点,进入下一个NSLog
的汇编断点处,一样利用dis -s
命令查看该汇编:
获得答案:
系统确实会在第一次调用系统C函数的时候对其进行绑定!
还记得正文开始的时候的那个问题吗?
那么是否是系统C函数能够hook,而自定义的C函数就绝对不能hook了呢?
很显然,国内外大神那么多,确定是能作到的,有兴趣的读者能够自行查阅Cydia Substrate。
这篇文章利用了一些LLDB命令行看了许多咱们想看的内容,如image list
,register read
还有dis -s
,在咱们正向开发中,LLDB就是一把利器,而在咱们玩逆向的时候,LLDB就成为了咱们某些是后的惟一途径了!因此,在下一篇文章中,笔者将会对LLDB进行更加详细的讲解,让你们看到LLBD的伟大。