探究Mach-O文件

4,590 阅读16分钟

较为详细的解析了Mach-O文件格式,并着重阐述了动态链接相关的知识点,开始吧~saonian~

简述

进程是可执行文件在内存中加载得到的结果,这种文件必须是操作系统理解的格式,这样操作系统才能解析文件,简历所需要的依赖(如库),初始化运行环境并执行。

Mach-O(Mach Object File Format)是macOS上的可执行文件,Linux和大部分Unix系统采用的是原生格式 ELF(Extensible Firmware Interface),windows支持的格式为PE32/PE32+macOS支持三种可执行文件格式:解释器脚本文件、通用二进制格式和Mach-O格式,如下图所示:

可执行格式 magic 用途
脚本 \x7FELF 主要用于 shell 脚本,但是也常用语其他解释器,如 Perl, AWK 等。也就是我们常见的脚本文件中在 #! 标记后的字符串,即为执行命令的指令方式,以文件的 stdin 来传递命令
通用二进制格式 0xcafebabe 0xbebafeca 包含多种架构支持的二进制格式,只在 macOS 上支持
Mach-O 0xfeedface(32 位) 0xfeedfacf(64 位) macOS 的原生二进制格式

通用二进制格式(Universal Binary)也称为“胖二进制格式(Fat Binary)”,主要是解决历史问题,以支持Power PC(PPC)架构以及Inter架构,是一种对多架构的二进制文件的打包集合。

其中常见的包括:可执行文件、动态库文件、动态链接器等都是Mach-O格式,具体可通过file命令查看具体的可执行文件格式,如下图:

Mach-O文件格式

其结构如下图所示,主要包括四部分组成:

img
  • Header头部

    描述了该文件的CPU类型、文件类型、加载命令等信息;

  • Load commands加载命令

    描述了文件中数据的具体组织结构,不同数据类型如何使用不同的加载命令表示;

  • Data数据段

    存放了包括代码、字符常量、类、方法等代码和数据,并且拥有多个Segment段,每个Segment段都包含零到多个Section节;

  • Loader info链接信息及其他

    文件末端包含了一系列链接信息,如动态链接器用来链接可执行文件或者依赖所需使用的符号表、字符串表等,以及签名信息等;

为何Segment段中存在Section节?

分段的目的主要:不同段可被映射到不同虚拟存储区域,便于读写权限管理;利用现代CPU缓存体系及程序的局部性原理,将指令和数据缓存分离有利用提升缓存命中率;指令或数据共享,有利于提升内存空间利用率。而分节主要是可以不完全按照page的大小进行内存对齐,提升内存空间利用率。

Header

Mach-O文件头部具体的数据结构如下(区分32位和64位架构):

//32bit
struct mach_header {
    uint32_t    magic;        /* mach magic number identifier */
    cpu_type_t    cputype;    /* cpu specifier */
    cpu_subtype_t    cpusubtype;    /* machine specifier */
    uint32_t    filetype;    /* type of file */
    uint32_t    ncmds;        /* number of load commands */
    uint32_t    sizeofcmds;    /* the size of all the load commands */
    uint32_t    flags;        /* flags */
};
//64bit
struct mach_header_64 {
    uint32_t    magic;        /* mach magic number identifier */
    cpu_type_t    cputype;    /* cpu specifier */
    cpu_subtype_t    cpusubtype;    /* machine specifier */
    uint32_t    filetype;    /* type of file */
    uint32_t    ncmds;        /* number of load commands */
    uint32_t    sizeofcmds;    /* the size of all the load commands */
    uint32_t    flags;        /* flags */
    uint32_t    reserved;    /* reserved */
};

