iOS逆向06 -- Mach-O

Mach-O文件
  • Mach-O是Mach Object文件格式的缩写,是mac以及iOS上可执行文件的格式,例如当Xcode App工程编译完成之后就会生成一个可执行文件,其格式就是Mach-O文件;
Mach-O的相关名词
  • Executable 可执行文件;
  • Dylib 动态库;
  • Bundle 无法被连接的动态库,只能通过dlopen()加载;
  • Image 指的是Executable,Dylib或者Bundle的一种,文中会多次使用Image这个名词;
  • Framework 动态库(可以是静态库)和对应的头文件和资源文件的集合;
Mach-O文件的常见类型
#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 */
                    /*  linking only, no section contents */
#define MH_DSYM     0xa     /* companion file with only debug */
                    /*  sections */
#define MH_KEXT_BUNDLE  0xb     /* x86_64 kexts */
  • MH_OBJECT:目标文件即 .o 文件 以及静态库文件即 .a 文件(多个.o文件合并在一起);
  • MH_EXECUTE:可执行文件,即App编译运行后生成的可执行文件,在/Products路径下;
  • MH_DYLIB:动态库文件,即.dylib文件 或者 .framework文件;
  • MH_DYLINKER:/usr/lib/dyld路径下的dyld文件;
  • MH_DSYM:Xcode打包后生成的符号表文件,即.dSYM文件;

查看文件的格式类型

  • 使用命令行 file 文件名

  • 查看自定义的目标.o文件

    • 终端输入:file YYPerson.o
    • 终端输出:YYPerson.o: Mach-O 64-bit object x86_64
  • 查看Xcode编译运行后生成的可执行文件

    • 终端输入:file SuningWeiDian
    • 终端输出:Mach-O 64-bit executable x86_64
  • 终端cd /usr/lib 然后 ls 列出所有lib文件;然后查看ACIPCBTLib.dylib文件

    • 终端输入:file ACIPCBTLib.dylib
    • 终端输出:ACIPCBTLib.dylib: Mach-O 64-bit dynamically linked shared library x86_64
  • 同上 查看 dyld文件

    • 终端输入:file dyld
    • 终端输出:dyld: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386]
      dyld (for architecture x86_64): Mach-O 64-bit dynamic linker x86_64
      dyld (for architecture i386):Mach-O dynamic linker i386
    • 可以看出 dyld 这个文件有点特殊;能同时支持x86_64与i386两种架构;
  • 查看打包之后生成的dSYM文件

    • 终端输入:file SuningWeiDian
    • 终端输出:Mach-O universal binary with 2 architectures: [arm_v7:Mach-O dSYM companion file arm_v7] [arm64] SuningWeiDian (for architecture armv7): Mach-O dSYM companion file arm_v7 SuningWeiDian (for architecture arm64): Mach-O 64-bit dSYM companion file arm64

通用二进制文件(Universal binary)

  • 在iOS中不同手机对应着可能不同的架构,如arm64、armv7、armv7s,为了兼容不同架构的手机,苹果推出了通用二进制文件,其能同时支持多个不同架构,因此通用二进制文件,比单一架构二进制文件要大很多,因此也称之为胖二进制文件
  • 当一个文件同时支持多个架构平台,比如同时支持 ARMV7、ARM64,就相当是两个 Mach-O 文件,编译器会编译两个Mach-O文件,然后合成一个Fat文件;
  • 例如上面的dSYM文件,就是通用二进制文件,支持两种架构;
  • 在Xcode工程中有配置支持不同架构的选项,如下图所示:
Snip20210616_14.png

Mach-O文件的基本结构

  • 先上一个官方截图,如下所示:
Snip20210119_19.png
  • 可以看出Mach-O文件主要包含三个部分:
    • Header:包含Mach-O文件的基本信息,例如文件类型,支持的CPU架构类型,加载指令的数量,所占内存大小等等;
    • Load Commands:不同数据段segment的加载命令,指导加载器加载数据;
    • Data:指数据段Segment,其有不同的Section组成;

