macOS Universal binaries & Mach-O Format

Reading time: 19 minutes

tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)

支持 HackTricks

基本信息

Mac OS 二进制文件通常被编译为 universal binaries。一个 universal binary 可以 在同一个文件中支持多个架构

这些二进制文件遵循 Mach-O 结构,基本由以下部分组成:

  • 头部
  • 加载命令
  • 数据

https://alexdremov.me/content/images/2022/10/6XLCD.gif

Fat Header

使用以下命令搜索文件: mdfind fat.h | grep -i mach-o | grep -E "fat.h$"

#define FAT_MAGIC	0xcafebabe
#define FAT_CIGAM	0xbebafeca	/* NXSwapLong(FAT_MAGIC) */

struct fat_header {
	uint32_t	magic;		/* FAT_MAGIC 或 FAT_MAGIC_64 */
	uint32_t	nfat_arch;	/* 后续结构的数量 */
};

struct fat_arch {
cpu_type_t	cputype;	/* cpu 说明符 (int) */
cpu_subtype_t	cpusubtype;	/* 机器说明符 (int) */
uint32_t	offset;		/* 文件偏移到此目标文件 */
uint32_t	size;		/* 此目标文件的大小 */
uint32_t	align;		/* 以 2 的幂为单位的对齐 */
};

头部包含 magic 字节,后面是文件 包含archs数量 (nfat_arch),每个架构将有一个 fat_arch 结构。

使用以下命令检查:

% file /bin/ls
/bin/ls: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64e:Mach-O 64-bit executable arm64e]
/bin/ls (for architecture x86_64):	Mach-O 64-bit executable x86_64
/bin/ls (for architecture arm64e):	Mach-O 64-bit executable arm64e

% otool -f -v /bin/ls
Fat headers
fat_magic FAT_MAGIC
nfat_arch 2
architecture x86_64
    cputype CPU_TYPE_X86_64
cpusubtype CPU_SUBTYPE_X86_64_ALL
capabilities 0x0
    offset 16384
    size 72896
    align 2^14 (16384)
architecture arm64e
    cputype CPU_TYPE_ARM64
cpusubtype CPU_SUBTYPE_ARM64E
capabilities PTR_AUTH_VERSION USERSPACE 0
    offset 98304
    size 88816
    align 2^14 (16384)

或者使用 Mach-O View 工具:

正如你所想,通常为 2 个架构编译的 universal binary 会使文件大小翻倍,而为 1 个架构编译的文件则不会。

Mach-O Header

头部包含有关文件的基本信息,例如用于识别它为 Mach-O 文件的 magic 字节和有关目标架构的信息。你可以在以下路径找到它: mdfind loader.h | grep -i mach-o | grep -E "loader.h$"

c
#define	MH_MAGIC	0xfeedface	/* the mach magic number */
#define MH_CIGAM	0xcefaedfe	/* NXSwapInt(MH_MAGIC) */
struct mach_header {
uint32_t	magic;		/* mach magic number identifier */
cpu_type_t	cputype;	/* cpu specifier (e.g. I386) */
cpu_subtype_t	cpusubtype;	/* machine specifier */
uint32_t	filetype;	/* type of file (usage and alignment for the file) */
uint32_t	ncmds;		/* number of load commands */
uint32_t	sizeofcmds;	/* the size of all the load commands */
uint32_t	flags;		/* flags */
};

#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */
struct mach_header_64 {
uint32_t	magic;		/* mach magic number identifier */
int32_t		cputype;	/* cpu specifier */
int32_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 */
};

Mach-O 文件类型

有不同的文件类型,可以在源代码中找到定义,例如这里。最重要的类型有:

  • MH_OBJECT: 可重定位目标文件(编译的中间产品,尚未成为可执行文件)。
  • MH_EXECUTE: 可执行文件。
  • MH_FVMLIB: 固定虚拟机库文件。
  • MH_CORE: 代码转储
  • MH_PRELOAD: 预加载的可执行文件(在 XNU 中不再支持)
  • MH_DYLIB: 动态库
  • MH_DYLINKER: 动态链接器
  • MH_BUNDLE: “插件文件”。使用 gcc 的 -bundle 生成,并由 NSBundledlopen 显式加载。
  • MH_DYSM: 伴随的 .dSym 文件(用于调试的符号文件)。
  • MH_KEXT_BUNDLE: 内核扩展。
