Commit e9dca546b053f7c7a8be921255a7bc0361e22425
1 parent
7d06413c
mraptor3: replaced by a redirection to mraptor + deprecation warning (issue #106)
Showing
1 changed file
with
6 additions
and
336 deletions
oletools/mraptor3.py
| 1 | #!/usr/bin/env python | 1 | #!/usr/bin/env python |
| 2 | -""" | ||
| 3 | -mraptor.py - MacroRaptor | ||
| 4 | 2 | ||
| 5 | -MacroRaptor is a script to parse OLE and OpenXML files such as MS Office | ||
| 6 | -documents (e.g. Word, Excel), to detect malicious macros. | 3 | +# mraptor3 is a stub that redirects to mraptor.py, for backwards compatibility |
| 7 | 4 | ||
| 8 | -Supported formats: | ||
| 9 | -- Word 97-2003 (.doc, .dot), Word 2007+ (.docm, .dotm) | ||
| 10 | -- Excel 97-2003 (.xls), Excel 2007+ (.xlsm, .xlsb) | ||
| 11 | -- PowerPoint 97-2003 (.ppt), PowerPoint 2007+ (.pptm, .ppsm) | ||
| 12 | -- Word/PowerPoint 2007+ XML (aka Flat OPC) | ||
| 13 | -- Word 2003 XML (.xml) | ||
| 14 | -- Word/Excel Single File Web Page / MHTML (.mht) | ||
| 15 | -- Publisher (.pub) | 5 | +import sys, os, warnings |
| 16 | 6 | ||
| 17 | -Author: Philippe Lagadec - http://www.decalage.info | ||
| 18 | -License: BSD, see source code or documentation | ||
| 19 | - | ||
| 20 | -MacroRaptor is part of the python-oletools package: | ||
| 21 | -http://www.decalage.info/python/oletools | ||
| 22 | -""" | ||
| 23 | - | ||
| 24 | -# === LICENSE ================================================================== | ||
| 25 | - | ||
| 26 | -# MacroRaptor is copyright (c) 2016-2018 Philippe Lagadec (http://www.decalage.info) | ||
| 27 | -# All rights reserved. | ||
| 28 | -# | ||
| 29 | -# Redistribution and use in source and binary forms, with or without modification, | ||
| 30 | -# are permitted provided that the following conditions are met: | ||
| 31 | -# | ||
| 32 | -# * Redistributions of source code must retain the above copyright notice, this | ||
| 33 | -# list of conditions and the following disclaimer. | ||
| 34 | -# * Redistributions in binary form must reproduce the above copyright notice, | ||
| 35 | -# this list of conditions and the following disclaimer in the documentation | ||
| 36 | -# and/or other materials provided with the distribution. | ||
| 37 | -# | ||
| 38 | -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
| 39 | -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
| 40 | -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
| 41 | -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||
| 42 | -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||
| 43 | -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||
| 44 | -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||
| 45 | -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||
| 46 | -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| 47 | -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| 48 | - | ||
| 49 | -#------------------------------------------------------------------------------ | ||
| 50 | -# CHANGELOG: | ||
| 51 | -# 2016-02-23 v0.01 PL: - first version | ||
| 52 | -# 2016-02-29 v0.02 PL: - added Workbook_Activate, FileSaveAs | ||
| 53 | -# 2016-03-04 v0.03 PL: - returns an exit code based on the overall result | ||
| 54 | -# 2016-03-08 v0.04 PL: - collapse long lines before analysis | ||
| 55 | -# 2016-07-19 v0.50 SL: - converted to Python 3 | ||
| 56 | -# 2016-08-26 PL: - changed imports for Python 3 | ||
| 57 | -# 2017-04-26 v0.51 PL: - fixed absolute imports (issue #141) | ||
| 58 | -# 2017-06-29 PL: - synced with mraptor.py 0.51 | ||
| 59 | -# 2018-05-25 v0.53 PL: - added Word/PowerPoint 2007+ XML (aka Flat OPC) issue #283 | ||
| 60 | - | ||
| 61 | -__version__ = '0.53' | ||
| 62 | - | ||
| 63 | -#------------------------------------------------------------------------------ | ||
| 64 | -# TODO: | ||
| 65 | - | ||
| 66 | - | ||
| 67 | -#--- IMPORTS ------------------------------------------------------------------ | ||
| 68 | - | ||
| 69 | -import sys, os, logging, optparse, re | 7 | +warnings.warn('mraptor3 is deprecated, mraptor should be used instead.', DeprecationWarning) |
| 70 | 8 | ||
| 71 | # IMPORTANT: it should be possible to run oletools directly as scripts | 9 | # IMPORTANT: it should be possible to run oletools directly as scripts |
| 72 | # in any directory without installing them with pip or setup.py. | 10 | # in any directory without installing them with pip or setup.py. |
| @@ -74,280 +12,12 @@ import sys, os, logging, optparse, re | @@ -74,280 +12,12 @@ import sys, os, logging, optparse, re | ||
| 74 | # And to enable Python 2+3 compatibility, we need to use absolute imports, | 12 | # And to enable Python 2+3 compatibility, we need to use absolute imports, |
| 75 | # so we add the oletools parent folder to sys.path (absolute+normalized path): | 13 | # so we add the oletools parent folder to sys.path (absolute+normalized path): |
| 76 | _thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__))) | 14 | _thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__))) |
| 77 | -# print('_thismodule_dir = %r' % _thismodule_dir) | ||
| 78 | _parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..')) | 15 | _parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..')) |
| 79 | -# print('_parent_dir = %r' % _thirdparty_dir) | ||
| 80 | -if not _parent_dir in sys.path: | 16 | +if _parent_dir not in sys.path: |
| 81 | sys.path.insert(0, _parent_dir) | 17 | sys.path.insert(0, _parent_dir) |
| 82 | 18 | ||
| 83 | -from oletools.thirdparty.xglob import xglob | ||
| 84 | -from oletools.thirdparty.tablestream import tablestream | ||
| 85 | - | ||
| 86 | -# import the python 3 version of olevba | ||
| 87 | -from oletools import olevba3 as olevba | ||
| 88 | -from oletools.olevba3 import TYPE2TAG | ||
| 89 | - | ||
| 90 | -# === LOGGING ================================================================= | ||
| 91 | - | ||
| 92 | -# a global logger object used for debugging: | ||
| 93 | -log = olevba.get_logger('mraptor') | ||
| 94 | - | ||
| 95 | - | ||
| 96 | -#--- CONSTANTS ---------------------------------------------------------------- | ||
| 97 | - | ||
| 98 | -# URL and message to report issues: | ||
| 99 | -# TODO: make it a common variable for all oletools | ||
| 100 | -URL_ISSUES = 'https://github.com/decalage2/oletools/issues' | ||
| 101 | -MSG_ISSUES = 'Please report this issue on %s' % URL_ISSUES | ||
| 102 | - | ||
| 103 | -# 'AutoExec', 'AutoOpen', 'Auto_Open', 'AutoClose', 'Auto_Close', 'AutoNew', 'AutoExit', | ||
| 104 | -# 'Document_Open', 'DocumentOpen', | ||
| 105 | -# 'Document_Close', 'DocumentBeforeClose', 'Document_BeforeClose', | ||
| 106 | -# 'DocumentChange','Document_New', | ||
| 107 | -# 'NewDocument' | ||
| 108 | -# 'Workbook_Open', 'Workbook_Close', | ||
| 109 | -# *_Painted such as InkPicture1_Painted | ||
| 110 | -# *_GotFocus|LostFocus|MouseHover for other ActiveX objects | ||
| 111 | -# reference: http://www.greyhathacker.net/?p=948 | ||
| 112 | - | ||
| 113 | -# TODO: check if line also contains Sub or Function | ||
| 114 | -re_autoexec = re.compile(r'(?i)\b(?:Auto(?:Exec|_?Open|_?Close|Exit|New)' + | ||
| 115 | - r'|Document(?:_?Open|_Close|_?BeforeClose|Change|_New)' + | ||
| 116 | - r'|NewDocument|Workbook(?:_Open|_Activate|_Close)' + | ||
| 117 | - r'|\w+_(?:Painted|Painting|GotFocus|LostFocus|MouseHover' + | ||
| 118 | - r'|Layout|Click|Change|Resize|BeforeNavigate2|BeforeScriptExecute' + | ||
| 119 | - r'|DocumentComplete|DownloadBegin|DownloadComplete|FileDownload' + | ||
| 120 | - r'|NavigateComplete2|NavigateError|ProgressChange|PropertyChange' + | ||
| 121 | - r'|SetSecureLockIcon|StatusTextChange|TitleChange|MouseMove' + | ||
| 122 | - r'|MouseEnter|MouseLeave|))\b') | ||
| 123 | - | ||
| 124 | -# MS-VBAL 5.4.5.1 Open Statement: | ||
| 125 | -RE_OPEN_WRITE = r'(?:\bOpen\b[^\n]+\b(?:Write|Append|Binary|Output|Random)\b)' | ||
| 126 | - | ||
| 127 | -re_write = re.compile(r'(?i)\b(?:FileCopy|CopyFile|Kill|CreateTextFile|' | ||
| 128 | - + r'VirtualAlloc|RtlMoveMemory|URLDownloadToFileA?|AltStartupPath|' | ||
| 129 | - + r'ADODB\.Stream|WriteText|SaveToFile|SaveAs|SaveAsRTF|FileSaveAs|MkDir|RmDir|SaveSetting|SetAttr)\b|' + RE_OPEN_WRITE) | ||
| 130 | - | ||
| 131 | -# MS-VBAL 5.2.3.5 External Procedure Declaration | ||
| 132 | -RE_DECLARE_LIB = r'(?:\bDeclare\b[^\n]+\bLib\b)' | ||
| 133 | - | ||
| 134 | -re_execute = re.compile(r'(?i)\b(?:Shell|CreateObject|GetObject|SendKeys|' | ||
| 135 | - + r'MacScript|FollowHyperlink|CreateThread|ShellExecute)\b|' + RE_DECLARE_LIB) | ||
| 136 | - | ||
| 137 | - | ||
| 138 | -# === CLASSES ================================================================= | ||
| 139 | - | ||
| 140 | -class Result_NoMacro(object): | ||
| 141 | - exit_code = 0 | ||
| 142 | - color = 'green' | ||
| 143 | - name = 'No Macro' | ||
| 144 | - | ||
| 145 | - | ||
| 146 | -class Result_NotMSOffice(object): | ||
| 147 | - exit_code = 1 | ||
| 148 | - color = 'green' | ||
| 149 | - name = 'Not MS Office' | ||
| 150 | - | ||
| 151 | - | ||
| 152 | -class Result_MacroOK(object): | ||
| 153 | - exit_code = 2 | ||
| 154 | - color = 'cyan' | ||
| 155 | - name = 'Macro OK' | ||
| 156 | - | ||
| 157 | - | ||
| 158 | -class Result_Error(object): | ||
| 159 | - exit_code = 10 | ||
| 160 | - color = 'yellow' | ||
| 161 | - name = 'ERROR' | ||
| 162 | - | ||
| 163 | - | ||
| 164 | -class Result_Suspicious(object): | ||
| 165 | - exit_code = 20 | ||
| 166 | - color = 'red' | ||
| 167 | - name = 'SUSPICIOUS' | ||
| 168 | - | ||
| 169 | - | ||
| 170 | -class MacroRaptor(object): | ||
| 171 | - """ | ||
| 172 | - class to scan VBA macro code to detect if it is malicious | ||
| 173 | - """ | ||
| 174 | - def __init__(self, vba_code): | ||
| 175 | - """ | ||
| 176 | - MacroRaptor constructor | ||
| 177 | - :param vba_code: string containing the VBA macro code | ||
| 178 | - """ | ||
| 179 | - # collapse long lines first | ||
| 180 | - self.vba_code = olevba.vba_collapse_long_lines(vba_code) | ||
| 181 | - self.autoexec = False | ||
| 182 | - self.write = False | ||
| 183 | - self.execute = False | ||
| 184 | - self.flags = '' | ||
| 185 | - self.suspicious = False | ||
| 186 | - self.autoexec_match = None | ||
| 187 | - self.write_match = None | ||
| 188 | - self.execute_match = None | ||
| 189 | - self.matches = [] | ||
| 190 | - | ||
| 191 | - def scan(self): | ||
| 192 | - """ | ||
| 193 | - Scan the VBA macro code to detect if it is malicious | ||
| 194 | - :return: | ||
| 195 | - """ | ||
| 196 | - m = re_autoexec.search(self.vba_code) | ||
| 197 | - if m is not None: | ||
| 198 | - self.autoexec = True | ||
| 199 | - self.autoexec_match = m.group() | ||
| 200 | - self.matches.append(m.group()) | ||
| 201 | - m = re_write.search(self.vba_code) | ||
| 202 | - if m is not None: | ||
| 203 | - self.write = True | ||
| 204 | - self.write_match = m.group() | ||
| 205 | - self.matches.append(m.group()) | ||
| 206 | - m = re_execute.search(self.vba_code) | ||
| 207 | - if m is not None: | ||
| 208 | - self.execute = True | ||
| 209 | - self.execute_match = m.group() | ||
| 210 | - self.matches.append(m.group()) | ||
| 211 | - if self.autoexec and (self.execute or self.write): | ||
| 212 | - self.suspicious = True | ||
| 213 | - | ||
| 214 | - def get_flags(self): | ||
| 215 | - flags = '' | ||
| 216 | - flags += 'A' if self.autoexec else '-' | ||
| 217 | - flags += 'W' if self.write else '-' | ||
| 218 | - flags += 'X' if self.execute else '-' | ||
| 219 | - return flags | ||
| 220 | - | ||
| 221 | - | ||
| 222 | -# === MAIN ==================================================================== | ||
| 223 | - | ||
| 224 | -def main(): | ||
| 225 | - """ | ||
| 226 | - Main function, called when olevba is run from the command line | ||
| 227 | - """ | ||
| 228 | - global log | ||
| 229 | - DEFAULT_LOG_LEVEL = "warning" # Default log level | ||
| 230 | - LOG_LEVELS = { | ||
| 231 | - 'debug': logging.DEBUG, | ||
| 232 | - 'info': logging.INFO, | ||
| 233 | - 'warning': logging.WARNING, | ||
| 234 | - 'error': logging.ERROR, | ||
| 235 | - 'critical': logging.CRITICAL | ||
| 236 | - } | ||
| 237 | - | ||
| 238 | - usage = 'usage: %prog [options] <filename> [filename2 ...]' | ||
| 239 | - parser = optparse.OptionParser(usage=usage) | ||
| 240 | - parser.add_option("-r", action="store_true", dest="recursive", | ||
| 241 | - help='find files recursively in subdirectories.') | ||
| 242 | - parser.add_option("-z", "--zip", dest='zip_password', type='str', default=None, | ||
| 243 | - help='if the file is a zip archive, open all files from it, using the provided password (requires Python 2.6+)') | ||
| 244 | - parser.add_option("-f", "--zipfname", dest='zip_fname', type='str', default='*', | ||
| 245 | - help='if the file is a zip archive, file(s) to be opened within the zip. Wildcards * and ? are supported. (default:*)') | ||
| 246 | - parser.add_option('-l', '--loglevel', dest="loglevel", action="store", default=DEFAULT_LOG_LEVEL, | ||
| 247 | - help="logging level debug/info/warning/error/critical (default=%default)") | ||
| 248 | - parser.add_option("-m", '--matches', action="store_true", dest="show_matches", | ||
| 249 | - help='Show matched strings.') | ||
| 250 | - | ||
| 251 | - # TODO: add logfile option | ||
| 252 | - | ||
| 253 | - (options, args) = parser.parse_args() | ||
| 254 | - | ||
| 255 | - # Print help if no arguments are passed | ||
| 256 | - if len(args) == 0: | ||
| 257 | - print('MacroRaptor %s - http://decalage.info/python/oletools' % __version__) | ||
| 258 | - print('This is work in progress, please report issues at %s' % URL_ISSUES) | ||
| 259 | - print(__doc__) | ||
| 260 | - parser.print_help() | ||
| 261 | - print('\nAn exit code is returned based on the analysis result:') | ||
| 262 | - for result in (Result_NoMacro, Result_NotMSOffice, Result_MacroOK, Result_Error, Result_Suspicious): | ||
| 263 | - print(' - %d: %s' % (result.exit_code, result.name)) | ||
| 264 | - sys.exit() | ||
| 265 | - | ||
| 266 | - # print banner with version | ||
| 267 | - print('MacroRaptor %s - http://decalage.info/python/oletools' % __version__) | ||
| 268 | - print('This is work in progress, please report issues at %s' % URL_ISSUES) | ||
| 269 | - | ||
| 270 | - logging.basicConfig(level=LOG_LEVELS[options.loglevel], format='%(levelname)-8s %(message)s') | ||
| 271 | - # enable logging in the modules: | ||
| 272 | - log.setLevel(logging.NOTSET) | ||
| 273 | - | ||
| 274 | - t = tablestream.TableStream(style=tablestream.TableStyleSlim, | ||
| 275 | - header_row=['Result', 'Flags', 'Type', 'File'], | ||
| 276 | - column_width=[10, 5, 4, 56]) | ||
| 277 | - | ||
| 278 | - exitcode = -1 | ||
| 279 | - global_result = None | ||
| 280 | - # TODO: handle errors in xglob, to continue processing the next files | ||
| 281 | - for container, filename, data in xglob.iter_files(args, recursive=options.recursive, | ||
| 282 | - zip_password=options.zip_password, zip_fname=options.zip_fname): | ||
| 283 | - # ignore directory names stored in zip files: | ||
| 284 | - if container and filename.endswith('/'): | ||
| 285 | - continue | ||
| 286 | - full_name = '%s in %s' % (filename, container) if container else filename | ||
| 287 | - # try: | ||
| 288 | - # # Open the file | ||
| 289 | - # if data is None: | ||
| 290 | - # data = open(filename, 'rb').read() | ||
| 291 | - # except: | ||
| 292 | - # log.exception('Error when opening file %r' % full_name) | ||
| 293 | - # continue | ||
| 294 | - if isinstance(data, Exception): | ||
| 295 | - result = Result_Error | ||
| 296 | - t.write_row([result.name, '', '', full_name], | ||
| 297 | - colors=[result.color, None, None, None]) | ||
| 298 | - t.write_row(['', '', '', str(data)], | ||
| 299 | - colors=[None, None, None, result.color]) | ||
| 300 | - else: | ||
| 301 | - filetype = '???' | ||
| 302 | - try: | ||
| 303 | - vba_parser = olevba.VBA_Parser(filename=filename, data=data, container=container) | ||
| 304 | - filetype = TYPE2TAG[vba_parser.type] | ||
| 305 | - except Exception as e: | ||
| 306 | - # log.error('Error when parsing VBA macros from file %r' % full_name) | ||
| 307 | - # TODO: distinguish actual errors from non-MSOffice files | ||
| 308 | - result = Result_Error | ||
| 309 | - t.write_row([result.name, '', filetype, full_name], | ||
| 310 | - colors=[result.color, None, None, None]) | ||
| 311 | - t.write_row(['', '', '', str(e)], | ||
| 312 | - colors=[None, None, None, result.color]) | ||
| 313 | - continue | ||
| 314 | - if vba_parser.detect_vba_macros(): | ||
| 315 | - vba_code_all_modules = '' | ||
| 316 | - try: | ||
| 317 | - for (subfilename, stream_path, vba_filename, vba_code) in vba_parser.extract_all_macros(): | ||
| 318 | - vba_code_all_modules += vba_code.decode('utf-8','replace') + '\n' | ||
| 319 | - except Exception as e: | ||
| 320 | - # log.error('Error when parsing VBA macros from file %r' % full_name) | ||
| 321 | - result = Result_Error | ||
| 322 | - t.write_row([result.name, '', TYPE2TAG[vba_parser.type], full_name], | ||
| 323 | - colors=[result.color, None, None, None]) | ||
| 324 | - t.write_row(['', '', '', str(e)], | ||
| 325 | - colors=[None, None, None, result.color]) | ||
| 326 | - continue | ||
| 327 | - mraptor = MacroRaptor(vba_code_all_modules) | ||
| 328 | - mraptor.scan() | ||
| 329 | - if mraptor.suspicious: | ||
| 330 | - result = Result_Suspicious | ||
| 331 | - else: | ||
| 332 | - result = Result_MacroOK | ||
| 333 | - t.write_row([result.name, mraptor.get_flags(), filetype, full_name], | ||
| 334 | - colors=[result.color, None, None, None]) | ||
| 335 | - if mraptor.matches and options.show_matches: | ||
| 336 | - t.write_row(['', '', '', 'Matches: %r' % mraptor.matches]) | ||
| 337 | - else: | ||
| 338 | - result = Result_NoMacro | ||
| 339 | - t.write_row([result.name, '', filetype, full_name], | ||
| 340 | - colors=[result.color, None, None, None]) | ||
| 341 | - if result.exit_code > exitcode: | ||
| 342 | - global_result = result | ||
| 343 | - exitcode = result.exit_code | ||
| 344 | - | ||
| 345 | - print('') | ||
| 346 | - print('Flags: A=AutoExec, W=Write, X=Execute') | ||
| 347 | - print('Exit code: %d - %s' % (exitcode, global_result.name)) | ||
| 348 | - sys.exit(exitcode) | 19 | +from oletools.mraptor import * |
| 20 | +from oletools.mraptor import __doc__, __version__ | ||
| 349 | 21 | ||
| 350 | if __name__ == '__main__': | 22 | if __name__ == '__main__': |
| 351 | main() | 23 | main() |
| 352 | - | ||
| 353 | -# Soundtrack: "Dark Child" by Marlon Williams |