Commit 005c28ca7eed08f1f552b7fc965e8252808acba5
1 parent
19b6fd45
mraptor: reverted to python 2.7 version, moved python 3 version to mraptor3.py
Showing
2 changed files
with
339 additions
and
9 deletions
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 | ... | ... |