【51CTO.com独家特稿】随着恶意PDF文件日益增多,人们对这种文档的恶意代码分析技术也越来越感兴趣。本文将教您如何分析一种特殊类型的恶意PDF文件:它们可以利用内嵌JavaScript解释器的安全漏洞。通过阅读本文,还有助于分析其他类型的恶意PDF文件,如利用PDF解析器内的安全漏洞的情形。虽然几乎所有的恶意PDF文档的攻击目标都是Windows操作系统,但是这里介绍的PDF语言是独立于操作系统的,它同时适用于在Windows、Linux和OSX上的PDF文档。
一、PDF中的Hello World
现在,我们从手工制作一个最简单的PDF文档开始入手,该文档只是在一个页面中显示文字Hello World而已。您很可能从未见过如此简陋的文档,但是它很适合于本文的需要,因为我们只对一个PDF文档的内部构造感兴趣。我们的文档仅仅包含显示一个页面所必需的最基本元素,如果您为该文件添加更多的格式的话,可读性会更好一些。该文档的特性之一是,只包含有一些ASCII字符,因此即使使用记事本这样最简单的编辑器,同样也能阅读它的内容。另一个特性是,其中含有大量(多余的)空格和缩排,这使得这个PDF的结构更加突出。最后一个特性是,其中的内容没有进行压缩处理。
二、头部
每个PDF文档必须以标明其为PDF文档的一行代码(即幻数)开头;它还规定了用于描述这个文档的PDF语言规范版本号:%PDF-1.1。
在一个PDF文档中,以符号%开头的行都是注释行,注释行的内容将被忽略,但是有两个例外:
文档的开头:%PDF-X.Y
文档的结尾:%%EOF
三、对象
在第一行之后,我们开始为我们的PDF文档添加对象,对象是PDF语言的基本元素。这些对象在文件中的出现顺序对页面显示时的布局没有任何影响。不过为简单起见,我们将按照逻辑顺序来介绍这些对象。需要注意的是,PDF语言是大小写敏感的。
我们首先介绍的是catalog(即目录)对象,它告诉PDF阅读程序(例如Adobe的Acrobat Reader),为了装配这个文档,需要从哪里开始查找对象:
1 0 obj /Type /Catalog /Outlines 2 0 R /Pages 3 0 R |
这实际上是一个间接对象,因为它具有一个编号,并且可以通过该编号来引用该对象。其语法很简单:一个编号、一个版本号、单词obj、对象本身,最后是单词endobj,如下所示:
1 0 obj object endobj |
通过联合使用对象编号和版本号,我们就能够唯一地引用一个对象。
我们第一个对象catalog的类型是字典类型,字典类型在pdf文档中非常常见。该类型以符号<<开头,并以符合>>作为结束,如下所示:
dictionary content |
字典的元素由键和值两部分组成,也就是说一个元素就是一个名/值对,即数据有一个名称,还有一个与之相对应的值;字典不仅可以存放元素,而且还能存放对象甚至其他字典。 大部分字典都是利用第一个元素来声明自身的类型,该元素以/type为键,其后跟一个类型本身的名称(对本例而言就是/Catalog)为值:
(/Type /Catalog) |
对象catalog必须给出在这个PDF中能找到的页面(对应于pages对象)和大纲(对应于outline对象),如下:
/Outlines 2 0 R /Pages 3 0 R |
2 0 R和3 0 R 分别表示引用间接对象2和间接对象3。间接对象2描述大纲,间接对象3描述页面。
下面开始为我们的PDF文档添加第二个间接对象:
2 0 obj /Type /Outlines /Count 0 endobj |
通过前面对间接对象1的说明,您现在应该对这个对象的语法并不陌生了。这个对象是一个/Outlines类型的字典。它具有一个键为/Count、值为0的元素,这意味着这个PDF文档没有大纲。我们可以通过编号2和版本0来引用这个对象。
让我们总结一下我们的PDF文档已有的内容:
PDF标识行
间接对象1:catalog
间接对象2:outline
在添加文字页面之前,让我们演示PDF语言的另一个特性。我们的1号对象catalog引用了我们的2号对象outline,如图1所示。
图1 引用间接对象 |
PDF语言还允许我们把2号对象直接嵌入到1号对象中,如图2所示。
图2 被嵌入到对象中的间接对象 |
事实上,outline对象的长度只有一行,并且对语义也没有什么影响,现在只是为了可读性才加上。先不管它,我们继续组装我们的PDF文档。我们前面定义了catalog(目录)和outlines(大纲)对象,接下来还得定义我们的页面。
除/Kids元素之外,下面的代码应该很容易理解。Kids 元素是一个页面列表;一个列表必须用方括弧括住。因此依据这个Pages对象来看,我们的文档中只有一个页面;这个页面的具体规定,见间接对象4(注意引用4 0 R ):
3 0 obj /Type /Pages /Kids [4 0 R] /Count 1 endobj |
要描述页面,我们必须规定页面的内容、用于显示这个页面的资源以及页面的大小。这些任务可由下面的代码来完成:
4 0 obj /Type /Page /Parent 3 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << /ProcSet [/PDF /Text] /Font << /F1 6 0 R >> endobj |
页面内容是由间接对象5来规定的。/MediaBox 是页面的尺寸。这个页面所用的资源是字体和PDF文字绘制例程。我们在间接对象6中将字体规定为[F1]。
间接对象5中存放的是页面内容,它是一种特殊的对象,即流对象。流对象可以用来保存由单词stream和endstream包围的对象内容。流对象的好处是允许使用多种类型的编码技术(在PDF语言中称为过滤器),例如压缩(例如zlib FlateDecode编码)。考虑到易读性,我们没有在这个流中实施压缩处理:
5 0 obj stream BT /F1 24 Tf 100 700 Td (Hello World) Tj ET endstream endobj |
这个流的内容是一组PDF文字绘制指令。这些指令是由BT和ET括起来的,实际上就是命令绘制例程做下面的事情:
使用大小为24的F1字体
转到100 700位置处
绘制文字:Hello World
在PDF语言中,字符串必须用圆括号括起来。
我们的PDF文档已经基本上组装好了。我们需要的最后一个对象是font(字体)对象:
6 0 obj /Type /Font Subtype /Type1 /Name /F1 /BaseFont /Helvetica /Encoding /MacRomanEncoding endobj |
现在,阅读这个结构您应该没有问题了。#p#
四、尾部
上面就是绘制一个页面所需的全部对象。但是仅有这些内容还不足以使阅读程序(即显示pdf文档的程序,如Adobe的Acrobat Reader)来读取和显示我们的PDF文档。绘制例程需要知道文档描述起始于哪个对象(即root对象),以及每个对象的索引之类的技术细节。
每个对象的索引称为交叉引用xref,它描述每个间接对象的编号、版本和绝对的文件位置。PDF文档中的第一个索引必须从版本为65535的0号对象开始:
标识符xref后面的第一个数字是第一个间接对象(这里是0号对象)的编号,第二个数字是xref表(7个表项)的大小。
第一栏是间接对象的绝对位置。第二行的值12表明间接对象1的起始地址距文件开头为12字节。第二栏是版本,第三栏指出对象正在使用(用n表示)还是已经释放(用f表示)。
定义交叉引用之后,我们在尾部中定义root对象:
trailer /Size 7 /Root 1 0 R |
不难看出,这是一个字典。最后,我们需要利用xref元素的绝对位置和幻数%%EOF来结束这个PDF文档:
startxref 644 %%EOF |
其中,644是在这个PDF文件内的xref的绝对位置。
五、PDF文档基础知识的回顾
我们一旦了解了PDF语言的语法和语义,就能轻松构建一个简单的PDF文档。为了便于阅读,我们在清单1中给出了完整的PDF文档。
%PDF-1.1 1 0 obj /Type /Catalog /Outlines 2 0 R /Pages 3 0 R endobj 2 0 obj /Type /Outlines /Count 0 endobj 3 0 obj /Type /Pages /Kids [4 0 R] /Count 1 endobj 4 0 obj /Type /Page /Parent 3 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << /ProcSet [/PDF /Text] /Font << /F1 6 0 R >> endobj 5 0 obj /Length 43 >> stream BT /F1 24 Tf 100 700 Td (Hello World) Tj ET endstream endobj 6 0 obj /Type /Font / Subtype /Type1 /Name /F1 /BaseFont /Helvetica /Encoding /MacRomanEncoding endobj xref 0 7 0000000000 65535 f 0000000012 00000 n 0000000089 00000 n 0000000145 00000 n 0000000214 00000 n 0000000419 00000 n 0000000520 00000 n trailer /Size 7 /Root 1 0 R startxref 644 %%EOF |
清单 1 完整的PDF文档页面内容#p#
六、添加有效载荷
因为我们想要分析带有JavaScript有效载荷的恶意PDF文档,因此需要了解如何添加JavaScript代码并设法使其运行。PDF语言支持为事件关联相应的动作。举例来说,当某个页面被查看的时候,可以执行相应的动作(例如 访问一个网站)。我们感兴趣的是在打开一个PDF文档的时候执行某个动作。通过为catalog对象添加一个/OpenAction键,我们可以让PDF文档在打开时无需人工介入就执行某个动作。
1 0 obj /Type /Catalog /Outlines 2 0 R /Pages 3 0 R /OpenAction 7 0 R endobj |
当打开我们的PDF文档的时候,间接对象7所规定的动作将被执行。我们可以规定一个URI动作。一个URI动作能够自动地打开一个URI,在我们这个例子中是一个URL:
7 0 obj /Type /Action /S /URI /URI (https://DidierStevens.com) endobj ss |
七、内嵌的 JavaScript
PDF语言支持内嵌的 JavaScript。然而,这个JavaScript引擎在与底层操作系统的交互方面能力非常有限,所以根本没法干坏事。 举例来说,嵌入到一个PDF文档的JavaScript代码不能访问任何文件。所以,恶意PDF文档必须利用某些安全漏洞才能摆脱JavaScript引擎的限制来执行任意的代码。我们可以使用下面的JavaScript动作,在PDF文档打开时添加并执行一些JavaScript脚本:
7 0 obj /Type /Action /S /JavaScript /JS (console.println("Hello")) endobj |
下面的代码将执行一个向JavaScript调试控制台显示Hello的脚本:
console.println("Hello") |
八、安全漏洞的利用
去年,许多恶意PDF文档利用了PDF的util.printf方法的JavaScript安全漏洞来发动攻击。Core Security Technologies发表了一个通报,其中含有一个PoC,如下所示:
var num = 12999999999999999999888888..... util.printf(„%45000f”,num) |
当这个JavaScript将被嵌入到一个PDF文档并在(在 Windows XP SP2上使用Adobe Acrobat Reader 8.1.2)打开的时候,它将试图执行地址0x30303030的代码而造成访问越界。 这意味着,通过缓冲区溢出,执行PoC将跳转至地址0x30303030(0x30是ASCII字符0的十六进制表示法)(即PoC一执行,控制流程(系统控制权)就交给0x30303030处的指令)。 因此要想利用该漏洞,我们需要编写我们的程序(shellcode),当然该程序需要从0x30303030处开始执行。
使用内嵌的JavaScript的问题是,我们不能直接写内存。Heap Spraying(堆喷射)是一种较易获得任意代码执行Exploit的技术手段。 每当我们在JavaScript中声明字符串并为其赋值时,这个字符串就会被写到一段内存中,这段内存是从堆中分配的,所谓堆就是专门预留给程序变量的一部分内存。 我们没有影响被使用的那些内存,所以我们不能命令JavaScript使用地址0x30303030的内存。 但是如果我们分配大量的字符串,那么很可能其中的一个字符串分配的内存中包含了地址0x30303030。 分配许许多多的字符串称为Heap Spraying(堆喷射)。
如果我们在堆喷射之后执行我们的PoC,就很有可能得到一个从地址0x30303030之前的某处开始、从地址0x30303030之后的某处终止的字符串,这样的话,该字符串中(起始于地址0x30303030)那些字节就会被CPU当作机器代码语句来执行。
但是,如何让我们指定的字符串包含用来利用起始于地址0x30303030的机器指令的正确语句呢? 同样,我们也无法直接完成这个任务;我们需要一种迂回战术。
如果我们设法使一个字符串被CPU当作机器代码程序(shellcode)来解释的话,那么CPU将开始执行我们从地址0x30303030开始的程序。 不过这个方法不太理想;我们的程序必须从它的第一条指令开始执行,而不是从中间的某个地方开始执行。为了解决这个问题,我们需要在程序前面填充大量NOP指令。 我们在用于堆喷射的字符串中存储这个NOP-sled,继之以我们shellcode。 NOP-sled是一个特殊程序,它的特性是每个指令的长度都是单字,而且每个指令都没有时间的操作(NOP,即空操作 ),那就是说 CPU不断执行下一个NOP指令,如此下去直到它到达我们的shellcode并且执行它(滑下NOP-sled)。
下面是一个堆喷射的范例,实际取自一个带有NOP-sled和shellcode的恶意PDF文档(参见 图 3)。
图3 JavaScript堆喷射 |
Sccs是带有shellcode的字符串,bgbl是带有NOP-code的字符串。
因为shellcode常常必须很小,所以它将通过网络下载另一个程序(恶意软件)并执行它。对于pdf文档来说,还有一种方法可用。第二阶段的程序可以嵌入到PDF文档,而shellcode可以从PDF文档提取并且执行。
九、分析恶意PDF文档
事实上,所有的pdf文档都包含非ASCII字符,因此我们需要使用一个十六进制编辑器来分析它们。我们打开一个可疑的PDF文档,并搜索字符串JavaScript(参见图 4)。
图4 JavaScript 对象 |
虽然只是有一点用于格式化对象的空格,但是您应该认出PDF对象的结构:对象31是一个JavaScript动作/S /JavaScript,脚本本身没有包含在这个对象中,但是可以在对象32(注意引用3 0 R)中找到。 搜索字符串“31 0 R”,我们发现对象16引用了对象31“/AA <> ”,以及一个页面/Type /Page ,如图5所示。
图5 页对象 |
/AA 是一个注释动作,这意味着当这个页面被查看的时候这个动作就会执行。因此,我们知道:当这个PDF文档被打开的时候,它将执行一个JavaScript脚本。 让我们看看这个脚本(对象32 )的样子。
对象32是一个流对象,而且它是经过压缩的(/Filter [/FlateDecode]),见图 6。
图6 流对象 |
为了对它进行解压,我们可以提取二进制流(1154字节长),并通过一个简单的Perl或者Python程序对它进行解压。使用Python语言,我们只需要导入zlib,然后就可以对数据进行解压了,假设我们已经将我们的二进制流存储在data中了:
import zlib decompressed = zlib.decompress(data) |
然而有一点非常清楚:那就是解压后的脚本是恶意的,它会对函数collectEmailInfo中的一个安全漏洞加以利用,如图7所示。
图7 利用collectEmailInfo |
十、结束语
随着恶意PDF文件日益增多,人们对这种文档的恶意代码分析技术也越来越感兴趣。本文向读者详细介绍了如何分析一种特殊类型的恶意PDF文件:它们可以利用内嵌JavaScript解释器的安全漏洞。当然,有了本文的基础,在分析其他类型的恶意PDF文件,如利用PDF解析器内的安全漏洞的情形的时候,您也能触类旁通。需要说明的是,虽然几乎所有的恶意PDF文档的攻击目标都是Windows操作系统,但是这里介绍的PDF语言是独立于操作系统的,它同时适用于在Windows、Linux和OSX上的PDF文档。
【51CTO.COM 独家特稿,转载请注明出处及作者!】
【编辑推荐】