Commit 005c28ca7eed08f1f552b7fc965e8252808acba5

Authored by decalage2
1 parent 19b6fd45

mraptor: reverted to python 2.7 version, moved python 3 version to mraptor3.py

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