OS X 上的动态链接库劫持 (上)

翻译水平有限,如有问题欢迎指出,谢谢。

OS X 上的动态链接库劫持 (上)


Patrick Wardle, Synack, USA.

翻译 i_82 <i.82@me.com>

译者注:该论文发表于 CanSecWest 2015,乌云知识库上同样有此篇文章的另一翻译,这篇翻译并非基于前者,特此声明。


动态链接库 (Dynamic Link Library, DLL) 劫持是一种广为人知的攻击方式,而人们一直认为,这种攻击方式只会影响 Windows 操作系统。但是,这篇论文将要证明的是,OS X 对于动态库劫持攻击 (dynamic library hijacks),也是同样脆弱 (be similarly vulnerable) 的。通过对 OS X 动态加载器 (dynamic loader) 的各个功能,以及未文档化 (undocumented) 的部分的恶意利用 (abuse),攻击者只需要“植入”精心设计构造 (specially crafted) 的动态库,就能将恶意代码 (malicious code) 自动载入易受威胁的应用程序 (vulnerable applications) 中。采用这种方式,此类攻击者能够执行很大范围内的恶意行为 (malicious) 和具有颠覆性 (subversive) 的破坏手段,包括,隐蔽驻留 (stealthy persistence),加载时进程注入 (load-time process injection),安全组件规避 (security software circumvention),与防火墙绕过 (Gatekeeper bypass, affording opportunities for remote infection. 为远程感染提供了支持)。因为这种攻击,恶意利用了操作系统合法的各项功能 (legitimate functionality),所以防范这种攻击不仅具有挑战性,而且似乎难以执行修补 (unlikely to be patched)。但是,这篇论文将展示一些技术和工具,能够发现 (uncover) 被感染的二进制可执行文件 (vulnerable binaries),也能够检测出是否发生了劫持。

背景 BACKGROUND

在详细介绍 OS X 上的动态库 (dylib) 劫持攻击之前,Windows 上的动态链接库 (DLL) 劫持将会被简要回顾 (briefly be reviewed)。因为这两种攻击方式从概念上来说非常相似,再次审视 (examining) 已得到充分理解 (well-understood) 的、在 Windows 上的攻击方式,有助于获得对前者的一些认识。

Windows 上基于动态链接库的劫持,Microsoft 已经给出了最佳解释:


当应用程序动态地载入动态链接库 (DLL),而未指定一个完全限定的路径名称 (specifying a fully qualified path name),Windows 会尝试通过搜索一个预先定义好的 (well-defined) 目录集合,来定位 DLL。如果攻击者非法获取了集合中任何一个目录的控制权限,就能够强制应用程序加载 DLL 的一份恶意副本,而并非原来的、所期望的 DLL。


在此重申 (reiterate),Windows 加载器默认的搜索行为,是在搜索 Windows 系统目录之前,先搜索其余各个目录 (例如应用程序目录或者当前工作目录)。如果应用程序采用一个未充分限定的路径 (即只指定了名称),以尝试加载系统库,这将会造成一些困难 (This can be problematic)。在这种情况下,一名攻击者也许能够在其中一个主搜索目录下,植入一个恶意的 DLL (其名称与某个合法的系统 DLL 名称相匹配)。而有了这个恶意的 DLL,Windows 载入器会在搜寻到合法的 DLL 之前,先搜寻到了攻击者的库,并且盲目地将其载入了受影响应用程序的上下文中。

这种攻击方式在图 1 和图 2 中进行了例证,受影响的应用程序 (图 1) 被已经实现植入到主搜索目录中的恶意 DLL 所劫持 (图 2)。

图 1 - 加载合法的系统 DLL, 图 2 - 加载攻击者的恶意 DLL

DLL 劫持攻击于 2010 年首次 (initially) 引来了骂名 (gained notoriety),并且迅速受到了媒体和恶意攻击者的关注。同时它又被人称作“二进制植入 (binary planting)”,“不安全库加载 (insecure library loading)”或“DLL 预制攻击 (DLL preloading)”。这项漏洞的发现,往往归功于 H.D. Moore [2, 3] (the discovery of this vulnerability is often attributed to H.D. Moore)。然而事实上,是美国国家安全局 (NSA) 首次,在 1998 年,也是早在 Moore 发现的 12 年前,注意到这一漏洞 (flaw)。在 NSA 未分类整理的一篇文档,“Windows NT 安全指导方案 (Windows NT Security Guidelines)” 当中,这一组织描述并为 DLL 劫持发出了警告:


