iOS逆向之Mach-O文件

2022-09-02 10:09:45 浏览数 (1)

前言

阅读笔者的其他文章,我们了解了编译过程中的预处理、词法分析、语法分析、编译、链接等步骤。经常和编译型语言打交道的开发者对于可执行文件的编译过程肯定不陌生。我们用 Xcode 构建一个程序的过程中,会把源文件 (.m 和 .h) 文件转换为一个可执行文件。这个可执行文件中包含的字节码将会被 CPU (iOS 设备中的 ARM 处理器或 Mac 上的 Intel 处理器) 执行。

但可执行文件和Mach-O文件有什么关系呢?其实可执行文件属于Mach-O文件类型,换句话说,可执行文件是一种Mach-O文件。所以,了解Mach-O文件的同时,我们也就了解了可执行文件。这篇文章将系统的介绍Mach-O的文件格式、内部结构,揭开iOS中可执行文件的面纱。希望本文对您有所帮助,表述有误和有待补充的地方还请大家不吝赐教。

Mach-O简介

Mach-O是Mach object的缩写(来源于官方文档)。Mach-O是iOS/macOS系统上应用程序、库的标准文件格式,它并不像Windows平台中的PE文件那样复杂。了解Mach-O文件格式,对于静态分析动态分析、动态调试、自动化测试及安全都有重要意义。

OS X支持多种应用程序环境,每种环境都有自己的运行时规则、约定和文件格式。在OS X中,内核扩展、命令行工具、应用程序、框架和库(共享的和静态的)都是使用Mach-O(Mach object)文件实现的。

综上,Mach-O文件是一种文件格式、一种文件标准。符合Mach-O标准格式的文件都是Mach-O文件。所以Mach-O文件并不止一种,常见的Mach-O文件包括程序的可执行文件、.o目标文件、.a静态库文件、动态库文件(.dylib、.framework)、dSYM文件、dyld

我们iOS/macOS工程中的C、C 、OC、Swift代码,最终编译生成的可执行文件都是Mach-O格式。

如何验证可执行文件是Mach-O文件?

如下,TRIP是笔者构建的一个iOS的可执行文件,使用MacOS系统自带的file命令可以查看其文件类型。通过命令行输出的信息,不难发现,可执行文件是Mach-O文件,且其是64位的文件,所以只能运行在arm64的CPU架构。

查看可执行文件格式查看可执行文件格式

另外,您也可以使用file命令查看.o目标文件、.a静态库文件、.dylib、.framework,看一下输出的是什么类型?

Mach-O文件格式

Mach-O文件由一下3部分数据区域组成:

  • Header(Mach-O头部)
  • Load Commands(加载命令)
  • Raw segment data(下图中的Data数据块)
Mach-O文件构成Mach-O文件构成

以下截图的文件定义内容都可以在 xnu源码 的 loader.h中找到。

Header

