Commit f028496d15715f6a5605e427554472ba3889049c

Authored by decalage2
1 parent 3056212d

crypto: fixed a bug in is_encrypted_ole

oletools/crypto.py
... ... @@ -89,8 +89,9 @@ http://www.decalage.info/python/oletools
89 89 # -----------------------------------------------------------------------------
90 90 # CHANGELOG:
91 91 # 2019-02-14 v0.01 CH: - first version with encryption check from oleid
  92 +# 2019-04-01 v0.54 PL: - fixed bug in is_encrypted_ole
92 93  
93   -__version__ = '0.01'
  94 +__version__ = '0.54dev13'
94 95  
95 96 import sys
96 97 import struct
... ... @@ -98,8 +99,8 @@ import os
98 99 from os.path import splitext, isfile
99 100 from tempfile import mkstemp
100 101 import zipfile
101   -from oletools.common.errors import CryptoErrorBase, WrongEncryptionPassword, \
102   - UnsupportedEncryptionError, MaxCryptoNestingReached, CryptoLibNotImported
  102 +import logging
  103 +
103 104 from olefile import OleFileIO
104 105  
105 106 try:
... ... @@ -107,11 +108,65 @@ try:
107 108 except ImportError:
108 109 msoffcrypto = None
109 110  
  111 +# IMPORTANT: it should be possible to run oletools directly as scripts
  112 +# in any directory without installing them with pip or setup.py.
  113 +# In that case, relative imports are NOT usable.
  114 +# And to enable Python 2+3 compatibility, we need to use absolute imports,
  115 +# so we add the oletools parent folder to sys.path (absolute+normalized path):
  116 +_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
  117 +_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
  118 +if _parent_dir not in sys.path:
  119 + sys.path.insert(0, _parent_dir)
  120 +
  121 +from oletools.common.errors import CryptoErrorBase, WrongEncryptionPassword, \
  122 + UnsupportedEncryptionError, MaxCryptoNestingReached, CryptoLibNotImported
  123 +from oletools.common.log_helper import log_helper
  124 +
110 125  
111 126 #: if there is an encrypted file embedded in an encrypted file,
112 127 #: how deep down do we go
113 128 MAX_NESTING_DEPTH = 10
114 129  
  130 +# === LOGGING =================================================================
  131 +
  132 +# TODO: use log_helper instead
  133 +
  134 +def get_logger(name, level=logging.CRITICAL+1):
  135 + """
  136 + Create a suitable logger object for this module.
  137 + The goal is not to change settings of the root logger, to avoid getting
  138 + other modules' logs on the screen.
  139 + If a logger exists with same name, reuse it. (Else it would have duplicate
  140 + handlers and messages would be doubled.)
  141 + The level is set to CRITICAL+1 by default, to avoid any logging.
  142 + """
  143 + # First, test if there is already a logger with the same name, else it
  144 + # will generate duplicate messages (due to duplicate handlers):
  145 + if name in logging.Logger.manager.loggerDict:
  146 + # NOTE: another less intrusive but more "hackish" solution would be to
  147 + # use getLogger then test if its effective level is not default.
  148 + logger = logging.getLogger(name)
  149 + # make sure level is OK:
  150 + logger.setLevel(level)
  151 + return logger
  152 + # get a new logger:
  153 + logger = logging.getLogger(name)
  154 + # only add a NullHandler for this logger, it is up to the application
  155 + # to configure its own logging:
  156 + logger.addHandler(logging.NullHandler())
  157 + logger.setLevel(level)
  158 + return logger
  159 +
  160 +# a global logger object used for debugging:
  161 +log = get_logger('crypto')
  162 +
  163 +def enable_logging():
  164 + """
  165 + Enable logging for this module (disabled by default).
  166 + This will set the module-specific logger level to NOTSET, which
  167 + means the main application controls the actual logging level.
  168 + """
  169 + log.setLevel(logging.NOTSET)
115 170  
116 171 def is_encrypted(some_file):
117 172 """
... ... @@ -141,7 +196,8 @@ def is_encrypted(some_file):
141 196 :type some_file: :py:class:`olefile.OleFileIO` or `str`
142 197 :returns: True if (and only if) the file contains encrypted content
143 198 """
144   - if not isinstance(some_file, str):
  199 + log.debug('is_encrypted')
  200 + if isinstance(some_file, OleFileIO):