重要的是,要确保渗透攻击者 (penetrators) 不能向这些目录的任何一个当中,插入一个 “伪造的” DLL,从而使得在搜索到一个合法的同名 DLL 之前,先搜索到恶意的 DLL。[4]


对于一名攻击者而言,DLL 劫持提供了许多有用的应用场景。举个例子,此类攻击能够允许一个恶意库被隐蔽地驻留 (在不修改注册表和操作系统其它组件的情况下),允许权限提升 (privileges to be escalated),并且甚至提供了远程感染的手段 (means)。

恶意软件作者们相当 (fairly) 迅速地意识到了 DLL 劫持的优势。在一篇以 “What the fxsst?” 的文章中,Mandiant (一家美国网络安全公司) 的研究员描述了他们是如何发现,为什么各种无关的恶意样本都被称作 “fxsst.dll”。

在更进一步的检查 (inspection) 当中, 他们发现,这些样本全都利用了 Windows Shell (Explorer.exe) 当中的一个 DLL 劫持漏洞,提供了一种隐蔽的、持久化的方式。特别地,因为 Explorer.exe 被安装在了 C:\Windows,在相同的目录植入一个叫做 fxsst.dll 的库能够导致恶意 DLL 被持久存储。因为加载器会在搜索存放合法 fxsst.dll 的系统目录之前,先搜索应用程序目录。

还有另一个利用了 DLL 劫持的、恶意软件的例子。这个例子是从一份被泄露的、银行木马程序 “Carberp” 源代码中得到的。这份源代码展示了通过利用 sysprep.exe 进行 DLL 劫持,来达到绕过用户账户访问控制 (User Account Control, UAC) 机制 (图 3) 的目的。sysprep.exe 执行后,是一个自动提权的进程,这意味着它不需要 UAC 提示就能进入高权限状态。不幸的是,它被发现是易被 DLL 劫持所利用的,因为它会加载一份恶意植入的 DLL (名字是 cryptbase.dll) 到它的提权进程上下文中。

图 3 - Carberp 利用 DLL 劫持来绕过 UAC

现如今,Windows 上的 DLL 劫持从某种程度上已经不常见了。Microsoft 迅速对攻击作出了相应,修补了易受威胁的应用程序,并且给出了如何使他人避免这一问题的、详细的解决方案 (即只需为导入的 DLL 指定一个绝对的、完全限定的路径)。此外,还引入了操作系统层面的缓解措施 (mitigations),通过 SafeDllSearchMode 与 (或) CWDIllegalInDllSearch 注册表键值,能够开启这项措施,通用地阻止绝大多数的 DLL 劫持攻击。

OS X 上的 Dylib 劫持

动态库劫持总是被默认为一个只有 Windows 才存在的问题。但是,正如一名有着远见卓识的 (astute) StackOverflow 用户于 2010 年所指出的,“任何允许第三方库动态链接的操作系统从理论上都容易受到动态库劫持的威胁” [9]。他的观点直到 2015 年才被证实 —— 这篇论文将揭示一种影响 OS X 的、同样惊人的动态库劫持攻击手段。

这项研究的目的是查明,OS X 是否易受动态库劫持的威胁。

特别地,这项研究想要解决的问题是:攻击者能否植入恶意的 OS X 动态库,而使操作系统的动态装载器自动将其装载至易受威胁的应用程序中?我们假设,与 Windows 上的 DLL 劫持及其类似,OS X 上这样的工具会给攻击者提供无数的 (myriad)、颠覆性的能力 (subversive capabilities)。比如,隐蔽驻留、加载时进程注入、安全组件规避,甚至也许还有“远程”感染能力。