Mach-O的header指定文件类型和文件目标体系结构,如ARM64、PPC、PPC64、IA-32或x86-64。用于校验Mach-O文件的合法性即确定文件的运行环境。我们可以使用 otool 的-h参数分析header的数据。下图是mach-o/loader.h中对Mach-O的header的定义。32bit 和 64bit 架构的CPU分别使用mach_header 与 mach_header_64 结构体来描述 Mach-O 头部。64bit 的结构只比 32bit 的结构多了一个reserved字段。下面以 64bit 的 mach_header_64 结构体为例介绍各个字段的定义:

  • magic:无符号32位整型数。被称为魔数,用于表示当前CPU是大端模式还是小端模式,iOS都是小端模式。加载器通过这个魔数来判断当前可执行文件的类型,即:32位还是64位。
    • 64bit 架构下,magic被定义为常量 #define MH_MAGIC_64 0xfeedfacf
    • 32bit 架构下,magic被定义为常量 #define MH_MAGIC 0xfeedface
  • cputype:cpu_type_t类型。标识CPU的架构,如ARM、ARM64、MIPS、X86_84等。iOS平台下,cputype通常如下:
    • #define CPU_TYPE_ARM ((cpu_type_t) 12)
    • #define CPU_TYPE_ARM64 ((cpu_type_t) (CPU_TYPE_ARM | CPU_ARCH_ABI64))
  • cpusubtype:cpu_subtype_t类型。表示具体的CPU类型,区分不同版本的处理器。iOS平台下,cpusubtype通常如下:
    • #define CPU_SUBTYPE_ARM_V7 ((cpu_subtype_t) 9)
    • #define CPU_SUBTYPE_ARM64_ALL ((cpu_subtype_t) 0)
  • filetype:标识Mach-O文件的类型,如可执行文件、符号文件、内核扩展文件、动态库文件等。iOS逆向中接触最多的就是可执行文件(MH_EXECUTE)和动态库文件(MH_DYLIB)。
  • ncmds:标识Mach-O文件中加载命令(Load Commands)的数量。
  • sizeofcmds:标识Mach-O文件加载命令所占用的总字节大小。
  • flags:标识Mach-O文件的一些标志信息。
    • 取值为 #define MH_PIE 0x200000 时说明启动ASLR(Address Space Layout Radomization,地址空间布局随机化)来增加程序的安全性。
    • #define MH_PIE 0x200000 仅被用于MH_EXECUTE,即可执行文件
  • reserved:系统保留字段,仅在mach_header_64结构中存在

当然,您也可以使用MachOView可视化工具来查看Mach-O的header。

Mach-O文件的Header定义Mach-O文件的Header定义

Load Commands

Load Commands译作加载命令。用于指定文件的逻辑结构和文件在虚拟内存中的布局(官方文档)。加载命令紧跟在 Mach-O的header之后,明确的告诉加载器如何处理二进制文件,有些命令是由内核处理的,有些是由动态链接器(dyld,用于加载动态库)处理的。load commands的结构体定义如下图,其定义在mach-o/loader.h中。

此结构只有2个字段:

  • cmd:表示当前加载命令的类型
  • cmdsize:表示当前加载命令的大小

根据不同类型的加载命令类型,内核会使用不同的函数来解析。

Mach-O的Load Command定义Mach-O的Load Command定义

macOS系统在进化的过程中,加载命令算是比较频繁被更新的一个数据结构体,截止到macOS 10.15系统,加载命令的类型cmd的取值共有53种。它们的定义如下。

代码语言:txt复制
/* Constants for the cmd field of all load commands, the type */
#define	LC_SEGMENT	0x1	/* segment of this file to be mapped */
#define	LC_SYMTAB	0x2	/* link-edit stab symbol table info */
#define	LC_SYMSEG	0x3	/* link-edit gdb symbol table info (obsolete) */
#define	LC_THREAD	0x4	/* thread */
#define	LC_UNIXTHREAD	0x5	/* unix thread (includes a stack) */
#define	LC_LOADFVMLIB	0x6	/* load a specified fixed VM shared library */
#define	LC_IDFVMLIB	0x7	/* fixed VM shared library identification */
#define	LC_IDENT	0x8	/* object identification info (obsolete) */
#define LC_FVMFILE	0x9	/* fixed VM file inclusion (internal use) */
#define LC_PREPAGE      0xa     /* prepage command (internal use) */
#define	LC_DYSYMTAB	0xb	/* dynamic link-edit symbol table info */
#define	LC_LOAD_DYLIB	0xc	/* load a dynamically linked shared library */
#define	LC_ID_DYLIB	0xd	/* dynamically linked shared lib ident */
#define LC_LOAD_DYLINKER 0xe	/* load a dynamic linker */
#define LC_ID_DYLINKER	0xf	/* dynamic linker identification */
#define	LC_PREBOUND_DYLIB 0x10	/* modules prebound for a dynamically */
				/*  linked shared library */
