Commit 3cae86e62d99a29858721ca6f99c8c2cf15be8e4
1 parent
e28b2001
olevba: if XLMMacroDeobfuscator is available, use it to extract and deobfuscate XLM macros
Showing
1 changed file
with
196 additions
and
80 deletions
oletools/olevba.py
| @@ -235,7 +235,7 @@ from __future__ import print_function | @@ -235,7 +235,7 @@ from __future__ import print_function | ||
| 235 | # for issue #619) | 235 | # for issue #619) |
| 236 | # 2021-04-14 PL: - added detection of Workbook_BeforeClose (issue #518) | 236 | # 2021-04-14 PL: - added detection of Workbook_BeforeClose (issue #518) |
| 237 | 237 | ||
| 238 | -__version__ = '0.56.2' | 238 | +__version__ = '0.60.dev2' |
| 239 | 239 | ||
| 240 | #------------------------------------------------------------------------------ | 240 | #------------------------------------------------------------------------------ |
| 241 | # TODO: | 241 | # TODO: |
| @@ -310,6 +310,18 @@ import colorclass | @@ -310,6 +310,18 @@ import colorclass | ||
| 310 | if os.name == 'nt': | 310 | if os.name == 'nt': |
| 311 | colorclass.Windows.enable(auto_colors=True) | 311 | colorclass.Windows.enable(auto_colors=True) |
| 312 | 312 | ||
| 313 | +from pyparsing import \ | ||
| 314 | + CaselessKeyword, CaselessLiteral, Combine, Forward, Literal, \ | ||
| 315 | + Optional, QuotedString,Regex, Suppress, Word, WordStart, \ | ||
| 316 | + alphanums, alphas, hexnums,nums, opAssoc, srange, \ | ||
| 317 | + infixNotation, ParserElement | ||
| 318 | + | ||
| 319 | +# attempt to import XLMMacroDeobfuscator (optional) | ||
| 320 | +try: | ||
| 321 | + from XLMMacroDeobfuscator import deobfuscator as xlmdeobfuscator | ||
| 322 | + XLMDEOBFUSCATOR = True | ||
| 323 | +except ImportError: | ||
| 324 | + XLMDEOBFUSCATOR = False | ||
| 313 | 325 | ||
| 314 | # IMPORTANT: it should be possible to run oletools directly as scripts | 326 | # IMPORTANT: it should be possible to run oletools directly as scripts |
| 315 | # in any directory without installing them with pip or setup.py. | 327 | # in any directory without installing them with pip or setup.py. |
| @@ -326,17 +338,14 @@ if _parent_dir not in sys.path: | @@ -326,17 +338,14 @@ if _parent_dir not in sys.path: | ||
| 326 | import olefile | 338 | import olefile |
| 327 | from oletools.thirdparty.tablestream import tablestream | 339 | from oletools.thirdparty.tablestream import tablestream |
| 328 | from oletools.thirdparty.xglob import xglob, PathNotFoundException | 340 | from oletools.thirdparty.xglob import xglob, PathNotFoundException |
| 329 | -from pyparsing import \ | ||
| 330 | - CaselessKeyword, CaselessLiteral, Combine, Forward, Literal, \ | ||
| 331 | - Optional, QuotedString,Regex, Suppress, Word, WordStart, \ | ||
| 332 | - alphanums, alphas, hexnums,nums, opAssoc, srange, \ | ||
| 333 | - infixNotation, ParserElement | 341 | +from oletools.thirdparty.oledump.plugin_biff import cBIFF |
| 334 | from oletools import ppt_parser | 342 | from oletools import ppt_parser |
| 335 | from oletools import oleform | 343 | from oletools import oleform |
| 336 | from oletools import rtfobj | 344 | from oletools import rtfobj |
| 337 | from oletools import crypto | 345 | from oletools import crypto |
| 338 | from oletools.common.io_encoding import ensure_stdout_handles_unicode | 346 | from oletools.common.io_encoding import ensure_stdout_handles_unicode |
| 339 | from oletools.common import codepages | 347 | from oletools.common import codepages |
| 348 | +from oletools import ftguess | ||
| 340 | 349 | ||
| 341 | # === PYTHON 2+3 SUPPORT ====================================================== | 350 | # === PYTHON 2+3 SUPPORT ====================================================== |
| 342 | 351 | ||
| @@ -2710,7 +2719,8 @@ class VBA_Parser(object): | @@ -2710,7 +2719,8 @@ class VBA_Parser(object): | ||
| 2710 | self.type = None | 2719 | self.type = None |
| 2711 | self.vba_projects = None | 2720 | self.vba_projects = None |
| 2712 | self.vba_forms = None | 2721 | self.vba_forms = None |
| 2713 | - self.contains_macros = None # will be set to True or False by detect_macros | 2722 | + self.contains_vba_macros = None # will be set to True or False by detect_vba_macros |
| 2723 | + self.contains_xlm_macros = None # will be set to True or False by detect_xlm_macros | ||
| 2714 | self.vba_code_all_modules = None # to store the source code of all modules | 2724 | self.vba_code_all_modules = None # to store the source code of all modules |
| 2715 | # list of tuples for each module: (subfilename, stream_path, vba_filename, vba_code) | 2725 | # list of tuples for each module: (subfilename, stream_path, vba_filename, vba_code) |
| 2716 | self.modules = None | 2726 | self.modules = None |
| @@ -2736,9 +2746,13 @@ class VBA_Parser(object): | @@ -2736,9 +2746,13 @@ class VBA_Parser(object): | ||
| 2736 | self.vba_stomping_detected = None | 2746 | self.vba_stomping_detected = None |
| 2737 | # will be set to True or False by detect_is_encrypted method | 2747 | # will be set to True or False by detect_is_encrypted method |
| 2738 | self.is_encrypted = False | 2748 | self.is_encrypted = False |
| 2749 | + # TODO: those are disabled for now: | ||
| 2739 | self.xlm_macrosheet_found = False | 2750 | self.xlm_macrosheet_found = False |
| 2740 | self.template_injection_found = False | 2751 | self.template_injection_found = False |
| 2741 | 2752 | ||
| 2753 | + # call ftguess to identify file type: | ||
| 2754 | + self.ftg = ftguess.FileTypeGuesser(self.filename, data=data) | ||
| 2755 | + log.debug('ftguess: file type=%s - container=%s' % (self.ftg.ftype.name, self.ftg.container)) | ||
| 2742 | # if filename is None: | 2756 | # if filename is None: |
| 2743 | # if isinstance(_file, basestring): | 2757 | # if isinstance(_file, basestring): |
| 2744 | # if len(_file) < olefile.MINIMAL_OLEFILE_SIZE: | 2758 | # if len(_file) < olefile.MINIMAL_OLEFILE_SIZE: |
| @@ -2752,7 +2766,7 @@ class VBA_Parser(object): | @@ -2752,7 +2766,7 @@ class VBA_Parser(object): | ||
| 2752 | self.open_ole(_file) | 2766 | self.open_ole(_file) |
| 2753 | 2767 | ||
| 2754 | # if this worked, try whether it is a ppt file (special ole file) | 2768 | # if this worked, try whether it is a ppt file (special ole file) |
| 2755 | - # TODO: instead of this we should have a function to test if it is a PPT | 2769 | + # TODO: instead of this we should have a function to test if it is a PPT (e.g. using ftguess) |
| 2756 | self.open_ppt() | 2770 | self.open_ppt() |
| 2757 | if self.type is None and zipfile.is_zipfile(_file): | 2771 | if self.type is None and zipfile.is_zipfile(_file): |
| 2758 | # Zip file, which may be an OpenXML document | 2772 | # Zip file, which may be an OpenXML document |
| @@ -2840,55 +2854,55 @@ class VBA_Parser(object): | @@ -2840,55 +2854,55 @@ class VBA_Parser(object): | ||
| 2840 | #TODO: if the zip file is encrypted, suggest to use the -z option, or try '-z infected' automatically | 2854 | #TODO: if the zip file is encrypted, suggest to use the -z option, or try '-z infected' automatically |
| 2841 | # check each file within the zip if it is an OLE file, by reading its magic: | 2855 | # check each file within the zip if it is an OLE file, by reading its magic: |
| 2842 | for subfile in z.namelist(): | 2856 | for subfile in z.namelist(): |
| 2843 | - log.debug("subfile {}".format(subfile)) | 2857 | + log.debug("OpenXML subfile {}".format(subfile)) |
| 2844 | with z.open(subfile) as file_handle: | 2858 | with z.open(subfile) as file_handle: |
| 2845 | - found_ole = False | ||
| 2846 | - template_injection_detected = False | ||
| 2847 | - xml_macrosheet_found = False | 2859 | + # found_ole = False |
| 2860 | + # template_injection_detected = False | ||
| 2861 | + # xml_macrosheet_found = False | ||
| 2848 | magic = file_handle.read(len(olefile.MAGIC)) | 2862 | magic = file_handle.read(len(olefile.MAGIC)) |
| 2849 | if magic == olefile.MAGIC: | 2863 | if magic == olefile.MAGIC: |
| 2850 | - found_ole = True | ||
| 2851 | - # in case we did not find an OLE file, | ||
| 2852 | - # there could be a XLM macrosheet or a template injection attempt | ||
| 2853 | - if not found_ole: | ||
| 2854 | - read_all_file = file_handle.read() | ||
| 2855 | - # try to detect template injection attempt | ||
| 2856 | - # https://ired.team/offensive-security/initial-access/phishing-with-ms-office/inject-macros-from-a-remote-dotm-template-docx-with-macros | ||
| 2857 | - subfile_that_can_contain_templates = "word/_rels/settings.xml.rels" | ||
| 2858 | - if subfile == subfile_that_can_contain_templates: | ||
| 2859 | - regex_template = b"Type=\"http://schemas\.openxmlformats\.org/officeDocument/\d{4}/relationships/attachedTemplate\"\s+Target=\"(.+?)\"" | ||
| 2860 | - template_injection_found = re.search(regex_template, read_all_file) | ||
| 2861 | - if template_injection_found: | ||
| 2862 | - injected_template_url = template_injection_found.group(1).decode() | ||
| 2863 | - message = "Found injected template in subfile {}. Template URL: {}"\ | ||
| 2864 | - "".format(subfile_that_can_contain_templates, injected_template_url) | ||
| 2865 | - log.info(message) | ||
| 2866 | - template_injection_detected = True | ||
| 2867 | - self.template_injection_found = True | ||
| 2868 | - # try to find a XML macrosheet | ||
| 2869 | - macro_sheet_footer = b"</xm:macrosheet>" | ||
| 2870 | - len_macro_sheet_footer = len(macro_sheet_footer) | ||
| 2871 | - last_bytes_to_check = read_all_file[-len_macro_sheet_footer:] | ||
| 2872 | - if last_bytes_to_check == macro_sheet_footer: | ||
| 2873 | - message = "Found XLM Macro in subfile: {}".format(subfile) | ||
| 2874 | - log.info(message) | ||
| 2875 | - xml_macrosheet_found = True | ||
| 2876 | - self.xlm_macrosheet_found = True | ||
| 2877 | - | ||
| 2878 | - if found_ole or xml_macrosheet_found or template_injection_detected: | ||
| 2879 | - log.debug('Opening OLE file %s within zip' % subfile) | ||
| 2880 | - with z.open(subfile) as file_handle: | ||
| 2881 | - ole_data = file_handle.read() | ||
| 2882 | - try: | ||
| 2883 | - self.append_subfile(filename=subfile, data=ole_data) | ||
| 2884 | - except OlevbaBaseException as exc: | ||
| 2885 | - if self.relaxed: | ||
| 2886 | - log.info('%s is not a valid OLE file (%s)' % (subfile, exc)) | ||
| 2887 | - log.debug('Trace:', exc_info=True) | ||
| 2888 | - continue | ||
| 2889 | - else: | ||
| 2890 | - raise SubstreamOpenError(self.filename, subfile, | ||
| 2891 | - exc) | 2864 | + # found_ole = True |
| 2865 | + # # in case we did not find an OLE file, | ||
| 2866 | + # # there could be a XLM macrosheet or a template injection attempt | ||
| 2867 | + # if not found_ole: | ||
| 2868 | + # read_all_file = file_handle.read() | ||
| 2869 | + # # try to detect template injection attempt | ||
| 2870 | + # # https://ired.team/offensive-security/initial-access/phishing-with-ms-office/inject-macros-from-a-remote-dotm-template-docx-with-macros | ||
| 2871 | + # subfile_that_can_contain_templates = "word/_rels/settings.xml.rels" | ||
| 2872 | + # if subfile == subfile_that_can_contain_templates: | ||
| 2873 | + # regex_template = b"Type=\"http://schemas\.openxmlformats\.org/officeDocument/\d{4}/relationships/attachedTemplate\"\s+Target=\"(.+?)\"" | ||
| 2874 | + # template_injection_found = re.search(regex_template, read_all_file) | ||
| 2875 | + # if template_injection_found: | ||
| 2876 | + # injected_template_url = template_injection_found.group(1).decode() | ||
| 2877 | + # message = "Found injected template in subfile {}. Template URL: {}"\ | ||
| 2878 | + # "".format(subfile_that_can_contain_templates, injected_template_url) | ||
| 2879 | + # log.info(message) | ||
| 2880 | + # template_injection_detected = True | ||
| 2881 | + # self.template_injection_found = True | ||
| 2882 | + # # try to find a XML macrosheet | ||
| 2883 | + # macro_sheet_footer = b"</xm:macrosheet>" | ||
| 2884 | + # len_macro_sheet_footer = len(macro_sheet_footer) | ||
| 2885 | + # last_bytes_to_check = read_all_file[-len_macro_sheet_footer:] | ||
| 2886 | + # if last_bytes_to_check == macro_sheet_footer: | ||
| 2887 | + # message = "Found XLM Macro in subfile: {}".format(subfile) | ||
| 2888 | + # log.info(message) | ||
| 2889 | + # xml_macrosheet_found = True | ||
| 2890 | + # self.xlm_macrosheet_found = True | ||
| 2891 | + # | ||
| 2892 | + # if found_ole or xml_macrosheet_found or template_injection_detected: | ||
| 2893 | + log.debug('Opening OLE file %s within zip' % subfile) | ||
| 2894 | + with z.open(subfile) as file_handle: | ||
| 2895 | + ole_data = file_handle.read() | ||
| 2896 | + try: | ||
| 2897 | + self.append_subfile(filename=subfile, data=ole_data) | ||
| 2898 | + except OlevbaBaseException as exc: | ||
| 2899 | + if self.relaxed: | ||
| 2900 | + log.info('%s is not a valid OLE file (%s)' % (subfile, exc)) | ||
| 2901 | + log.debug('Trace:', exc_info=True) | ||
| 2902 | + continue | ||
| 2903 | + else: | ||
| 2904 | + raise SubstreamOpenError(self.filename, subfile, | ||
| 2905 | + exc) | ||
| 2892 | z.close() | 2906 | z.close() |
| 2893 | # set type only if parsing succeeds | 2907 | # set type only if parsing succeeds |
| 2894 | self.type = TYPE_OpenXML | 2908 | self.type = TYPE_OpenXML |
| @@ -3134,7 +3148,7 @@ class VBA_Parser(object): | @@ -3134,7 +3148,7 @@ class VBA_Parser(object): | ||
| 3134 | if s.startswith(b'E'): | 3148 | if s.startswith(b'E'): |
| 3135 | xlm_macros.append('Formula or Macro: %s' % bytes2str(s[1:])) | 3149 | xlm_macros.append('Formula or Macro: %s' % bytes2str(s[1:])) |
| 3136 | if xlm_macro_found: | 3150 | if xlm_macro_found: |
| 3137 | - self.contains_macros = True | 3151 | + self.contains_xlm_macros = True |
| 3138 | self.xlm_macros = xlm_macros | 3152 | self.xlm_macros = xlm_macros |
| 3139 | self.type = TYPE_SLK | 3153 | self.type = TYPE_SLK |
| 3140 | 3154 | ||
| @@ -3150,7 +3164,7 @@ class VBA_Parser(object): | @@ -3150,7 +3164,7 @@ class VBA_Parser(object): | ||
| 3150 | # On Python 2, store it as a raw bytes string | 3164 | # On Python 2, store it as a raw bytes string |
| 3151 | # On Python 3, convert it to unicode assuming it was encoded with UTF-8 | 3165 | # On Python 3, convert it to unicode assuming it was encoded with UTF-8 |
| 3152 | self.vba_code_all_modules = bytes2str(data) | 3166 | self.vba_code_all_modules = bytes2str(data) |
| 3153 | - self.contains_macros = True | 3167 | + self.contains_vba_macros = True |
| 3154 | # set type only if parsing succeeds | 3168 | # set type only if parsing succeeds |
| 3155 | self.type = TYPE_TEXT | 3169 | self.type = TYPE_TEXT |
| 3156 | 3170 | ||
| @@ -3257,6 +3271,20 @@ class VBA_Parser(object): | @@ -3257,6 +3271,20 @@ class VBA_Parser(object): | ||
| 3257 | self.vba_projects.append((vba_root, project_path, dir_path)) | 3271 | self.vba_projects.append((vba_root, project_path, dir_path)) |
| 3258 | return self.vba_projects | 3272 | return self.vba_projects |
| 3259 | 3273 | ||
| 3274 | + def detect_macros(self): | ||
| 3275 | + """ | ||
| 3276 | + Detect the potential presence of VBA or Excel4/XLM macros in the file, | ||
| 3277 | + by calling detect_vba_macros and detect_xlm_macros. | ||
| 3278 | + (if the no_xlm option is set, XLM macros are not checked) | ||
| 3279 | + | ||
| 3280 | + :return: bool, True if at least one VBA project has been found, False otherwise | ||
| 3281 | + """ | ||
| 3282 | + vba = self.detect_vba_macros() | ||
| 3283 | + xlm = False | ||
| 3284 | + if not self.no_xlm: | ||
| 3285 | + xlm = self.detect_xlm_macros() | ||
| 3286 | + return (vba or xlm) | ||
| 3287 | + | ||
| 3260 | def detect_vba_macros(self): | 3288 | def detect_vba_macros(self): |
| 3261 | """ | 3289 | """ |
| 3262 | Detect the potential presence of VBA macros in the file, by checking | 3290 | Detect the potential presence of VBA macros in the file, by checking |
| @@ -3273,28 +3301,28 @@ class VBA_Parser(object): | @@ -3273,28 +3301,28 @@ class VBA_Parser(object): | ||
| 3273 | :return: bool, True if at least one VBA project has been found, False otherwise | 3301 | :return: bool, True if at least one VBA project has been found, False otherwise |
| 3274 | """ | 3302 | """ |
| 3275 | log.debug("detect vba macros") | 3303 | log.debug("detect vba macros") |
| 3276 | - #TODO: return None or raise exception if format not supported | ||
| 3277 | - #TODO: return the number of VBA projects found instead of True/False? | 3304 | + # TODO: return None or raise exception if format not supported |
| 3305 | + # TODO: return the number of VBA projects found instead of True/False? | ||
| 3278 | # if this method was already called, return the previous result: | 3306 | # if this method was already called, return the previous result: |
| 3279 | - if self.contains_macros is not None: | ||
| 3280 | - return self.contains_macros | 3307 | + if self.contains_vba_macros is not None: |
| 3308 | + return self.contains_vba_macros | ||
| 3281 | # if OpenXML/PPT, check all the OLE subfiles: | 3309 | # if OpenXML/PPT, check all the OLE subfiles: |
| 3282 | if self.ole_file is None: | 3310 | if self.ole_file is None: |
| 3283 | for ole_subfile in self.ole_subfiles: | 3311 | for ole_subfile in self.ole_subfiles: |
| 3284 | log.debug("ole subfile {}".format(ole_subfile)) | 3312 | log.debug("ole subfile {}".format(ole_subfile)) |
| 3285 | ole_subfile.no_xlm = self.no_xlm | 3313 | ole_subfile.no_xlm = self.no_xlm |
| 3286 | if ole_subfile.detect_vba_macros(): | 3314 | if ole_subfile.detect_vba_macros(): |
| 3287 | - self.contains_macros = True | 3315 | + self.contains_vba_macros = True |
| 3288 | return True | 3316 | return True |
| 3289 | # otherwise, no macro found: | 3317 | # otherwise, no macro found: |
| 3290 | - self.contains_macros = False | 3318 | + self.contains_vba_macros = False |
| 3291 | return False | 3319 | return False |
| 3292 | # otherwise it's an OLE file, find VBA projects: | 3320 | # otherwise it's an OLE file, find VBA projects: |
| 3293 | vba_projects = self.find_vba_projects() | 3321 | vba_projects = self.find_vba_projects() |
| 3294 | if len(vba_projects) == 0: | 3322 | if len(vba_projects) == 0: |
| 3295 | - self.contains_macros = False | 3323 | + self.contains_vba_macros = False |
| 3296 | else: | 3324 | else: |
| 3297 | - self.contains_macros = True | 3325 | + self.contains_vba_macros = True |
| 3298 | # Also look for VBA code in any stream including orphans | 3326 | # Also look for VBA code in any stream including orphans |
| 3299 | # (happens in some malformed files) | 3327 | # (happens in some malformed files) |
| 3300 | ole = self.ole_file | 3328 | ole = self.ole_file |
| @@ -3318,26 +3346,100 @@ class VBA_Parser(object): | @@ -3318,26 +3346,100 @@ class VBA_Parser(object): | ||
| 3318 | log.debug(repr(data)) | 3346 | log.debug(repr(data)) |
| 3319 | if b'Attribut\x00' in data: | 3347 | if b'Attribut\x00' in data: |
| 3320 | log.debug('Found VBA compressed code') | 3348 | log.debug('Found VBA compressed code') |
| 3321 | - self.contains_macros = True | 3349 | + self.contains_vba_macros = True |
| 3322 | except IOError as exc: | 3350 | except IOError as exc: |
| 3323 | if self.relaxed: | 3351 | if self.relaxed: |
| 3324 | log.info('Error when reading OLE Stream %r' % d.name) | 3352 | log.info('Error when reading OLE Stream %r' % d.name) |
| 3325 | log.debug('Trace:', exc_trace=True) | 3353 | log.debug('Trace:', exc_trace=True) |
| 3326 | else: | 3354 | else: |
| 3327 | raise SubstreamOpenError(self.filename, d.name, exc) | 3355 | raise SubstreamOpenError(self.filename, d.name, exc) |
| 3328 | - if (not self.no_xlm) and self.detect_xlm_macros(): | ||
| 3329 | - self.contains_macros = True | ||
| 3330 | - return self.contains_macros | 3356 | + return self.contains_vba_macros |
| 3331 | 3357 | ||
| 3332 | def detect_xlm_macros(self): | 3358 | def detect_xlm_macros(self): |
| 3359 | + """ | ||
| 3360 | + Detect the potential presence of Excel 4/XLM macros in the file, by checking | ||
| 3361 | + if it contains a macro worksheet. Both OLE and OpenXML files are supported. | ||
| 3362 | + Only Excel files may contain XLM macros, and also SLK and CSV files. | ||
| 3363 | + | ||
| 3364 | + If XLMMacroDeobfuscator is available, it will be used. Otherwise plugin_biff | ||
| 3365 | + is used as fallback (plugin_biff only supports OLE files, not XLSX or XLSB) | ||
| 3366 | + | ||
| 3367 | + :return: bool, True if at least one macro worksheet has been found, False otherwise | ||
| 3368 | + """ | ||
| 3333 | log.debug("detect xlm macros") | 3369 | log.debug("detect xlm macros") |
| 3370 | + # if this method was already called, return the previous result: | ||
| 3371 | + if self.contains_xlm_macros is not None: | ||
| 3372 | + return self.contains_xlm_macros | ||
| 3334 | # if this is a SLK file, the analysis was done in open_slk: | 3373 | # if this is a SLK file, the analysis was done in open_slk: |
| 3335 | if self.type == TYPE_SLK: | 3374 | if self.type == TYPE_SLK: |
| 3336 | - return self.contains_macros | ||
| 3337 | - from oletools.thirdparty.oledump.plugin_biff import cBIFF | 3375 | + return self.contains_xlm_macros |
| 3376 | + # TODO: check also CSV files for formulas? | ||
| 3338 | self.xlm_macros = [] | 3377 | self.xlm_macros = [] |
| 3378 | + # check if the file is Excel, otherwise return False | ||
| 3379 | + if not self.ftg.is_excel(): | ||
| 3380 | + self.contains_xlm_macros = False | ||
| 3381 | + return False | ||
| 3382 | + if XLMDEOBFUSCATOR: | ||
| 3383 | + # XLMMacroDeobfuscator is available, use it: | ||
| 3384 | + # But it only works with files on disk for now | ||
| 3385 | + if not self.file_on_disk: | ||
| 3386 | + log.warning('XLMMacroDeobfuscator only works with files on disk, not in memory. Analysis might be less complete.') | ||
| 3387 | + else: | ||
| 3388 | + try: | ||
| 3389 | + return self._extract_xlm_xlmdeobf() | ||
| 3390 | + except Exception: | ||
| 3391 | + log.error('Error when running XLMMacroDeobfuscator') | ||
| 3392 | + # fall back to plugin_biff: | ||
| 3339 | if self.ole_file is None: | 3393 | if self.ole_file is None: |
| 3394 | + # TODO: handle OpenXML here | ||
| 3340 | return False | 3395 | return False |
| 3396 | + return self._extract_xlm_plugin_biff() | ||
| 3397 | + | ||
| 3398 | + def _extract_xlm_xlmdeobf(self): | ||
| 3399 | + """ | ||
| 3400 | + Run XLMMacroDeobfuscator to detect and extract XLM macros | ||
| 3401 | + :return: bool, True if at least one macro worksheet has been found, False otherwise | ||
| 3402 | + """ | ||
| 3403 | + log.debug('Calling XLMMacroDeobfuscator to detect and extract XLM macros') | ||
| 3404 | + xlmdeobfuscator.SILENT = True | ||
| 3405 | + # we build the output as a list of strings: | ||
| 3406 | + xlm = ["RAW EXCEL4/XLM MACRO FORMULAS:"] | ||
| 3407 | + # First, extract only formulas without emulation | ||
| 3408 | + result = xlmdeobfuscator.process_file(file=self.filename, | ||
| 3409 | + noninteractive=True, | ||
| 3410 | + noindent=True, | ||
| 3411 | + # output_formula_format='CELL:[[CELL_ADDR]], [[INT-FORMULA]]', | ||
| 3412 | + return_deobfuscated=True, | ||
| 3413 | + timeout=30, | ||
| 3414 | + extract_only=True, | ||
| 3415 | + ) | ||
| 3416 | + if len(result) == 0: | ||
| 3417 | + # no XLM macro was found | ||
| 3418 | + self.contains_xlm_macros = False | ||
| 3419 | + return False | ||
| 3420 | + xlm += result | ||
| 3421 | + xlm.append('- ' * 38) | ||
| 3422 | + xlm.append('EMULATION - DEOBFUSCATED EXCEL4/XLM MACRO FORMULAS:') | ||
| 3423 | + result = xlmdeobfuscator.process_file(file=sys.argv[1], | ||
| 3424 | + noninteractive=True, | ||
| 3425 | + noindent=True, | ||
| 3426 | + # output_formula_format='CELL:[[CELL_ADDR]], [[INT-FORMULA]]', | ||
| 3427 | + return_deobfuscated=True, | ||
| 3428 | + timeout=30, | ||
| 3429 | + ) | ||
| 3430 | + xlm += result | ||
| 3431 | + log.debug(xlm) | ||
| 3432 | + self.xlm_macros = xlm | ||
| 3433 | + self.contains_xlm_macros = True | ||
| 3434 | + return True | ||
| 3435 | + | ||
| 3436 | + | ||
| 3437 | + def _extract_xlm_plugin_biff(self): | ||
| 3438 | + """ | ||
| 3439 | + Run plugin_biff to detect and extract XLM macros | ||
| 3440 | + :return: bool, True if at least one macro worksheet has been found, False otherwise | ||
| 3441 | + """ | ||
| 3442 | + log.debug('_extract_xlm_plugin_biff') | ||
| 3341 | for excel_stream in ('Workbook', 'Book'): | 3443 | for excel_stream in ('Workbook', 'Book'): |
| 3342 | if self.ole_file.exists(excel_stream): | 3444 | if self.ole_file.exists(excel_stream): |
| 3343 | log.debug('Found Excel stream %r' % excel_stream) | 3445 | log.debug('Found Excel stream %r' % excel_stream) |
| @@ -3360,9 +3462,11 @@ class VBA_Parser(object): | @@ -3360,9 +3462,11 @@ class VBA_Parser(object): | ||
| 3360 | # ref: https://inquest.net/blog/2020/03/18/Getting-Sneakier-Hidden-Sheets-Data-Connections-and-XLM-Macros | 3462 | # ref: https://inquest.net/blog/2020/03/18/Getting-Sneakier-Hidden-Sheets-Data-Connections-and-XLM-Macros |
| 3361 | biff_plugin = cBIFF(name=[excel_stream], stream=data, options='-o DCONN -s') | 3463 | biff_plugin = cBIFF(name=[excel_stream], stream=data, options='-o DCONN -s') |
| 3362 | self.xlm_macros += biff_plugin.Analyze() | 3464 | self.xlm_macros += biff_plugin.Analyze() |
| 3465 | + self.contains_xlm_macros = True | ||
| 3363 | return True | 3466 | return True |
| 3364 | except: | 3467 | except: |
| 3365 | log.exception('Error when running oledump.plugin_biff, please report to %s' % URL_OLEVBA_ISSUES) | 3468 | log.exception('Error when running oledump.plugin_biff, please report to %s' % URL_OLEVBA_ISSUES) |
| 3469 | + self.contains_xlm_macros = False | ||
| 3366 | return False | 3470 | return False |
| 3367 | 3471 | ||
| 3368 | def detect_is_encrypted(self): | 3472 | def detect_is_encrypted(self): |
| @@ -3420,6 +3524,12 @@ class VBA_Parser(object): | @@ -3420,6 +3524,12 @@ class VBA_Parser(object): | ||
| 3420 | for ole_subfile in self.ole_subfiles: | 3524 | for ole_subfile in self.ole_subfiles: |
| 3421 | for results in ole_subfile.extract_macros(): | 3525 | for results in ole_subfile.extract_macros(): |
| 3422 | yield results | 3526 | yield results |
| 3527 | + # we also need to yield XLM macros | ||
| 3528 | + if self.xlm_macros: | ||
| 3529 | + vba_code = '' | ||
| 3530 | + for line in self.xlm_macros: | ||
| 3531 | + vba_code += "' " + line + '\n' | ||
| 3532 | + yield ('xlm_macro', 'xlm_macro', 'xlm_macro.txt', vba_code) | ||
| 3423 | else: | 3533 | else: |
| 3424 | # This is an OLE file: | 3534 | # This is an OLE file: |
| 3425 | self.find_vba_projects() | 3535 | self.find_vba_projects() |
| @@ -3528,12 +3638,16 @@ class VBA_Parser(object): | @@ -3528,12 +3638,16 @@ class VBA_Parser(object): | ||
| 3528 | 3638 | ||
| 3529 | def analyze_macros(self, show_decoded_strings=False, deobfuscate=False): | 3639 | def analyze_macros(self, show_decoded_strings=False, deobfuscate=False): |
| 3530 | """ | 3640 | """ |
| 3531 | - runs extract_macros and analyze the source code of all VBA macros | 3641 | + runs extract_macros and analyze the source code of all VBA+XLM macros |
| 3532 | found in the file. | 3642 | found in the file. |
| 3533 | All results are stored in self.analysis_results. | 3643 | All results are stored in self.analysis_results. |
| 3534 | If called more than once, simply returns the previous results. | 3644 | If called more than once, simply returns the previous results. |
| 3645 | + | ||
| 3646 | + :return: list of tuples (type, keyword, description) | ||
| 3647 | + (type = 'AutoExec', 'Suspicious', 'IOC', 'Hex String', 'Base64 String' or 'Dridex String') | ||
| 3535 | """ | 3648 | """ |
| 3536 | - if self.detect_vba_macros(): | 3649 | + # Check if there are VBA or XLM macros: |
| 3650 | + if self.detect_macros(): | ||
| 3537 | # if the analysis was already done, avoid doing it twice: | 3651 | # if the analysis was already done, avoid doing it twice: |
| 3538 | if self.analysis_results is not None: | 3652 | if self.analysis_results is not None: |
| 3539 | return self.analysis_results | 3653 | return self.analysis_results |
| @@ -3552,12 +3666,13 @@ class VBA_Parser(object): | @@ -3552,12 +3666,13 @@ class VBA_Parser(object): | ||
| 3552 | 'this may have been used to hide malicious code' | 3666 | 'this may have been used to hide malicious code' |
| 3553 | scanner.suspicious_keywords.append((keyword, description)) | 3667 | scanner.suspicious_keywords.append((keyword, description)) |
| 3554 | scanner.results.append(('Suspicious', keyword, description)) | 3668 | scanner.results.append(('Suspicious', keyword, description)) |
| 3555 | - if self.xlm_macrosheet_found: | 3669 | + if self.contains_xlm_macros: |
| 3556 | log.debug('adding XLM macrosheet found to suspicious keywords') | 3670 | log.debug('adding XLM macrosheet found to suspicious keywords') |
| 3557 | - keyword = 'XLM macrosheet' | ||
| 3558 | - description = 'XLM macrosheet found. It could contain malicious code' | 3671 | + keyword = 'XLM macro' |
| 3672 | + description = 'XLM macro found. It may contain malicious code' | ||
| 3559 | scanner.suspicious_keywords.append((keyword, description)) | 3673 | scanner.suspicious_keywords.append((keyword, description)) |
| 3560 | scanner.results.append(('Suspicious', keyword, description)) | 3674 | scanner.results.append(('Suspicious', keyword, description)) |
| 3675 | + # TODO: this has been temporarily disabled | ||
| 3561 | if self.template_injection_found: | 3676 | if self.template_injection_found: |
| 3562 | log.debug('adding Template Injection to suspicious keywords') | 3677 | log.debug('adding Template Injection to suspicious keywords') |
| 3563 | keyword = 'Template Injection' | 3678 | keyword = 'Template Injection' |
| @@ -4029,7 +4144,7 @@ class VBA_Parser_CLI(VBA_Parser): | @@ -4029,7 +4144,7 @@ class VBA_Parser_CLI(VBA_Parser): | ||
| 4029 | try: | 4144 | try: |
| 4030 | #TODO: handle olefile errors, when an OLE file is malformed | 4145 | #TODO: handle olefile errors, when an OLE file is malformed |
| 4031 | print('Type: %s'% self.type) | 4146 | print('Type: %s'% self.type) |
| 4032 | - if self.detect_vba_macros(): | 4147 | + if self.detect_macros(): |
| 4033 | # run analysis before displaying VBA code, in order to colorize found keywords | 4148 | # run analysis before displaying VBA code, in order to colorize found keywords |
| 4034 | self.run_analysis(show_decoded_strings=show_decoded_strings, deobfuscate=deobfuscate) | 4149 | self.run_analysis(show_decoded_strings=show_decoded_strings, deobfuscate=deobfuscate) |
| 4035 | #print 'Contains VBA Macros:' | 4150 | #print 'Contains VBA Macros:' |
| @@ -4109,7 +4224,7 @@ class VBA_Parser_CLI(VBA_Parser): | @@ -4109,7 +4224,7 @@ class VBA_Parser_CLI(VBA_Parser): | ||
| 4109 | print('MACRO SOURCE CODE WITH DEOBFUSCATED VBA STRINGS (EXPERIMENTAL):\n\n') | 4224 | print('MACRO SOURCE CODE WITH DEOBFUSCATED VBA STRINGS (EXPERIMENTAL):\n\n') |
| 4110 | print(self.reveal()) | 4225 | print(self.reveal()) |
| 4111 | else: | 4226 | else: |
| 4112 | - print('No VBA macros found.') | 4227 | + print('No VBA or XLM macros found.') |
| 4113 | except OlevbaBaseException: | 4228 | except OlevbaBaseException: |
| 4114 | raise | 4229 | raise |
| 4115 | except Exception as exc: | 4230 | except Exception as exc: |
| @@ -4164,7 +4279,7 @@ class VBA_Parser_CLI(VBA_Parser): | @@ -4164,7 +4279,7 @@ class VBA_Parser_CLI(VBA_Parser): | ||
| 4164 | #TODO: handle olefile errors, when an OLE file is malformed | 4279 | #TODO: handle olefile errors, when an OLE file is malformed |
| 4165 | result['type'] = self.type | 4280 | result['type'] = self.type |
| 4166 | macros = [] | 4281 | macros = [] |
| 4167 | - if self.detect_vba_macros(): | 4282 | + if self.detect_macros(): |
| 4168 | for (subfilename, stream_path, vba_filename, vba_code) in self.extract_all_macros(): | 4283 | for (subfilename, stream_path, vba_filename, vba_code) in self.extract_all_macros(): |
| 4169 | curr_macro = {} | 4284 | curr_macro = {} |
| 4170 | if hide_attributes: | 4285 | if hide_attributes: |
| @@ -4207,7 +4322,7 @@ class VBA_Parser_CLI(VBA_Parser): | @@ -4207,7 +4322,7 @@ class VBA_Parser_CLI(VBA_Parser): | ||
| 4207 | #TODO: replace print by writing to a provided output file (sys.stdout by default) | 4322 | #TODO: replace print by writing to a provided output file (sys.stdout by default) |
| 4208 | try: | 4323 | try: |
| 4209 | #TODO: handle olefile errors, when an OLE file is malformed | 4324 | #TODO: handle olefile errors, when an OLE file is malformed |
| 4210 | - if self.detect_vba_macros(): | 4325 | + if self.detect_macros(): |
| 4211 | # print a waiting message only if the output is not redirected to a file: | 4326 | # print a waiting message only if the output is not redirected to a file: |
| 4212 | if sys.stdout.isatty(): | 4327 | if sys.stdout.isatty(): |
| 4213 | print('Analysis...\r', end='') | 4328 | print('Analysis...\r', end='') |
| @@ -4216,7 +4331,7 @@ class VBA_Parser_CLI(VBA_Parser): | @@ -4216,7 +4331,7 @@ class VBA_Parser_CLI(VBA_Parser): | ||
| 4216 | deobfuscate=deobfuscate) | 4331 | deobfuscate=deobfuscate) |
| 4217 | flags = TYPE2TAG[self.type] | 4332 | flags = TYPE2TAG[self.type] |
| 4218 | macros = autoexec = suspicious = iocs = hexstrings = base64obf = dridex = vba_obf = '-' | 4333 | macros = autoexec = suspicious = iocs = hexstrings = base64obf = dridex = vba_obf = '-' |
| 4219 | - if self.contains_macros: macros = 'M' | 4334 | + if self.contains_vba_macros: macros = 'M' |
| 4220 | if self.nb_autoexec: autoexec = 'A' | 4335 | if self.nb_autoexec: autoexec = 'A' |
| 4221 | if self.nb_suspicious: suspicious = 'S' | 4336 | if self.nb_suspicious: suspicious = 'S' |
| 4222 | if self.nb_iocs: iocs = 'I' | 4337 | if self.nb_iocs: iocs = 'I' |
| @@ -4351,6 +4466,7 @@ def parse_args(cmd_line_args=None): | @@ -4351,6 +4466,7 @@ def parse_args(cmd_line_args=None): | ||
| 4351 | def process_file(filename, data, container, options, crypto_nesting=0): | 4466 | def process_file(filename, data, container, options, crypto_nesting=0): |
| 4352 | """ | 4467 | """ |
| 4353 | Part of main function that processes a single file. | 4468 | Part of main function that processes a single file. |
| 4469 | + This is meant to be used only for the command-line interface of olevba | ||
| 4354 | 4470 | ||
| 4355 | This handles exceptions and encryption. | 4471 | This handles exceptions and encryption. |
| 4356 | 4472 |