Commit c73350c2b5cec98a60cff6c1ad7059e46180332a

Authored by Christian Herdtweck
1 parent 1d8a1759

crypto: Create check_encryption with code from oleid

oletools/crypto.py 0 → 100644
  1 +#!/usr/bin/env python
  2 +"""
  3 +crypto.py
  4 +
  5 +Module to be used by other scripts and modules in oletools, that provides
  6 +information on encryption in OLE files.
  7 +
  8 +.. seealso:: [MS-OFFCRYPTO]
  9 +
  10 +crypto is part of the python-oletools package:
  11 +http://www.decalage.info/python/oletools
  12 +"""
  13 +
  14 +# === LICENSE =================================================================
  15 +
  16 +# crypto is copyright (c) 2014-2019 Philippe Lagadec (http://www.decalage.info)
  17 +# All rights reserved.
  18 +#
  19 +# Redistribution and use in source and binary forms, with or without
  20 +# modification, are permitted provided that the following conditions are met:
  21 +#
  22 +# * Redistributions of source code must retain the above copyright notice,
  23 +# this list of conditions and the following disclaimer.
  24 +# * Redistributions in binary form must reproduce the above copyright notice,
  25 +# this list of conditions and the following disclaimer in the documentation
  26 +# and/or other materials provided with the distribution.
  27 +#
  28 +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  29 +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  30 +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  31 +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  32 +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  33 +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  34 +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  35 +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  36 +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  37 +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  38 +# POSSIBILITY OF SUCH DAMAGE.
  39 +
  40 +# -----------------------------------------------------------------------------
  41 +# CHANGELOG:
  42 +# 2019-02-14 v0.01 CH: - first version with encryption check from oleid
  43 +
  44 +__version__ = '0.01'
  45 +
  46 +import struct
  47 +
  48 +
  49 +def is_encrypted(olefile):
  50 + """
  51 + Determine whether document contains encrypted content.
  52 +
  53 + This should return False for documents that are just write-protected or
  54 + signed or finalized. It should return True if ANY content of the file is
  55 + encrypted and can therefore not be analyzed by other oletools modules
  56 + without given a password.
  57 +
  58 + Exception: there are way to write-protect an office document by embedding
  59 + it as encrypted stream with hard-coded standard password into an otherwise
  60 + empty OLE file. From an office user point of view, this is no encryption,
  61 + but regarding file structure this is encryption, so we return `True` for
  62 + these.
  63 +
  64 + This should not raise exceptions needlessly.
  65 +
  66 + This implementation is rather simple: it returns True if the file contains
  67 + streams with typical encryption names (c.f. [MS-OFFCRYPTO]). It does not
  68 + test whether these streams actually contain data or whether the ole file
  69 + structure contains the necessary references to these. It also checks the
  70 + "well-known property" PIDSI_DOC_SECURITY if the SummaryInformation stream
  71 + is accessible (c.f. [MS-OLEPS] 2.25.1)
  72 +
  73 + :param olefile: An opened OleFileIO or a filename to such a file
  74 + :type olefile: :py:class:`olefile.OleFileIO` or `str`
  75 + :returns: True if (and only if) the file contains encrypted content
  76 + """
  77 + if isinstance(olefile, str):
  78 + ole = olefile.OleFileIO(olefile)
  79 + else:
  80 + ole = olefile # assume it is an olefile.OleFileIO
  81 +
  82 + # check well known property for password protection
  83 + # (this field may be missing for Powerpoint2000, for example)
  84 + # TODO: check whether password protection always implies encryption. Could
  85 + # write-protection or signing with password trigger this as well?
  86 + if ole.exists("\x05SummaryInformation"):
  87 + suminfo_data = ole.getproperties("\x05SummaryInformation")
  88 + if 0x13 in suminfo_data and (suminfo_data[0x13] & 1):
  89 + return True
  90 +
  91 + # check a few stream names
  92 + # TODO: check whether these actually contain data and whether other
  93 + # necessary properties exist / are set
  94 + elif ole.exists('EncryptionInfo'):
  95 + return True
  96 + # or an encrypted ppt file
  97 + elif ole.exists('EncryptedSummary') and \
  98 + not ole.exists('SummaryInformation'):
  99 + return True
  100 +
  101 + # Word-specific old encryption:
  102 + if ole.exists('WordDocument'):
  103 + # check for Word-specific encryption flag:
  104 + stream = None
  105 + try:
  106 + stream = ole.openstream(["WordDocument"])
  107 + # pass header 10 bytes
  108 + stream.read(10)
  109 + # read flag structure:
  110 + temp16 = struct.unpack("H", stream.read(2))[0]
  111 + f_encrypted = (temp16 & 0x0100) >> 8
  112 + if f_encrypted:
  113 + return True
  114 + except Exception:
  115 + raise
  116 + finally:
  117 + if stream is not None:
  118 + stream.close()
  119 +
  120 + # no indication of encryption
  121 + return False