#define	LC_ROUTINES	0x11	/* image routines */
#define	LC_SUB_FRAMEWORK 0x12	/* sub framework */
#define	LC_SUB_UMBRELLA 0x13	/* sub umbrella */
#define	LC_SUB_CLIENT	0x14	/* sub client */
#define	LC_SUB_LIBRARY  0x15	/* sub library */
#define	LC_TWOLEVEL_HINTS 0x16	/* two-level namespace lookup hints */
#define	LC_PREBIND_CKSUM  0x17	/* prebind checksum */

/*
 * load a dynamically linked shared library that is allowed to be missing
 * (all symbols are weak imported).
 */
#define	LC_LOAD_WEAK_DYLIB (0x18 | LC_REQ_DYLD)

#define	LC_SEGMENT_64	0x19	/* 64-bit segment of this file to be
				   mapped */
#define	LC_ROUTINES_64	0x1a	/* 64-bit image routines */
#define LC_UUID		0x1b	/* the uuid */
#define LC_RPATH       (0x1c | LC_REQ_DYLD)    /* runpath additions */
#define LC_CODE_SIGNATURE 0x1d	/* local of code signature */
#define LC_SEGMENT_SPLIT_INFO 0x1e /* local of info to split segments */
#define LC_REEXPORT_DYLIB (0x1f | LC_REQ_DYLD) /* load and re-export dylib */
#define	LC_LAZY_LOAD_DYLIB 0x20	/* delay load of dylib until first use */
#define	LC_ENCRYPTION_INFO 0x21	/* encrypted segment information */
#define	LC_DYLD_INFO 	0x22	/* compressed dyld information */
#define	LC_DYLD_INFO_ONLY (0x22|LC_REQ_DYLD)	/* compressed dyld information only */
#define	LC_LOAD_UPWARD_DYLIB (0x23 | LC_REQ_DYLD) /* load upward dylib */
#define LC_VERSION_MIN_MACOSX 0x24   /* build for MacOSX min OS version */
#define LC_VERSION_MIN_IPHONEOS 0x25 /* build for iPhoneOS min OS version */
#define LC_FUNCTION_STARTS 0x26 /* compressed table of function start addresses */
#define LC_DYLD_ENVIRONMENT 0x27 /* string for dyld to treat
				    like environment variable */
#define LC_MAIN (0x28|LC_REQ_DYLD) /* replacement for LC_UNIXTHREAD */
#define LC_DATA_IN_CODE 0x29 /* table of non-instructions in __text */
#define LC_SOURCE_VERSION 0x2A /* source version used to build binary */
#define LC_DYLIB_CODE_SIGN_DRS 0x2B /* Code signing DRs copied from linked dylibs */
#define	LC_ENCRYPTION_INFO_64 0x2C /* 64-bit encrypted segment information */
#define LC_LINKER_OPTION 0x2D /* linker options in MH_OBJECT files */
#define LC_LINKER_OPTIMIZATION_HINT 0x2E /* optimization hints in MH_OBJECT files */
#define LC_VERSION_MIN_TVOS 0x2F /* build for AppleTV min OS version */
#define LC_VERSION_MIN_WATCHOS 0x30 /* build for Watch min OS version */
#define LC_NOTE 0x31 /* arbitrary data included within a Mach-O file */
#define LC_BUILD_VERSION 0x32 /* build for platform min OS version */
#define LC_DYLD_EXPORTS_TRIE (0x33 | LC_REQ_DYLD) /* used with linkedit_data_command, payload is trie */
#define LC_DYLD_CHAINED_FIXUPS (0x34 | LC_REQ_DYLD) /* used with linkedit_data_command */
#define LC_FILESET_ENTRY      (0x35 | LC_REQ_DYLD) /* used with fileset_entry_command */

我们已经说了,?所有的这些加载命令由系统内核加载器直接使用,或由动态链接器处理。其中几个常见的重要加载命令有LC_SEGMENT/LCSEGMENT_64、LC_LOAD_DYLIB、LC_CODE_SIGNATURE、LC_LOAD_DYLINKER、LC_MAIN、LC_ENCRYPTION_INFO等。

LC_SEGMENT/LC_SEGMENT_64

