浅析iOS Mach-O文件的懒加载和非懒加载
什么是Mach-O文件
Mach-O是Mach Object的英文缩写,是一种文件格式,用于描述苹果操作系统下的可执行程序、动态库等;和Win下的PE、Linux下的ELF属于同一类文件。
Mach-O文件的组成架构
Mach-O的组成分为3个部分
Header:描述Mach-O的类型、cpu类型、要加载的命令等
Load Commonds:描述各个Section数据的分布、程序加载到内存中的位置及辅助描述信息
此区包含多个Segment,每个Segment包括多个Section
Data:实际的数据部分
我们用一张图来描述上述的分布是如何加载到内存中的:
首先我们需要简单熟悉下苹果的空间布局随机化技术ASLR,Address Space Layout Randomization(这里只做简单介绍,不做深究),简单理解就是程序加载到内存的位置不固定,加载时系统会生成一个随机基地址,MachO文件会在此基础上进行展开,也就是一个随机锚点,这样可以有效增加黑客的内存注入的难度
从上图可以看出,Mach-O中的描述信息对可执行代码在内存中的分布至关重要,规划了可执行程序的内存架构
1、在Mach-O运行时,操作系统根据Section Header定义的Section Data在Mach-O文件中的位置(offset)和尺寸(size),将相应的Section Data数据加载到Section Header指定的内存(address)偏移处。
2、Seaction Header指定的address是相对于ALSLR地址的
3、Mach-O文件中有多个Section Data区,每个区根据功用代表的不同的数据类型和读写权限,下图详细说明
Load Comonad下有
Segment(__PAGEZERO):用于指定空指针的内存位置
Segment(__TEXT):程序运行的代码,只读区
Section(__text):代码指令
Section(__stubs):动态库的函数存根,函数锚点,存放__la_symbol_ptr中的地址
Section(__stub_helper):辅助定位动态库函数地址
Segment(__DATA):程序运行时数存储数据的部分,可读可写区
Section(__got):存放动态库中的全局变量,加载到内存前是空数据,用于非懒加载
Section(__la_symbol_ptr):懒加载动态库中的函数地址,加载到内存前存放的是__stub_helper中的地址
Section(__data):存放当前Mach-O已初始化的全局变量
Section(__bss):存放当前Mach-O未初始化的全局变量和静态局部变量
Section(SYMTAB):存放符号表
Section(DYSYMTAB):存放间接符号表
Section(STRTAB):存放字符串表
为什么要进行懒加载和非懒加载
通过对Mach-O文件的分析,我们基本上可以知道文件在内存中的分布,Section Data的数据并不能直接运行,不同的Section Data存放不同的数据,需要在运行前进行加载整合,此整合过程就是加载过程。
对动态库中的数据,根据其数据类型,分为了懒加载和非懒加载
懒加载:用于动态库中函数地址的加载
非懒加载:用于动态库中的全局变量的加载
下面以一个简单的例子说明懒加载和非懒加载的加载过程
#import <Cocoa/Cocoa.h>
extern NSString *const helloworld; // 引用动态库中的变量
int main(int argc, const char *argv[])
{
NSLog(@"%@",helloworld);
NSLog(@"%@",helloworld);
}
非懒加载过程
1、Xcode开启汇编debug
2、添加BreakPoint
3、运行程序
4、x86下面的rip寄存下相当于arm下的pc寄存器,可以看出当前%rip为0x10cb63611
计算%rip+0x11a68=0x10cb63611+0x11a68=0x10cb75079,根据8字节对齐方式,实际使用的内存地址是0x10cb75080
执行lldb的,memory read 0x10cb75080,结果是0x0111232bb8就是上图所展示的实际helloworld的存储地址,那么0x10cb75080是什么地址呢
5、执行lldb的,image lookup -a 0x10cb75080,相对于ASLR的地址为0x1000018080,下面已经明确了在__got中,同时偏移为120个字节
6、__got的首地址为0x180008,首地址+120的偏移=0x180008+0x78=0x18080,刚好指向_helloworld符号。
细心的同学就发现了,Data列中全部为00000000000,为什么Value中可以知道符号为helloworld呢
7、这里MachOView主动做了加载,正常情况下这里确实为0,MachOView怎么查到此位置就是helloworld的呢?
上面我们知道了helloworld相对于__got首地址的偏移是120个字节,每个__got项占用8个字节(刚好是一个64位指针的大小)
120/8 = 15
18080所处内存地址相对首项是第15(从0开始算)项
8、__got header中flag下的Reserved1(Indirect Sys Index),相对于简介符号表的位置为68
9、Reserved1 + __got中的位置 = 68 + 15 = 83,得到间接符号表中的位置
间接符号表基地址(0x29888)+ 单项字节数4 * 项数(83)=0x29888+4*83= 0x29DD4
_helloworld对应的Data列为00007F0,此位置为符号表中的第00007F0项目
10、符号表的基地址为0x00021738,单项size为16个字节
符号表基地址(0x00021738)+ 单项Size(16) * 第0x00007F0项 = 0x00021738 + 0x10 * 0x00007F0 = 0x00029638
0x00029638对应的符号表项中,字符串表的String Table Index为0x0000B20
11、字符串表的基地址为0x00029B00
字符串表基地址(0x00029B00)+ 字符串表的Index(0x0000B20) = 0x2A620
就是_hellworld的字符串
总结:经过那么多的步骤,从__got区,经过间接符号表、符号表、字符串表,最终获取到变量的符号名称helloworld,但是helloworld变量所对应的内容(字符串)还是存在于动态库中,因内容过多,此处不做细致分析,另起文章再析。
对上述过程进行汇总,得到如下图,更简单明晰
懒加载过程
1、Xcode开启汇编debug(同非懒加载)
2、添加BreakPoint
3、运行程序
4、添加汇编断点,NSLog函数的地址都是0x10ab00d82
4、查看0x10ab00d82对应的内存地址,在__TEXT段下,__stubs区,相对__stubs的基地址偏移为18,
stubs的基地址(0x00011D70)+ 18(10进制) = 0x00011D82,就是NSLog的位置,但是Data列对应的数据是FF2548630000,并不像是函数的地址,所以此处应该也是MachOView自己计算的
5、FF2548630000是什么呢,我们看下lldb中0x10ab00d82中存的数据是按照直接读取的顺序刚好是FF2548630000
6、加汇编断点,并走进去,这里的数据jmpq *0x6348(%rip),就是FF2548630000的值,也就是stubs中存的都是汇编的16进制表示
7、rip寄存器前面介绍了(和pc寄存器一样都是指令寄存器),rip存的数据就是当前指令0x10ab00d82
*0x6348(%rip) = *(0x10ab00d82 + 0x6348)= *(0x10AB070CA),根据地址8字节对齐,0x10AB070CA需转换为0x10AB070D0
学过C的都知道,*代表的是解引用,因此此代码就代表的是从0x10AB070D0内存取值,在进行分析内存中取值的内容前,先确定0x10AB070D0内存到底在何处。
0x10AB070D0在Mach-O的位置是__DATA段,__la_symbol_ptr区,偏移为24
__la_symbol_ptr区的基地址为0x00180B8 + 24(10进制) = 0x180D0,DATA列就是0x10AB070D0所存的值0x100011F1E
8、0x100011F1E位于__stub_helper区,可以看到只有2行汇编
pushq $0x5f
jmpq 0x100011f04
0x100011f04位于__stub_helper区的第一行,也就是所有的函数符号寻找都是在通过偏移(如0x5f)传入dyld_stub_binder函数来寻址的,dyld_stub_binder的寻址方式有点复杂,这里不做细致分析,另起文章介绍
9、在首次寻址结束后,dyld_stub_binder会将寻到的函数地址存入到__la_symbol_ptr对应的DATA中,后面再调用NSLog,__la_symbol_ptr中存入的就是正常的函数地址,不再是__stub_helper中的位置,不需要再寻址了(感兴趣的可以分析下fishhook的源码,fishhook利用的就是懒加载的过程,直接改写__la_symbol_ptr中对应的DATA为被hook的函数地址)。
总结:对懒加载的过程进行如下简介
1、初始时__la_symbol_ptr的内容都是指向__stub_helper区
2、函数调用时,代码中存储的地址是stub区的地址
3、动态库函数定位过程,通过stub->la_symbol_ptr->stub_helper逐区定位函数地址,并最终通过dyld_stub_binder 寻找到函数地址,并写入__la_symbol_ptr区
4、本Mach-O文件中的函数地址不需要懒加载,函数地址已存在__text区中
5、后面再调用同一个函数,直接经过stub->la_symbol_ptr即可获取到函数地址
- 点赞
- 收藏
- 关注作者
评论(0)