一. 先给出一个结构图,大致了解一下内部的结构:
主要结构分成三个部分:
Header部分:保存了该文件的一些基本信息,如平台,文件类型,加载命令的个数等
loadCommends部分:根据这里的数据来确定内存的分布
Data部分:存放具体的代码和数据
data部分是以段来划分的,segment段类型如下图:
1:__PAGEZERO段: 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用;
2: __TEXT 段: 包含了执行代码以及其他只读数据。 为了让内核将它 直接从可执行文件映射到共享内存, 静态连接器设置该段的虚拟内存权限为不允许写。当这个段被映射到内存后,可以被所有进程共享。(这主要用在frameworks, bundles和共享库等程序中,也可以为同一个可执行文件的多个进程拷贝使用)
3: __DATA段: 包含了程序数据,该段可写;
4: __OBJC段: Objective-C运行时支持库;
5: __LINKEDIT段: 含有为动态链接库使用的原始数据,比如符号,字符串,重定位表条目等等。
每种类型的段又会按不同的功能划分为几个区(section, 名称小写,加两个下横线作为前缀)如下:
TEXT 段中的section具体类型和作用
- _text:只有可执行机器码(主程序代码)
- _cstring: 去重后的c字符串
- _const: 初始化的常量
- _stubs: 符号桩,本质上就是一小段会直接跳入到lazybinding的表的对应项指针指向的地址的代码(???)
- _stubs_helper: 辅助函数,上述lazybinding表中没有找到符号地址都指向这
- _unwind_info:用于存储异常请况信息>
- _eh_frame 调试辅助信息
DATA 段中section的具体类型和作用
- _data :初始化过得可变的数据,即全局变量和静态变量的存储是放在一块的,都放在全局区(静态区),初始化的全局变量和静态变量在一块区域
- _const: 没有初始化过得常量
- _bss: 没有初始化的静态变量
- _common: 没有初始化过的符号声明
- _mod_init_func : 初始化函数:在main之前调用
- _mod_term_func: 终止函数,在main返回之后调用
- _nl_symbol_ptr: 在非lazy-binding的指针表中 的每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号(符号的指针)
- __la_symbol_ptr:lazy-binding的指针表,每个表项中的指针一开始指向stub_helper(没有找到的符号指针)
注意: 虽然段类型是不一样的,但是加载都是使用LC_SEGMENT_64 这个命令, 只是其中加载的段的信息不同
二.具体分析
1 header结构:以64位结构来分析
- magic指定是32位还是64位
- cputype和cpusubtype是表示cpu的架构是x86还是x64等,即平台和版本
- filetype:文件类型:标识是执行文件还是动态库等
- ncmds: 表示接下来的加载命令的个数
- sizeofcmds: 加载命令的总长度
- flags:ldid动态加载需要的标记位
- 最后的保留位不解释
2.load commands:常见的命令
2.1 :LC_SEGMENT 命令解析
分为LC_SEGMENT 和LC_SEGMENT_64,其结构如下:
![image.png](https://upload-images.jianshu.io/upload_images/1974361-1ff8666adfb898f8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
其中字段的含义:
1,Command 是指对段的操作指令,
2,CommandSize 是指令的大小 此处是72 = 0x48 —> 0X20 + 0X48 = 0X68 我们看到最后的参数的其实地址是64,所以是最后一个参数的大小是4,如何证明,看下一个指令是从0X68开始
3,Segment Name 是指令操作的段的名称
4,VM Address 是指令操作的段的所在的内存起始地址
5,VM Size 是段的大小 比如虽然该段占文件大小为0 ,但是具体在虚拟空间大小为4294967296
6,File Offset 是段在文件的偏移量
7,File Size 是段在文件中的大小 比如PAGEZERO 段占文件的大小是0 ,
8,Number of Sections : 表示段里面包含多少个section
9,Maximum VM Protection: 段页面所需要的最高内存保护(4=r,2=w,1=x)
前两个字段可以使用下面的结构描述,但是没什么用
因为还有一个比较全的命令结构描述结构
LC_SEGMENT 命令的结构
下面看一下:
问题1: 如何找到这些LC_SEGMENT加载命令的?
代码展示:
// 声明几个查找量:
segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL;
segment_command_t *text_segment = NULL;
segment_command_t *data_segment = NULL;
struct symtab_command* symtab_cmd = NULL;
struct dysymtab_command* dysymtab_cmd = NULL;
// 初始化游标
// header = 0x100000000 - 二进制文件基址默认偏移
// sizeof(mach_header_t) = 0x20 - Mach-O Header 部分
// 首先需要跳过 Mach-O Header
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
// 遍历每一个 Load Command,游标每一次偏移每个命令的 Command Size 大小
// header -> ncmds: Load Command 加载命令数量
// cur_seg_cmd -> cmdsize: Load 大小
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
// 取出当前的 Load Command
cur_seg_cmd = (segment_command_t *)cur;
// Load Command 的类型是 LC_SEGMENT
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
// 比对一下 Load Command 的 name 是否为 __LINKEDIT
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
// 检索到 __LINKEDIT 找到LINKEDIT段
linkedit_segment = cur_seg_cmd;
}
if (strcmp(cur_seg_cmd->segname, SEG_TEXT) == 0) {
// 检索到 __TEXT段
text_segment = cur_seg_cmd;
}
if (strcmp(cur_seg_cmd->segname, SEG_DATA) == 0) {
// 检索到 DATA 段
data_segment = cur_seg_cmd;
}
}
// 判断当前 Load Command 是否是 LC_SYMTAB 类型
// LC_SEGMENT - 代表当前区域链接器信息
else if (cur_seg_cmd->cmd == LC_SYMTAB) {
// 检索到 LC_SYMTAB
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
}
// 判断当前 Load Command 是否是 LC_DYSYMTAB 类型
// LC_DYSYMTAB - 代表动态链接器信息区域
else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
// 检索到 LC_DYSYMTAB
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}
问题2: 拿到这些段命令后,如何找到段的真实地址,又如何找到基址?
举个例子 : 如何找到LinkeEdit段的基址?
_dyld_get_image_header(i): 可以拿到程序的首地址,也是mach-o header的首地址
_dyld_get_image_slide(i): 可以拿到ASLR 偏移量
// slide: ASLR 偏移量
// vmaddr: SEG_LINKEDIT 的虚拟地址
// fileoff: SEG_LINKEDIT 地址偏移
// 式①:base = SEG_LINKEDIT真实地址 - SEG_LINKEDIT地址偏移
// 式②:SEG_LINKEDIT真实地址 = SEG_LINKEDIT虚拟地址 + ASLR偏移量
// 将②代入①:Base = SEG_LINKEDIT虚拟地址 + ASLR偏移量 - SEG_LINKEDIT地址偏移
uintptr_t base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
注意: 这里的基址不是Linkedit段的首地址. 该段的文件地址偏移是并不是基于该首地址 ,那是基于那开始偏移?
看下图:
我们发现TEXT段的文件偏移地址为0,按上面的公式, TEXT段的首地址就也就是我们说的基址所在的位置, 进而说明文件偏移是排除了mach_oheader部分,load_command部分,从
TEXT segment开始算文件偏移的开始
mach_o 未加载的时候, 都是从mach-o 文件开始计算偏移
但是加载到内存后,因为会去掉mach_oheader部分,load_command部分,所以mach-o就是从segment开始算
有了内存中mach_o的真实的基址base,就可以根据这个基址, 找到其他段的真实地址, 如何找?
每个段都有自己的load_command , 而load_commend 中又包含各自的文件偏移, 这些偏移都是基于base 的
比如: DATA的load_command 中fileoffset 为
2.2 :LC_SYMTAB 命令解析
有了上述的base,可以看其他命令
通过 base + symtab 的偏移量 计算 symtab 表的首地址
代码如下:
// 通过 base + symtab 的偏移量 计算 symtab 表的首地址,并获取 nlist_t 结构体实例
nlist_t *symtab = (nlist_t *)(base + symtab_cmd->symoff);
// 通过 base + stroff 字符表偏移量计算字符表中的首地址,获取字符串表
char *strtab = (char *)(base + symtab_cmd->stroff);
2.3 :LC_DYSYMTAB 命令解析
// 通过 base + indirectsymoff 偏移量来计算动态符号表的首地址
uint32_t *indirect_symtab = (uint32_t *)(base + dysymtab_cmd->indirectsymoff);
就可以找到动态符号表的地址
2.4 如何根据找到段中区load_command?
下面给出找到DATA段中_la_symbol_ptr 的load_command
// 归零游标,复用
cur = (uintptr_t)header + sizeof(mach_header_t);
// 再次遍历 Load Commands
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
// Load Command 的类型是 LC_SEGMENT
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
// 查询 Segment Name 过滤出 __DATA 或者 __DATA_CONST
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
continue;
}
// 遍历 Segment 中的 Section
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
// 取出 Section
section_t *sect = (section_t *)(cur + sizeof(segment_command_t)) + j;
// flags & SECTION_TYPE 通过 SECTION_TYPE 掩码获取 flags 记录类型的 8 bit
// 如果 section 的类型为 S_LAZY_SYMBOL_POINTERS
// 找到了load_command段中section的命令
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
// 进行 rebinding 重写操作
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
// 这个类型代表 non-lazy symbol 指针 Section
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
}
}
section的数据结构,加载命令中描述的section
- sectname:比如_text、stubs
- segname :该section所属的segment,比如__TEXT
- addr : 该section在内存的起始位置
- size: 该section的大小
- offset: 该section的文件偏移
- align :字节大小对齐
- reloff :重定位入口的文件偏移
- nreloc: 需要重定位的入口数量
- flags:包含section的type和attributes
2.5 找到了section load_commad ,如何找到某个section 的内容?
比如: lazy_symbol_ptr section 中的某项
我们在上面已经拿到了动态符号表这个段的首地址, 同时也知道了_lazy_symbol_ptr 区的load_comand
// 在 Indirect Symbol 表中检索到对应位, 找到动态符号表的非懒加载符号的对应位置
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1
注意: resered1 就是告诉动态符号表,从动态符号表哪个地方开始是懒加载的符号,其他的是非懒加载的符号[就是程序加载的时候的加载的符号,
懒加载是符号运行时才加载的]
找到这个地址section的地址, 注意: 这里并没有使用偏移, 因为这里提供了Address, 直接加上ASLR偏移就知道了_DATA段中的_la_symbols_ptr区的地址
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
我们可以看到上图, 这个section中的内容是一条条的,且每个占用一个指针大小,我们可以遍历section中所有的数据
动态符号表首个懒加载符号对应位置已经被找到,就是上面计算的indirect_symbol_indices
从上面可以知道, 懒加载section的所有符号都包含在动态符号表中,且在动态符号表的某一位置开始是一一对应的
uint32_t symtab_index = indirect_symbol_indices[i]; 循环里面,根据懒加载符号在动态符号表首位置,计算出这个符号在符号表的index
然后就可以拿到这些懒加载函数的名称
// 获取符号名在字符表中的偏移地址
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
// 获取符号名
char *symbol_name = strtab + strtab_offset;
下图就是从懒加载section中的所有符号找到其在动态符号表中的位置,然后更具该位置找到符号表中的位置,再找到strtable 的位置,既可以找到懒加载符号的名称
上述是为了解释fishhook 中我没有理解的问题,是怎么找指定函数的?
// 在 Indirect Symbol 表中检索到对应位置
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
// 获取 _DATA.__nl_symbol_ptr(或__la_symbol_ptr) Section
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
// 用 size / 一阶指针来计算个数,遍历整个 Section
for (uint i = 0; i < section->size / sizeof(void *); i++) {
// 通过下标来获取每一个 Indirect Address 的 Value
// 这个 Value 也是外层寻址时需要的下标
uint32_t symtab_index = indirect_symbol_indices[i];
if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
continue;
}
// 获取符号名在字符表中的偏移地址
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
// 获取符号名
char *symbol_name = strtab + strtab_offset;
// 过滤掉符号名小于 4 位的符号
if (strnlen(symbol_name, 2) < 2) {
continue;
}
// 取出 rebindings 结构体实例数组,开始遍历链表
struct rebindings_entry *cur = rebindings;
while (cur) {
// 对于链表中每一个 rebindings 数组的每一个 rebinding 实例
// 依次在 String Table 匹配符号名
for (uint j = 0; j < cur->rebindings_nel; j++) {
// 符号名与方法名匹配
if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
// 如果是第一次对跳转地址进行重写
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
// 保存原始跳转地址
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
// 重写跳转地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
// 完成后不再对当前 Indirect Symbol 处理
// 继续迭代到下一个 Indirect Symbol
goto symbol_loop;
}
}
// 链表遍历
cur = cur->next;
}
fishhook 是在什么地方替换呢?是在indirect_symbol_bindings替换函数,而这个是_DATA.__nl_symbol_ptr(或__la_symbol_ptr) 中Section的地方
所以我们会看到__la_symbol_ptr 这个section在链接函数后,替换函数后地址都会变