oletools/oleid.py
@@ -93,6 +93,7 @@ except ImportError: @@ -93,6 +93,7 @@ except ImportError:
93 sys.path.insert(0, PARENT_DIR) 93 sys.path.insert(0, PARENT_DIR)
94 del PARENT_DIR 94 del PARENT_DIR
95 from oletools.thirdparty.prettytable import prettytable 95 from oletools.thirdparty.prettytable import prettytable
  96 +from oletools import crypto
96 97
97 import olefile 98 import olefile
98 99
@@ -279,20 +280,7 @@ class OleID(object): @@ -279,20 +280,7 @@ class OleID(object):
279 self.indicators.append(encrypted) 280 self.indicators.append(encrypted)
280 if not self.ole: 281 if not self.ole:
281 return None 282 return None
282 - # check if bit 1 of security field = 1:  
283 - # (this field may be missing for Powerpoint2000, for example)  
284 - if self.suminfo_data is None:  
285 - self.check_properties()  
286 - if 0x13 in self.suminfo_data:  
287 - if self.suminfo_data[0x13] & 1:  
288 - encrypted.value = True  
289 - # check if this is an OpenXML encrypted file  
290 - elif self.ole.exists('EncryptionInfo'):  
291 - encrypted.value = True  
292 - # or an encrypted ppt file  
293 - if self.ole.exists('EncryptedSummary') and \  
294 - not self.ole.exists('SummaryInformation'):  
295 - encrypted.value = True 283 + encrypted.value = crypto.is_encrypted(self.ole)
296 return encrypted 284 return encrypted
297 285
298 def check_word(self): 286 def check_word(self):
@@ -316,27 +304,7 @@ class OleID(object): @@ -316,27 +304,7 @@ class OleID(object):
316 return None, None 304 return None, None
317 if self.ole.exists('WordDocument'): 305 if self.ole.exists('WordDocument'):
318 word.value = True 306 word.value = True
319 - # check for Word-specific encryption flag:  
320 - stream = None  
321 - try:  
322 - stream = self.ole.openstream(["WordDocument"])  
323 - # pass header 10 bytes  
324 - stream.read(10)  
325 - # read flag structure:  
326 - temp16 = struct.unpack("H", stream.read(2))[0]  
327 - f_encrypted = (temp16 & 0x0100) >> 8  
328 - if f_encrypted:  
329 - # correct encrypted indicator if present or add one  
330 - encrypt_ind = self.get_indicator('encrypted')  
331 - if encrypt_ind:  
332 - encrypt_ind.value = True  
333 - else:  
334 - self.indicators.append('encrypted', True, name='Encrypted')  
335 - except Exception:  
336 - raise  
337 - finally:  
338 - if stream is not None:  
339 - stream.close() 307 +
340 # check for VBA macros: 308 # check for VBA macros:
341 if self.ole.exists('Macros'): 309 if self.ole.exists('Macros'):
342 macros.value = True 310 macros.value = True