otool工具

  • otool是Mac系统自带的,可以查看Mach-O文件特定部分和段的内容的工具;
  • 下面使用的资源是自己本地工程Mach-O文件结构生成一个Mach-O文件结构.app文件,其包内容中有一个Mach-O目标文件:Mach-O文件结构,下面利用otool的常见命令行操作Mach-O文件结构
  • otool -h Mach-O文件结构获取Mach-O文件的Header头信息,输出结果如下:
Mach header
magic     cputype    cpusubtype  caps   filetype  ncmds sizeofcmds  flags
0xfeedfacf 16777223   3           0x00      2       75    8304       0x00218085
  • otool -L Mach-O文件结构查看Mach-O文件所使用的动态库,会打印出App中所有的动态库如下所示:
Snip20210111_37.png
  • objdump --macho --private-headers Mach-O文件结构,输出结果如下:
image.png

MachOView图形化界面工具

  • otool是通过命令行来查看Mach-O文件的结构,但是不够直观,而MachOView是一款图形化的查看Mach-O文件结构的工具软件,更加直观;
  • 点击这里 进行下载;
  • Mach-O文件结构这个Mach-O文件直接拖入MachOView中,如下所示:
    image.png

Mach-O文件三部分的详细分析

  • 源码查看 在Xcode中按下Command+Shift+O 然后输入loader.h 可以定位到系统关于Mach-O文件的定义;
第一部分:Mach_Header
  • 定义如下所示:
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 */
};
  • magic:提供给系统内核,用来判断文件是否是Mach-O的文件格式;
  • cputype:表示支持的CPU类型,一般有armv7, armv64, x86, x86_64 这几种类型;
  • filetype:表示Mach-O的具体文件类型,如果是可执行文件就是 MH_EXECUTE,如果是动态库就是 MH_DYLIB,详情见文章顶部;
  • ncmds:表示Mach-O文件中所有Load Commands(加载命令)的总个数;
  • sizeofcmds:表示Load Commands所有(加载命令)占用的字节总大小;
  • flags:表示文件的标志信息;
第二部分:Load Commands
  • Load Commands紧跟在Mach_Header之后,这些加载指令告诉loader加载器如何加载二进制数据,本质就是确定如何加载段segment数据,其定义如下所示:
struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};
  • 属性cmd:表示Load Commands(加载命令)的类型;
  • 属性cmdsize:表示当前的加载命令所占内存大小;
  • 使用MachOView工具查看Mach-O文件的Load Commands部分可以看到:
image.png
  • 常见的加载命令的简介如下所示:
    • LC_SEGMENT_64:将该段(64位)映射到进程地址空间中;
    • LC_DYLD_INF0_0NLY:加载动态链接库信息(重定向地址、弱引用绑定、懒加载绑定、开放函数等的偏移值等信息);
    • LC_SYMTAB:加载符号表;
    • LC_DYSYMTAB:加载动态符号表;
    • LC_LOAD_DYLINKER:加载动态加载库,可以看出示例使用的是/usr/lib/dyld;
    • LC.UUID: 确定文件的唯一标识,crash解析中也会有这个,去检测dysm文件crash文件是否匹配;
    • LC_VERSION_MIN_IPHONEOS:确定二进制文件要求的最低操作系统版本;
    • LC.SOURCE.VERSION:构建二进制文件的源代码版本号;
    • LC.MAIN:主程序的入口,dyld获取该地址,然后跳转到该处执行;
    • LC_ENCRYPTION_INFO_64:加载加密信息;
    • LC_LOADJDYLIB:加载额外的动态库;
    • LC_FUNCTION_STARTS:定义一个函数起始地址表,使调试器和其他程序易于看到一个地址是否在函数内;
    • LC_DATA_IN_CODE:定义在代码段内的非指令的表;
    • LC_CODE_SIGNATURE:获取应用签名信息;
  • 下面以加载指令LC_SEGMENT_64为例,此加载指令的结构如下所示:
image.png
  • LC_SEGMENT_64此加载指令属于segment段加载指令,其结构体源码如下所示:
struct segment_command { /* for 32-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT */
    uint32_t    cmdsize;    /* includes sizeof section structs */
    char        segname[16];    /* segment name */
    uint32_t    vmaddr;     /* memory address of this segment */
    uint32_t    vmsize;     /* memory size of this segment */
    uint32_t    fileoff;    /* file offset of this segment */
    uint32_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:加载命令的类型;
  • cmdsize:加载命令的所占内存大小;
  • segname: 加载目标段Segment的名称,常见的段segment有 __PAGEZERO__LINKEDIT__TEXT__DATA
    • __PAGEZERO 在可执行文件有的,动态库里没有,这个段开始地址为0(NULL指针指向的位置),是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常;
    • __TEXT代码段,里面主要是存放代码的,该段是可读可执行,但是不可写;
    • __DATA数据段,里面主要是存放数据,该段是可读可写,但不可执行;
    • __LINKEDIT:用于存放签名信息,该段是只可读,不可写不可执行;
  • 段Segment类型的截图如下:
image.png
  • vmaddr:段Segment的虚拟内存地址;
  • vmsize:段Segment的虚拟内存大小;
  • fileoff:段Segment的在文件中的偏移量;
  • filesize:段Segment在文件中所占的内存大小;
  • nsects:段Segment包含节区sections的数量;
  • maxprot:表示页面所需要的最高内存保护;
  • initprot:表示页面初始的内存保护;
  • flags:表示段的标志信息;

第三部分:Data数据部分

  • Data数据部分,就是指段Segment的数据,而Segment段是由多个Section组成的,所以其主体部分为Section,而Section的头部信息Section Header是存放在段的加载命令中,其结构如下所示:
image.png
  • 首先来介绍一下Section Header,当一个段segmemt包含多个节区Section,节区头Section Header会以数组的形式存储在段加载命令中,如上截图所示,毋庸置疑其是描述Section的结构信息的;
  • Section的源码如下所示:
struct section_64 { /* for 64-bit architectures */
    char        sectname[16];   /* name of this section */
    char        segname[16];    /* segment this section goes in */
    uint64_t    addr;       /* memory address of this section */
    uint64_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) */
    uint32_t    reserved3;  /* reserved */
};
  • sectname:section的名称,常见的section有_text、stubs等等;
  • segname :当前section所隶属的Segment,例如__TEXT(代码段);
  • addr : section在内存的起始位置;
  • size: section所占内存大小;
  • offset: section在文件中的偏移量;
  • align:字节大小对齐,2的align次方;
  • reloff:重定位入口的文件偏移;
  • nreloc: 需要重定位的入口数量;
  • flags:包含section的type和attributes;
__TEXT段中的Section组成如下所示:
  • __text:代码节,存放机器编译后的代码;
  • __stubs:用于辅助做动态链接代码(dyld);
  • __stub_helper:用于辅助做动态链接(dyld);
  • __objc_methname:objc的方法名称;
  • __cstring:代码运行中包含的字符串常量,比如代码中定义#define kGeTuiPushAESKey "DWE2#@e2!",那DWE2#@e2!会存在这个区里;
  • __objc_classname: objc类名;
  • __objc_methtype: objc方法类型;
  • __ustring
  • __gcc_except_tab
  • __const:存储const修饰的常量;
  • __dof_RACSignal
  • __dof_RACCompou
  • __unwind_info
__DATA段中的Section组成如下所示:
  • __got:存储引用符号的实际地址,类似于动态符号表;
  • __la_symbol_ptr:lazy symbol pointers,懒加载的函数指针地址,和__stubs和stub_helper配合使用,具体原理暂留;
  • __mod_init_func:模块初始化的方法;
  • __const:存储constant常量的数据,比如使用extern导出的const修饰的常量;
  • __cfstring:使用Core Foundation字符串;
  • __objc_classlist:objc类列表,保存类信息,映射了__objc_data的地址;
  • __objc_nlclslist:Objective-C 的 +load 函数列表,比 __mod_init_func 更早执行;
  • __objc_catlist:categories分类;
  • __objc_nlcatlist:Objective-C 的categories的 +load函数列表;
  • __objc_protolist:objc协议列表;
  • __objc_imageinfo:objc镜像信息;
  • __objc_const:objc常量,保存objc_classdata结构体数据,用于映射类相关数据的地址,比如类名,方法名等;
  • __objc_selrefs:引用到的objc方法;
  • __objc_protorefs:引用到的objc协议;
  • __objc_classrefs:引用到的objc类;
  • __objc_superrefs:objc超类引用;
  • __objc_ivar:objc ivar指针,存储属性;
  • __objc_data:objc的数据,用于保存类需要的数据,最主要的内容是映射,__objc_const地址,用于找到类的相关数据;
  • __data:暂时没理解,从日志看存放了协议和一些固定了地址已经初始化的静态量;
  • __bss:存储未初始化的静态量,比如:static NSThread *_networkRequestThread = nil,其中这里面的size表示应用运行占用的内存,不是实际的占用空间,所以计算大小的时候应该去掉这部分数据;
  • __common:存储导出的全局的数据,类似于static,但是没有用static修饰,比如KSCrash里面NSDictionary* g_registerOrders,g_registerOrders就存储在__common里面;
