#!/usr/bin/env python """ mraptor.py - MacroRaptor MacroRaptor is a script to parse OLE and OpenXML files such as MS Office documents (e.g. Word, Excel), to detect malicious macros. Supported formats: - Word 97-2003 (.doc, .dot), Word 2007+ (.docm, .dotm) - Excel 97-2003 (.xls), Excel 2007+ (.xlsm, .xlsb) - PowerPoint 97-2003 (.ppt), PowerPoint 2007+ (.pptm, .ppsm) - Word/PowerPoint 2007+ XML (aka Flat OPC) - Word 2003 XML (.xml) - Word/Excel Single File Web Page / MHTML (.mht) - Publisher (.pub) Author: Philippe Lagadec - http://www.decalage.info License: BSD, see source code or documentation MacroRaptor is part of the python-oletools package: http://www.decalage.info/python/oletools """ # === LICENSE ================================================================== # MacroRaptor is copyright (c) 2016-2021 Philippe Lagadec (http://www.decalage.info) # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #------------------------------------------------------------------------------ # CHANGELOG: # 2016-02-23 v0.01 PL: - first version # 2016-02-29 v0.02 PL: - added Workbook_Activate, FileSaveAs # 2016-03-04 v0.03 PL: - returns an exit code based on the overall result # 2016-03-08 v0.04 PL: - collapse long lines before analysis # 2016-08-31 v0.50 PL: - added macro trigger InkPicture_Painted # 2016-09-05 PL: - added Document_BeforeClose keyword for MS Publisher (.pub) # 2016-10-25 PL: - fixed print for Python 3 # 2016-12-21 v0.51 PL: - added more ActiveX macro triggers # 2017-03-08 PL: - fixed absolute imports # 2018-05-25 v0.53 PL: - added Word/PowerPoint 2007+ XML (aka Flat OPC) issue #283 # 2019-04-04 v0.54 PL: - added ExecuteExcel4Macro, ShellExecuteA, XLM keywords # 2019-11-06 v0.55 PL: - added SetTimer # 2020-04-20 v0.56 PL: - added keywords RUN and CALL for XLM macros (issue #562) # 2021-04-14 PL: - added Workbook_BeforeClose (issue #518) __version__ = '0.56.2' #------------------------------------------------------------------------------ # TODO: #--- IMPORTS ------------------------------------------------------------------ import sys, optparse, re, os # IMPORTANT: it should be possible to run oletools directly as scripts # in any directory without installing them with pip or setup.py. # In that case, relative imports are NOT usable. # And to enable Python 2+3 compatibility, we need to use absolute imports, # so we add the oletools parent folder to sys.path (absolute+normalized path): _thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__))) # print('_thismodule_dir = %r' % _thismodule_dir) _parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..')) # print('_parent_dir = %r' % _thirdparty_dir) if not _parent_dir in sys.path: sys.path.insert(0, _parent_dir) from oletools.thirdparty.xglob import xglob from oletools.thirdparty.tablestream import tablestream from oletools import olevba from oletools.olevba import TYPE2TAG from oletools.common.log_helper import log_helper # === LOGGING ================================================================= # a global logger object used for debugging: log = log_helper.get_or_create_silent_logger('mraptor') #--- CONSTANTS ---------------------------------------------------------------- # URL and message to report issues: # TODO: make it a common variable for all oletools URL_ISSUES = 'https://github.com/decalage2/oletools/issues' MSG_ISSUES = 'Please report this issue on %s' % URL_ISSUES # 'AutoExec', 'AutoOpen', 'Auto_Open', 'AutoClose', 'Auto_Close', 'AutoNew', 'AutoExit', # 'Document_Open', 'DocumentOpen', # 'Document_Close', 'DocumentBeforeClose', 'Document_BeforeClose', # 'DocumentChange','Document_New', # 'NewDocument' # 'Workbook_Open', 'Workbook_Close', # *_Painted such as InkPicture1_Painted # *_GotFocus|LostFocus|MouseHover for other ActiveX objects # reference: http://www.greyhathacker.net/?p=948 # TODO: check if line also contains Sub or Function re_autoexec = re.compile(r'(?i)\b(?:Auto(?:Exec|_?Open|_?Close|Exit|New)' + r'|Document(?:_?Open|_Close|_?BeforeClose|Change|_New)' + r'|NewDocument|Workbook(?:_Open|_Activate|_Close|_BeforeClose)' + r'|\w+_(?:Painted|Painting|GotFocus|LostFocus|MouseHover' + r'|Layout|Click|Change|Resize|BeforeNavigate2|BeforeScriptExecute' + r'|DocumentComplete|DownloadBegin|DownloadComplete|FileDownload' + r'|NavigateComplete2|NavigateError|ProgressChange|PropertyChange' + r'|SetSecureLockIcon|StatusTextChange|TitleChange|MouseMove' + r'|MouseEnter|MouseLeave|OnConnecting))|Auto_Ope\b') # TODO: "Auto_Ope" is temporarily here because of a bug in plugin_biff, which misses the last byte in "Auto_Open"... # MS-VBAL 5.4.5.1 Open Statement: RE_OPEN_WRITE = r'(?:\bOpen\b[^\n]+\b(?:Write|Append|Binary|Output|Random)\b)' re_write = re.compile(r'(?i)\b(?:FileCopy|CopyFile|Kill|CreateTextFile|' + r'VirtualAlloc|RtlMoveMemory|URLDownloadToFileA?|AltStartupPath|WriteProcessMemory|' + r'ADODB\.Stream|WriteText|SaveToFile|SaveAs|SaveAsRTF|FileSaveAs|MkDir|RmDir|SaveSetting|SetAttr)\b|' + RE_OPEN_WRITE) # MS-VBAL 5.2.3.5 External Procedure Declaration RE_DECLARE_LIB = r'(?:\bDeclare\b[^\n]+\bLib\b)' re_execute = re.compile(r'(?i)\b(?:Shell|CreateObject|GetObject|SendKeys|RUN|CALL|' + r'MacScript|FollowHyperlink|CreateThread|ShellExecuteA?|ExecuteExcel4Macro|EXEC|REGISTER|SetTimer)\b|' + RE_DECLARE_LIB) # === CLASSES ================================================================= class Result_NoMacro(object): exit_code = 0 color = 'green' name = 'No Macro' class Result_NotMSOffice(object): exit_code = 1 color = 'green' name = 'Not MS Office' class Result_MacroOK(object): exit_code = 2 color = 'cyan' name = 'Macro OK' class Result_Error(object): exit_code = 10 color = 'yellow' name = 'ERROR' class Result_Suspicious(object): exit_code = 20 color = 'red' name = 'SUSPICIOUS' class MacroRaptor(object): """ class to scan VBA macro code to detect if it is malicious """ def __init__(self, vba_code): """ MacroRaptor constructor :param vba_code: string containing the VBA macro code """ # collapse long lines first self.vba_code = olevba.vba_collapse_long_lines(vba_code) self.autoexec = False self.write = False self.execute = False self.flags = '' self.suspicious = False self.autoexec_match = None self.write_match = None self.execute_match = None self.matches = [] def scan(self): """ Scan the VBA macro code to detect if it is malicious :return: """ m = re_autoexec.search(self.vba_code) if m is not None: self.autoexec = True self.autoexec_match = m.group() self.matches.append(m.group()) m = re_write.search(self.vba_code) if m is not None: self.write = True self.write_match = m.group() self.matches.append(m.group()) m = re_execute.search(self.vba_code) if m is not None: self.execute = True self.execute_match = m.group() self.matches.append(m.group()) if self.autoexec and (self.execute or self.write): self.suspicious = True def get_flags(self): flags = '' flags += 'A' if self.autoexec else '-' flags += 'W' if self.write else '-' flags += 'X' if self.execute else '-' return flags # === MAIN ==================================================================== def main(): """ Main function, called when olevba is run from the command line """ DEFAULT_LOG_LEVEL = "warning" # Default log level usage = 'usage: mraptor [options] <filename> [filename2 ...]' parser = optparse.OptionParser(usage=usage) parser.add_option("-r", action="store_true", dest="recursive", help='find files recursively in subdirectories.') parser.add_option("-z", "--zip", dest='zip_password', type='str', default=None, help='if the file is a zip archive, open all files from it, using the provided password (requires Python 2.6+)') parser.add_option("-f", "--zipfname", dest='zip_fname', type='str', default='*', help='if the file is a zip archive, file(s) to be opened within the zip. Wildcards * and ? are supported. (default:*)') parser.add_option('-l', '--loglevel', dest="loglevel", action="store", default=DEFAULT_LOG_LEVEL, help="logging level debug/info/warning/error/critical (default=%default)") parser.add_option("-m", '--matches', action="store_true", dest="show_matches", help='Show matched strings.') # TODO: add logfile option (options, args) = parser.parse_args() # Print help if no arguments are passed if len(args) == 0: print('MacroRaptor %s - http://decalage.info/python/oletools' % __version__) print('This is work in progress, please report issues at %s' % URL_ISSUES) print(__doc__) parser.print_help() print('\nAn exit code is returned based on the analysis result:') for result in (Result_NoMacro, Result_NotMSOffice, Result_MacroOK, Result_Error, Result_Suspicious): print(' - %d: %s' % (result.exit_code, result.name)) sys.exit() # print banner with version print('MacroRaptor %s - http://decalage.info/python/oletools' % __version__) print('This is work in progress, please report issues at %s' % URL_ISSUES) log_helper.enable_logging(level=options.loglevel) # enable logging in the modules: olevba.enable_logging() t = tablestream.TableStream(style=tablestream.TableStyleSlim, header_row=['Result', 'Flags', 'Type', 'File'], column_width=[10, 5, 4, 56]) exitcode = -1 global_result = None # TODO: handle errors in xglob, to continue processing the next files for container, filename, data in xglob.iter_files(args, recursive=options.recursive, zip_password=options.zip_password, zip_fname=options.zip_fname): # ignore directory names stored in zip files: if container and filename.endswith('/'): continue full_name = '%s in %s' % (filename, container) if container else filename # try: # # Open the file # if data is None: # data = open(filename, 'rb').read() # except: # log.exception('Error when opening file %r' % full_name) # continue if isinstance(data, Exception): result = Result_Error t.write_row([result.name, '', '', full_name], colors=[result.color, None, None, None]) t.write_row(['', '', '', str(data)], colors=[None, None, None, result.color]) else: filetype = '???' try: vba_parser = olevba.VBA_Parser(filename=filename, data=data, container=container) filetype = TYPE2TAG[vba_parser.type] except Exception as e: # log.error('Error when parsing VBA macros from file %r' % full_name) # TODO: distinguish actual errors from non-MSOffice files result = Result_Error t.write_row([result.name, '', filetype, full_name], colors=[result.color, None, None, None]) t.write_row(['', '', '', str(e)], colors=[None, None, None, result.color]) continue if vba_parser.detect_vba_macros(): vba_code_all_modules = '' try: vba_code_all_modules = vba_parser.get_vba_code_all_modules() except Exception as e: # log.error('Error when parsing VBA macros from file %r' % full_name) result = Result_Error t.write_row([result.name, '', TYPE2TAG[vba_parser.type], full_name], colors=[result.color, None, None, None]) t.write_row(['', '', '', str(e)], colors=[None, None, None, result.color]) continue mraptor = MacroRaptor(vba_code_all_modules) mraptor.scan() if mraptor.suspicious: result = Result_Suspicious else: result = Result_MacroOK t.write_row([result.name, mraptor.get_flags(), filetype, full_name], colors=[result.color, None, None, None]) if mraptor.matches and options.show_matches: t.write_row(['', '', '', 'Matches: %r' % mraptor.matches]) else: result = Result_NoMacro t.write_row([result.name, '', filetype, full_name], colors=[result.color, None, None, None]) if result.exit_code > exitcode: global_result = result exitcode = result.exit_code log_helper.end_logging() print('') print('Flags: A=AutoExec, W=Write, X=Execute') print('Exit code: %d - %s' % (exitcode, global_result.name)) sys.exit(exitcode) if __name__ == '__main__': main() # Soundtrack: "Dark Child" by Marlon Williams
Memory