bash
# Checking the mac header of a binary
otool -arch arm64e -hv /bin/ls
Mach header
magic  cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64    ARM64          E USR00     EXECUTE    19       1728   NOUNDEFS DYLDLINK TWOLEVEL PIE

或使用 Mach-O View

Mach-O 标志

源代码还定义了几个用于加载库的标志:

  • MH_NOUNDEFS:没有未定义的引用(完全链接)
  • MH_DYLDLINK:Dyld 链接
  • MH_PREBOUND:动态引用预绑定。
  • MH_SPLIT_SEGS:文件分割 r/o 和 r/w 段。
  • MH_WEAK_DEFINES:二进制文件具有弱定义符号
  • MH_BINDS_TO_WEAK:二进制文件使用弱符号
  • MH_ALLOW_STACK_EXECUTION:使堆栈可执行
  • MH_NO_REEXPORTED_DYLIBS:库没有 LC_REEXPORT 命令
  • MH_PIE:位置无关可执行文件
  • MH_HAS_TLV_DESCRIPTORS:有一个包含线程局部变量的部分
  • MH_NO_HEAP_EXECUTION:堆/数据页面不执行
  • MH_HAS_OBJC:二进制文件具有 oBject-C 部分
  • MH_SIM_SUPPORT:模拟器支持
  • MH_DYLIB_IN_CACHE:用于共享库缓存中的 dylibs/frameworks。

Mach-O 加载命令

文件在内存中的布局在这里指定,详细说明了 符号表的位置、执行开始时主线程的上下文以及所需的 共享库。向动态加载器 (dyld) 提供了有关二进制文件加载到内存中的过程的指令。

使用 load_command 结构,该结构在提到的 loader.h 中定义:

objectivec
struct load_command {
uint32_t cmd;           /* type of load command */
uint32_t cmdsize;       /* total size of command in bytes */
};

有大约 50 种不同类型的加载命令,系统以不同方式处理它们。最常见的有: LC_SEGMENT_64LC_LOAD_DYLINKERLC_MAINLC_LOAD_DYLIBLC_CODE_SIGNATURE

LC_SEGMENT/LC_SEGMENT_64

tip

基本上,这种类型的加载命令定义了 如何加载 __TEXT(可执行代码)和 __DATA(进程数据),根据二进制文件执行时在数据部分中指示的 偏移量

这些命令 定义了段,在执行进程时被 映射虚拟内存空间 中。

不同类型 的段,例如 __TEXT 段,它包含程序的可执行代码,以及 __DATA 段,它包含进程使用的数据。这些 段位于 Mach-O 文件的数据部分 中。

每个段 可以进一步 划分 为多个 加载命令结构 包含关于 这些节 在各自段中的 信息

在头部,首先找到 段头

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 */
int32_t		maxprot;	/* maximum VM protection */
int32_t		initprot;	/* initial VM protection */
	uint32_t	nsects;		/* number of sections in segment */
	uint32_t	flags;		/* flags */
};

段头的示例:

该头部定义了 其后出现的节头的数量

c
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 */
};

示例 节标题

如果你 添加 节偏移 (0x37DC) + 架构开始的偏移,在这种情况下 0x18000 --> 0x37DC + 0x18000 = 0x1B7DC

也可以通过 命令行 获取 头信息

bash
otool -lv /bin/ls

