在上一篇文章中,我们介绍了如何修复dyld以恢复内存执行。这种方法的优点之一是,我们将加载Mach-O二进制文件的许多复杂工作委托给macOS。但如果我们在不使用dyld的情况下,创建我们自己的加载器呢?所有这些字节映射是如何工作的?
接下来,我们将介绍如何在不使用dyld的情况下在MacOS Ventura中为Mach-O包构建内存加载器,以及Mach-O文件的组成,dyld如何处理加载命令以将区域映射到内存中。
为了配合苹果向ARM架构的迁移,这篇文章将重点介绍MacOS Ventura的AARCH64版本和针对MacOS 12.0及更高版本的XCode。
什么是Mach-O文件?
首先介绍一下Mach-O文件的架构,建议先阅读一下Aidan Steele的Mach-O文件格式参考。
当我们在处理ARM版本的MacOS时,会假设正在查看的Mach-O没有被封装在Universal 2格式中,因此在文件开头我们首先会遇到的是Mach_header_64:
要构造加载器,我们需要检查以下几个字段:
magic-此字段应包含MH_magic_64的值;
Cputype-对于M1,应为CPU_TYPE_ARM64。
filetype -我们将检查这篇文章的MH_BUNDLE类型,但加载不同类型也应该很容易。
如果Mach-O是正常的,我们可以立即处理mach_header_64结构体后面的load命令。
加载命令
顾名思义,load命令是一种数据结构,用于指示dyld如何加载Mach-O区域。
每个load命令由load_command结构表示:
cmd字段最终决定load_command实际表示的内容,以LC_UUID的一个非常简单的load_command为例,该命令用于将UUID与二进制数据关联起来。其结构如下:
如上所述,这与load_command结构重叠,这就是为什么我们有匹配字段的原因。以下就是我们将看到的各种负载命令所支持的情况。
Mach-O段
加载Mach-O时,我们要处理的第一个load_command是LC_SEGMENT_64。
segment命令告诉dyld如何将Mach-O的一个区域映射到虚拟内存中,它应该有多大,应该有什么样的保护,以及文件的内容在哪里。让我们来看看它的结构:
出于本文的目的,我们将关注:
segname -段的名称,例如__TEXT;
vmaddr -应该加载段的虚拟地址。例如,如果它被设置为0x4000,那么我们将在分配的内存基数+ 0x4000处加载段;
vmsize -要分配的虚拟内存的大小;
fileoff -从文件开始到应复制到虚拟内存的Mach-O内容的偏移量;
filesize -要从文件中复制的字节数;
maxprot-应分配给虚拟内存区域的最大内存保护值;
initprot -应分配给虚拟内存区域的初始内存保护;
nsects -遵循此段结构的节数。
要注意,虽然dyld依赖mmap将Mach-O的片段拉入内存,但如果我们的初始进程是作为一个加固进程执行的(并且没有com.apple.security.cs. c . data . data之类的文件)。使用mmap是不可能的,除非我们提供的bundle是使用与代理应用程序相同的开发人员证书进行签名的。此外,我们正在尝试构建一个内存加载器,因此在这种情况下从磁盘拉二进制文件没有多大意义。
为了解决这个问题,在此POC中,我们将预先分配我们的blob内存并复制它,例如:
与之前的dyld文章一样,我们需要在主机二进制文件中使用正确的授权来允许无符号可执行内存。
节
从上面的字段中可以看到,段加载命令中存在另一个引用,这就是一个节(section)。
由于节位于段中,虽然它将继承其内存保护,但它有自己的大小和要加载的文件内容。每个段的数据结构附加到segment命令中,其结构为:
同样,我们将只关注其中几个字段,这些字段对于我们构建加载器的直接目的很有帮助:
sectname -节的名称,例如__text;
segname -与此节关联的段的名称;
addr -用于此节的虚拟地址偏移量;
size -文件中(以及虚拟内存中的)节的大小;
offset - Mach-O文件中部分内容的偏移量;
flags - flags可以分配给一个节,这个节帮助确定reserved1,reserved2和reserved3中的值。
由于我们已经分配了每个段,所以加载器将遍历每个段描述符,确保将正确的文件内容复制到虚拟内存中。需要注意的是,在复制时可能需要更新内存保护。MacOS for ARM不允许读/写/执行内存页(除非com.apple.security.cs. c。allow-jit授权与MAP_JIT一起使用),因此我们需要在复制时适应这一点:
符号
随着我们的加载器开始成型,接下来需要看看如何处理符号(Symbol)。符号在Mach-O二进制文件的加载过程中扮演着重要的角色,它将名称和序数关联到内存区域,以供我们稍后参考。
符号是通过LC_SYMTAB的加载命令来处理的,如下所示:
同样,我们将关注构建加载器所需的字段:
symoff -从文件开始到包含每个符号信息的nlist结构数组的偏移量;
nsyms -符号(或nlist结构)的数量;
stroff -符号查找所使用的字符串的文件偏移量。
显然,接下来我们需要知道nlist是什么:
此结构为我们提供了有关命名符号的信息:
n_strx -从符号字符串字段到该符号字符串的偏移量;
n_value -包含符号的值,例如地址。
因为我们稍后需要引用符号,所以我们的加载器需要存储这些信息以备以后使用:
dylib’s
接下来是LC_LOAD_DYLIB加载命令,该命令引用在运行时加载的额外dylib’s。
我们需要的项在dylib结构成员中找到,特别是dylib.name.offset,它是从这个加载命令的开头到包含要加载的dylib的字符串的偏移量。
稍后,当涉及到重定位时,我们将需要这些信息,其中dylib’s的导入顺序起着重要作用,因此我们将构建一个dylib’s数组,供以后使用:
迁移
现在就要介绍Mach-O更复杂的部分——迁移。
Mach-O是用XCode构建的,目标是macOS 12.0和更高版本,使用LC_DYLD_CHAINED_FIXUPS的加载命令。关于这一切是如何工作的,没有太多的文档,但Noah Martin对iOS 15查找链的研究值得参考,我们还可以在这里找到苹果XNU repo中使用的结构体的详细信息。
Dyld’s的源代码告诉我们,该加载命令以结构linkedit_data_command开始:
使用dataoff便能找到标头:
我们需要做的第一件事是收集所有导入并构造一个稍后将引用的有序数组。为此,我们将使用以下字段:
symbols_offset -从该结构开始到导入所使用的符号字符串的偏移量;
imports_count -导入项的数量;
imports_format -任何导入符号的格式。
imports_offset -从该结构开始到导入表的偏移量。
每个导入项的数据结构都依赖于imports_format字段,但通常我看到的是DYLD_CHAINED_IMPORT格式:
可以看出这是一个32位数组项,有lib_ordinal字段,它是我们之前从LC_LOAD_DYLIB加载命令构建的有序dylib数组的索引。索引从1开始,而不是0,这意味着第一个索引是1,然后是2……
如果索引值为0或253,则该项引用this-image(当前正在执行的二进制文件)。这就是我们之前构造符号字典的原因,因为现在我们可以简单地将自己二进制文件中引用的符号名称解析为其地址:
name_offset是从dyld_chained_fixups_header收集的symbols_offset字符串的偏移量。
使用这些信息,我们需要构建一个有序的导入数组,因为我们需要马上引用这个有序数组。
构建了一个导入列表后,将开始链式启动,这可以从dyld_chained_fixups_header结构的starts_offset标头字段中找到。
链式启动的结构是:
为了导航,我们需要遍历seg_info_offset中的每个项,这为我们提供了指向dyld_chained_starts_in_segment的指针列表:
首先要注意这个结构,有时segment_offset是0,但不知道为什么,看起来dyld也识别了这个,只是忽略了它们。
我们需要找到每个reloc链的开始位置的字段如下:
pointer_format-链使用的DYLD_CHAINED_PTR_结构的类型;
segment_offset-段起始地址在内存中的绝对偏移量;
page_count-page_start成员数组中的页数;
page_start-从页面到链开始的偏移量。
当我们在一个段中有一个有效的偏移量时,我们可以开始遵循reloc链。遍历每个项,我们需要检查第一位,以确定该项是一个rebase(设置为0)还是一个bind(设置为1):
在rebase的情况下,将该项转换为dyld_chained_ptr_64_rebase,并使用目标偏移量更新该项到已分配内存的基数。
在绑定的情况下,我们使用dyld_chained_ptr_64_bind,序数字段是我们前面构建的导入数组的偏移量。
然后,我们需要移动到下一个bind或rebase,这是通过执行next*4(4字节是步长)来完成的。我们重复此操作,直到下一个字段为0,表示链已结束。
构建加载器
现在一切就绪,开始构建加载器。步骤如下:
1.分配内存区域;
2.根据LC_SEGMENT_64命令将每个段加载到虚拟内存中;
3.将每个节加载到每个段中;
4.从LC_LOAD_DYLIB命令构建dylib的有序集合;
5.从LC_SYMTAB命令构建一个符号集合。
6.遍历LC_DYLD_CHAINED_FIXUPS链并对每个reloc进行bind或rebase。
一旦完成,我们就可以使用LC_SYMTAB中的数据来引用我们想要输入的符号并传递执行。如果一切顺利,我们将看到Mach-O被加载到内存中并开始执行:
这个POC的所有代码都已添加到Dyld-DeNeuralyzer项目。
虽然你可以使用其中的代码加载C/ c++包,但如果你尝试加载Objective-C包,你会看到如下的内容:
这是因为在加载Objective-C Mach-O时dyld中发生了一些事情,具体原因我们下一部分再讲。
本文翻译自:https://blog.xpnsec.com/building-a-mach-o-memory-loader-part-1/