32位和64位架构头部结构没有大的区别,只是64位多了一个保留字段,具体的字段名称如下:

  • magic:魔数,用于确认该文件是32位还是64位

  • cputype,CPU类型,如armx86_64

  • cpusubtype,CPU具体类型,如arm64armv7

  • filetype,文件类型,如可执行文件、库文件、动态链接器、符号文件和调试信息等,其中MH_EXECUTE代表可执行文件,具体的文件类型定义如下:

    /* Constants for the filetype field of the mach_header
     */
    #define    MH_OBJECT    0x1        /* relocatable object file */
    #define    MH_EXECUTE    0x2        /* demand paged executable file */
    #define    MH_FVMLIB    0x3        /* fixed VM shared library file */
    #define    MH_CORE        0x4        /* core file */
    #define    MH_PRELOAD    0x5        /* preloaded executable file */
    #define    MH_DYLIB    0x6        /* dynamically bound shared library */
    #define    MH_DYLINKER    0x7        /* dynamic link editor */
    #define    MH_BUNDLE    0x8        /* dynamically bound bundle file */
    #define    MH_DYLIB_STUB    0x9        /* shared library stub for static */
    #define    MH_DSYM        0xa        /* companion file with only debug */
    #define    MH_KEXT_BUNDLE    0xb        /* x86_64 kexts */
    
  • ncmd,加载命令条数

  • sizeofcmds,所有加载命令在文件中占用地址空间大小

  • reserved,保留字段

  • flags,标志位,具体的定义如下:

    #define    MH_NOUNDEFS    0x1        // 目前没有未定义的符号,不存在链接依赖
    #define    MH_DYLDLINK    0x4        // 该文件是dyld的输入文件,无法被再次静态链接
    #define    MH_PIE 0x200000        // 加载程序在随机的地址空间,只在 MH_EXECUTE中使用
    #define    MH_TWOLEVEL    0x80    // 两级名称空间
    

除了用MachOView能查看MachO文件信息,还可以通过otool命令查看,我们先来分析Header中的内容:otool -h xxx来查看。

Load commands

Load commands紧跟在头部之后(如下图),这些加载指令清晰地告诉加载器如何处理二进制数据,有些命令是由内核处理的,有些是由动态链接器处理的,常见的加载命令如下:

  • LC_SEGMENT/LC_SEGMENT_64: 将该段(32/64位)映射到进程地址空间中,包含了Segment中所有Section加载信息;

    其中_PAGEZERO段不具有访问权限,用来处理空指针,其值为0;TEXT为代码段,_DATA/_DATA_CONST为可读写的数据段;_LINKEDIT链接段包含了一些符号表、间接符号表、rebase操作码、绑定操作码、导出符号、函数启动信息、数据表、代码签名、字符串表等数据,该加载命令下没有Section,需要配合LC_SYMTAB来解析symbol tablestring table

    _LINKEDIT加载命令信息中的文件偏移为0x4000(十进制16384)正好对应Dynamic Loader Info起始地址,文件大小为0x5840(十进制22592)=0x9840(0x9830+10)-0x4000,正好对应从Dynamic Loader Info到文件末尾的数据部分;

  • LC_DYLD_INFO_ONLY:加载动态链接库信息(重定向地址、弱引用绑定、懒加载绑定、开放函数等的偏移值等信息)

  • LC_SYMTAB:载入符号表地址

  • LC_DYSYMTAB:载入动态符号表地址

  • LC_LOAD_DYLINKER:加载动态加载库

  • LC_UUID:确定文件的唯一标识,crash解析中也会有这个,去检测dysm文件和crash文件是否匹配

  • LC_VERSION_MIN_MACOSX/LC_VERSION_MIN_IPHONEOS:确定二进制文件要求的最低操作系统版本

  • LC_SOURCE_VERSION:构建该二进制文件使用的源代码版本

  • LC_MAIN:设置程序主线程的入口地址和栈大小

  • LC_ENCRYPTION_INFO_64:获取加密信息

  • LC_LOAD_DYLIB:加载额外的动态库

  • LC_FUNCTION_STARTS:定义一个函数起始地址表,使调试器和其他程序易于看到一个地址是否在函数内

  • LC_DATA_IN_CODE:定义在代码段内的非指令的表

  • LC_CODE_SIGNATURE:获取应用签名信息

具体的加载命令的数据结构如下(64位格式,与32位格式差别不大):

struct segment_command_64 { /* for 64-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT_64 */
    uint32_t    cmdsize;    /* includes sizeof section_64 structs */
    char        segname[16];    /* segment name */
    uint64_t    vmaddr;        /* memory address of this segment */
    uint64_t    vmsize;        /* memory size of this segment */
    uint64_t    fileoff;    /* file offset of this segment */
    uint64_t    filesize;    /* amount to map from the file */
    vm_prot_t    maxprot;    /* maximum VM protection */
    vm_prot_t    initprot;    /* initial VM protection */
    uint32_t    nsects;        /* number of sections in segment */
    uint32_t    flags;        /* flags */
};
  • cmd:就是Load commands的类型,这里LC_SEGMENT_64代表将文件中64位的段映射到进程的地址空间;
  • cmdsize:代表load command的大小
  • segment name:段的名称
  • VM Address :段的虚拟内存地址
  • VM Size : 段的虚拟内存大小
  • file offset:段在文件中偏移量
  • file size:段在文件中的大小
  • nsects:标示了Segment中有多少secetion