理应注意的是,在这项任务之上,有着一些约束限制。首先,成功被限制于,除了创建文件及必需的目录之外,不允许对系统进行任何修改。换句话说,这次研究忽略了,需要颠覆现有二进制文件 (比如 patching),或对现存操作系统设置文件的修改 (比如 “auto-run” 属性表等等),这一攻击设想 (attack scenarios)。因为这种形式的攻击广为人知,而且防范和检测都微不足道,所以我们将之忽略。这项研究同样寻找了一种劫持方式,完全独立于用户环境。OS X 提供了各种合法的手段,通过能够强制装载器自动将恶意库,装载到目标进程中的方式 (in a manner that),来控制环境。这些手段,诸如设置 DYLD_INSERT_LIBRARIES 环境变量,都是用户指定的,并且,也是众所周知和容易检测的。这一类,都是没什么意义的,并且我们将之忽略。

这项研究从分析 OS X 动态链接器和装载器 dyld 开始。这个在 /usr/bin 中找到的二进制文件,提供了标准的装载器和链接器功能,包括查找、加载和链接动态库。

因为 Apple 已经将 dyld 开源 [10],分析相当的直接。比如,阅读源代码能为 dyld 的操作,当一个可执行文件被加载,并且它依赖的库被加载和链接起来的时候,提供了一种合适的理解 (a decent understanding)。接下来简要地概括一下 dyld 所做的初始几步 (关注于那些与这篇论文中所描述的与攻击相关的内容)。


  1. 当任何新进程启动之后,内核将用户模式入口点 (user-mode entry point) 设置为指向 __dyld_start (dyldStartup.s)。这一功能简单地建立了栈,然后跳转到了 dyldbootstrap::start(),它会依次调用装载器的 _main();

  2. Dyld 的 _main() 函数 (dyld.cpp) 唤起 (invokes) link(),而 link() 接着调用一个 ImageLoader 对象的 link() 方法,以开始对主可执行文件 (executable) 的链接过程;

  3. ImageLoader 类暴露 (exposes) 了许多函数,dyld 调用它们,从而执行各个二进制文件映像载入逻辑。举个例子,这个类包含了一个 link() 方法,当调用它的时候,会唤起对象的 recursiveLoadLibraries() 方法,以执行所有依赖动态库的加载。

  4. ImageLoader 的 recursiveLoadLibraries() 方法决定了所有必需的库,并且唤起了它们每一个的 context.loadLibrary() 方法。context 简单来说是一个函数指针的结构体,在方法与函数之间进行传递。这个结构的 loadLibrary 成员由 libraryLocator() 函数 (dyld.cpp) 进行初始化,而该函数只是简单地调用了 load() 函数。

  5. load() 函数 (dyld.cpp) 调用了同样文件中的各个 helper 函数,从 loadPhase0() 到 loadPhase5()。每个函数都负责解决装载过程的特定任务,比如解析路径,或是处理能够影响装载过程的环境变量。

  6. 在调用 loadPhase5() 之后,loadPhase6() 函数最后从系统将必需的 dylib 载入 (映射) 进内存。然后它调用了一个 ImageLoaderMachO 类的实例,并以此执行每一个 dylib 上 Mach-O 特定的加载和链接逻辑。

在基本理解了 dyld 的初始加载逻辑过后,此研究转而寻找,能被利用来执行 dylib 劫持的逻辑。特别地,此研究关注于装载器的相关代码,比如,如果无法找到某个 dylib 时处理错误的代码,或者在多个位置搜索 dylib 的代码。一旦这些对于装载器的其中一个设想成立,那么执行 OS X dylib 劫持就是有希望的。

初始的设想已经被首先查明。在这个案例中,假设装载器解决无法找到某个 dylib 的情况时,一名攻击者 (能够辨明这种情况的人) 能够在推测的位置放置一个恶意的 dylib。在那之后,装载器将能够“查找”到植入的 dylib,并盲目地载入攻击者的恶意代码。

回顾装载器调用 ImageLoader 类的 recursiveLoadLibraries() 方法以查找和载入所有的库。正如图 4 中所展示的,载入的代码被包裹在了一个 try/catch 块当中以检查载入失败的 dylib。

图 4 - dylib 载入失败时的处理错误的逻辑