常见的由此命令加载的段:

  • __PAGEZERO 它指示内核映射 地址零,以便无法读取、写入或执行。结构中的maxprot和minprot变量设置为零,以指示此页面上没有读写执行权限
  • 此分配对于缓解NULL指针解引用漏洞非常重要。这是因为XNU强制实施一个硬页面零,确保内存的第一页(仅第一页)不可访问(在i386中除外)。一个二进制文件可以通过制作一个小的__PAGEZERO(使用-pagezero_size)来满足这些要求,以覆盖前4k,并使其余的32位内存在用户模式和内核模式下可访问。
  • __TEXT:包含可执行 代码,具有读取执行权限(不可写)。此段的常见部分:
  • __text:编译的二进制代码
  • __const:常量数据(只读)
  • __ [c/u/os_log]string:C、Unicode或os日志字符串常量
  • __stubs__stubs_helper:在动态库加载过程中涉及
  • __unwind_info:堆栈展开数据。
  • 请注意,所有这些内容都是签名的,但也被标记为可执行(为不一定需要此权限的部分(如专用字符串部分)创建更多的利用选项)。
  • __DATA:包含可读可写的数据(不可执行)。
  • __got: 全局偏移表
  • __nl_symbol_ptr:非惰性(加载时绑定)符号指针
  • __la_symbol_ptr:惰性(使用时绑定)符号指针
  • __const:应为只读数据(实际上不是)
  • __cfstring:CoreFoundation字符串
  • __data:全局变量(已初始化)
  • __bss:静态变量(未初始化)
  • __objc_*(__objc_classlist,__objc_protolist等):由Objective-C运行时使用的信息
  • __DATA_CONST:__DATA.__const不保证是常量(写权限),其他指针和GOT也是如此。此部分使用mprotect使__const、一些初始化程序和GOT表(解析后)只读
  • __LINKEDIT:包含链接器(dyld)所需的信息,例如符号、字符串和重定位表条目。它是一个通用容器,包含不在__TEXT__DATA中的内容,其内容在其他加载命令中描述。
  • dyld信息:重定位、非惰性/惰性/弱绑定操作码和导出信息
  • 函数开始:函数的起始地址表
  • 代码中的数据:__text中的数据岛
  • 符号表:二进制中的符号
  • 间接符号表:指针/存根符号
  • 字符串表
  • 代码签名
  • __OBJC:包含由Objective-C运行时使用的信息。尽管这些信息也可能在__DATA段中找到,在各种__objc_*部分中。
  • __RESTRICT:一个没有内容的段,只有一个名为**__restrict**(也为空)的单一部分,确保在运行二进制文件时,它将忽略DYLD环境变量。

正如在代码中所看到的,段也支持标志(尽管它们并不常用):

  • SG_HIGHVM:仅核心(未使用)
  • SG_FVMLIB:未使用
  • SG_NORELOC:段没有重定位
  • SG_PROTECTED_VERSION_1:加密。例如,Finder用于加密文本__TEXT段。

LC_UNIXTHREAD/LC_MAIN

LC_MAIN包含entryoff属性中的入口点。在加载时,dyld简单地此值添加到(内存中的)二进制文件基址,然后跳转到此指令以开始执行二进制代码。

**LC_UNIXTHREAD包含启动主线程时寄存器必须具有的值。这已经被弃用,但dyld**仍在使用它。可以通过以下方式查看寄存器设置的值:

bash
otool -l /usr/lib/dyld
[...]
Load command 13
cmd LC_UNIXTHREAD
cmdsize 288
flavor ARM_THREAD_STATE64
count ARM_THREAD_STATE64_COUNT
x0  0x0000000000000000 x1  0x0000000000000000 x2  0x0000000000000000
x3  0x0000000000000000 x4  0x0000000000000000 x5  0x0000000000000000
x6  0x0000000000000000 x7  0x0000000000000000 x8  0x0000000000000000
x9  0x0000000000000000 x10 0x0000000000000000 x11 0x0000000000000000
x12 0x0000000000000000 x13 0x0000000000000000 x14 0x0000000000000000
x15 0x0000000000000000 x16 0x0000000000000000 x17 0x0000000000000000
x18 0x0000000000000000 x19 0x0000000000000000 x20 0x0000000000000000
x21 0x0000000000000000 x22 0x0000000000000000 x23 0x0000000000000000
x24 0x0000000000000000 x25 0x0000000000000000 x26 0x0000000000000000
x27 0x0000000000000000 x28 0x0000000000000000  fp 0x0000000000000000
lr 0x0000000000000000 sp  0x0000000000000000  pc 0x0000000000004b70
cpsr 0x00000000

[...]

LC_CODE_SIGNATURE