LC_SEGMENT和LC_SEGMENT_64是段加载命令,段(segment)中又包含节(section)。每个段都定义了一个虚拟内存区域。动态链接器dyld负责把这个区域映射到进程地址空间。LC_SEGMENT和LC_SEGMENT_64分别使用segment_commend与segment_commend_64表示。他们的字段名都一样,不同的是每个字段所占用的bit长度。

  • cmd 当前加载命令的类型,也就是上面列举的那些加载命令
  • cmdsize 当前加载命令的大小
  • segname16 段(segment)名称,占16字节
  • vmaddr 段的虚拟内存地址
  • vmsize 段的虚拟内存大小
  • fileoff 段在文件中的偏移量
  • filesize 段在文件中的大小
  • maxprot 段的最高内存保护(r、w、x) prot是protection的缩写
  • initprot 段的初始内存保护(r、w、x)prot含义同上
  • nsects 段中节(section)的数量。一个段可以包含0个或多个节
  • flags 段的标志信息

从下图的注释中可以看出:段加载命令指定了文件的哪部分映射到进程地址空间。段加载命令在内存中的大小(即vmsize 虚拟内存大小)可能≥其在文件中的大小(filesize)。系统从fileoff处加载filesize大小的内容到虚拟内存的vmaddr处,大小为vmsize。如果需要的话,段剩余的虚拟内存部分将会被被初始化填充为0。段的最大权限和初始权限分别由maxprot和initprot指定。initprot指定的权限可以被修改,但是不能超过maxprot指定的权限。例如,逆向时关注的代码段(__TEXT)的初始化和最高内存权限都是可读(r)、可执行(x)、不可写(w),这就是未越狱状态下不能inline hook的原因。

Load Command中的segment commandLoad Command中的segment command

关于段名称(segname16),通常使用下划线和大写字母表示(比如代码段__TEXT),通常看到的段加载命令有:__PAGEZERO、__TEXT、__DATA、__LINKEDIT,如下图:

LC_SEGMENT_64LC_SEGMENT_64
  • __PAGEZERO:静态链接器创建了__PAGEZERO作为可执行文件的第一个段,该段在虚拟内存的位置和大小都为0,不能读、不能写、不能执行,只能用来处理空指针。我们把指针指向空就是指向这里,程序访问空指针时,会得到一个EXC_BAD_ACCESS错误。
  • __TEXT:包含了可执行的代码和其他一些只读的数据(比如const 常量)。静态链接器设置该段的虚拟内存权限为可读、可执行。所以系统允许进程执行这些代码,但是不可修改代码。
  • __DATA:包含了可以被修改的数据(比如全局静态变量)。静态链接器设置该段的虚拟内存权限为可读、可写。所以,位于这个段的数据可以被修改。
  • __LINKEDIT:包含了动态链接库的原始数据,如符号、字符串、重定位表条目等。

我们上面说过了,一个段(segment)可以包含0个或多个节(section)。节则包含load Command中定义的段的原始数据。下图是32bit和64bit的节的定义:

  • sectname16 当前节的名称,长度为16字节
  • segname16 当前节所在段的名称,长度为16字节,和段中定义的segname16是一样的
  • addr 节在内存中的地址
  • size 节所占用的字节大小
  • offset 节在文件中的偏移量
  • align 节的字节对齐大小(2的幂运算,比如2的4次幂)
  • reloff 重定位入口的文件偏移量
  • nreloc 需要重定位的入口数量
  • flags 节的类型和属性
  • reserved1/2/3 系统保留字段
Data中的Section的定义Data中的Section的定义

段和节的命名规则是:段的名称全部大写(比如__TEXT),而节的名称是小写(比如__text)。

__TEXT段

__TEXT段(segment)所包含的节(section)__TEXT段(segment)所包含的节(section)

本文章仅介绍常见的几个节:

  • __text:主程序代码
  • __stubs、__stubs_helper:帮助动态连接器dyld绑定符号
  • __cstring:只读的C语言字符串
  • __objc_methname:OC方法名
  • __objc_classname:OC类名
  • __objc_methtype:OC方法类型(方法签名)
  • __const:const关键字修饰的只读常量

