From 31727b3b258d78eb5ec6cc14aa3df63bad2ce77a Mon Sep 17 00:00:00 2001 From: Thomas Patzke Date: Mon, 21 May 2018 23:07:47 +0200 Subject: [PATCH] Added Windows Defender ATP backend Missing: * Aggregations --- Makefile | 1 + tools/sigma/backends.py | 182 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+) diff --git a/Makefile b/Makefile index 63ec3e5f2..653361f2d 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ test-sigmac: coverage run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t xpack-watcher rules/ > /dev/null coverage run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t splunk rules/ > /dev/null coverage run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t logpoint rules/ > /dev/null + coverage run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t wdatp rules/ > /dev/null coverage run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t es-dsl rules/ > /dev/null coverage run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t splunk -f 'level>=high,level<=critical,status=stable,logsource=windows' rules/ > /dev/null ! coverage run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t splunk -f 'level>=high,level<=critical,status=xstable,logsource=windows' rules/ > /dev/null diff --git a/tools/sigma/backends.py b/tools/sigma/backends.py index 662ef98ce..eda22abcd 100644 --- a/tools/sigma/backends.py +++ b/tools/sigma/backends.py @@ -851,6 +851,188 @@ class SplunkBackend(SingleTextQueryBackend): else: return " | stats %s(%s) as val by %s | search val %s %s" % (agg.aggfunc_notrans, agg.aggfield, agg.groupfield, agg.cond_op, agg.condition) +class WindowsDefenderATPBackend(SingleTextQueryBackend): + """Converts Sigma rule into Windows Defender ATP Hunting Queries.""" + identifier = "wdatp" + active = True + + reEscape = re.compile('("|\\\\(?![*?]))') + reClear = None + andToken = " and " + orToken = " or " + notToken = "not " + subExpression = "(%s)" + listExpression = "(%s)" + listSeparator = ", " + valueExpression = "\"%s\"" + nullExpression = "isnull(%s)" + notNullExpression = "isnotnull(%s)" + mapExpression = "%s == %s" + mapListsSpecialHandling = True + mapListValueExpression = "%s in %s" + + def __init__(self, *args, **kwargs): + """Initialize field mappings""" + super().__init__(*args, **kwargs) + self.fieldMappings = { # mapping between Sigma and ATP field names + # Supported values: + # (field name mapping, value mapping): distinct mappings for field name and value, may be a string (direct mapping) or function maps name/value to ATP target value + # (mapping function,): receives field name and value as parameter, return list of 2 element tuples (destination field name and value) + # (replacement, ): Replaces field occurrence with static string + "AccountName" : (self.id_mapping, self.default_value_mapping), + "CommandLine" : ("ProcessCommandLine", self.default_value_mapping), + "ComputerName" : (self.id_mapping, self.default_value_mapping), + "DestinationHostname" : ("RemoteUrl", self.default_value_mapping), + "DestinationIp" : ("RemoteIP", self.default_value_mapping), + "DestinationIsIpv6" : ("RemoteIP has \":\"", ), + "DestinationPort" : ("RemotePort", self.default_value_mapping), + "Details" : ("RegistryValueData", self.default_value_mapping), + "EventType" : ("ActionType", self.default_value_mapping), + "Image" : ("FolderPath", self.default_value_mapping), + "ImageLoaded" : ("FolderPath", self.default_value_mapping), + "LogonType" : (self.id_mapping, self.logontype_mapping), + "NewProcessName" : ("FolderPath", self.default_value_mapping), + "ObjectValueName" : ("RegistryValueName", self.default_value_mapping), + "ParentImage" : ("InitiatingProcessFolderPath", self.default_value_mapping), + "SourceImage" : ("InitiatingProcessFolderPath", self.default_value_mapping), + "TargetFilename" : ("FolderPath", self.default_value_mapping), + "TargetImage" : ("FolderPath", self.default_value_mapping), + "TargetObject" : ("RegistryKey", self.default_value_mapping), + "User" : (self.decompose_user, ), + } + + def id_mapping(self, src): + """Identity mapping, source == target field name""" + return src + + def default_value_mapping(self, val): + op = "==" + if "*" in val[1:-1]: # value contains * inside string - use regex match + op = "matches regex" + val = re.sub('([".^$]|\\\\(?![*?]))', '\\\\\g<1>', val) + val = re.sub('\\*', '.*', val) + val = re.sub('\\?', '.', val) + else: # value possibly only starts and/or ends with *, use prefix/postfix match + if val.endswith("*") and val.startswith("*"): + op = "contains" + val = self.cleanValue(val[1:-1]) + elif val.endswith("*"): + op = "startswith" + val = self.cleanValue(val[:-1]) + elif val.startswith("*"): + op = "endswith" + val = self.cleanValue(val[1:]) + + return "%s \"%s\"" % (op, val) + + def logontype_mapping(self, src): + """Value mapping for logon events to reduced ATP LogonType set""" + logontype_mapping = { + 2: "Interactive", + 3: "Network", + 4: "Batch", + 5: "Service", + 7: "Interactive", # unsure + 8: "Network", + 9: "Interactive", # unsure + 10: "Remote interactive (RDP) logons", # really the value? + 11: "Interactive" + } + try: + return logontype_mapping[int(src)] + except KeyError: + raise NotSupportedError("Logon type %d unknown and can't be mapped" % src) + + def decompose_user(self, src_field, src_value): + """Decompose domain\\user User field of Sysmon events into ATP InitiatingProcessAccountDomain and InititatingProcessAccountName.""" + reUser = re.compile("^(.*?)\\\\(.*)$") + m = reUser.match(src_value) + if m: + domain, user = m.groups() + return (("InitiatingProcessAccountDomain", domain), ("InititatingProcessAccountName", user)) + else: # assume only user name is given if backslash is missing + return (("InititatingProcessAccountName", src_value),) + + def generate(self, sigmaparser): + self.table = None + try: + self.product = sigmaparser.parsedyaml['logsource']['product'] + self.service = sigmaparser.parsedyaml['logsource']['service'] + except KeyError: + self.product = None + self.service = None + + super().generate(sigmaparser) + + def generateBefore(self, parsed): + if self.table is None: + raise NotSupportedError("No WDATP table could be determined from Sigma rule") + return "%s | where " % self.table + + def generateMapItemNode(self, node): + """ + ATP queries refer to event tables instead of Windows logging event identifiers. This method catches conditions that refer to this field + and creates an appropriate table reference. + """ + key, value = node + if type(value) == list: # handle map items with values list like multiple OR-chained conditions + return self.generateORNode( + [(key, v) for v in value] + ) + elif key == "EventID": # EventIDs are not reflected in condition but in table selection + if self.product == "windows": + if self.service == "sysmon" and value == 1 \ + or self.service == "security" and value == 4688: # Process Execution + self.table = "ProcessCreationEvents" + return None + elif self.service == "sysmon" and value == 3: # Network Connection + self.table = "NetworkCommunicationEvents" + return None + elif self.service == "sysmon" and value == 7: # Image Load + self.table = "ImageLoadEvents" + return None + elif self.service == "sysmon" and value == 8: # Create Remote Thread + self.table = "MiscEvents" + return "ActionType == \"CreateRemoteThread\"" + elif self.service == "sysmon" and value == 11: # File Creation + self.table = "FileCreationEvents" + return None + elif self.service == "sysmon" and value == 13 \ + or self.service == "security" and value == 4657: # Set Registry Value + self.table = "RegistryEvents" + return "ActionType == \"SetValue\"" + elif self.service == "security" and value == 4624: + self.table = "LogonEvents" + return None + elif type(value) in (str, int): # default value processing + try: + mapping = self.fieldMappings[key] + except KeyError: + raise NotSupportedError("No mapping defined for field '%s'" % key) + if len(mapping) == 1: + mapping = mapping[0] + if type(mapping) == str: + return mapping + elif callable(mapping): + conds = mapping(key, value) + return self.generateSubexpressionNode( + self.generateANDNode( + [cond for cond in mapping(key, value)] + ) + ) + elif len(mapping) == 2: + result = list() + for mapitem, val in zip(mapping, node): # iterate mapping and mapping source value synchronously over key and value + if type(mapitem) == str: + result.append(mapitem) + elif callable(mapitem): + result.append(mapitem(val)) + return "{} {}".format(*result) + else: + raise TypeError("Backend does not support map values of type " + str(type(value))) + + return super().generateMapItemNode(node) + class GrepBackend(BaseBackend, QuoteCharMixin): """Generates Perl compatible regular expressions and puts 'grep -P' around it""" identifier = "grep"