Imported Upstream version 0.4.3 upstream/0.4.3
authorEmanuele Acri (Kali Developer) <crossbower@kali.org>
Mon, 23 Sep 2013 12:29:14 +0000 (14:29 +0200)
committerEmanuele Acri (Kali Developer) <crossbower@kali.org>
Mon, 23 Sep 2013 12:29:14 +0000 (14:29 +0200)
pdf-parser.py [changed mode: 0755->0644]

old mode 100755 (executable)
new mode 100644 (file)
index 4d6736d..1a86cc4
@@ -2,10 +2,10 @@
 
 __description__ = 'pdf-parser, use it to parse a PDF document'
 __author__ = 'Didier Stevens'
-__version__ = '0.3.9'
-__date__ = '2012/03/11'
+__version__ = '0.4.3'
+__date__ = '2013/09/18'
 __minimum_python_version__ = (2, 5, 1)
-__maximum_python_version__ = (3, 1, 2)
+__maximum_python_version__ = (3, 3, 0)
 
 """
 Source code put in public domain by Didier Stevens, no Copyright
@@ -36,10 +36,16 @@ History:
   2010/09/22: V0.3.8 Changed dump option, updated PrettyPrint, added debug option
   2011/12/17: fixed bugs empty objects
   2012/03/11: V0.3.9 fixed bugs double nested [] in PrettyPrintSub (thanks kurt)
+  2013/01/11: V0.3.10 Extract and dump bug fixes by Priit; added content option
+  2013/02/16: Performance improvement in cPDFTokenizer by using StringIO for token building by Christophe Vandeplas; xrange replaced with range
+  2013/02/16: V0.4.0 added http/https support; added error handling for missing file or URL; ; added support for ZIP file with password 'infected'
+  2013/03/13: V0.4.1 fixes for Python 3
+  2013/04/11: V0.4.2 modified PrettyPrintSub for strings with unprintable characters
+  2013/05/04: Added options searchstream, unfiltered, casesensitive, regex
+  2013/09/18: V0.4.3 fixed regression bug -w option
 
 Todo:
   - handle printf todo
-  - check 'real raw' option
   - fix PrettyPrint
   - support for JS hex string EC61C64349DB8D88AF0523C4C06E0F4D.pdf.vir
 
@@ -51,11 +57,15 @@ import zlib
 import binascii
 import hashlib
 import sys
+import zipfile
 if sys.version_info[0] >= 3:
-    import io
-    cStringIO = io.StringIO
+    from io import StringIO
+    import urllib.request
+    urllib23 = urllib.request
 else:
-    import cStringIO
+    from cStringIO import StringIO
+    import urllib2
+    urllib23 = urllib2
 
 CHAR_WHITESPACE = 1
 CHAR_DELIMITER = 2
@@ -73,6 +83,13 @@ PDF_ELEMENT_TRAILER = 4
 PDF_ELEMENT_STARTXREF = 5
 PDF_ELEMENT_MALFORMED = 6
 
+#Convert 2 Bytes If Python 3
+def C2BIP3(string):
+    if sys.version_info[0] > 2:
+        return bytes([ord(x) for x in string])
+    else:
+        return string
+
 def CopyWithoutWhiteSpace(content):
     result = []
     for token in content:
@@ -86,7 +103,31 @@ def Obj2Str(content):
 class cPDFDocument:
     def __init__(self, file):
         self.file = file
-        self.infile = open(file, 'rb')
+        if file.lower().startswith('http://') or file.lower().startswith('https://'):
+            try:
+                if sys.hexversion >= 0x020601F0:
+                    self.infile = urllib23.urlopen(file, timeout=5)
+                else:
+                    self.infile = urllib23.urlopen(file)
+            except urllib23.HTTPError:
+                print('Error accessing URL %s' % file)
+                print(sys.exc_info()[1])
+                sys.exit()
+        elif file.lower().endswith('.zip'):
+            try:
+                self.zipfile = zipfile.ZipFile(file, 'r')
+                self.infile = self.zipfile.open(self.zipfile.infolist()[0], 'r', C2BIP3('infected'))
+            except:
+                print('Error opening file %s' % file)
+                print(sys.exc_info()[1])
+                sys.exit()
+        else:
+            try:
+                self.infile = open(file, 'rb')
+            except:
+                print('Error opening file %s' % file)
+                print(sys.exc_info()[1])
+                sys.exit()
         self.ungetted = []
         self.position = -1
 
@@ -95,7 +136,7 @@ class cPDFDocument:
             self.position += 1
             return self.ungetted.pop()
         inbyte = self.infile.read(1)
-        if not inbyte:
+        if not inbyte or inbyte == '':
             self.infile.close()
             return None
         self.position += 1
@@ -130,24 +171,26 @@ class cPDFTokenizer:
             self.oPDF = None
             return None
         elif CharacterClass(self.byte) == CHAR_WHITESPACE:
-            self.token = ''
+            file_str = StringIO()
             while self.byte != None and CharacterClass(self.byte) == CHAR_WHITESPACE:
-                self.token = self.token + chr(self.byte)
+                file_str.write(chr(self.byte))
                 self.byte = self.oPDF.byte()
             if self.byte != None:
                 self.oPDF.unget(self.byte)
             else:
                 self.oPDF = None
+            self.token = file_str.getvalue()
             return (CHAR_WHITESPACE, self.token)
         elif CharacterClass(self.byte) == CHAR_REGULAR:
-            self.token = ''
+            file_str = StringIO()
             while self.byte != None and CharacterClass(self.byte) == CHAR_REGULAR:
-                self.token = self.token + chr(self.byte)
+                file_str.write(chr(self.byte))
                 self.byte = self.oPDF.byte()
             if self.byte != None:
                 self.oPDF.unget(self.byte)
             else:
                 self.oPDF = None
+            self.token = file_str.getvalue()
             return (CHAR_REGULAR, self.token)
         else:
             if self.byte == 0x3C:
@@ -165,20 +208,21 @@ class cPDFTokenizer:
                     self.oPDF.unget(self.byte)
                     return (CHAR_DELIMITER, '>')
             elif self.byte == 0x25:
-                self.token = ''
+                file_str = StringIO()
                 while self.byte != None:
-                    self.token = self.token + chr(self.byte)
+                    file_str.write(chr(self.byte))
                     if self.byte == 10 or self.byte == 13:
                         self.byte = self.oPDF.byte()
                         break
                     self.byte = self.oPDF.byte()
                 if self.byte != None:
                     if self.byte == 10:
-                        self.token = self.token + chr(self.byte)
+                        file_str.write(chr(self.byte))
                     else:
                         self.oPDF.unget(self.byte)
                 else:
                     self.oPDF = None
+                self.token = file_str.getvalue()
                 return (CHAR_DELIMITER, self.token)
             return (CHAR_DELIMITER, chr(self.byte))
 
@@ -323,6 +367,12 @@ class cPDFElementTrailer:
         self.type = PDF_ELEMENT_TRAILER
         self.content = content
 
+def IIf(expr, truepart, falsepart):
+    if expr:
+        return truepart
+    else:
+        return falsepart
+
 class cPDFElementIndirectObject:
     def __init__(self, id, version, content):
         self.type = PDF_ELEMENT_INDIRECT_OBJECT
@@ -371,6 +421,19 @@ class cPDFElementIndirectObject:
                 data += Canonicalize(self.content[i][1])
         return data.upper().find(keyword.upper()) != -1
 
+    def StreamContains(self, keyword, filter, casesensitive, regex):
+        if not self.ContainsStream():
+            return False
+        streamData = self.Stream(filter)
+        if filter and streamData == 'No filters':
+            streamData = self.Stream(False)
+        if regex:
+            return re.search(keyword, streamData, IIf(casesensitive, 0, re.I))
+        elif casesensitive:
+            return keyword in streamData
+        else:
+            return keyword.lower() in streamData.lower()
+
     def Stream(self, filter=True):
         state = 'start'
         countDirectories = 0
@@ -384,6 +447,8 @@ class cPDFElementIndirectObject:
                     countDirectories -= 1
                 if countDirectories == 1 and self.content[i][0] == CHAR_DELIMITER and EqualCanonical(self.content[i][1], '/Filter'):
                     state = 'filter'
+                elif countDirectories == 0 and self.content[i][0] == CHAR_REGULAR and self.content[i][1] == 'stream':
+                    state = 'stream-whitespace'
             elif state == 'filter':
                 if self.content[i][0] == CHAR_DELIMITER and self.content[i][1][0] == '/':
                     filters = [self.content[i][1]]
@@ -419,8 +484,11 @@ class cPDFElementIndirectObject:
             if EqualCanonical(filter, '/FlateDecode') or EqualCanonical(filter, '/Fl'):
                 try:
                     data = FlateDecode(data)
-                except:
-                    return 'FlateDecode decompress failed'
+                except zlib.error, e:
+                    message = 'FlateDecode decompress failed'
+                    if len(data) > 0 and ord(data[0]) & 0x0F != 8:
+                        message += ', unexpected compression method: %02x' % ord(data[0])
+                    return message + '. zlib.error %s' % e.message
             elif EqualCanonical(filter, '/ASCIIHexDecode') or EqualCanonical(filter, '/AHx'):
                 try:
                     data = ASCIIHexDecode(data)
@@ -544,7 +612,11 @@ class cPDFParseDictionary:
                 if e[1] == []:
                     print('%s  %s' % (prefix, e[0]))
                 elif type(e[1][0]) == type(''):
-                    print('%s  %s %s' % (prefix, e[0], ''.join(e[1]).strip()))
+                    value = ''.join(e[1]).strip()
+                    reprValue = repr(value)
+                    if "'" + value + "'" != reprValue:
+                        value = reprValue
+                    print('%s  %s %s' % (prefix, e[0], value))
                 else:
                     print('%s  %s' % (prefix, e[0]))
                     self.PrettyPrintSub(prefix + '    ', e[1])
@@ -574,7 +646,7 @@ def PrintObject(object, options):
             print(' %s' % FormatOutput(dataPrecedingStream, options.raw))
         oPDFParseDictionary = cPDFParseDictionary(dataPrecedingStream, options.nocanonicalizedoutput)
     else:
-        if options.debug:
+        if options.debug or options.raw:
             print(' %s' % FormatOutput(object.content, options.raw))
         oPDFParseDictionary = cPDFParseDictionary(object.content, options.nocanonicalizedoutput)
     print('')
@@ -586,22 +658,33 @@ def PrintObject(object, options):
             print(' %s' % FormatOutput(object.content, options.raw))
         else:
             print(' %s' % FormatOutput(filtered, options.raw))
+    if options.content:
+        if object.ContainsStream():
+            stream = object.Stream(False)
+            if stream != []:
+                print(' %s' % FormatOutput(stream, options.raw))
+        else:
+            print(''.join([token[1] for token in object.content]))
+
+
     if options.dump:
         filtered = object.Stream(options.filter == True)
+        if filtered == []:
+            filtered = ''
         try:
             fDump = open(options.dump, 'wb')
             try:
-                fDump.write(filtered)
+                fDump.write(C2BIP3(filtered))
             except:
-                print('Error writing file %s' % options.extract)
+                print('Error writing file %s' % options.dump)
             fDump.close()
         except:
-            print('Error writing file %s' % options.extract)
+            print('Error writing file %s' % options.dump)
     print('')
     return
 
 def Canonicalize(sIn):
-    if sIn == "":
+    if sIn == '':
         return sIn
     elif sIn[0] != '/':
         return sIn
@@ -662,7 +745,7 @@ def FlateDecode(data):
     return zlib.decompress(data)
 
 def RunLengthDecode(data):
-    f = cStringIO.StringIO(data)
+    f = StringIO(data)
     decompressed = ''
     runLength = ord(f.read(1))
     while runLength:
@@ -679,10 +762,10 @@ def RunLengthDecode(data):
 #### LZW code sourced from pdfminer
 # Copyright (c) 2004-2009 Yusuke Shinyama <yusuke at cs dot nyu dot edu>
 #
-# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
-# documentation files (the "Software"), to deal in the Software without restriction, including without limitation 
-# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 
-# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 
+# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+# and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 
 class LZWDecoder(object):
     def __init__(self, fp):
@@ -721,7 +804,7 @@ class LZWDecoder(object):
     def feed(self, code):
         x = ''
         if code == 256:
-            self.table = [ chr(c) for c in xrange(256) ] # 0-255
+            self.table = [ chr(c) for c in range(256) ] # 0-255
             self.table.append(None) # 256
             self.table.append(None) # 257
             self.prevbuf = ''
@@ -760,13 +843,13 @@ class LZWDecoder(object):
 ####
 
 def LZWDecode(data):
-    return ''.join(LZWDecoder(cStringIO.StringIO(data)).run())
+    return ''.join(LZWDecoder(StringIO(data)).run())
 
 def Main():
     """pdf-parser, use it to parse a PDF document
     """
 
-    oParser = optparse.OptionParser(usage='usage: %prog [options] pdf-file\n' + __description__, version='%prog ' + __version__)
+    oParser = optparse.OptionParser(usage='usage: %prog [options] pdf-file|zip-file|url\n' + __description__, version='%prog ' + __version__)
     oParser.add_option('-s', '--search', help='string to search in indirect objects (except streams)')
     oParser.add_option('-f', '--filter', action='store_true', default=False, help='pass stream object through filters (FlateDecode, ASCIIHexDecode, ASCII85Decode, LZWDecode and RunLengthDecode only)')
     oParser.add_option('-o', '--object', help='id of indirect object to select (version independent)')
@@ -776,11 +859,16 @@ def Main():
     oParser.add_option('-a', '--stats', action='store_true', default=False, help='display stats for pdf document')
     oParser.add_option('-t', '--type', help='type of indirect object to select')
     oParser.add_option('-v', '--verbose', action='store_true', default=False, help='display malformed PDF elements')
-    oParser.add_option('-x', '--extract', help='filename to extract to')
+    oParser.add_option('-x', '--extract', help='filename to extract malformed content to')
     oParser.add_option('-H', '--hash', action='store_true', default=False, help='display hash of objects')
     oParser.add_option('-n', '--nocanonicalizedoutput', action='store_true', default=False, help='do not canonicalize the output')
     oParser.add_option('-d', '--dump', help='filename to dump stream content to')
     oParser.add_option('-D', '--debug', action='store_true', default=False, help='display debug info')
+    oParser.add_option('-c', '--content', action='store_true', default=False, help='display the content for objects without streams or with streams without filters')
+    oParser.add_option('--searchstream', help='string to search in streams')
+    oParser.add_option('--unfiltered', action='store_true', default=False, help='search in unfiltered streams')
+    oParser.add_option('--casesensitive', action='store_true', default=False, help='case sensitive search in streams')
+    oParser.add_option('--regex', action='store_true', default=False, help='use regex to search in streams')
     (options, args) = oParser.parse_args()
 
     if len(args) != 1:
@@ -822,7 +910,7 @@ def Main():
                     return
         else:
             selectIndirectObject = True
-            if not options.search and not options.object and not options.reference and not options.type:
+            if not options.search and not options.object and not options.reference and not options.type and not options.searchstream:
                 selectComment = True
                 selectXref = True
                 selectTrailer = True
@@ -891,13 +979,16 @@ def Main():
                             rawContent = FormatOutput(object.content, True)
                             print(' len: %d md5: %s' % (len(rawContent), hashlib.md5(rawContent).hexdigest()))
                             print('')
+                        elif options.searchstream:
+                            if object.StreamContains(options.searchstream, not options.unfiltered, options.casesensitive, options.regex):
+                                PrintObject(object, options)
                         else:
                             PrintObject(object, options)
                     elif object.type == PDF_ELEMENT_MALFORMED:
                         try:
                             fExtract = open(options.extract, 'wb')
                             try:
-                                fExtract.write(object.content)
+                                fExtract.write(C2BIP3(object.content))
                             except:
                                 print('Error writing file %s' % options.extract)
                             fExtract.close()