除了使用MachOView查看,还可以通过otool -l xxx查看,如下图所示:

对于段的地址大小可通过size -l -m xxx查看,如下图:

下面重点阐述几个重要的加载命令,便于后续理解整个程序启动、动态加载、逆向等知识点。

LC_SEGMENT_64(__PAGEZERO)

该加载命令的内容如下图所示:

其中虚拟地址范围为0x0~0x100000000正好对应4GB空间,该文件的起始虚拟地址空间也是从0x100000000开始,即所有代码和数据都是被加载到4GB之后的地址。对应的文件内容大小为0,即在该文件中不占用实际空间,且具有不可读写不可执行权限,这样内核就可以识别到空指针或指针截断的错误的范围该地址空间的调用而抛出段异常,如EXC_BAD_ACCESS异常。

LC_SEGMENT_64(__LINKEDIT) & LC_DYLD_INFO_ONLY

__LINKEDIT包含了动态链接相关的信息,如虚拟地址空间地址及文件偏移、文件权限等,而LC_DYLD_INFO_ONLY加载命令,包含了重定位、绑定及导出等偏移/大小信息。

LC_SYMTAB

对于LC_SYMTAB加载命令,其数据结构定义如下:

struct symtab_command {
    uint32_t cmd;     /* LC_SYMTAB */
    uint32_t cmdsize; /* sizeof(struct symtab_command) */
    uint32_t symoff;  /* symbol table offset */
    uint32_t nsyms;   /* number of symbol table entries */
    uint32_t stroff;  /* string table offset */
    uint32_t strsize; /* string table size in bytes */
};

这个命令告诉了链接器(包括静态链接器或动态链接器)Symbol TableString Table的位置及大小信息。

其中符号的结构由内核定义,如下:

struct nlist_64 {
    union {
        uint32_t n_strx;   /* index into the string table */
    } n_un;
    uint8_t  n_type;       /* type flag, see below */
    uint8_t  n_sect;       /* section number or NO_SECT */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};
  • n_un,符号的名字在字符串表中的序号(在一个 Mach-O 文件里,具有唯一性)
  • n_sect,符号所在的 section index(内部符号有效值从 1 开始,最大为 255;外部符号为0)
  • n_value,符号的地址值(在链接过程中,会随着其 section 发生变化)
  • n_type是一个 8 bit 的复合字段:
    • bit[5:8]: 如果不为 0,表示这是一个与调试有关的符号,值意义类型详见mach-o/stab.h
    • bit[4:5]: 若为 1,则表示该符号是私有的(外部符号)
    • bit[1:4]: 符号类型
      • N_UNDF (0x0): 未定义
      • N_ABS (0x2): 符号地址指向到绝对地址,链接器后期不会再修改
      • N_SECT (0xe): 本地符号,即符号定义于当前 Mach-O
      • N_PBUD (0xc): 预绑定符号
      • N_INDR (0xa): 表示该符号和另一个符号是同一个,n_value指向到 string table,即该同名符号的名字
    • bit[0:1]: 表示这是外部符号,即该符号要么定义在外部,要么定义在本地但是可以被外部使用;
LC_DYSYMTAB

对于LC_DYSYMTAB加载命令,其数据结构如下:

struct dysymtab_command {
    uint32_t cmd;	/* LC_DYSYMTAB */
    uint32_t cmdsize;	/* sizeof(struct dysymtab_command) */
    uint32_t ilocalsym;	/* index to local symbols */
    uint32_t nlocalsym;	/* number of local symbols */
    uint32_t iextdefsym;/* index to externally defined symbols */
    uint32_t nextdefsym;/* number of externally defined symbols */
    uint32_t iundefsym;	/* index to undefined symbols */
    uint32_t nundefsym;	/* number of undefined symbols */
    uint32_t tocoff;	/* file offset to table of contents */
    uint32_t ntoc;	/* number of entries in table of contents */
    uint32_t modtaboff;	/* file offset to module table */
    uint32_t nmodtab;	/* number of module table entries */
    uint32_t extrefsymoff;	/* offset to referenced symbol table */
    uint32_t nextrefsyms;	/* number of referenced symbol table entries */
    uint32_t indirectsymoff; /* file offset to the indirect symbol table */
    uint32_t nindirectsyms;  /* number of indirect symbol table entries */
    uint32_t extreloff;	/* offset to external relocation entries */
    uint32_t nextrel;	/* number of external relocation entries */
    uint32_t locreloff;	/* offset to local relocation entries */
    uint32_t nlocrel;	/* number of local relocation entries */
};

