Mach-O 中的导入函数动态链接






4.83/5 (8投票s)
了解 Mach-O 库中导入函数的链接原理,我们可以实现一个相当有趣的效果:将它们的调用重定向到我们自己的代码,而我们自己的代码又可以使用原始的函数。
目录
- 引言
- Mach-O 简介
- Fat Binary
- 动态链接研究
- 在导入表中查找元素
- 有用链接
- 历史
引言
了解 Mach-O 库中导入函数的链接原理,我们可以实现一个相当有趣的效果:将它们的调用重定向到我们自己的代码,而我们自己的代码又可以使用原始的函数。要做到这一点,只需假装成一个动态加载器,并在内存中修改目标库的导入表即可。让我们看看 Mach-O 格式,并学习动态加载器如何执行其导入表的重定位。
Mach-O 简介
理解 Mach-O 的最好方式是看下面的图
似乎人类尚未更清晰地描述其结构。粗略地说,一切看起来都像这样
- Header – 这里存储了目标架构的信息以及对文件后续内容解释的各种选项。
- Load commands — 这些命令告知如何以及在哪里加载 Mach-O 部分:段(见下文)、符号表,并告知该文件依赖哪些库以首先加载它们。
- Segments — 这些描述了要加载代码或数据的段的内存区域。
对于第二个近似,我们将不得不研究一些实用工具-解析器
otool
— 是一个与系统一起提供的命令行程序。它可以显示文件不同部分的内容:头、加载命令、段、节等。特别是使用-v
(详细)键调用它很有用。MachOView
— 在 GPL 下分发,有自己的 GUI,并且仅在 Mac OS 10.6 及更高版本上运行。它包含了 Mach-O 完整内容的查看器,并根据其他分区的数据添加了一些分区信息,这非常方便。
实际上,用户使用 MachOView
处理不同的示例就足以学习 Mach-O 了。但对于 Mach-O 开发来说还不够,因为我们不知道头、加载命令、段、节、符号表的精确结构以及其字段的确切描述。但有规范就不是大问题。它始终可以在 Apple 官方网站上找到。并且安装了开发工具后,我们可以查看 /usr/include/mach-o 中的头文件(尤其是 loader.h)。
此外,值得记住的是,虽然文件内容在内存中的顺序与在磁盘上的顺序相同,但链接器在加载时仍可能删除符号表和整个字符串表的一些部分。此外,它可以在必要时在内存中设置实际偏移量的值,而文件中的这些值可能为零或对应于磁盘上的偏移量。
头结构很简单(以 32 位架构为例;64 位架构差别不大)
struct mach_header
{
uint32_t magic;
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
uint32_t filetype;
uint32_t ncmds;
uint32_t sizeofcmds;
uint32_t flags;
};
一切从一个魔术值开始(0xFEEDFACE 或反之,取决于机器字中字节顺序的约定)。然后定义处理器架构类型、加载命令的数量和大小,以及描述其他特性的标志。
例如
现有加载命令如下所示
LC_SEGMENT
— 包含特定段的各种信息:大小、节数、文件偏移量以及加载后的内存偏移量LC_SYMTAB
— 加载符号和字符串表LC_DYSYMTAB
— 创建导入表;符号数据取自符号表LC_LOAD_DYLIB
— 定义对某个第三方库的依赖
例如(分别对应 32 位和 64 位版本)
最重要的段是以下几个
- __TEXT — 可执行代码和其他只读数据
- __DATA — 可写数据;包括可被动态加载器在懒加载绑定时修改的导入表
- __OBJC — Objective-C 语言运行时标准库的各种信息
- __IMPORT — 仅适用于 32 位架构的导入表(我仅在 Mac OS 10.5 上生成过它)
- __LINKEDIT — 在这里,动态加载器为其已加载的模块放置数据(符号表、字符串表等)
任何加载命令都以以下字段开头
struct load_command { uint32_t cmd; //command numeric code uint32_t cmdsize; //size of the current command in bytes };
在这些字段之后,根据命令类型,可能还有许多不同的字段。
例如
列出的段中最有趣的节是以下几个
- __TEXT,__text — 代码本身
- __TEXT,__cstring — 常量字符串(用双引号括起来)
- __TEXT,__const — 各种常量
- __DATA,__data — 已初始化变量(字符串和数组)
- __DATA,__la_symbol_ptr — 导入函数指针表
- __DATA,__bss — 未初始化静态变量
- __IMPORT,__jump_table — 导入函数调用的存根
抢先说一句,在一个 Mach-O 中,可能只有 __IMPORT,__jump_table
(对于 32 位,Mac OS 10.5),或者 __DATA,__la_symbol_ptr
(对于 64 位,或 Mac OS 10.6 及更高版本)作为导入表。
段中的节具有以下结构
struct section
{
char sectname[16];
char segname[16];
uint32_t addr;
uint32_t size;
uint32_t offset;
uint32_t align;
uint32_t reloff;
uint32_t nreloc;
uint32_t flags;
uint32_t reserved1;
uint32_t reserved2;
};
我们有段的名称和节本身,大小,文件中的偏移量以及动态加载器将其放置的内存中的地址。此外,还有特定于某个节的其他信息。
例如
Fat Binary
当然,值得一提的是,可执行文件和库“学会了”一次存储多个可执行代码的变体。这是因为 Apple 公司对目标架构进行了多次渐进式更改(Motorola -> IBM -> Intel)。一般情况下,这类文件称为 fat binary。事实上,它们是多个 Mach-O 组合在一个文件中,但最后一个的头是特殊的。它包含关于支持的架构数量和类型的信息以及指向每个架构的偏移量。具有上述结构的简单 Mach-O 位于该偏移量处。
用 C 语言表示如下
struct fat_header
{
uint32_t magic;
uint32_t nfat_arch;
};
其中 magic
表示 0xCAFEBABE(或反之,我们应该记住不同处理器上机器字的字节顺序不同)。然后,正好是 nfat_arch(数量)
个如下描述的结构
struct fat_arch
{
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
uint32_t offset;
uint32_t size;
uint32_t align;
};
实际上,字段名称本身就说明了问题:处理器类型,特定 Mach-O 在文件中的偏移量,大小和对齐。
实验程序
让我们以 C 语言采用以下文件来研究导入函数调用的工作方式
//File test.c
void libtest(); //from libtest.dylib
int main()
{
libtest(); //calls puts() from libSystem.B.dylib
return 0;
}
//File libtest.c
#include <stdio.h>
void libtest() //just a simple library function
{
puts("libtest: calls the original puts()");
}
动态链接研究
我们将仅限于 Intel 处理器。假设我们有 Mac OS 10.5。我们将这些文件添加到新的 Xcode 项目中,编译(32 位版本),并在调试模式下启动它。我们在 libtest.dylib 库的 libtest() 函数中调用 puts()
函数的那一行停止。这是 libtest() 的汇编列表
再执行一步
并在内存中查看
这就是导入表中的那个单元格(在本例中是 __IMPORT, __jump_table
单元格),如果使用懒加载绑定,它将作为动态加载器(__dyld_stub_binding_helper_interface
函数)调用的跳板,或者直接跳转到目标函数。这可以通过以下 puts()
调用得到证实
并在内存中
因此,我们看到动态加载器将间接调用 CALL (0xE8)
的指令替换为间接跳转 JMP (0xE9)
的指令。这意味着,要重定向 __jump_table
的元素,只需在替换函数开头写入间接跳转指令而不是其初始内容就足够了。
这里还有一个有趣的时刻。为什么不使用 JMP
来跳转到动态加载器(链接器)?这是因为 CALL
(它会将返回地址保存在堆栈上)将帮助链接器确定是哪个导入表的元素调用了它。因此,它将帮助确定该符号是什么,并通过用间接 JMP
替换它来解决该符号,并将其跳转到所需的函数。
现在,让我们将项目迁移到 Mac OS 10.6 并为 32 位和 64 位架构编译 fat binary。以防万一,您可以在 Xcode 中这样做
首先,我们编译,然后启动 64 位变体(仅为例:Snow Leopard 上的导入表对于 32 位版本也将是相同的),然后再次停止在 puts()
的调用处
再次是一个简单的 CALL
。我们继续看
这里我们可以看到与简单的 __IMPORT, __jump_table
的区别。
欢迎来到 __TEXT, __symbol_stub1
。这个表是每个导入函数的 JMP 指令集合。在我们的例子中,只有一个这样的指令,如上所示。每条这样的指令都会执行跳转到 __DATA, __la_symbol_ptr
表中相应单元格定义的地址。后者是此 Mach-O 的导入表。
但是,让我们继续研究。如果我们查看跳转将要执行的地址
我们将看到以下内容
我们进入了 __TEXT, __stub_helper
段。实际上,这是 Mach-O 的 PLT(过程链接表)。通过第一条指令(在本例中,它是与 R11 连接的 LEA,但它也可以是简单的 PUSH
),动态链接器会记住哪个符号需要重定位。第二条指令始终指向同一个地址——函数 __dyld_stub_binding_helper
的开头,它将执行链接
动态链接器执行 puts()
的重定位后,__DATA, __la_symbol_ptr
中的相应单元格将如下所示
这是来自 libSystem.B.dylib 模块的 puts()
函数的地址。这意味着,通过将地址替换为我们自己的地址,我们将获得所需的调用重定向效果。
因此,通过这个具体的例子,我们了解了动态链接是如何执行的,Mach-O 中的导入表是什么,以及它们由哪些元素组成。现在,让我们进行 Mach-O 的分析。
在导入表中查找元素
我们需要通过符号名在导入表中找到相应的单元格。其算法相当非凡。
首先,我们需要在符号表中找到符号本身。后者是以下结构的数组
struct nlist
{
union
{
int32_t n_strx;
} n_un;
uint8_t n_type;
uint8_t n_sect;
int16_t n_desc;
uint32_t n_value;
};
其中 n_un.n_strx
是此符号名称的偏移量(以字节为单位,从字符串表开头算起)。其余部分涉及符号类型、其所在的节等。简而言之,以下是我们实验库 libtest.dylib(32 位版本)的最后几个元素
字符串表是名称的链,每个名称都以零结尾。但值得注意的是,编译器会在每个名称前面加上下划线字符 "_"。这就是为什么在字符串表中,“puts”这个名称看起来像 "_puts"。
这是一个例子。
我们可以通过相应的加载命令(LC_SYMTAB)找到符号表和字符串表的位置
但符号表并非统一。它有几个分区。我们特别关注其中一个:它包含未定义符号,即动态链接的符号。此外,MachOView 用蓝色背景突出显示这些符号。要确定符号表的哪个部分反映了未定义符号的子集,我们需要查看动态符号的加载命令(LC_DYSYMTAB
)
这是它在 C 语言中的表示
struct dysymtab_command { uint32_t cmd; uint32_t cmdsize; uint32_t ilocalsym; uint32_t nlocalsym; uint32_t iextdefsym; uint32_t nextdefsym; uint32_t iundefsym; uint32_t nundefsym; uint32_t tocoff; uint32_t ntoc; uint32_t modtaboff; uint32_t nmodtab; uint32_t extrefsymoff; uint32_t nextrefsyms; uint32_t indirectsymoff; uint32_t nindirectsyms; uint32_t extreloff; uint32_t nextrel; uint32_t locreloff; uint32_t nlocrel; };
在这里,dysymtab_command.iundefsym
是符号表中的一个索引,未定义符号的子集从此开始。dysymtab_command.nundefsym
是未定义符号的数量。由于我们知道要查找未定义符号,因此我们应该仅在此子集中的符号表中查找它。
现在,一个非常重要的时刻:通过名称查找符号时,对我们最重要的就是记住它在符号表中从头开始的索引。这是因为另一个重要的表——间接符号表——由这些索引的数值组成。我们可以通过 dysymtab_command.indirectsymoff
的值找到它;dysymtab_command.nindirectsyms
定义了索引的数量。
在本例中,此表只有一个元素(在实际情况中,元素要多得多)
最后,让我们看看 __IMPORT, __jump_table
段,我们需要找到它的元素。它看起来像这样
此段的 section.reserved1
字段非常重要(MachOView 称之为 Indirect Sym Index)。它表示间接符号表中的索引,__jump_table
元素的相互唯一对应关系从此开始。我们记得间接符号表中的元素是符号表中的索引。你明白我的意思了吗?
但在将所有内容汇总在一起之前,让我们看看 Snow Leopard 中的情况,以提供完整的图景。__DATA, __la_symbol_ptr
在这里起着导入表的作用。事实上,差异并不太明显。
这是符号加载命令
这里是它的最后几个元素
有两个未定义符号(蓝色背景)。这对应于动态符号加载命令(LC_DYSYMTAB
)的数据
此外,间接符号表中有四个元素而不是一个
但是,如果我们查看 __la_symbol_ptr
段的 reserved1
字段,我们会发现它与间接符号表的元素之间的相互唯一映射关系不是从开头开始,而是从第四个元素(索引为 3)开始的
__la_symbol_ptr
段描述的导入表的内容将如下所示
了解 Mach-O 的所有这些细节后,我们可以制定在导入表中搜索所需元素的算法。这是下一篇文章要讨论的内容。
相关链接
- Mac OS X ABI Mach-O 文件格式参考
- Mach-O 编程主题
- 动态链接:ELF vs Mach-O
- 动态符号表对决:ELF vs Mach-O,第二轮
- 通过 Apple Mac OS X 上的动态加载器进行运行时二进制加载
- 让你的 Mach-O 飞起来 — Black Hat
- 高级 Mac OS X 物理内存分析 — Black Hat
- 破解 Mac OS X
历史
- 2011 年 4 月 26 日:初始帖子