不出意料地,如果库加载失败,这里有着处理异常 (带有一条信息) 的逻辑。相当有趣的是,只有当一个名为 “required” 的变量设置为 true 的时候,这个异常才会被抛出。此外,代码中的注释表明,载入 “weak” 库失败是被允许的。这看上去表明,在某些情况下,装载器允许一些库的丢失 —— 这真是太棒了!

深入装载器的源代码,能够揭示 “required” 变量是在哪里设置的。特别地,ImageLoaderMachO 类中的 doGetDependentLibraries() 方法解析 (parses) 了装载指令 (load commands, 随后将具体阐述) 并根据装载指令是否是 LC_LOAD_WEAK_DYLIB 的类型,设置该变量。

装载指令是是 Mach-O 文件格式 (OS X 的原生二进制文件格式) 必需的组成部分 (integral component)。嵌入并紧跟着 Mach-O 头部后面 (Embedded immediately following the Mach-O header),它们提供了给装载器的各个指令。举个例子,有些指令用来指定二进制文件的内存布局,有些指定主线程的初始执行状态,有些制定了二进制所依赖动态库的相关信息。查看经过编译的二进制文件的装载指令,可以使用 MachOView [11] 或者 /usr/bin/otool -l (如图 6)。

图 5 - 设置 “required” 变量 (源文件 ?), 图 6 - 使用 MachOView 转储 Calculator.app 的装载指令

图 5 中的代码展示了装载器遍历了二进制文件中所有的装载指令,寻找那些指定 dylib 导入行为的。这些装载指令的格式 (比如 LC_LOAD_DYLIB,LC_LOAD_WEAK_DYLIB 等) 可以在 mach-o/loader.h 文件中找到。

图 7 - LCLOAD* 装载指令的格式

对于每个可执行文件紧密链接的 dylib 来说,它们都会包含 LCLOAD* (LC_LOAD_DYLIB,LC_LOAD_WEAK_DYLIB 等) 装载指令。正如图 4 和图 5 中装载器代码例证的那样,LC_LOAD_DYLIB 装载指令指定了必需的 dylib,而通过 LC_LOAD_WEAK_DYLIB 导入的库 (即 “weak”) 则是可选的。在前者的情况下,如果无法找到必需的 dylib,将会抛出一个异常,导致装载器终止并结束进程。但是,在后者 (LC_LOAD_WEAK_DYLIB) 的情况下,dylib 是可选的。如果这样的 “weak” dylib 无法找到,将不会造成任何损害,并且主二进制文件将仍然能继续执行。

图 8 - 尝试加载一个 “weak” dylib (LC_LOAD_WEAK_DYLIB)

这种装载器逻辑满足了第一种假设的劫持场景,并正因为这样,提供了一种 OS X 上进行 dylib 劫持的攻击方式。即,如图 9 中例证的,如果一个二进制文件指定了某个无法找到的 weak 导入,攻击者能够在推测出的位置放置一个恶意 dylib。在那之后,装载器将会“找到”攻击者的 dylib 并且盲目地将这些恶意代码载入到受影响二进制文件的进程空间中。

图 9 - 通过一个恶意的 “weak” dylib 劫持应用程序

回顾前文,另一种劫持攻击,假设 (hypothesized) 存在一种设想,装载器会在多个位置搜寻动态库。在这种情况下,据认为 (it was thought that) 一名攻击者能够在其中一个主搜索目录中放置一个恶意 dylib (如果合法的 dylib 出现在另一个位置)。有可能装载器那时会首先找到攻击者的恶意 dylib,并且这样,天真地载入了攻击者的恶意库。

在 OS X 上,装载指令,比如 LC_LOAD_DYLIB 总是为动态库指定一条路径,相对于 (as opposed to) Windows 来说,也许只提供了库的名称。因为提供了一条路径,dyld 通常不需要搜索各个目录,寻找动态库。作为替代,它能够简单地,直接到指定的目录中去加载 dylib。但是,分析 dyld 的源代码,揭露了一种情况,这种一般性的行为并没有发生。

看看在 dyld.cpp 中的 loadPhase3() 函数,披露出了一些有趣的逻辑,如图 10 所示。

图 10 - 载入 “rpath” 依赖库