__DATA段

__DATA段(segment)所包含的节(section)__DATA段(segment)所包含的节(section)
  • __got:全局非懒绑定符号指针表
  • __la_symbol__ptr:懒绑定符号指针表
  • __mod_init_func:C 类的构造函数
  • __const:未初始化过的常量
  • __cfstring:Core Foundation字符串
  • __objc_classlist:OC类列表
  • __objc_nlcatlist:实现了 (void)load方法的Objctive_C类列表
  • __objc_catlist:OC分类(Category)列表
  • __objc_protlist:OC协议(Protocol)列表
  • __objc_imageinfo:镜像信息,可用它区分Objective-C 1.0和2.0
  • __objc_const:OC初始化过的常量
  • __objc_selrefs:OC选择器(SEL)引用列表
  • __objc_protorefs:OC协议引用列表
  • __objc_classrefs:OC类引用列表
  • __objc_superrefs:OC超类(父类)引用列表
  • __objc_ivar:OC类的实例变量
  • __objc_data:OC初始化过的变量
  • __data:实际初始化数据段
    • 该怎么理解呢???
  • __common:未初始化过的符号声明
  • __bss:未初始化的全局变量

LC_LOAD_DYLIB

LC_LOAD_DYLIB是我们要介绍的第二个加载命令。LC_LOAD_DYLIB指向的都是程序依赖库的加载信息,使用MachOView查看LC_LOAD_DYLIB加载命令,可以发现程序加载的一些常见的动态库,例如:WebKit、UIKit、CoreFoundation,如下:

使用MachOView查看部分LC_LOAD_DYLIB加载命令使用MachOView查看部分LC_LOAD_DYLIB加载命令

动态链接共享库通过两个要素来标识。动态库的路径名和兼容性版本号。 路径名就是查找到的用于执行的库的名称,即dylib结构体中定义的name字段。路径名必须匹配。兼容性版本号必须大于或等于用户使用的版本号。时间戳用于记录库构建并复制到user中的时间,因此可以使用它来确定运行时使用的库是否与构建程序时使用的库完全相同。

代码语言:txt复制
/*
 * A dynamically linked shared library (filetype == MH_DYLIB in the mach header)
 * contains a dylib_command (cmd == LC_ID_DYLIB) to identify the library.
 * An object that uses a dynamically linked shared library also contains a
 * dylib_command (cmd == LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB, or
 * LC_REEXPORT_DYLIB) for each library it uses.
 */
struct dylib_command {
	uint32_t	cmd;		/* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
					   LC_REEXPORT_DYLIB */
	uint32_t	cmdsize;	/* includes pathname string */
	struct dylib	dylib;		/* the library identification */
};

/*
 * Dynamicly linked shared libraries are identified by two things.  The
 * pathname (the name of the library as found for execution), and the
 * compatibility version number.  The pathname must match and the compatibility
 * number in the user of the library must be greater than or equal to the
 * library being used.  The time stamp is used to record the time a library was
 * built and copied into user so it can be use to determined if the library used
 * at runtime is exactly the same as used to built the program.
 */
struct dylib {
    union lc_str  name;			/* library's path name */
    uint32_t timestamp;			/* library's build time stamp */
    uint32_t current_version;		/* library's current version number */
    uint32_t compatibility_version;	/* library's compatibility vers number*/
};
  • name:依赖库的完整路径,动态链接器dyld在加载动态库时会通过此路径来加载
  • timestamp:依赖库构建时的时间戳
  • current_version:当前版本号
  • compatibility_version:兼容版本号

另外,LC_LOAD_WEAK_DYLIB也标识需要加载一个动态库,且结构同上。通过LC_LOAD_WEAK_DYLIB声明的依赖库是可选的,缺少这类依赖库不会影响程序执行。而LC_LOAD_DYLIB依赖库若没有找到,加载器会放弃加载并结束该进程。

