在上篇文章MachO文件解析已经详细介绍了MachO,并且由MachO引出了dyld,再由dyld讲述了App的启动流程,而在App的启动流程中又说到了一些关键的名称如:LC_LOAD_DYLINKER、LC_LOAD_DYLIB以及objc的回调函数_dyld_objc_notify_register等等。
这篇文章我们来了解一下,符号表、fishhook相关的内容。
用到的工具:fishhook
接下来本文会从以下几点进行阐述:
- HOOK概述
- fishHook的简单使用
- fishHook原理探究
- fishHook源码分析
1.HOOK概述
HOOK,中文译为“挂钩”或“钩子”。在iOS逆向中是指改变程序运行流程的一种技术。通过hook可以让别人的程序执行自己所写的代码。在逆向中经常使用这种技术。所以在学习过程中,我们重点要了解其原理,这样能够对恶意代码进行有效的防护。
1.1 Hook 流程图
如上图,这就是我们HOOK代码大概流程。
1.2 HOOK的方式
1.****Method Swizzld
利用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/
1.3 Cydia Substrate简介
Cydia Substrate的原名为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 就是对此函数做了一层封装。
1.4 Cydia Substrate特点
1.MobileLoader:MobileLoader用于加载第三方dylib在运行的应用程序中。启动时MobileLoader会根据规则把指定目录的第三方的动态库加载进去,第三方的动态库也就是我们写的破解程序.
2.safe mode: 破解程序本质是dylib,寄生在别人进程里。 系统进程一旦出错,可能导致整个进程崩溃,崩溃后就会造成iOS瘫痪。所以CydiaSubstrate引入了安全模式,在安全模 式下所有基于CydiaSubstratede 的三方dylib都会被禁用,便于查错与修复。
2.fishHook的简单使用
fishHook代码地址:fishhook
struct rebinding {
const char *name;//需要HOOK的函数名称,C字符串
void *replacement;//新函数的地址
void **replaced;//原始函数地址的指针!
};
关键函数:
FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[],
size_t rebindings_nel);
参数一:存放rebinding结构体的数组(可以同时交换多个函数)
参数二:rebindings数组的长度
FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel);
hook系统的NSLog函数fishhook简单使用代码
运行代码,点击屏幕,会发现我们的代码执行了,
hook C 函数fishhook 系统C函数
运行代码,我们会发现,我们的hook方法失效了,这是为什么么呢?下面我们来一起探究下吧。。。。
3.fishHook原理探究
在上篇文章已经提到了在dyld启动app的第二个步骤就是加载共享缓存库,共享缓存库包括Foundation框架,NSLog是被包含在Foundation框架的。有了这个贡献缓存库,就可以解决多个app共同调用一个库等问题了。
3.1 fishHook 加载流程
首先,我们想一下OC为什么可以hook呢?C为什么不能hook?
OC为什么可以hook? 因为OC底层使用的Runtime技术,通过运行时去找到方法的实现,所以OC可以hook。
C为什么不能hook?
C语言函数通常是静态的,编译之后,从汇编代码变成了内存地址。它都是通过内存地址去找的,在编译的时候就绑定了的,所以hook不了。
那么问题来了,为什么问题来了,为什么fishhook可以hook到C方法呢?
原因是:iOS系统实现了一个动态缓存库技术,一些公共的系统库放进内存中的某个地方,当某个iOS项目启动后,machO文件会在Data段创建一个指针,dyld动态将machO中Data段中这个指针指向外部函数,这里的指针指向内部函数的调用,指向外部函数的地址,而这个指针也就是我们通常说的符号;这也是为什么fishhook中函数名为rebind_symbols(重新绑定符号),实际上是修改这个指针指向外部函数的地址,这也就是为什么修改不了内部函数和自定义函数,只能修改machO外部函数(在符号表中能找到的函数)。由于苹果实现了ASLR技术(不了解ASLR,看这篇逆向学习笔记8——ASLR),所以这些动态缓存库函数在APP项目的内存地址不确定,每次启动APP的时候都会有相应的变化,这是C语言的动态表现。
通过MacOView分析,我们知道了iOS应用启动时的启动流程:
- 启动APP会执行dyld,加载程序
- 进入dyld:main函数
- 配置一些环境
- 加载共享缓存库
- 实例化主程序
- 加载动态库
- 链接主程序
通过以上分析,我们可以利用dyld加载的时候对C函数进行修改。
简单总结下就是:
- 在MachO文件中有个PIC(贡献缓存库),它在Data段创建了一个指针,这个指针指向外部函数。
- 当我们调用C方法、函数的时候,这个指针就会起加载(绑定)这个外部的C函数。
- 而我们的fishhook正是利用了这一步骤进行hook的。
注意:
fishhook 修改的是内存的地址
下面,我们使用MachOView来看一下吧
这里我们代码写的少,所以就直接用 2fishhook hook系统C函数这个案例来讲解了。
首先,我们来看看NSLog的地址在什么时候被加载的,也就是NSLog到底在哪里。
1.使用MachOView查看,并使用验证
// 在viewDidLoad中添加以下两句代码,断点断到第一句代码
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"测试1");
NSLog(@"测试1");
}
1.在第一个NSLog使用断点断住,在Project中,得到当前APP的 MachO文件。使用MachOView进行分析。我们可以找到如下内容。
懒加载表 非懒加载表
2.获得当前NSLog的偏移量。
3.在LLDB中使用 image list列出当前加载的镜像。第一个地址为当前MachO文件在内存中的首地址
4.通过首地址加上当前的偏移量,我们可以获得如下NSLog在内存中的位置。
5.由于iOS是小端模式,我们的地址从右往左读,如上图第二个lldb的内容,最后我们会看到libdyld.dylib`dyld_stub_binder:。上图中有体现。
6.我们的断点在往下走一步,会发现我们获取MachO文件的首地址内存发生了变化。上图中有体现。
7.这时我们查看一下当前的地址,看看有什么?,如图,我们的NSLog被打印出来了。
到这里,说明了我们的NSLog在我们的系统共享缓存区里,第一次没使用的时候需要绑定,第二次直接去找地址,不用绑定。
2.使用汇编的方式查看
1.同上面的第1,2,3步,通过Debug -> Debug WorkFlow -> Always Show Disassembly 调试,如下图:
我们同样可以得到 libdyld.dylib`dyld_stub_binder:函数
4.fishHook源码分析
4.1、fishhook的总体思路
Facebook的开源库fishhook就可以完美的实现这个任务。
先上一张官网原理图:
总体来说,步骤是这样的:
- 先找到四张表Lazy Symbol Pointer Table、Indirect Symbol Table、Symbol Table、String Table。
- MachO有个规律:Lazy Symbol Pointer Table中第index行代表的函数和Indirect Symbol Table中第index行代表的函数是一样的。
- Indirect Symbol Table中value值表示Symbol Table的index。
- 找到Symbol Table的中对应index的对象,其data代表String Table的偏移值。
- 用String Table的基值,也就是第一行的pFile值,加上Symbol Table的中取到的偏移值,就能得到Indirect Symbol Table中value(这个value代表函数的偏移值)代表的函数名了。
4.2、源码分析
fishhook的源码总共只有250行左右,所以结合MachO慢慢看,其实一点也不费劲,接下来,我们简单的分析一下吧。
1.fishhook为维护一个链表,用来储存需要hook的所有函数。
// 给需要rebinding的方法结构体开辟出对应的空间
// 生成对应的链表结构(rebindings_entry),并将新的entry插入头部
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel)
2.根据linkedit的基值,找到对应的三张表:symbol_table、string_table和indirect_symtab。
// 找到linkedit的头地址
// linkedit_base其实就是MachO的头地址!!!可以通过查看linkedit_base值和image list命令查看验证!!!(文末附有验证图)
/**********************************************************
Linkedit虚拟地址 = PAGEZERO(64位下1G) + FileOffset
MachO地址 = PAGEZERO + ASLR
上面两个公式是已知的 得到下面这个公式
MachO文件地址 = Linkedit虚拟地址 - FileOffset + ASLR(slide)
**********************************************************/
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);
3.最核心的一个步骤,查找并且替换目标函数。
// 在四张表(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)
在了解了fishhook的简单使用之后,我们就可以做一些基本的防护的内容了,参见代码
关于几个名词的解释。
几个名词(pFile 、offset 、File Offset)的解释
- 首先,这三个都是表示相对于MachO的内存偏移,只不过其含义被细分了。
- pFile 和 offset含义相近,不过offset更详细,能够对应上具体某一个符号(DATA? TEXT?)。比如文件里面有许多类,类里面有许多的属性,pFile就代表各个类的偏移值,offset代表各个属性的偏移值。
- File Offset 这个存在于Segment的字段中。用于从Segment快速找到其代表的「表」真正的偏移值。
参考文章:
作者:一缕清风扬万里
原文地址:https://www.jianshu.com/p/95896fb96a03