Dyld 将会遍历一个 rp->paths 向量,动态地创建路径 (出现在 “newPath” 变量内),这些路径之后会通过 loadPhase4() 函数加载。这的确看上去满足 (fulfil) 第二种劫持设想的需要 (即 dyld 在不同的位置搜索同样的 dylib),但还需要进一步的测试。

如图 10 所示,dyld 源代码第一行中的注释,提到了术语 (term) “@rpath”。根据 Apple 参考文档,这是一个特殊的装载器关键词 (在 OS X 10.5, Leopard 中被引入),将一个动态库指定为一个 “run-path-dependent library” [12]。Apple 解释到,run-path 依赖库是 “被创建时,它的完整安装名称 (路径) 尚不清楚的一个依赖库。” [12]。其它的在线文档,比如 [13] 和 [14] 提供了更多细节,描述了这些库的角色和作用,并且解释了 @rpath 关键词是如何启用的:“框架和动态库最终将只构建一次,同时用于系统范围的安装和内建,而不改变它们的安装名;并且允许应用程序为所提供的库提供替代位置,或者,甚至覆写为一个深度内建的库所指定的位置” [14]。

当这项功能允许软件开发者更容易部署复杂的应用程序,它同样能够被非法利用 (abuse) 来执行 dylib 劫持。这的确可行,因为,为了充分利用 run-path 依赖库,“一个可执行文件提供了 run-path 搜索路径的列表,这一列表将会被动态装载器在装载时遍历 (traverses)” [12]。这个问题能够在 dyld 代码中的各个地方被意识到,包括图 10 中所展示的代码片段。

因为 run-path 依赖库相对来说比较新奇 (novel),并且有点儿不为人所知,所以提供一个构造合法的 run-path 依赖库和一个链接到它的示例程序,看起来会更考虑周到 (prudent) 一些。

一个 run-path 依赖库,是一个正常的库,它的安装名以 “@rpath” 为前缀修饰。为了在 Xcode 中创建一个这样的库,只需要将 dylib 的安装目录设置为 “@rpath”,正如图 11 所示。

图 11 - 构建一个 run-path 依赖库

run-path 依赖库编译完成之后,LC_ID_DYLIB 装载指令 (包含了关于 dylib 的识别信息) 显示了 dylib 的 run-path。特别地,LC_ID_DYLIB 装载指令中的 “name” (path) 包含了 dylib 的 bundle 路径 (rpathLib.framework/Versions/A/rpathLib),以 “@rpath” 关键词为前缀修饰。

图 12 - 内建在 dylib 的 “安装名” (路径) 中的 “@rpath”

创建一个链接了 run-path 依赖库的应用程序也同样相当 (fairly) 直接。首先,将 run-path 依赖库添加到 Xcode 的 “Link Binary With Libraries” 列表中。然后将 run-path 搜索目录添加到 “Runpath Search Paths” 列表中。正如将要展示的那样,在装载时,这些搜索目录会被动态装载器遍历,以定位 run-path 依赖库。

图 13 - 链接 “@rpath” dylib 并指定 run-path 搜索路径

应用程序构建完成之后,对其装载指令的转储,揭露了与 run-path 库依赖相关联的各个指令。这里出现了,标准的 LC_LOAD_DYLIB 装载指令,为了描述对于 run-path 依赖 dylib 的依赖关系,如图 14 所示。

图 14 - 对 “@rpath” dylib 的依赖关系

在图 14 中,值得注意的是,指代 run-path 依赖 dylib 的安装名,以 “@rpath” 为前缀,而且匹配 run-path 依赖 dylib 中 LC_ID_DYLIB 装载指令中的名称值 (图 12)。

该应用程序内建的 LC_LOAD_DYLIB 装载指令,带有 run-path 依赖 dylib 的值,告诉装载器,“我依赖于 rpathLib 这个 dylib,但是当我被构建的时候,我不知道它将会安装在哪,所以请在装载的时候,用我内建的 run-path 搜索路径进行搜寻,并且装载它!”

run-path 搜索路径,被输入到 Xcode 的 “Runpath Search Paths” 列表中,生成了 LC_RPATH 装载指令 —— 每个搜索目录都有一条。对编译好的应用程序进行转储,揭露了内建的 LC_RPATH 装载指令,如图 15 所示。

