Commit c73350c2b5cec98a60cff6c1ad7059e46180332a
1 parent
1d8a1759
crypto: Create check_encryption with code from oleid
Showing
2 changed files
with
124 additions
and
35 deletions
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 |