LC_CODE_SIGNATURE

常见的Mach-O文件类型

如下,列举了所有属于Mach-O格式的文件类型,不难看出,一共有11中Mach-O文件,iOS开发涉及到的主要是MH_OBJECT、MH_EXECUTE、MH_DYLIB、MH_DYLINKER、MH_DSYM。如下图:

Mach-O文件类型Mach-O文件类型

MH_OBJECT

如注释所示,MH_OBJECT是可再定址的目标文件。其实就是.o文件和静态库(.a静态库、.framework静态库)。

  • .o目标文件:源文件编译而成的目标文件
  • .a静态库文件:由多个.o文件编译链接合并而成的文件

我们可以借助clang命令将C语言的.c文件和OC的.m文件编译成.o目标文件:

代码语言:txt复制
clang -c C文件.c -o 目标文件.o     // 将C语言.c源码文件编译成.o目标文件
clang -c OC文件.m -o 目标文件.o    // 将OC语言.m源码文件编译成.o目标文件

-c 是compile的意思。代表只执行预处理、编译、汇编步骤。即-c可以生成目标文件

-o 是output的意思。代表指定输出的文件的目录和名称,省略-o参数目标文件默认和源文件同名

如果不使用clang,也可以使用GCC,参数意义和Clang相同,如下:

代码语言:txt复制
gcc -c C文件.c -o 目标文件.o
gcc -c OC文件.c -o 目标文件.o

然后借助file命令验证.o文件是Mach-O文件格式:

代码语言:txt复制
file 目标文件名.o // 利用file命令查看文件类型 main.o: Mach-O 64-bit object x86_64

举个例子

我们桌面新建一个mach-o文件夹,并新建一个main.c文件,命令如下:

代码语言:txt复制
$ mkdir ~/Desktop/mach-o
$ cd !$
$ touch main.c

然后,我们准备了如下代码,并写入到了main.c文件,保存:

代码语言:txt复制
#include <stdio.h>

int main(int argc, const char * argv[]) {

    // insert code here...

    printf("Hello, World!n");

    return 0;

}

最后终端执行如下命令:

代码语言:txt复制
$ clang -c main.c -o main.o

目录下会出现一个main.o文件,使用file命令可以验证main.o是否为mach-o文件:

代码语言:txt复制
$ file main.o
main.o: Mach-O 64-bit object x86_64

同理也可以验证.a静态库是Mach-O文件格式:

如下,我们打包的libAFNetworking.a也是一个Mach-O文件,只不过是一个Universal binary(通用二进制),关于通用二进制,我们后面介绍。

代码语言:txt复制
$ file libAFNetworking.a
libAFNetworking.a: Mach-O universal binary with 3 architectures: [arm_v7:current ar archive] [x86_64] [arm64]
libAFNetworking.a (for architecture armv7):	current ar archive
libAFNetworking.a (for architecture x86_64):	current ar archive
libAFNetworking.a (for architecture arm64):	current ar archive

MH_EXECUTE

MH_EXECUTE代表可执行文件。可执行文件是可以在终端中直接运行的文件。可执行文件和目标文件的区别主要在于可执行文件比目标文件多了链接的过程,这也是可执行文件可执行的原因。还是以main.c为例,我们把main.c编译链接为可执行文件,命令如下:

代码语言:txt复制
$ clang -o main.out main.c // .c源文件编译链接成可执行文件

上面命令会在目录下生成一个名为main.out的可执行文件,mac终端执行如下命令运行可执行文件:

代码语言:txt复制
$ ./main.out 

Hello, World!

如上,终端输出了main.c运行后的结果Hello,World!

然后通过file命令查看可执行文件是否为Mach-O文件:

代码语言:txt复制
file main.out

main.out: Mach-O 64-bit executable x86_64

当然我们也可以直接查看一个App的执行文件,如下TRIP是笔者逆向获取的某App的可执行文件:

代码语言:txt复制
$ file TRIP
TRIP: Mach-O 64-bit executable arm64