图 15 - 内建的 run-path 搜索路径 (目录)

现在对于 run-path 依赖 dylib 有了一个实际的理解,并且也有了一个链接到它的应用程序,现在能够很容易理解 dyld 的源代码中,负责在装载时处理这种场景的代码了。

当一个应用程序启动后,dyld 将解析其 LCLOAD* 装载指令,以装载和链接所有依赖的 dylib。为了处理 run-path 依赖库,dyld 执行了两个清晰 (distinct) 的步骤:它取出 (extract) 了所有内建的 run-path 搜索路径,然后使用该列表寻找并加载所有的 run-path 依赖库。

为了取出所有内建的 run-path 搜索路径,dyld 调用了 ImageLoader 类中的 getRPaths() 方法。该方法 (被 recursiveLoadLibraries() 方法所调用) 简单地为应用程序解析了所有的 LC_RPATH 装载指令。对于每一个这样的装载指令,它取出了 run-path 搜索路径,并且将其添加到一个向量中 (vector,向量,可以理解为列表),如图 16 所示。

图 16 - 取出并保存所有的内建 run-path 搜索路径

有了包含所有内建 run-path 搜索路径的列表,dyld 现在能够“解析”所有依赖的 run-path 依赖库。此处的逻辑在 dyld.cpp 的 loadPhase3() 函数中被执行。特别地,代码 (如图 17 所示) 检查了依赖库的名称 (路径) 是否是以 “@rpath” 关键词为前缀。如果是这样,dyld 会遍历取出的 run-path 搜索路径的列表,将导入路径中的 “@rpath” 关键词以当前搜索路径替换。然后,dyld 会尝试从新解析出的目录中加载 dylib。

图 17 - 搜索 run-path 搜索目录以搜寻 “@rpath” dylib

重要的,需要注意的一点是,dylid 搜索的目录顺序是确定的 (deterministic),并且是符合内建的 LC_RPATH 装载指令的顺序的。同样,正如图 17 中代码片段所示,在找到依赖 dylib 之前,或所有的路径都被穷举殆尽之前,搜索将会一直持续。

图 18 从概念上例证了这次搜索。可以看到,装载器 (dyld) 搜索了各个内建的 run-path 搜索路径,为了找到必需的 run-path 依赖 dylib。注意在这个例子假设的情况下,dylib 是在第二个搜索目录 (即非主目录) 中被找到的 (如图 18)。

图 18 - Dyld 搜索多个 run-path 搜索目录

精明的读者们能够意识到,这样的装载器逻辑为 dylib 劫持攻击开辟了一条新的大道。特别地,如果应用程序连接了 run-path 依赖库,有着多个内建的 run-path 搜索路径,并且在主搜索路径无法搜索到某些 run-path 依赖库,攻击者就能够利用,并实施此类劫持。这样的劫持能够通过简单地,在任意一个主搜索路径中,植入恶意 dylib 来实现。有了植入好的恶意 dylib,在随后的任何时候,应用一旦运行,装载器都会首先找到恶意 dylib,并且盲目地装载它 (如图 19)。



图 19 - 通过一个恶意的 “@rpath” dylib 劫持一个应用程序

总结一下至今为止,我们的发现:OS X 系统会受到劫持攻击的影响,鉴于可能会存在任何满足如下条件的应用程序:


  1. 包含引用了不存在的 dylib 的 LC_LOAD_WEAK_DYLIB 装载指令

  2. 同时包含,引用了 run-path 依赖库 (“@rpath”) 的 LC_LOAD*_DYLIB 装载指令,和多个 LC_RPATH 装载指令,并且在主 run-path 搜索路径中找不到 run-path 依赖库。

本文的剩余部分将首先按步阐述一次完整的 dylib 劫持攻击,然后展示几种不同的攻击场景 (驻留,加载时进程注入,“远程”感染等),最后将总结一些防范 (counter) 此类攻击的方法。

为了帮助读者更深入地理解 dylib 劫持攻击,我们将给出一次尝试劫持攻击的细节,遇到的错误,和最终成功的完整过程,这样似乎更加周到一些。有了这些知识的武装,就能够非常容易地 (trivial),理解自动化攻击、攻击各类场景,以及如何进行实际防御了。