包含关于 Macho-O 文件的代码签名 的信息。它仅包含一个 偏移量,指向 签名 blob。这通常位于文件的最末尾。
然而,您可以在 这篇博客文章 和这个 gists 中找到一些关于此部分的信息。

LC_ENCRYPTION_INFO[_64]

支持二进制加密。然而,当然,如果攻击者设法破坏了进程,他将能够以未加密的方式转储内存。

LC_LOAD_DYLINKER

包含 动态链接器可执行文件的路径,该文件将共享库映射到进程地址空间。值始终设置为 /usr/lib/dyld。重要的是要注意,在 macOS 中,dylib 映射发生在 用户模式,而不是内核模式。

LC_IDENT

过时,但当配置为在崩溃时生成转储时,会创建一个 Mach-O 核心转储,并在 LC_IDENT 命令中设置内核版本。

LC_UUID

随机 UUID。它对任何直接的事情都很有用,但 XNU 将其与其他进程信息一起缓存。它可以在崩溃报告中使用。

LC_DYLD_ENVIRONMENT

允许在进程执行之前向 dyld 指示环境变量。这可能非常危险,因为它可能允许在进程内部执行任意代码,因此此加载命令仅在使用 #define SUPPORT_LC_DYLD_ENVIRONMENT 构建的 dyld 中使用,并进一步限制处理仅限于形式为 DYLD_..._PATH 的变量,指定加载路径。

LC_LOAD_DYLIB

此加载命令描述了一个 动态 依赖关系,指示 加载器 (dyld) 加载和链接该库。每个 Mach-O 二进制文件所需的库都有一个 LC_LOAD_DYLIB 加载命令。

  • 此加载命令是 dylib_command 类型的结构(其中包含一个描述实际依赖动态库的 struct dylib):
objectivec
struct dylib_command {
uint32_t        cmd;            /* LC_LOAD_{,WEAK_}DYLIB */
uint32_t        cmdsize;        /* includes pathname string */
struct dylib    dylib;          /* the library identification */
};

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*/
};

您还可以通过命令行获取此信息:

bash
otool -L /bin/ls
/bin/ls:
/usr/lib/libutil.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.0.0)

一些潜在的与恶意软件相关的库包括:

  • DiskArbitration: 监控 USB 驱动器
  • AVFoundation: 捕获音频和视频
  • CoreWLAN: Wifi 扫描。

note

Mach-O 二进制文件可以包含一个或 多个 构造函数,这些构造函数将在 LC_MAIN 指定的地址 之前执行
任何构造函数的偏移量保存在 __mod_init_func 段的 __DATA_CONST 部分中。

Mach-O 数据

文件的核心是数据区域,由加载命令区域中定义的多个段组成。每个段中可以包含多种数据部分,每个部分 保存特定类型的代码或数据

tip

数据基本上是包含所有由加载命令 LC_SEGMENTS_64 加载的 信息 的部分。

https://www.oreilly.com/api/v2/epubs/9781785883378/files/graphics/B05055_02_38.jpg

这包括:

  • 函数表: 包含有关程序函数的信息。
  • 符号表: 包含有关二进制文件使用的外部函数的信息
  • 还可能包含内部函数、变量名称等。

要检查它,您可以使用 Mach-O View 工具:

或者从命令行:

bash
size -m /bin/ls

Objetive-C 常见部分

__TEXT 段 (r-x):

  • __objc_classname: 类名 (字符串)
  • __objc_methname: 方法名 (字符串)
  • __objc_methtype: 方法类型 (字符串)

__DATA 段 (rw-):

  • __objc_classlist: 所有 Objective-C 类的指针
  • __objc_nlclslist: 非懒加载 Objective-C 类的指针
  • __objc_catlist: 类别的指针
  • __objc_nlcatlist: 非懒加载类别的指针
  • __objc_protolist: 协议列表
  • __objc_const: 常量数据
  • __objc_imageinfo, __objc_selrefs, objc__protorefs...

Swift

  • _swift_typeref, _swift3_capture, _swift3_assocty, _swift3_types, _swift3_proto, _swift3_fieldmd, _swift3_builtin, _swift3_reflstr

tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)

支持 HackTricks