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 | 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 | ... | ... |