From 18d0ac2148d76aaf1dfe659c1a11cf74021ebae9 Mon Sep 17 00:00:00 2001 From: Felix Bauer Date: Tue, 20 Aug 2019 10:47:36 +0200 Subject: [PATCH] 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. --- peekaboo/config.py | 13 ++- peekaboo/locale/de/LC_MESSAGES/peekaboo.mo | Bin 4074 -> 4392 bytes peekaboo/locale/de/LC_MESSAGES/peekaboo.po | 73 ++++++++----- peekaboo/locale/peekaboo.pot | 66 +++++++----- peekaboo/ruleset/engine.py | 9 +- peekaboo/ruleset/rules.py | 61 ++++++++++- peekaboo/sample.py | 20 ++-- peekaboo/toolbox/ms_office.py | 61 ----------- peekaboo/toolbox/ole.py | 120 +++++++++++++++++++++ ruleset.conf.sample | 19 ++-- tests/test-data/office/blank.doc | Bin 0 -> 22528 bytes tests/test-data/office/empty.doc | 0 tests/test-data/office/legitmacro.xls | Bin 0 -> 35840 bytes tests/test-data/office/suspiciousMacro.doc | Bin 0 -> 28672 bytes test.py => tests/test.py | 56 +++++++++- 15 files changed, 355 insertions(+), 143 deletions(-) delete mode 100644 peekaboo/toolbox/ms_office.py create mode 100644 peekaboo/toolbox/ole.py create mode 100644 tests/test-data/office/blank.doc create mode 100644 tests/test-data/office/empty.doc create mode 100644 tests/test-data/office/legitmacro.xls create mode 100644 tests/test-data/office/suspiciousMacro.doc rename test.py => tests/test.py (93%) 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 c89b24bd31567c0a4424e046d87a89f80374a0ee..af5a9622f24acfd5037490e697db779bf5e62acc 100644 GIT binary patch delta 1012 zcmaLUUr19?9KiA4rZ!vCsYPjN9yLwbjA@!CCYB?72_q|d=w)tq?zX&>yA6>7pQC)~ zl3^555J}R5EU5V~;roIk(wJLmk)m)iOIsrMCy z*MzT`zncH7OZWbMH{Bu~bZ_A>zQHZ zqne9&gudYw8OM2S!m2GIiv6XZhE5d0TD*>XaS8X~2h=w%^{tNwv6TKv3}Y0#aUOLA zRrYb+OWp1EtC1$QzxQ1H%G z5wTP%9ygoam%n&%&_yDMY8EpBfk7NVQ18YX~&!^{tjo_9}NC)`G!SL{efTo9=7HH delta 856 zcmb8s-%FEG7{Kx8ebqL#C3|3KP{io-X0^I~+dps1S! z;Sk+qcY!w%^b$ouMMSX6yd2SGg+!O#Sl0KrL3G&*htKmoJ3Bv~vwmYt zjBdsS%VdAQJGMxcbOq1jTWrUDbqq{m4D;B74vyeW?7~;rixuq0-)QFb*YE6ClCuOM z3J&Jc!!dk@W`Q4g2wNLOA~=M{a2gNbZA{`r+>aY*{`U<#F}O>FGfNB)V=tO@CNV1| zk$Hj+3TxPgPj`M0DWmDgHcntyKx7bS(A;cuG@WyC0B>Lp zALA5m*6Jr(MB?OgXx3Y4k-gGK@P>kUqc5mG*irq`UQFhfXBS;pd9&^nN1ar0FUOF7 zs$-+IvLCPeX!)vP&DL9ijJ_V!q3mFKSfxgCnW2GH%2$o2EGw%oHa-8pbvv6sTRIj> rL?#vrj#}`PyHqHu_$4({yy|$W;4aPi^P#e>UxYjKgK$7^hmZULQp$Vf 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 0000000000000000000000000000000000000000..a7b04c5f51d22c6e1445b81a27d49a0f07648833 GIT binary patch literal 22528 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 literal 0 HcmV?d00001 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]