MH_DYLIB 动态库文件

MH_DYLIB是指动态库文件。我们常说的动态库包括如下两种:包括后缀名为.dylib的动态库(比如libobjc.dylib)和后缀名为.framework的动态库。

  • .dylib。后缀名为.dylib格式的动态库文件,比如libobjc.dylib。
    • 目录为/usr/lib
  • .framework。后缀名为.framework格式的动态库文件,比如UIKit.framework中的UIKit。
    • 模拟器的framework目录为/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/IPhoneSimulator.sdk/System/Library/Frameworks/
    • 真机的framework目录为/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks
代码语言:txt复制
$ file /usr/lib/libobjc.A.dylib
/usr/lib/libobjc.A.dylib: Mach-O universal binary with 3 architectures: [x86_64:Mach-O 64-bit dynamically linked shared library x86_64] [x86_64h:Mach-O 64-bit dynamically linked shared library x86_64h] [i386:Mach-O dynamically linked shared library i386]
/usr/lib/libobjc.A.dylib (for architecture x86_64):	Mach-O 64-bit dynamically linked shared library x86_64
/usr/lib/libobjc.A.dylib (for architecture x86_64h):	Mach-O 64-bit dynamically linked shared library x86_64h
/usr/lib/libobjc.A.dylib (for architecture i386):	Mach-O dynamically linked shared library i386

MH_DYLINKER

MH_DYLINKER是指的dyld,也是Mach-O格式的文件。dyld被称为动态链接编辑器(dynamic link editer),也有动态加载器(dynamic loader)的说法。在iPhone中的目录为 /usr/lib/dyld 中,在macOS的目录也是 /usr/lib/dyld。

那么dyld到底是做什么的?这要从动态库说起。

动态库不能直接运行,而是需要通过系统的动态链接加载器进行加载到内存后执行,动态链接加载器在系统中以一个用户态的可执行文件形式存在,一般应用程序会在Mach-O文件部分指定一个LC_LOAD_DYLINKER的加载命令,此加载命令指定了dyld的路径,通常它的默认值是“/usr/lib/dyld”。系统内核在加载Mach-O文件时,会使用/usr/lib/dyld路径指定的程序作为动态库的加载器(也就是dyld)来加载dylib。

综上,我们知道,dyld就是用来加载动态库的一个特殊的Mach-O文件。既然他是Mach-O格式的文件,那么他的结构和其他类型的Mach-O文件(比如MH_EXECUTE)并无本质区别。

我们同样使用file命令来看下macOS上的dyld是否为Mach-O格式:

代码语言:txt复制
$ file /usr/lib/dyld
/usr/lib/dyld: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386]
/usr/lib/dyld (for architecture x86_64):	Mach-O 64-bit dynamic linker x86_64
/usr/lib/dyld (for architecture i386):	Mach-O dynamic linker i386

如上,不难看出,dyld是一个Mach-O文件,且是一个拥有2个架构的通用二进制文件。分别是x86_64和i386。关于dyld的更详细的作用和加载动态库的流程分析,读者可以自行搜索其他文章。

MH_DSYM

release模式下,打模拟器包或真机包就会在app同级目录下生成一个.dSYM文件,如下:

dSYMdSYM

这个.dSYM格式的文件是iOS App的符号表,存储着二进制文件的符号信息。通常我们在分析线上崩溃的时候会用到这个文件。

.dSYM文件本质上是一个文件夹,我们需要通过【显示包内容】来找到DWARF目录下和App同名的文件,其路径为xxx.dSYM/Contents/Resources/DWARF/xxxx。同样,我们使用file命令验证下.dSYM里面的这个文件是不是Mach-O文件:

代码语言:txt复制
$ file Resources/DWARF/DSYMDemo
Resources/DWARF/DSYMDemo: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dSYM companion file x86_64] [arm64:Mach-O 64-bit dSYM companion file arm64]
Resources/DWARF/DSYMDemo (for architecture x86_64):	Mach-O 64-bit dSYM companion file x86_64
Resources/DWARF/DSYMDemo (for architecture arm64):	Mach-O 64-bit dSYM companion file arm64