145 201 return is_encrypted_ole(some_file) # assume it is OleFileIO
146 202 if zipfile.is_zipfile(some_file):
147 203 return is_encrypted_zip(some_file)
... ... @@ -151,6 +207,8 @@ def is_encrypted(some_file):
151 207  
152 208 def is_encrypted_zip(filename):
153 209 """Specialization of :py:func:`is_encrypted` for zip-based files."""
  210 + log.debug('is_encrypted_zip')
  211 + # TODO: distinguish OpenXML from normal zip files
154 212 # try to decrypt a few bytes from first entry
155 213 with zipfile.ZipFile(filename, 'r') as zipper:
156 214 first_entry = zipper.infolist()[0]
... ... @@ -164,6 +222,7 @@ def is_encrypted_zip(filename):
164 222  
165 223 def is_encrypted_ole(ole):
166 224 """Specialization of :py:func:`is_encrypted` for ole files."""
  225 + log.debug('is_encrypted_ole')
167 226 # check well known property for password protection
168 227 # (this field may be missing for Powerpoint2000, for example)
169 228 # TODO: check whether password protection always implies encryption. Could
... ... @@ -176,10 +235,11 @@ def is_encrypted_ole(ole):
176 235 # check a few stream names
177 236 # TODO: check whether these actually contain data and whether other
178 237 # necessary properties exist / are set
179   - elif ole.exists('EncryptionInfo'):
  238 + if ole.exists('EncryptionInfo'):
  239 + log.debug('found stream EncryptionInfo')
180 240 return True
181 241 # or an encrypted ppt file
182   - elif ole.exists('EncryptedSummary') and \
  242 + if ole.exists('EncryptedSummary') and \
183 243 not ole.exists('SummaryInformation'):
184 244 return True
185 245  
... ...
oletools/olevba.py
... ... @@ -216,7 +216,7 @@ from __future__ import print_function
216 216 # 2019-03-18 PL: - added XLM/XLF macros detection for Excel OLE files
217 217 # 2019-03-25 CH: - added decryption of password-protected files
218 218  
219   -__version__ = '0.54dev12'
  219 +__version__ = '0.54dev13'
220 220  
221 221 #------------------------------------------------------------------------------
222 222 # TODO:
... ... @@ -436,6 +436,7 @@ def enable_logging():
436 436 log.setLevel(logging.NOTSET)
437 437 # Also enable logging in the ppt_parser module:
438 438 ppt_parser.enable_logging()
  439 + crypto.enable_logging()
439 440  
440 441  
441 442  
... ... @@ -3825,11 +3826,12 @@ def process_file(filename, data, container, options, crypto_nesting=0):
3825 3826 raise ValueError('unexpected output mode: "{0}"!'.format(options.output_mode))
3826 3827  
3827 3828 # even if processing succeeds, file might still be encrypted
3828   - log.debug('Checking for encryption')
  3829 + log.debug('Checking for encryption (normal)')
3829 3830 if not crypto.is_encrypted(filename):
  3831 + log.debug('no encryption detected')
3830 3832 return RETURN_OK
3831 3833 except Exception as exc:
3832   - log.debug('Checking for encryption')
  3834 + log.debug('Checking for encryption (after exception)')
3833 3835 if crypto.is_encrypted(filename):
3834 3836 pass # deal with this below
3835 3837 else:
... ...
setup.py
... ... @@ -48,7 +48,7 @@ import os, fnmatch
48 48 #--- METADATA -----------------------------------------------------------------
49 49  
50 50 name = "oletools"
51   -version = '0.54dev12'
  51 +version = '0.54dev13'
52 52 desc = "Python tools to analyze security characteristics of MS Office and OLE files (also called Structured Storage, Compound File Binary Format or Compound Document File Format), for Malware Analysis and Incident Response #DFIR"
53 53 long_desc = open('oletools/README.rst').read()
54 54 author = "Philippe Lagadec"
... ...