回顾之前所描述的,为了例证和解释如何链接 run-path 依赖 dylib 所创建的,样本应用程序 (“rPathApp.app”)。该应用程序将会成为我们劫持攻击的目标。

dylib 劫持只可能针对存在漏洞的应用程序 (也就是说,满足前面描述的两种劫持情况的一种)。 因为本样例程序 (“rPathApp.app”) 链接了一个 run-path 依赖 dylib,因此它也许对于第二种劫持设想,是易受威胁的。最简单的、检测此类弱点的方法,是开启装载器的调试日志输出功能 (logging),然后在命令行中简单地运行该程序即可。为了启用这种日志输出功能,需要设置 DYLD_PRINT_RPATHS 环境变量。这将会导致 dyld 打印有关 @rpath 路径展开 (expansions) 和 dylib 装载尝试的日志信息。查看输出能够很快地揭露任何易受威胁的路径展开 (即 第一次关键的展开操作指向了一个不存在的 dylib),如图 20 所示。

图 20 - 易受威胁的 (测试) 应用程序,rPathApp

图 20 展示了加载器第一次寻找必需的目标 dylib (rPathLib) 时,在指定位置没有发现。与图 19 中显示的一样,在这种情况下,攻击者可以在该主 run-path 搜索目录中植入恶意 dylib,并且之后,装载器将会盲目地加载它。

我们创建了一个简单的 dylib,来扮演劫持者的恶意库。为了在加载时得到自动执行,dylib 实现了一个构造函数。该构造函数在 dylib 成功装载时会被操作系统自动执行。这是一个很好的特性,可以加以利用,因为通常情况下,dylib 当中的代码不会得到执行,除非主应用程序通过一些导出函数调用到它们。

图 21 - dylib 中的构造函数将会被自动执行

编译完成后,将 dylib 重命名,以匹配目标库 (合法的) 名称:rPathLib。接下来,创建需要的目录结构 (Library/One/rpathLib.framework/ Versions/A/),并将“恶意的” dylib 拷贝进去。这保证了无论何时启动应用,dyld 在搜索 run-path 依赖 dylib 的过程中,将会首先寻找 (和加载) 劫持者的 dylib。

图 22 - “恶意的” dylib 被放置在了主 run-path 搜索路径中

不凑巧,初次劫持的尝试失败了,并且应用程序意外地崩溃了,如图 23 所示。

图 23 - 成功!然后崩溃并燃烧了起来

虽然失败了,但,好消息是,装载器找到了并且尝试装载劫持者的 dylib (看图 23 中,“RPATH successful expansion…” 的调试信息)。并且,尽管程序崩溃了,但是崩溃之前,dylib 装载器还是抛出了一条详尽、完备的异常信息。这条异常似乎是自解释的:劫持者 dylib 的版本与必需的 (或期望的) 版本不兼容。重新深入装载器的源代码,找到了触发这次异常的代码片段,如图 24 所示。

正如我们所看到的那样,装载器唤起了 doGetLibraryInfo() 方法,从被装载库的 LC_ID_DYLIB 装载命令中,提取兼容性和当前版本号。提取出的兼容版本号 (“minVersion”) 之后会与应用程序所需要的进行对比。如果版本号太低,一个不兼容的异常就会被抛出。

修复这一不兼容问题 (并且这样能够避免异常) 相当容易,只需在 Xcode 当中更新一下版本号,然后重新编译,如图 25 所示。

图 25 - 设置兼容性及当前版本号

转储重编译的劫持者 dylib 中的 LC_ID_DYLIB 装载指令,确认了已被更新的版本号 (并且现在已经兼容了),如图 26 所示。

图 26 - 内建的兼容性与当前版本号

更新版本号过后的劫持者的 dylib,被重新拷贝到了应用程序的主 run-path 搜索目录中。重新启动存在漏洞的应用程序,发现装载器“找到了”劫持者的 dylib,并尝试装载它。哎呀 (Alas, 语气词),尽管 dylib 现在似乎已经兼容了 (即版本号检查通过),但是抛出了一个新的异常,应用程序也再次崩溃了。如图 27 所示。

