import ntpath import logging import re import sys import click from lxml import etree, objectify from tabulate import tabulate def is_admin_only_path(path): """ This is pretty naive, but seems to do the work for now... """ path = ntpath.splitdrive(path)[1].lower() if path.startswith('\\windows') or path.startswith('\\programdata') or path.startswith('\\program files'): return True return False def _read_config_to_json(sysmon_config): parser = etree.XMLParser(remove_comments=True) tree = objectify.parse(sysmon_config, parser=parser) root = tree.getroot() event_filtering = root.find('EventFiltering') configuration = [] for rule in event_filtering.getchildren(): rule_type = rule.tag on_match = rule.get('onmatch') single_rule = { 'rule_type': rule_type, 'on_match': on_match, 'conditions': [] } for condition in rule.iterchildren(): cond_operator = condition.get('condition') cond_content = condition.text cond_type = condition.tag single_rule['conditions'].append({ 'operator': cond_operator, 'content': cond_content, 'condition_type': cond_type }) configuration.append(single_rule) return configuration def _find_possible_bypasses(configuration): possible_bypasses = [] for rule in configuration: # We are currently interested only in possible bypasses of execution and exfiltration... if rule['rule_type'] not in ['ProcessCreate', 'NetworkConnect']: continue # We are only interested in exclusions if rule['on_match'] != 'exclude': continue for condition in rule['conditions']: operator = condition['operator'] cond_type = condition['condition_type'] content = condition['content'] if operator == 'is': # We probabably cannot do anything if this is an exact path, # maybe if we have write permissions there... continue elif operator == 'begin with': # Possible bypass if write permissions exist here continue elif operator == 'contains': if not is_admin_only_path(condition['content']): possible_bypasses.append({ 'rule_type': rule['rule_type'], 'description': 'Any {} containing {}'.format(cond_type, content) }) elif operator == 'end with': if rule['rule_type'] == 'ProcessCreate': # For some reason some rules uses ends with but specifies a complete path, # Yet we could do something like: c:\temp\malicious.exe c:\legitimate\excluded\path.exe if re.match(r'\w:\\', content, re.IGNORECASE): continue possible_bypasses.append({ 'rule_type': rule['rule_type'], 'description': 'Any {} that ends with {}'.format(cond_type, content) }) elif operator == 'image': possible_bypasses.append({ 'rule_type': rule['rule_type'], 'description': 'Any Image with the name {}'.format(content) }) else: logging.debug(rule['rule_type']) logging.debug(condition) return possible_bypasses @click.command() @click.argument('sysmon_config', type=click.Path(exists=True)) @click.option('-v', '--verbose', type=click.BOOL, is_flag=True, default=False) def analyze_config(sysmon_config, verbose): logging.basicConfig(stream=sys.stdout, level=logging.DEBUG if verbose else logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logging.debug('Analyzing {}'.format(sysmon_config)) configuration = _read_config_to_json(sysmon_config) possible_bypasses = _find_possible_bypasses(configuration) print(tabulate(possible_bypasses, headers='keys'))