主要包含了本地、外部符号、未定义外部符号、间接符号表的位置及数目,其中indriectsymoff指定了Dynamic Symbol Table的文件偏移位置及数目;

可使用otool -I xxx来获取间接符号表内容;

其中间接符号包含了符号名、符号所处的节及符号间接地址,其所处的Section处在__stubs__got、及__la_symbol_ptr等节;

对于后续需要动态链接定位的符号头部,如LC_SEGMENT_64中的_TEXT.__stubs_DATA_CONST.__got_DATA.__la_symbol_ptr,其头部字段中包含了Indirect Sym Index(Reserverd1)字段,该字段指明在Indirect Symbol Table间接符号表中的条目序号,如下图:

_la_symbol_ptr中的符号在间接符号表中的起始条目序号为26。

LC_LOAD_DYLINKER

该加载命令包含了重要的程序启动动态链接器的路径,如下图x86_64的为/usr/lib/dyld

Segment & Section

Section的数据结构

struct section { /* for 32-bit architectures */
    char        sectname[16];    /* name of this section */
    char        segname[16];    /* segment this section goes in */
    uint32_t    addr;        /* memory address of this section */
    uint32_t    size;        /* size in bytes of this section */
    uint32_t    offset;        /* file offset of this section */
    uint32_t    align;        /* section alignment (power of 2) */
    uint32_t    reloff;        /* file offset of relocation entries */
    uint32_t    nreloc;        /* number of relocation entries */
    uint32_t    flags;        /* flags (section type and attributes)*/
    uint32_t    reserved1;    /* reserved (for offset or index) */
    uint32_t    reserved2;    /* reserved (for count or sizeof) */
};
  • sectname:比如_textstubs
  • segname :section所属的segment,比如_TEXT
  • addr :section在内存的起始位置
  • size:section的大小
  • offset:section的文件偏移
  • align :字节大小对齐
  • reloff :重定位入口的文件偏移
  • nreloc: 需要重定位的入口数量
  • flags:包含sectiontypeattributes

常见的 Section如下表所示:

Section 用途
_TEXT.__text 主程序代码
_TEXT.__cstring C 语言字符串
_TEXT.__const const 关键字修饰的常量
_TEXT.__stubs 用于 Stub 的占位代码,很多地方称之为桩代码,用于重定向到 lazynon-lazy 符号的 section,被标记为 S_SYMBOL_STUBSTEXT Segment 里代码和 dylib 外部符号的引用地址对函数符号的引用都指向了 stubs。其中每项都是 jmp 代码间接寻址,可跳到la_symbol_ptr Section 中。
_TEXT.__stubs_helper Stub 无法找到真正的符号地址后的最终指向
_TEXT.__objc_methname Objective-C 方法名称
_TEXT.__objc_methtype Objective-C 方法类型
_TEXT.__objc_classname Objective-C 类名称
_TEXT.__eh_frame 调试辅助信息
_TEXT.__unwind_info 用于存储处理异常情况信息
_DATA.__data 初始化过的可变数据
_DATA.__la_symbol_ptr lazy binding 的指针表,表中的指针一开始都指向 __stub_helper
_DATA.nl_symbol_ptr lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号
_DATA.__got 全局偏移表
_DATA.__const 没有初始化过的常量
_DATA.__cfstring 程序中使用的 Core Foundation 字符串(CFStringRefs
_DATA.__bss BSS,存放为初始化的全局变量,即常说的静态内存分配
_DATA.__common 没有初始化过的符号声明
_DATA.__mod_init_func 初始化函数,在main之前调用
_DATA.__mod_term_func 终止函数,在main返回之后调用
_DATA.__objc_classlist Objective-C 类列表
_DATA.__objc_protolist Objective-C 原型
_DATA.__objc_imginfo Objective-C 镜像信息
_DATA.__objc_selfrefs Objective-C self 引用
_DATA.__objc_protorefs Objective-C 原型引用
_DATA.__objc_superrefs Objective-C 超类引用
_DATA.__got

对于_DATA.__got节,其内容如下图所示:

其类似一个表,每个条目是一个地址值,定义的是Non-Lazy Symbol Pointers即非懒加载符号地址,所有条目的内容都是0。其引入的目的是解决程序在链接阶段存放不能确定目的地址的符号,当镜像被加载时,动态链接器dyld会对每个条目对应的符号进行重定位,将其真正的地址写入,作为条目的内容。对于dyld如何确定符号信息的,可以通过上面的Indirect Symbol Table中的符号看出,包含了符号名称、间接符号地址。

_DATA.__la_symbol_ptr

与之对应的是_DATA.__la_symbol_ptr节,其内容如下图所示:

其实际内容都指向了_TEXT.__stub_helper节,最终通过jumpq指令跳转到了dyld_stub_binder符号,即__got节中的Non_Lazy Symbol Pointer中的条目,该符号为一个函数,定义于dyld_stub_binder.S,由 dyld 提供。

dyld_stub_binder函数其大致逻辑是:内部会寻找锁调用符号的真实地址,并写入_la_symbol_ptr条目中,然后跳转到真实地址执行;

_TEXT.__stubs

对于_TEXT.__stubs节,其内容如下:

该内容也是一个表,每个条目都是一段数据,称为“符号桩”。通过otool -v xx -s _TEXT __stubs命令查看内容如下:

其内容都是jmpq跳转指令,跳转的地址以第一条地址为例计算:

0x100003000 = 0x100001dbc(rip) + 0x1244

该地址指向的是__la_symbol_ptr节,而该节最终都指向了dyld_stub_binder

Loader info

链接加载信息包含了动态加载信息Dynamic Loader Info(包含了重定向地址、弱引用绑定、懒加载绑定、开放函数等的偏移值等信息,其加载命令为LC_DYLD_INFO_ONLY),函数起始地址表Function Starts(其加载命令为LC_FUNCTION_STARTS),符号表Symbol Table,动态符号表Dynamic Symbol Table,代码段非指令表Data in Code Table,字符串表String Table(以空值为终止符)及代码签名Code Signature,如下图所示:

Dynamic Loader Info

由于地址空间随机化技术(ddress space layout randomization, ASLR)和地址无关可执行技术(position-indendent excutable, PIE),使得程序在内存的加载地址是随机的,因此需要程序在动态链接阶段将内部地址进行修正。Rebase 数据描述了哪些是对指向 MachO 内部的引用并将其修正,而 Bind 数据描述哪些是指向外部的引用并进行修正。Lazy Bind 数据描述了哪些符号需要延迟绑定,即仅在第一次使用时才会绑定,不会在启动时进行,提高启动效率;Export数据描述了对外可见的符号。其内容都是以操作数(Opcodes)、立即数(immediate)以及采用uleb128/sleb128编码的偏移值组成。

PIE(position-independent executable)是一种生成地址无关可执行程序的技术。如果编译器在生成可执行程序的过程中使用了PIE,那么当可执行程序被加载到内存中时其加载地址存在不可预知性。PIE还有个孪生兄弟PIC(position-independent code)。其作用和PIE相同,都是使被编译后的程序能够随机的加载到某个内存地址。区别在于PIC是在生成动态链接库时使用(Linux中的so),PIE是在生成可执行文件时使用。

Rebase举例,其协议和操作就是找到地址后将其值加上偏移即可,具体的获取操作数和立即数是通过REBASE_OPCODE_MASK(0xF0)REBASE_IMMEDIATE_MASK(0x0F)对数据进行与&操作,如0x100004000的数据字节0x11,其操作数为0x10=0x11&0xF0对应的是REBASE_OPCODE_SET_TYPE_IMM,立即数0x01=0x11&0x0Ftype=1(REBASE_TYPE_POINTER),具体的操作数及立即数对应的逻辑可查阅dyld源码。

注意:MachOView中标注的Actions存在误导性,重定位、绑定等操作都是按照字节数据顺序读取并操作直至完整的读取完所有的数据,其标注具体原因未知,待确认补充!

Dynamic Symbol Table

对于Dynamic Symbol Table中的Indirect Symbols其内容为一个表,每个条目的内容为其在Symbol Table中的序号,如下图:

其内容为0x3c=60,对应的就是符号表第60个符号,通过符号表中的起始地址0x4380,每个符号占用0x10,则0x4740=0x4380+0x10*0x3c,对应的就是_CFRunLoopAddSource符号地址。

String Table

对于字符串表String Table中内容为所有的符号名称,每个名称中间通过空字符串间隔,如下图所示:

string table

Symbol Table中的String Table Index字段就是字符串表中对应的第index个字符串。

Reference

趣探 Mach-O:文件格式分析

MachO 文件结构详解

Mach-O 文件格式探索

深入剖析Macho (1)

深入理解Macho文件(二)- 消失的__OBJC段与新生的_DATA段

《深入理解Mac OSX & iOS操作系统》

loader.h

Mach-O 与动态链接

Mach-O 与静态链接

Apple 操作系统可执行文件 Mach-O

iOS 应用的启动过程

APP漏洞扫描器之未使用地址空间随机化