如上,我们同样可以验证.dSYM中的符号表是一个Mach-O格式的文件。

Xcode中查看Mach-O类型

对于一个XCode的项目,我们可以在Target/Build Settings/Mach-O Type中查看当前项目的类型。Execute代表是可执行文件,即一个iOS App。其他类型还有Dynamic Library、Bundle、Static Library、Relocatable Object File。

Xcode查看Mach-O TypeXcode查看Mach-O Type

Universal Binary

Universal Binary即通用二进制文件,又被戏称为Fat Binary(胖二进制文件)。之所以被称为通用二进制(胖二进制)是因为一个通用二进制文件包含了多种不同架构的二进制文件。通用二进制文件可以在不同的架构的CPU上执行。因为需要储存多种架构的代码,通用二进制文件通常比单一平台二进制的程序要大。由于两种架构有共同的一些资源,所以并不会达到单一版本的两倍之多。由于执行过程中,只调用一部分代码,运行起来也不需要额外的内存。

关于Xcode中的building settings->Architectures的$(ARCHS_STANDARD)是一个环境变量,在不同的Xcode上代表不同的架构。比如在Xcode11上可能代表arm64、armv7、armv7s、x86_64。但在Xcode4上可能只代表armv7、armv7s、x86_64。如果Architectures环境变量支持的架构不能满足我们的开发要求,我们可以通过other添加架构。比如:

另外,Building settings->Valid Architectures代表可用的架构,这个参数也是支持配置的,如下是美团在不同打包模式下支持的架构:

最终我们打包的ipa包支持的架构取决于Architectures和Valid Architectures的交集。

一些命令

  • 查看Mach-O架构信息可以使用file命令或lipo -info命令,例如:
代码语言:txt复制
$ file TRIP
TRIP: Mach-O 64-bit executable arm64

$ lipo -info TRIP
Non-fat file: TRIP is architecture: arm64
  • 使用lipo -thin分离通用二进制文件;使用lipo -create生成通用二进制文件。例如:
代码语言:txt复制
$ lipo -thin arm64  xxx.dylib -output xxx_arm64.dylib
$ lipo -create xxx.x86_64.a xxx.arm64.a -output ./fat_binary.a

一些概念

otool:是Mac自带的命令行工具。可以查看Mach-O特定部分和段的内容

MachOView:一款查看Mach-O文件的图形界面工具。与otool功能类似,但是一个可视化的图形界面工具。MachOView源码下载地址:https://github.com/gdbinit/MachOView。下载源码使用Xcode编译后即可查看Mach-O文件布局。

dyld:dyld是一种特殊的Mach-O文件。用于将可执行文件、系统动态库、bundle文件加载进内存(dyld只能加载这三种Mach-O文件)。换句话说,app的可执行文件、动态库都是由dyld这个Mach-O文件加载进内存的。但dyld不能加载自己,dyld由系统加载。dyld目录为/usr/lib/dyld。iPhone和macOS上都有dyld,且路径一样。dyld代码是开源的,可以查看源码。

ldid:ldid是saurik制作的一个工具,用于轻松地修改二进制文件的权限(entitlements)。ldid还为二进制签名生成SHA1散列,因此iPhone内核可执行修改后的二进制。Cydia中的包名为“链接身份编辑器”,由Cydia/Telesphoreo存储库托管。详见维基百科:ldid

一些文章

dyld详解

Mach-O官方文档

维基百科-ASLR

Mach-O 可执行文件

dylib动态库加载过程分析

总结

本篇文章主要介绍了一下内容:

  1. Mach-O文件格式和文件结构
  2. 常见的Mach-O文件
  3. 通用二进制文件

下一篇文章将对ASLR进行介绍。

参考文章

Mach-O官方文档

Objc中国 — Mach-O 可执行文件

《iOS应用逆向与安全之道》

ios

0 人点赞