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 93 sys.path.insert(0, PARENT_DIR)
94 94 del PARENT_DIR
95 95 from oletools.thirdparty.prettytable import prettytable
  96 +from oletools import crypto
96 97  
97 98 import olefile
98 99  
... ... @@ -279,20 +280,7 @@ class OleID(object):
279 280 self.indicators.append(encrypted)
280 281 if not self.ole:
281 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 284 return encrypted
297 285  
298 286 def check_word(self):
... ... @@ -316,27 +304,7 @@ class OleID(object):
316 304 return None, None
317 305 if self.ole.exists('WordDocument'):
318 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 308 # check for VBA macros:
341 309 if self.ole.exists('Macros'):
342 310 macros.value = True
... ...