ios逆向 - mach-o文件分析

一. 先给出一个结构图,大致了解一下内部的结构:

image.png

主要结构分成三个部分:

  • 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 这个命令, 只是其中加载的段的信息不同

image.png

二.具体分析

1 header结构:以64位结构来分析

image.png
  • magic指定是32位还是64位
  • cputype和cpusubtype是表示cpu的架构是x86还是x64等,即平台和版本
  • filetype:文件类型:标识是执行文件还是动态库等
  • ncmds: 表示接下来的加载命令的个数
  • sizeofcmds: 加载命令的总长度
  • flags:ldid动态加载需要的标记位
  • 最后的保留位不解释

2.load commands:常见的命令

image.png

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)

前两个字段可以使用下面的结构描述,但是没什么用

image.png

因为还有一个比较全的命令结构描述结构

LC_SEGMENT 命令的结构

image.png

下面看一下:

问题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段的首地址. 该段的文件地址偏移是并不是基于该首地址 ,那是基于那开始偏移?

看下图:

image.png

我们发现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 为

image.png

2.2 :LC_SYMTAB 命令解析

有了上述的base,可以看其他命令

通过 base + symtab 的偏移量 计算 symtab 表的首地址

image.png

代码如下:

// 通过 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 命令解析

image.png
// 通过 base + indirectsymoff 偏移量来计算动态符号表的首地址

uint32_t *indirect_symtab = (uint32_t *)(base + dysymtab_cmd->indirectsymoff);

就可以找到动态符号表的地址

image.png

2.4 如何根据找到段中区load_command?

image.png

下面给出找到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

image.png
  • 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 中的某项

image.png

我们在上面已经拿到了动态符号表这个段的首地址, 同时也知道了_lazy_symbol_ptr 区的load_comand

image.png

// 在 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);

image.png

我们可以看到上图, 这个section中的内容是一条条的,且每个占用一个指针大小,我们可以遍历section中所有的数据

image.png

动态符号表首个懒加载符号对应位置已经被找到,就是上面计算的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 的位置,既可以找到懒加载符号的名称

image.jpeg

上述是为了解释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在链接函数后,替换函数后地址都会变

推荐阅读更多精彩内容