diff --git a/peekaboo/config.py b/peekaboo/config.py index 5c182bb5..af5b81f9 100644 --- a/peekaboo/config.py +++ b/peekaboo/config.py @@ -41,6 +41,7 @@ class PeekabooConfigParser( # pylint: disable=too-many-ancestors exist or cannot be opened. """ LOG_LEVEL = object() RELIST = object() + IRELIST = object() def __init__(self, config_file): # super() does not work here because ConfigParser uses old-style @@ -114,7 +115,14 @@ def getlist(self, section, option, raw=False, vars=None, fallback=None): self.lists[section][option] = value return value - def getrelist(self, section, option, raw=False, vars=None, fallback=None): + def getirelist(self, section, option, raw=False, vars=None, fallback=None, flags=None): + """ Special getter for lists of regular expressions that are compiled to match + case insesitive (IGNORECASE). Returns the compiled expression objects in a + list ready for matching and searching. + """ + return self.getrelist(section, option, raw=raw, vars=vars, fallback=fallback, flags=re.IGNORECASE) + + def getrelist(self, section, option, raw=False, vars=None, fallback=None, flags=0): """ Special getter for lists of regular expressions. Returns the compiled expression objects in a list ready for matching and searching. """ @@ -137,7 +145,7 @@ def getrelist(self, section, option, raw=False, vars=None, fallback=None): compiled_res = [] for regex in strlist: try: - compiled_res.append(re.compile(regex)) + compiled_res.append(re.compile(regex, flags)) except (ValueError, TypeError) as error: raise PeekabooConfigException( 'Failed to compile regular expression "%s" (section %s, ' @@ -203,6 +211,7 @@ def get_by_type(self, section, option, fallback=None, option_type=None): # these only work when given explicitly as option_type self.LOG_LEVEL: self.get_log_level, self.RELIST: self.getrelist, + self.IRELIST: self.getirelist, } return getter[option_type](section, option, fallback=fallback) diff --git a/peekaboo/locale/de/LC_MESSAGES/peekaboo.mo b/peekaboo/locale/de/LC_MESSAGES/peekaboo.mo index c89b24bd..af5a9622 100644 Binary files a/peekaboo/locale/de/LC_MESSAGES/peekaboo.mo and b/peekaboo/locale/de/LC_MESSAGES/peekaboo.mo differ diff --git a/peekaboo/locale/de/LC_MESSAGES/peekaboo.po b/peekaboo/locale/de/LC_MESSAGES/peekaboo.po index d82f699e..77ab1270 100644 --- a/peekaboo/locale/de/LC_MESSAGES/peekaboo.po +++ b/peekaboo/locale/de/LC_MESSAGES/peekaboo.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PeekabooAV 1.6.2\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2019-04-17 09:26+0000\n" +"POT-Creation-Date: 2019-08-20 10:35+0200\n" "PO-Revision-Date: 2019-02-14 22:02+0000\n" "Last-Translator: Michael Weiser \n" "Language: de\n" @@ -15,28 +15,28 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.4.0\n" +"Generated-By: Babel 2.7.0\n" #: peekaboo/queuing.py:382 msgid "Sample initialization failed" msgstr "Initialisierung der zu analysierenden Datei fehlgeschlagen" -#: peekaboo/sample.py:186 +#: peekaboo/sample.py:185 #, python-format msgid "File \"%s\" %s is being analyzed" msgstr "Datei \"%s\" %s wird analysiert" -#: peekaboo/sample.py:239 +#: peekaboo/sample.py:238 #, python-format msgid "File \"%s\" is considered \"%s\"" msgstr "Die Datei \"%s\" wird als \"%s\" betrachtet" -#: peekaboo/sample.py:299 +#: peekaboo/sample.py:298 #, python-format msgid "File \"%s\": %s" msgstr "Datei \"%s\": %s" -#: peekaboo/sample.py:495 +#: peekaboo/sample.py:497 #, python-format msgid "Sample %s successfully submitted to Cuckoo as job %d" msgstr "Erfolgreich an Cuckoo gegeben %s als Job %d" @@ -100,55 +100,73 @@ msgstr "Ja" msgid "No" msgstr "Nein" -#: peekaboo/ruleset/engine.py:118 +#: peekaboo/ruleset/engine.py:147 msgid "Rule aborted with error" msgstr "Regel mit Fehler abgebrochen" -#: peekaboo/ruleset/rules.py:133 +#: peekaboo/ruleset/rules.py:122 msgid "File is not yet known to the system" msgstr "Datei ist dem System noch nicht bekannt" -#: peekaboo/ruleset/rules.py:154 +#: peekaboo/ruleset/rules.py:143 #, python-format msgid "Failure to determine sample file size: %s" msgstr "Ermittlung der Dateigröße fehlgeschlagen: %s" -#: peekaboo/ruleset/rules.py:159 +#: peekaboo/ruleset/rules.py:148 #, python-format msgid "File has more than %d bytes" msgstr "Datei hat mehr als %d bytes" -#: peekaboo/ruleset/rules.py:165 +#: peekaboo/ruleset/rules.py:154 #, python-format msgid "File is only %d bytes long" -msgstr "" +msgstr "Die Datei ist nur %d bytes groß" -#: peekaboo/ruleset/rules.py:187 +#: peekaboo/ruleset/rules.py:176 msgid "File type is on whitelist" msgstr "Dateityp ist auf Whitelist" -#: peekaboo/ruleset/rules.py:191 +#: peekaboo/ruleset/rules.py:180 msgid "File type is not on whitelist" msgstr "Dateityp ist nicht auf Whitelist" -#: peekaboo/ruleset/rules.py:213 +#: peekaboo/ruleset/rules.py:202 msgid "File type is on the list of types to analyze" msgstr "Dateityp ist auf der Liste der zu analysiserenden Typen" -#: peekaboo/ruleset/rules.py:218 +#: peekaboo/ruleset/rules.py:207 #, python-format msgid "File type is not on the list of types to analyse (%s)" msgstr "Dateityp ist nicht auf der Liste der zu analysierenden Typen (%s)" -#: peekaboo/ruleset/rules.py:231 +#: peekaboo/ruleset/rules.py:223 +msgid "File is not an office document" +msgstr "Die Datei ist kein Office Dokument" + +#: peekaboo/ruleset/rules.py:247 msgid "The file contains an Office macro" msgstr "Die Datei beinhaltet ein Office-Makro" -#: peekaboo/ruleset/rules.py:235 +#: peekaboo/ruleset/rules.py:251 msgid "The file does not contain a recognizable Office macro" msgstr "Die Datei beinhaltet kein erkennbares Office-Makro" -#: peekaboo/ruleset/rules.py:265 peekaboo/ruleset/rules.py:402 +#: peekaboo/ruleset/rules.py:272 +msgid "The file contains an Office macro which runs at document open" +msgstr "" +"Die Datei beinhaltet ein Office Makro welches beim Öffnen der Datei " +"ausgeführt wird" + +#: peekaboo/ruleset/rules.py:277 +msgid "" +"The file does not contain a recognizable Office macro that is run at " +"document open" +msgstr "" +"Die Datei beinhaltet kein erkennbares Office Makro welches beim Öffnen " +"ausgeführt wird" + +#: peekaboo/ruleset/rules.py:307 peekaboo/ruleset/rules.py:445 msgid "" "Behavioral analysis by Cuckoo has produced an error and did not finish " "successfully" @@ -156,40 +174,41 @@ msgstr "" "Die Verhaltensanalyse durch Cuckoo hat einen Fehler produziert und konnte" " nicht erfolgreich abgeschlossen werden" -#: peekaboo/ruleset/rules.py:322 +#: peekaboo/ruleset/rules.py:365 msgid "No signature suggesting malware detected" msgstr "Keine Signatur erkannt die auf Schadcode hindeutet" -#: peekaboo/ruleset/rules.py:327 +#: peekaboo/ruleset/rules.py:370 #, python-format msgid "The following signatures have been recognized: %s" msgstr "Folgende Signaturen wurden erkannt: %s" -#: peekaboo/ruleset/rules.py:346 +#: peekaboo/ruleset/rules.py:389 #, python-format msgid "Cuckoo score >= %s: %s" msgstr "" -#: peekaboo/ruleset/rules.py:351 +#: peekaboo/ruleset/rules.py:394 #, python-format msgid "Cuckoo score < %s: %s" msgstr "" -#: peekaboo/ruleset/rules.py:375 +#: peekaboo/ruleset/rules.py:418 #, python-format msgid "The file attempts to contact at least one domain on the blacklist (%s)" msgstr "" "Die Datei versucht mindestens eine Domain aus der Blacklist zu " "kontaktieren (%s)" -#: peekaboo/ruleset/rules.py:381 +#: peekaboo/ruleset/rules.py:424 msgid "File does not seem to attempt contact with domains on the blacklist" msgstr "Datei scheint keine Domains aus der Blacklist kontaktieren zu wollen" -#: peekaboo/ruleset/rules.py:418 +#: peekaboo/ruleset/rules.py:461 msgid "Behavioral analysis by Cuckoo completed successfully" msgstr "Die Verhaltensanalyse durch Cuckoo wurde erfolgreich abgeschlossen" -#: peekaboo/ruleset/rules.py:435 +#: peekaboo/ruleset/rules.py:478 msgid "File does not seem to exhibit recognizable malicious behaviour" msgstr "Datei scheint keine erkennbaren Schadroutinen zu starten" + diff --git a/peekaboo/locale/peekaboo.pot b/peekaboo/locale/peekaboo.pot index 56b61130..085c4471 100644 --- a/peekaboo/locale/peekaboo.pot +++ b/peekaboo/locale/peekaboo.pot @@ -8,35 +8,35 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2019-04-17 09:26+0000\n" +"POT-Creation-Date: 2019-08-20 10:35+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.4.0\n" +"Generated-By: Babel 2.7.0\n" #: peekaboo/queuing.py:382 msgid "Sample initialization failed" msgstr "" -#: peekaboo/sample.py:186 +#: peekaboo/sample.py:185 #, python-format msgid "File \"%s\" %s is being analyzed" msgstr "" -#: peekaboo/sample.py:239 +#: peekaboo/sample.py:238 #, python-format msgid "File \"%s\" is considered \"%s\"" msgstr "" -#: peekaboo/sample.py:299 +#: peekaboo/sample.py:298 #, python-format msgid "File \"%s\": %s" msgstr "" -#: peekaboo/sample.py:495 +#: peekaboo/sample.py:497 #, python-format msgid "Sample %s successfully submitted to Cuckoo as job %d" msgstr "" @@ -99,93 +99,107 @@ msgstr "" msgid "No" msgstr "" -#: peekaboo/ruleset/engine.py:118 +#: peekaboo/ruleset/engine.py:147 msgid "Rule aborted with error" msgstr "" -#: peekaboo/ruleset/rules.py:133 +#: peekaboo/ruleset/rules.py:122 msgid "File is not yet known to the system" msgstr "" -#: peekaboo/ruleset/rules.py:154 +#: peekaboo/ruleset/rules.py:143 #, python-format msgid "Failure to determine sample file size: %s" msgstr "" -#: peekaboo/ruleset/rules.py:159 +#: peekaboo/ruleset/rules.py:148 #, python-format msgid "File has more than %d bytes" msgstr "" -#: peekaboo/ruleset/rules.py:165 +#: peekaboo/ruleset/rules.py:154 #, python-format msgid "File is only %d bytes long" msgstr "" -#: peekaboo/ruleset/rules.py:187 +#: peekaboo/ruleset/rules.py:176 msgid "File type is on whitelist" msgstr "" -#: peekaboo/ruleset/rules.py:191 +#: peekaboo/ruleset/rules.py:180 msgid "File type is not on whitelist" msgstr "" -#: peekaboo/ruleset/rules.py:213 +#: peekaboo/ruleset/rules.py:202 msgid "File type is on the list of types to analyze" msgstr "" -#: peekaboo/ruleset/rules.py:218 +#: peekaboo/ruleset/rules.py:207 #, python-format msgid "File type is not on the list of types to analyse (%s)" msgstr "" -#: peekaboo/ruleset/rules.py:231 +#: peekaboo/ruleset/rules.py:223 +msgid "File is not an office document" +msgstr "" + +#: peekaboo/ruleset/rules.py:247 msgid "The file contains an Office macro" msgstr "" -#: peekaboo/ruleset/rules.py:235 +#: peekaboo/ruleset/rules.py:251 msgid "The file does not contain a recognizable Office macro" msgstr "" -#: peekaboo/ruleset/rules.py:265 peekaboo/ruleset/rules.py:402 +#: peekaboo/ruleset/rules.py:272 +msgid "The file contains an Office macro which runs at document open" +msgstr "" + +#: peekaboo/ruleset/rules.py:277 +msgid "" +"The file does not contain a recognizable Office macro that is run at " +"document open" +msgstr "" + +#: peekaboo/ruleset/rules.py:307 peekaboo/ruleset/rules.py:445 msgid "" "Behavioral analysis by Cuckoo has produced an error and did not finish " "successfully" msgstr "" -#: peekaboo/ruleset/rules.py:322 +#: peekaboo/ruleset/rules.py:365 msgid "No signature suggesting malware detected" msgstr "" -#: peekaboo/ruleset/rules.py:327 +#: peekaboo/ruleset/rules.py:370 #, python-format msgid "The following signatures have been recognized: %s" msgstr "" -#: peekaboo/ruleset/rules.py:346 +#: peekaboo/ruleset/rules.py:389 #, python-format msgid "Cuckoo score >= %s: %s" msgstr "" -#: peekaboo/ruleset/rules.py:351 +#: peekaboo/ruleset/rules.py:394 #, python-format msgid "Cuckoo score < %s: %s" msgstr "" -#: peekaboo/ruleset/rules.py:375 +#: peekaboo/ruleset/rules.py:418 #, python-format msgid "The file attempts to contact at least one domain on the blacklist (%s)" msgstr "" -#: peekaboo/ruleset/rules.py:381 +#: peekaboo/ruleset/rules.py:424 msgid "File does not seem to attempt contact with domains on the blacklist" msgstr "" -#: peekaboo/ruleset/rules.py:418 +#: peekaboo/ruleset/rules.py:461 msgid "Behavioral analysis by Cuckoo completed successfully" msgstr "" -#: peekaboo/ruleset/rules.py:435 +#: peekaboo/ruleset/rules.py:478 msgid "File does not seem to exhibit recognizable malicious behaviour" msgstr "" diff --git a/peekaboo/ruleset/engine.py b/peekaboo/ruleset/engine.py index 96af1c65..6e736f52 100644 --- a/peekaboo/ruleset/engine.py +++ b/peekaboo/ruleset/engine.py @@ -49,6 +49,7 @@ class RulesetEngine(object): CuckooEvilSigRule, CuckooScoreRule, OfficeMacroRule, + OfficeMacroWithSuspiciousKeyword, RequestsEvilDomainRule, CuckooAnalysisFailedRule, ContainsPeekabooYarRule, @@ -127,7 +128,7 @@ def __exec_rule(self, sample, rule_class): rule wrapper for in/out logging and reporting """ rule_name = rule_class.rule_name - logger.debug("Processing rule '%s' for %s" % (rule_name, sample)) + logger.debug("Processing rule '%s' for %s", rule_name, sample) try: rule = rule_class(config=self.config, db_con=self.db_con) @@ -138,8 +139,8 @@ def __exec_rule(self, sample, rule_class): raise # catch all other exceptions for this rule except Exception as e: - logger.warning("Unexpected error in '%s' for %s" % (rule_name, - sample)) + logger.warning("Unexpected error in '%s' for %s", rule_name, + sample) logger.exception(e) # create "fake" RuleResult result = RuleResult("RulesetEngine", result=Result.failed, @@ -147,5 +148,5 @@ def __exec_rule(self, sample, rule_class): further_analysis=False) sample.add_rule_result(result) - logger.info("Rule '%s' processed for %s" % (rule_name, sample)) + logger.info("Rule '%s' processed for %s", rule_name, sample) return result diff --git a/peekaboo/ruleset/rules.py b/peekaboo/ruleset/rules.py index 797858a4..6635c0c8 100644 --- a/peekaboo/ruleset/rules.py +++ b/peekaboo/ruleset/rules.py @@ -31,6 +31,8 @@ from peekaboo.ruleset import Result, RuleResult from peekaboo.exceptions import PeekabooAnalysisDeferred, \ CuckooSubmitFailedException, PeekabooRulesetConfigError +from peekaboo.toolbox.ole import Oletools, OletoolsReport, \ + OleNotAnOfficeDocumentException logger = logging.getLogger(__name__) @@ -207,13 +209,40 @@ def evaluate(self, sample): False) -class OfficeMacroRule(Rule): +class OleRule(Rule): + """ A common base class for rules that evaluate the Ole report. """ + def evaluate(self, sample): + """ Report the sample as bad if it contains a macro. """ + if sample.oletools_report is None: + try: + ole = Oletools() + report = ole.get_report(sample) + sample.register_oletools_report(OletoolsReport(report)) + except OleNotAnOfficeDocumentException: + return self.result(Result.unknown, + _("File is not an office document"), + True) + except Exception: + raise + + return self.evaluate_report(sample.oletools_report) + + def evaluate_report(self, report): + """ Evaluate an Ole report. + + @param report: The Ole report. + @returns: RuleResult containing verdict. + """ + raise NotImplementedError + + +class OfficeMacroRule(OleRule): """ A rule checking the sample for Office macros. """ rule_name = 'office_macro' - def evaluate(self, sample): + def evaluate_report(self, report): """ Report the sample as bad if it contains a macro. """ - if sample.office_macros: + if report.has_office_macros(): return self.result(Result.bad, _("The file contains an Office macro"), False) @@ -224,6 +253,32 @@ def evaluate(self, sample): True) +class OfficeMacroWithSuspiciousKeyword(OleRule): + """ A rule checking the sample for Office macros. """ + rule_name = 'office_macro_with_suspicious_keyword' + + def get_config(self): + # get list of keywords from config file + self.suspicious_keyword_list = self.get_config_value( + 'keyword', [], option_type=self.config.IRELIST) + if not self.suspicious_keyword_list: + raise PeekabooRulesetConfigError( + "Empty suspicious keyword list, check %s rule config." % + self.rule_name) + + def evaluate_report(self, report): + if report.has_office_macros_with_suspicious_keyword(self.suspicious_keyword_list): + return self.result(Result.bad, + _("The file contains an Office macro which " + "runs at document open"), + False) + + return self.result(Result.unknown, + _("The file does not contain a recognizable " + "Office macro that is run at document open"), + True) + + class CuckooRule(Rule): """ A common base class for rules that evaluate the Cuckoo report. """ def evaluate(self, sample): diff --git a/peekaboo/sample.py b/peekaboo/sample.py index 35c661da..d4c5a624 100644 --- a/peekaboo/sample.py +++ b/peekaboo/sample.py @@ -38,7 +38,6 @@ from datetime import datetime from peekaboo.toolbox.files import guess_mime_type_from_file_contents, \ guess_mime_type_from_filename -from peekaboo.toolbox.ms_office import has_office_macros from peekaboo.ruleset import Result @@ -91,6 +90,7 @@ def __init__(self, file_path, cuckoo=None, status_change=None, self.__submit_path = None self.__cuckoo_job_id = -1 self.__cuckoo_report = None + self.__oletools_report = None self.__done = False self.__status_change = status_change self.__result = Result.unchecked @@ -101,7 +101,6 @@ def __init__(self, file_path, cuckoo=None, status_change=None, self.__sha256sum = None self.__mimetypes = None self.__file_extension = None - self.__office_macros = None self.__base_dir = base_dir self.__job_hash = None self.__job_hash_regex = job_hash_regex @@ -461,14 +460,6 @@ def mimetypes(self): def job_id(self): return self.__cuckoo_job_id - @property - def office_macros(self): - """ Determines if this sample contains any office macros. """ - if not self.__office_macros: - self.__office_macros = has_office_macros(self.__path) - - return self.__office_macros - @property def file_size(self): """ Determine and cache sample file size @@ -484,6 +475,11 @@ def cuckoo_report(self): """ Returns the cuckoo report """ return self.__cuckoo_report + @property + def oletools_report(self): + """ Returns the oletools report """ + return self.__oletools_report + @property def submit_path(self): """ Returns the path to use for submission to Cuckoo """ @@ -506,6 +502,10 @@ def register_cuckoo_report(self, report): """ Records a Cuckoo report for later evaluation. """ self.__cuckoo_report = report + def register_oletools_report(self, report): + """ Records a Oletools report for alter evaluation. """ + self.__oletools_report = report + def cleanup(self): """ Clean up after the sample has been analysed, removing a potentially created workdir. """ diff --git a/peekaboo/toolbox/ms_office.py b/peekaboo/toolbox/ms_office.py deleted file mode 100644 index b5ab902d..00000000 --- a/peekaboo/toolbox/ms_office.py +++ /dev/null @@ -1,61 +0,0 @@ -############################################################################### -# # -# Peekaboo Extended Email Attachment Behavior Observation Owl # -# # -# toolbox/ # -# ms_office.py # -############################################################################### -# # -# Copyright (C) 2016-2019 science + computing ag # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or (at # -# your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, but # -# WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # -# General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -############################################################################### - -""" Tool functions for handling office macros. """ - -import logging -from oletools.olevba import VBA_Parser - - -logger = logging.getLogger(__name__) - -MS_OFFICE_EXTENSIONS = [ - ".doc", ".docm", ".dotm", ".docx", - ".ppt", ".pptm", ".pptx", ".potm", ".ppam", ".ppsm", - ".xls", ".xlsm", ".xlsx", -] - - -def has_office_macros(office_file): - """ - Detects macros in Microsoft Office documents. - - @param office_file: The MS Office document to check for macros. - @return: True if macros where found, otherwise False. - If VBA_Parser crashes it returns False too. - """ - file_extension = office_file.split('.')[-1] - if file_extension not in MS_OFFICE_EXTENSIONS: - return False - try: - # VBA_Parser reports macros for office documents - vbaparser = VBA_Parser(office_file) - return vbaparser.detect_vba_macros() - except TypeError: - # The given file is not an office document. - return False - except Exception as error: - logger.exception(error) - return False diff --git a/peekaboo/toolbox/ole.py b/peekaboo/toolbox/ole.py new file mode 100644 index 00000000..e4a3ce04 --- /dev/null +++ b/peekaboo/toolbox/ole.py @@ -0,0 +1,120 @@ +############################################################################### +# # +# Peekaboo Extended Email Attachment Behavior Observation Owl # +# # +# toolbox/ # +# ole.py # +############################################################################### +# # +# Copyright (C) 2016-2019 science + computing ag # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or (at # +# your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, but # +# WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # +# General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +############################################################################### + + +import logging +import re +from oletools.olevba import VBA_Parser + +logger = logging.getLogger(__name__) + + +class OleNotAnOfficeDocumentException(Exception): + pass + +class Oletools(object): + """ Parent class, defines interface to Oletools. """ + def __init__(self): + self.MS_OFFICE_EXTENSIONS = [ + "doc", "docm", "dotm", "docx", + "ppt", "pptm", "pptx", "potm", "ppam", "ppsm", + "xls", "xlsm", "xlsx", + ] + + def get_report(self, sample): + """ Return oletools report or create if not already cached. """ + if sample.oletools_report != None: + return sample.oletools_report + + report = {} + if sample.file_extension not in self.MS_OFFICE_EXTENSIONS: + raise OleNotAnOfficeDocumentException(sample.file_extension) + + try: + vbaparser = VBA_Parser(sample.file_path) + + # List from oletools/olevba.py#L553 + oletype = ('OLE', 'OpenXML', 'FlatOPC_XML', 'Word2003_XML', 'MHTML', 'PPT') + + # check if ole detects it as an office file + if vbaparser.type not in oletype: + raise OleNotAnOfficeDocumentException(sample.file_extension) + + # VBA_Parser reports macros for office documents + report['has_macros'] = vbaparser.detect_vba_macros() or vbaparser.detect_xlm_macros() + try: + report['vba'] = vbaparser.reveal() + except TypeError: + # no macros + pass + vbaparser.close() + except IOError: + raise + except TypeError: + # The given file is not an office document. + pass + except Exception as error: + logger.exception(error) + sample.register_oletools_report(report) + return report + + +class OletoolsReport(object): + """ Represents a custom Oletools report. """ + def __init__(self, report): + self.report = report + + def has_office_macros(self): + """ + Detects macros in Microsoft Office documents. + + @return: True if macros where found, otherwise False. + If VBA_Parser crashes it returns False too. + """ + + try: + return self.report['has_macros'] + except KeyError: + return False + + def has_office_macros_with_suspicious_keyword(self, suspicious_keywords): + """ + Detects macros with supplied suspicious keywords in Microsoft Office documents. + + @param suspicious_keywords: List of suspicious keyword regexes. + @return: True if macros with keywords where found, otherwise False. + If VBA_Parser crashes it returns False too. + """ + suspicious = False + try: + vba = self.report['vba'] + for w in suspicious_keywords: + if re.search(w, vba): + suspicious = True + break + except KeyError: + return False + + return suspicious diff --git a/ruleset.conf.sample b/ruleset.conf.sample index c8909a96..9a9ea739 100644 --- a/ruleset.conf.sample +++ b/ruleset.conf.sample @@ -9,13 +9,14 @@ rule.1 : known rule.2 : file_larger_than rule.3 : file_type_on_whitelist rule.4 : file_type_on_greylist -rule.5 : cuckoo_evil_sig -rule.6 : cuckoo_score -rule.7 : office_macro -#rule.8 : requests_evil_domain -rule.9 : cuckoo_analysis_failed -#rule.10 : contains_peekabooyar -rule.11 : final_rule +#rule.5 : office_macro +#rule.6 : office_macro_with_suspicious_keyword +rule.7 : cuckoo_evil_sig +rule.8 : cuckoo_score +#rule.9 : requests_evil_domain +rule.10 : cuckoo_analysis_failed +#rule.11 : contains_peekabooyar +rule.12 : final_rule # rule specific configuration options # the section name equals the name of the rule @@ -71,6 +72,10 @@ greylist.34 : application/vnd.ms-excel.template.macroEnabled.12 greylist.35 : application/vnd.ms-excel greylist.36 : application/msword +[office_macro_with_suspicious_keyword] +keyword.1 : AutoOpen +keyword.2 : AutoClose + [cuckoo_evil_sig] signature.1 : A potential heapspray has been detected. .* signature.2 : A process attempted to delay the analysis task. diff --git a/tests/test-data/office/blank.doc b/tests/test-data/office/blank.doc new file mode 100644 index 00000000..a7b04c5f Binary files /dev/null and b/tests/test-data/office/blank.doc differ diff --git a/tests/test-data/office/empty.doc b/tests/test-data/office/empty.doc new file mode 100644 index 00000000..e69de29b diff --git a/tests/test-data/office/legitmacro.xls b/tests/test-data/office/legitmacro.xls new file mode 100644 index 00000000..cfb3caa8 Binary files /dev/null and b/tests/test-data/office/legitmacro.xls differ diff --git a/tests/test-data/office/suspiciousMacro.doc b/tests/test-data/office/suspiciousMacro.doc new file mode 100644 index 00000000..f618a97e Binary files /dev/null and b/tests/test-data/office/suspiciousMacro.doc differ diff --git a/test.py b/tests/test.py similarity index 93% rename from test.py rename to tests/test.py index 8321ac79..39318fb6 100755 --- a/test.py +++ b/tests/test.py @@ -51,7 +51,8 @@ from peekaboo.ruleset.rules import FileTypeOnWhitelistRule, \ FileTypeOnGreylistRule, CuckooAnalysisFailedRule, \ KnownRule, FileLargerThanRule, CuckooEvilSigRule, \ - CuckooScoreRule, RequestsEvilDomainRule, FinalRule + CuckooScoreRule, RequestsEvilDomainRule, FinalRule, \ + OfficeMacroRule, OfficeMacroWithSuspiciousKeyword from peekaboo.toolbox.cuckoo import CuckooReport from peekaboo.db import PeekabooDatabase, PeekabooDatabaseError # pylint: enable=wrong-import-position @@ -537,7 +538,6 @@ def test_3_sample_attributes(self): self.assertEqual(self.sample.cuckoo_report, None) self.assertEqual(self.sample.done, False) self.assertEqual(self.sample.submit_path, None) - self.assertFalse(self.sample.office_macros) self.assertEqual(self.sample.file_size, 4) def test_4_initialised_sample_attributes(self): @@ -565,7 +565,6 @@ def test_4_initialised_sample_attributes(self): self.assertEqual(self.sample.done, False) self.assertRegexpMatches( self.sample.submit_path, '/%s.py$' % self.sample.sha256sum) - self.assertFalse(self.sample.office_macros) self.assertEqual(self.sample.file_size, 4) def test_5_mark_done(self): @@ -730,6 +729,57 @@ def test_rule_file_type_on_whitelist(self): result = rule.evaluate(MimetypeSample(types)) self.assertEqual(result.further_analysis, expected) + def test_rule_office_ole(self): + """ Test rule office_ole. """ + config = '''[office_macro_with_suspicious_keyword] + keyword.1 : AutoOpen + keyword.2 : AutoClose + keyword.3 : suSPi.ious''' + rule = OfficeMacroWithSuspiciousKeyword(CreatingConfigParser(config)) + # sample factory to create samples from real files + factory1 = SampleFactory( + cuckoo=None, base_dir=None, job_hash_regex=None, + keep_mail_data=False, processing_info_dir=None) + # sampe factory to create samples with defined content + factory2 = CreatingSampleFactory( + cuckoo=None, base_dir=None, job_hash_regex=None, + keep_mail_data=False, processing_info_dir=None) + tests_data_dir = os.path.dirname(os.path.abspath(__file__))+"/test-data" + + combinations = [ + # no office document file extension + [Result.unknown, factory2.make_sample('test.nodoc', 'test')], + # test with empty file + [Result.unknown, factory1.make_sample(tests_data_dir+'/office/empty.doc')], + # office document with 'suspicious' in macro code + [Result.bad, factory1.make_sample(tests_data_dir+'/office/suspiciousMacro.doc')], + # test with blank word doc + [Result.unknown, factory1.make_sample(tests_data_dir+'/office/blank.doc')], + # test with legitimate macro + [Result.unknown, factory1.make_sample(tests_data_dir+'/office/legitmacro.xls')] + ] + for expected, sample in combinations: + result = rule.evaluate(sample) + self.assertEqual(result.result, expected) + + # test if macro present + rule = OfficeMacroRule(CreatingConfigParser(config)) + combinations = [ + # no office document file extension + [Result.unknown, factory2.make_sample('test.nodoc', 'test')], + # test with empty file + [Result.unknown, factory1.make_sample(tests_data_dir+'/office/empty.doc')], + # office document with 'suspicious' in macro code + [Result.bad, factory1.make_sample(tests_data_dir+'/office/suspiciousMacro.doc')], + # test with blank word doc + [Result.unknown, factory1.make_sample(tests_data_dir+'/office/blank.doc')], + # test with legitimate macro + [Result.bad, factory1.make_sample(tests_data_dir+'/office/legitmacro.xls')] + ] + for expected, sample in combinations: + result = rule.evaluate(sample) + self.assertEqual(result.result, expected) + def test_config_file_type_on_whitelist(self): """ Test whitelist rule configuration. """ config = '''[file_type_on_whitelist]