diff options
author | Felix Bauer <jack@ai4me.de> | 2019-08-20 10:47:36 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-20 10:47:36 +0200 |
commit | 18d0ac2148d76aaf1dfe659c1a11cf74021ebae9 (patch) | |
tree | b663d8ad8a6d42e80329aff75329aa28540364c0 | |
parent | bf5f7a7c906f13cb3fdf22a17d182cac8d03fe17 (diff) |
Add an additional static test to the ruleset to check for office macros with suspicious keywords (#87)
The new rule is deactivated by default, it uses oletools to check SUSPICIOUSKEYWORDS
in the macro code of office documents.
Ole is now an analyser module much like Cuckoo inside the toolbox.
All logic has been moved there. Sample is merely there for caching.
Evaluation of Rules uses the toolbox and report back.
Regex based matching of MS office files for configurable keywords.
Also detection of macros has been improved.
Tests for correct handling of none office file extension
Tests for correct handling of empty file with correct extension
Tests for correct detection of office file with suspicious macro
Tests for correct pass of blank office document
Tests for correct handling of empty word doc.
Tests for correct non detection of Excel file with macro.
-rw-r--r-- | peekaboo/config.py | 13 | ||||
-rw-r--r-- | peekaboo/locale/de/LC_MESSAGES/peekaboo.mo | bin | 4074 -> 4392 bytes | |||
-rw-r--r-- | peekaboo/locale/de/LC_MESSAGES/peekaboo.po | 73 | ||||
-rw-r--r-- | peekaboo/locale/peekaboo.pot | 66 | ||||
-rw-r--r-- | peekaboo/ruleset/engine.py | 9 | ||||
-rw-r--r-- | peekaboo/ruleset/rules.py | 61 | ||||
-rw-r--r-- | peekaboo/sample.py | 20 | ||||
-rw-r--r-- | peekaboo/toolbox/ms_office.py | 61 | ||||
-rw-r--r-- | peekaboo/toolbox/ole.py | 120 | ||||
-rw-r--r-- | ruleset.conf.sample | 19 | ||||
-rw-r--r-- | tests/test-data/office/blank.doc | bin | 0 -> 22528 bytes | |||
-rw-r--r-- | tests/test-data/office/empty.doc | 0 | ||||
-rw-r--r-- | tests/test-data/office/legitmacro.xls | bin | 0 -> 35840 bytes | |||
-rw-r--r-- | tests/test-data/office/suspiciousMacro.doc | bin | 0 -> 28672 bytes | |||
-rwxr-xr-x | tests/test.py (renamed from test.py) | 56 |
15 files changed, 355 insertions, 143 deletions
diff --git a/peekaboo/config.py b/peekaboo/config.py index 5c182bb..af5b81f 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 @@ class PeekabooConfigParser( # pylint: disable=too-many-ancestors 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 @@ class PeekabooConfigParser( # pylint: disable=too-many-ancestors 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 @@ class PeekabooConfigParser( # pylint: disable=too-many-ancestors # 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 Binary files differindex c89b24b..af5a962 100644 --- a/peekaboo/locale/de/LC_MESSAGES/peekaboo.mo +++ b/peekaboo/locale/de/LC_MESSAGES/peekaboo.mo diff --git a/peekaboo/locale/de/LC_MESSAGES/peekaboo.po b/peekaboo/locale/de/LC_MESSAGES/peekaboo.po index d82f699..77ab127 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 <michael.weiser@gmx.de>\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 56b6113..085c447 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 <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\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 96af1c6..6e736f5 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 @@ class RulesetEngine(object): 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 @@ class RulesetEngine(object): 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 @@ class RulesetEngine(object): 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 797858a..6635c0c 100644 --- a/peekaboo/ruleset/rules.py +++ b/peekaboo/ruleset/rules.py @@ -31,6 +31,8 @@ import logging 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 @@ class FileTypeOnGreylistRule(Rule): 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 @@ class OfficeMacroRule(Rule): 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 35c661d..d4c5a62 100644 --- a/peekaboo/sample.py +++ b/peekaboo/sample.py @@ -38,7 +38,6 @@ from builtins import open 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 @@ class Sample(object): 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 @@ class Sample(object): 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 @@ -462,14 +461,6 @@ class Sample(object): 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 @@ -485,6 +476,11 @@ class Sample(object): 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 """ return self.__submit_path @@ -506,6 +502,10 @@ class Sample(object): """ 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 b5ab902..0000000 --- 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 <http://www.gnu.org/licenses/>. # -# # -############################################################################### - -""" 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 0000000..e4a3ce0 --- /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 <http://www.gnu.org/licenses/>. # +# # +############################################################################### + + +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 c8909a9..9a9ea73 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 Binary files differnew file mode 100644 index 0000000..a7b04c5 --- /dev/null +++ b/tests/test-data/office/blank.doc diff --git a/tests/test-data/office/empty.doc b/tests/test-data/office/empty.doc new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test-data/office/empty.doc diff --git a/tests/test-data/office/legitmacro.xls b/tests/test-data/office/legitmacro.xls Binary files differnew file mode 100644 index 0000000..cfb3caa --- /dev/null +++ b/tests/test-data/office/legitmacro.xls diff --git a/tests/test-data/office/suspiciousMacro.doc b/tests/test-data/office/suspiciousMacro.doc Binary files differnew file mode 100644 index 0000000..f618a97 --- /dev/null +++ b/tests/test-data/office/suspiciousMacro.doc @@ -51,7 +51,8 @@ from peekaboo.ruleset.engine import RulesetEngine 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 @@ class TestSample(unittest.TestCase): self.assertEqual(self.sample.cuckoo_report, None) |