Commit efe28a7b3c4ee89bc0db7b5d0b1721471831cb19

Authored by Philippe Lagadec
1 parent 76616bb7

added new tool mraptor - MacroRaptor

Showing 1 changed file with 291 additions and 0 deletions
oletools/mraptor.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 documents
  6 +(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 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 +
  52 +__version__ = '0.01'
  53 +
  54 +#------------------------------------------------------------------------------
  55 +# TODO:
  56 +
  57 +
  58 +#--- IMPORTS ------------------------------------------------------------------
  59 +
  60 +import sys, logging, optparse, re
  61 +
  62 +from thirdparty.xglob import xglob
  63 +from thirdparty.tablestream import tablestream
  64 +
  65 +import olevba
  66 +
  67 +# === LOGGING =================================================================
  68 +
  69 +# a global logger object used for debugging:
  70 +log = olevba.get_logger('mraptor')
  71 +
  72 +
  73 +#--- CONSTANTS ----------------------------------------------------------------
  74 +
  75 +# URL and message to report issues:
  76 +URL_ISSUES = 'https://bitbucket.org/decalage/oletools/issues'
  77 +MSG_ISSUES = 'Please report this issue on %s' % URL_ISSUES
  78 +
  79 +# 'AutoExec', 'AutoOpen', 'Auto_Open', 'AutoClose', 'Auto_Close', 'AutoNew', 'AutoExit',
  80 +# 'Document_Open', 'DocumentOpen',
  81 +# 'Document_Close', 'DocumentBeforeClose',
  82 +# 'DocumentChange','Document_New',
  83 +# 'NewDocument'
  84 +# 'Workbook_Open', 'Workbook_Close',
  85 +
  86 +# TODO: check if line also contains Sub or Function
  87 +re_autoexec = re.compile(r'(?i)\b(?:Auto(?:Exec|_?Open|_?Close|Exit|New)' +
  88 + r'|Document(?:_?Open|_Close|BeforeClose|Change|_New)' +
  89 + r'|NewDocument|Workbook(?:_Open|_Activate|_Close))\b')
  90 +
  91 +# MS-VBAL 5.4.5.1 Open Statement:
  92 +RE_OPEN_WRITE = r'(?:\bOpen\b[^\n]+\b(?:Write|Append|Binary|Output|Random)\b)'
  93 +
  94 +re_write = re.compile(r'(?i)\b(?:FileCopy|CopyFile|Kill|CreateTextFile|'
  95 + + r'VirtualAlloc|RtlMoveMemory|URLDownloadToFileA?|AltStartupPath|'
  96 + + r'ADODB\.Stream|WriteText|SaveToFile|SaveAs|SaveAsRTF|FileSaveAs|MkDir|RmDir|SaveSetting|SetAttr)\b|' + RE_OPEN_WRITE)
  97 +
  98 +# MS-VBAL 5.2.3.5 External Procedure Declaration
  99 +RE_DECLARE_LIB = r'(?:\bDeclare\b[^\n]+\bLib\b)'
  100 +
  101 +re_execute = re.compile(r'(?i)\b(?:Shell|CreateObject|GetObject|SendKeys|'
  102 + + r'MacScript|FollowHyperlink|CreateThread|ShellExecute)\b|' + RE_DECLARE_LIB)
  103 +
  104 +# short tag to display file types in triage mode:
  105 +TYPE2TAG = {
  106 + olevba.TYPE_OLE: 'OLE',
  107 + olevba.TYPE_OpenXML: 'OpX',
  108 + olevba.TYPE_Word2003_XML: 'XML',
  109 + olevba.TYPE_MHTML: 'MHT',
  110 + olevba.TYPE_TEXT: 'TXT',
  111 +}
  112 +
  113 +
  114 +# === CLASSES =================================================================
  115 +
  116 +class MacroRaptor(object):
  117 + """
  118 + class to scan VBA macro code to detect if it is malicious
  119 + """
  120 + def __init__(self, vba_code):
  121 + """
  122 + MacroRaptor constructor
  123 + :param vba_code: string containing the VBA macro code
  124 + """
  125 + # TODO: collapse long lines first
  126 + self.vba_code = vba_code
  127 + self.autoexec = False
  128 + self.write = False
  129 + self.execute = False
  130 + self.flags = ''
  131 + self.suspicious = False
  132 + self.autoexec_match = None
  133 + self.write_match = None
  134 + self.execute_match = None
  135 + self.matches = []
  136 +
  137 + def scan(self):
  138 + """
  139 + Scan the VBA macro code to detect if it is malicious
  140 + :return:
  141 + """
  142 + m = re_autoexec.search(self.vba_code)
  143 + if m is not None:
  144 + self.autoexec = True
  145 + self.autoexec_match = m.group()
  146 + self.matches.append(m.group())
  147 + m = re_write.search(self.vba_code)
  148 + if m is not None:
  149 + self.write = True
  150 + self.write_match = m.group()
  151 + self.matches.append(m.group())
  152 + m = re_execute.search(self.vba_code)
  153 + if m is not None:
  154 + self.execute = True
  155 + self.execute_match = m.group()
  156 + self.matches.append(m.group())
  157 + if self.autoexec and (self.execute or self.write):
  158 + self.suspicious = True
  159 +
  160 + def get_flags(self):
  161 + flags = ''
  162 + flags += 'A' if self.autoexec else '-'
  163 + flags += 'W' if self.write else '-'
  164 + flags += 'X' if self.execute else '-'
  165 + return flags
  166 +
  167 +
  168 +# === MAIN ====================================================================
  169 +
  170 +def main():
  171 + """
  172 + Main function, called when olevba is run from the command line
  173 + """
  174 + global log
  175 + DEFAULT_LOG_LEVEL = "warning" # Default log level
  176 + LOG_LEVELS = {
  177 + 'debug': logging.DEBUG,
  178 + 'info': logging.INFO,
  179 + 'warning': logging.WARNING,
  180 + 'error': logging.ERROR,
  181 + 'critical': logging.CRITICAL
  182 + }
  183 +
  184 + usage = 'usage: %prog [options] <filename> [filename2 ...]'
  185 + parser = optparse.OptionParser(usage=usage)
  186 + parser.add_option("-r", action="store_true", dest="recursive",
  187 + help='find files recursively in subdirectories.')
  188 + parser.add_option("-z", "--zip", dest='zip_password', type='str', default=None,
  189 + help='if the file is a zip archive, open all files from it, using the provided password (requires Python 2.6+)')
  190 + parser.add_option("-f", "--zipfname", dest='zip_fname', type='str', default='*',
  191 + help='if the file is a zip archive, file(s) to be opened within the zip. Wildcards * and ? are supported. (default:*)')
  192 + parser.add_option('-l', '--loglevel', dest="loglevel", action="store", default=DEFAULT_LOG_LEVEL,
  193 + help="logging level debug/info/warning/error/critical (default=%default)")
  194 + parser.add_option("-m", '--matches', action="store_true", dest="show_matches",
  195 + help='Show matched strings.')
  196 +
  197 + # TODO: add logfile option
  198 +
  199 + (options, args) = parser.parse_args()
  200 +
  201 + # Print help if no arguments are passed
  202 + if len(args) == 0:
  203 + print __doc__
  204 + parser.print_help()
  205 + sys.exit()
  206 +
  207 + # print banner with version
  208 + print 'MacroRaptor %s - http://decalage.info/python/oletools' % __version__
  209 + print 'This is work in progress, please report issues at %s' % URL_ISSUES
  210 +
  211 + logging.basicConfig(level=LOG_LEVELS[options.loglevel], format='%(levelname)-8s %(message)s')
  212 + # enable logging in the modules:
  213 + log.setLevel(logging.NOTSET)
  214 +
  215 + t = tablestream.TableStream(style=tablestream.TableStyleSlim,
  216 + header_row=['Result', 'Flags', 'Type', 'File'],
  217 + column_width=[10, 5, 4, 56])
  218 +
  219 + # TODO: handle errors in xglob, to continue processing the next files
  220 + for container, filename, data in xglob.iter_files(args, recursive=options.recursive,
  221 + zip_password=options.zip_password, zip_fname=options.zip_fname):
  222 + # ignore directory names stored in zip files:
  223 + if container and filename.endswith('/'):
  224 + continue
  225 + full_name = '%s in %s' % (filename, container) if container else filename
  226 + # try:
  227 + # # Open the file
  228 + # if data is None:
  229 + # data = open(filename, 'rb').read()
  230 + # except:
  231 + # log.exception('Error when opening file %r' % full_name)
  232 + # continue
  233 + if isinstance(data, Exception):
  234 + result = '* ERROR *'
  235 + result_color = 'yellow'
  236 + t.write_row([result, '', '', full_name],
  237 + colors=[result_color, None, None, None])
  238 + t.write_row(['', '', '', str(data)],
  239 + colors=[None, None, None, result_color])
  240 + else:
  241 + try:
  242 + vba_parser = olevba.VBA_Parser(filename=filename, data=data, container=container)
  243 + filetype = TYPE2TAG[vba_parser.type]
  244 + except Exception as e:
  245 + # log.error('Error when parsing VBA macros from file %r' % full_name)
  246 + result = '* ERROR *'
  247 + result_color = 'yellow'
  248 + t.write_row([result, '', TYPE2TAG[vba_parser.type], full_name],
  249 + colors=[result_color, None, None, None])
  250 + t.write_row(['', '', '', str(e)],
  251 + colors=[None, None, None, result_color])
  252 + continue
  253 + if vba_parser.detect_vba_macros():
  254 + vba_code_all_modules = ''
  255 + try:
  256 + for (subfilename, stream_path, vba_filename, vba_code) in vba_parser.extract_all_macros():
  257 + vba_code_all_modules += vba_code + '\n'
  258 + except Exception as e:
  259 + # log.error('Error when parsing VBA macros from file %r' % full_name)
  260 + result = '* ERROR *'
  261 + result_color = 'yellow'
  262 + t.write_row([result, '', TYPE2TAG[vba_parser.type], full_name],
  263 + colors=[result_color, None, None, None])
  264 + t.write_row(['', '', '', str(e)],
  265 + colors=[None, None, None, result_color])
  266 + continue
  267 + mraptor = MacroRaptor(vba_code_all_modules)
  268 + mraptor.scan()
  269 + if mraptor.suspicious:
  270 + result = 'SUSPICIOUS'
  271 + result_color = 'red'
  272 + else:
  273 + result = 'Macro OK'
  274 + result_color = 'cyan'
  275 + t.write_row([result, mraptor.get_flags(), filetype, full_name],
  276 + colors=[result_color, None, None, None])
  277 + if mraptor.matches and options.show_matches:
  278 + t.write_row(['', '', '', 'Matches: %r' % mraptor.matches])
  279 + else:
  280 + result = 'No Macro'
  281 + result_color = 'green'
  282 + t.write_row([result, '', filetype, full_name],
  283 + colors=[result_color, None, None, None])
  284 +
  285 + print ''
  286 + print 'Flags: A=AutoExec, W=Write, X=Execute'
  287 +
  288 +if __name__ == '__main__':
  289 + main()
  290 +
  291 +# Soundtrack: "Dark Child" by Marlon Williams