image.png
Mach-O文件的结构分析
  • 首先创建两个.c文件分别为a.cb.c,代码如下:
//a.c文件
#include <stdio.h>

//显式的说明了a的存储空间是在程序的其他地方分配的,在文件中其他位置或者其他文件中寻找a这个变量
extern int global_var;

void func(int a);

int main(int argc, const char * argv[]) {
    int a = 100;
    func(a + global_var);
    return 0;
}
//b.c文件
#include <stdio.h>

int global_var = 1;

void func(int a) {
    global_var = a;
}
  • 在进行代码分析之前,首先介绍两个概念模块符号

  • 模块:我们可以理解一个源代码文件为一个模块。比如上面a模块和b模块。我们现在写一个程序,不可能所有代码都在一个源代码文件上,都是分模块的,一般一个类在一个源文件上,就成为一个模块,模块化好处就是复用、维护,还有编译时候,未改动的模块,不用重新编译,直接用之前编译好的缓存;

  • 符号:简单理解就是函数名和变量名,比如上面总共有三个符号:global_varmainfunc

  • 将a.c与b.c文件分别编译生成目标文件a.ob.o文件,可通过终端命令来实现,输入:xcrun -sdk iphoneos clang -c a.c b.c -target arm64-apple-ios12.2

  • 将生成的a.o与b.o目标文件,进行静态链接,生成一个最终的目标文件,命名为ab,可通过终端命令来实现,输入:xcrun -sdk iphoneos clang a.o b.o -o ab -target arm64-apple-ios12.2,最终的所有文件如下所示:

    image.png

  • 可使用file 文件名,查看文件类型,如下所示:

    image.png

  • a.ob.oab均属于Mach-O文件,可使用MachOView打开进行查看;

  • 使用MachOView打开a.o文件,内容如下所示:

    Snip20220104_121.png

  • adrp x10 #0:其中#0是全局变量global_var的临时内存地址,是编译器暂时用#0代替的;

  • ldr w11 [x10]:将x10寄存器中的内存地址中的数值,也就是全局变量global_var,写入w11寄存器中;

  • add w0 w9 w11:将w9与w11中的数值相加,即100+1计算结果赋值给w0,w0寄存器中存储着func函数的参数;

  • bl #0x3c:其中#0x3c是func函数的临时地址,是编译器暂时用#0x3c代替的;

  • 使用MachOView打开ab文件,内容如下所示:

    Snip20220104_118.png

  • a.ob.o在经过链接器进行静态链接之后,生成ab文件,在ab文件中的全局变量global_var与函数func的内存地址是真正的内存地址,那么链接器是怎么进行调整的;

  • 全局变量global_var与函数func的内存地址从a.oab经过了链接器的静态链接,这两个符号的内存地址在前后发生了变化,现在我们来探索其中的工作原理;

  • 首先在a.o文件中包含了一个重定位表,其专门保存了所有需要进行重定位的符号,根据符号信息可以在当前文件的符号表中查看符号的详细信息;

  • 在进行a.ob.o文件链接时,会将a.o里面有这两符号的引用,然后b.o里面有这两符号的定义,一起合并到全局符号表里,最后在全局符号表中,对符号进行重定位,修正符号的正确地址;

    Snip20220104_130.png

推荐阅读更多精彩内容