美文网首页
ios逆向 - mach-o文件分析

ios逆向 - mach-o文件分析

作者: ldzSpace | 来源:发表于2018-11-30 19:24 被阅读0次

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

    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://img.haomeiwen.com/i1974361/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在链接函数后,替换函数后地址都会变

    相关文章

      网友评论

          本文标题:ios逆向 - mach-o文件分析

          本文链接:https://www.haomeiwen.com/subject/eoqkcqtx.html