图 27 - “符号未找到” 异常

再一次,异常信息非常的详细,清楚地解释了装载器为什么抛出它,并且因此强行终止了应用程序。应用程序链接动态库的目的就是,为了获取动态库被导出的的功能 (比如函数,对象等)。一旦必需的 dylib 被装载进内存,装载器将尝试解析 (通过导入符号表) 依赖库试图导出的、必需的功能,如果功能未被发现,那么链接失败,装载与链接进程以外终止,并因此使进程崩溃。

有几种方法能够确保劫持者的 dylib 导出了正确地符号表,这样它才能被完全地链接进目标程序。其中一个简单的方法,就是让劫持者的 dylib 直接仿造目标 (合法的) dylib 所有的导出信息,实现并且导出代码。尽管这样也许会成功,但是似乎有些复杂,并且只适用于特定的 dylib (即针对其它目标 dylib 时需要导出其它符号)。一个更为优雅的方案是,简单地指引编译器到其它的地方搜寻它需要的符号。当然,也就是到原来的、合法的 dylib 中去找。在这个场景当中,劫持者的 dylib 将简单地扮演一个代理,或者一个 “re-exporter” dylib,并且,因为装载器将跟随它的重导出指令,也就不会有链接错误被抛出。

图 28 - 重导出到合法的 dylib

需要付出一些努力,才能让重导出功能无缝地 (完美地) 工作。第一步是回到 Xcode,将多个链接器标识 (flag) 添加到劫持者 dylib 项目中。这些标识包括 “-Xlinker”,“reexport_library”,然后,还有指向目标库的,包含易受威胁应用程序所依赖的,实际导出信息的路径。

图 29 - 开启重导出的、必需的链接器标识

这些链接器标识生成了一个内建的 LC_REEXPORT_DYLIB 装载指令,包含了指向目标 (合法的) 库的路径,如图 30 所示。

图 30 - 内建的 LC_REEXPORT_DYLIB 装载指令

但是,还没有完,因为劫持者 dylib 的重导出目标是一个 run-path 依赖库,内建的 LC_REEXPORT_DYLIB 中的名称字段 (从合法 dylib 的 LC_ID_DYLIB 装载指令中取出的),是以 “@rpath” 开头的。这样是存在问题的,因为不像 LC_LOAD*_DYLIB 装载指令,dylib 不会解析 LC_REEXPORT_DYLIB 中的 run-path 依赖路径。换句话说,装载器将会尝试从文件系统中直接装载 “@rpath/rpathLib.framework/Versions/A/rpathLib”。这样,当然,很显然会失败。

解决方案是,处理内建的 “@rpath” 路径,提供 LC_REEXPORT_DYLIB 装载指令中目标库的完整路径。这需要借助 Apple 的一款开发者工具:install_name_tool。为了更新 LC_REEXPORT_DYLIB 装载指令中内建的安装名称 (路径),该工具执行的时候,需要使用 -change 标识,现有存在的名称 (LC_REEXPORT_DYLIB 中的),新名称,最后是指向劫持者 dylib 的路径,如图 31 所示。

图 31 - 使用 install_tool_name 更新内建的名称 (路径)

在 LC_REEXPORT_DYLIB 装载指令中的路径正确更新后,劫持者 dylib 被重新拷贝到了应用程序的主 run-path 搜索目录,然后应用程序被重启。如图 32 所示,最终成功执行了。

图 32 - 成功地在一个易受威胁的应用程序上实现 dylib 劫持

总结一下,因为 rPathApp 应用程序连接了一个 run-path 依赖库,而无法在初始的 run-path 搜索目录中找到该依赖库,因此它是容易受到 dylib 劫持攻击的威胁的。 在初始搜索目录中,植入一个特殊兼容的恶意 dylib,会导致装载器,在每次应用程序启动时,都盲目地装载劫持者的 dylib。因为该恶意 dylib 包含了正确的版本信息,并且也重导出了合法 dylib 中所有的符号,所有需要的符号都能被解析,因此保证了应用程序中的各项功能不会丢失或受损。


OS X 上的动态链接库劫持 (下)