def thread_inject(process_event, ctx=None):
    """ Given a suspicious process, detects all processes that it injected into.
    A more complex pivot, because the destination may also act legitimately
    if the prior threads were not tampered with. Thus, this may require
    greater granularity (thread level) in order to better resolve its pivots
    to ignore those that were benign.
    :type process_event: Process
    """
    window = {'start': process_event.time - datetime.timedelta(minutes=1)}
    if process_event.duration is not None:
        window[
            'end'] = process_event.time + process_event.duration + datetime.timedelta(
                minutes=1)

    # Ignore thread creations into the kernel
    criteria = Criteria(src_pid=process_event.state.pid)
    criteria['hostname'] = process_event.state.hostname
    terms = ThreadEvent.search('remote_create', **criteria) & (
        FieldQuery("target_pid").wildcard("*") &
        (FieldQuery('target_pid') != 4))

    for result in ctx.query(terms, **window):
        remote_thread = ThreadEvent.update_existing(action='remote_create',
                                                    **result)
        for process_field in ProcessEvent.fields:
            if process_event.state[process_field] is not None:
                thread_field = 'src_' + process_field
                if thread_field in ThreadEvent.fields and thread_field not in remote_thread.state:
                    remote_thread.state[thread_field] = process_event.state[
                        process_field]

        remote_thread.save()
        yield remote_thread
def cmd_copy(process_event, ctx):
    """
    :param Process process_event: The input event to pivot on
    :param DataModelQueryLayer ctx: Search context to query over
    """
    window = {'start': process_event.time - datetime.timedelta(minutes=1)}
    if process_event.duration is not None:
        window[
            'end'] = process_event.time + process_event.duration + datetime.timedelta(
                minutes=1)

    if process_event.state.exe.lower(
    ) == 'cmd.exe' and 'copy' in process_event.state.command_line.lower():
        args = command_to_argv(process_event.state.command_line)
        lower_args = [_.lower() for _ in args]

        if len(lower_args
               ) >= 5 and lower_args[1] == '/c' and lower_args[2] == 'copy':
            copy_args = args[3:]
            flags = ('/A', '/B', '/D', '/V', '/N', '/Y', '/-Y', '/Z', '/L')
            copy_args = [_ for _ in copy_args if _.upper() not in flags]
            if len(copy_args) != 2:
                return

            source_file, dest_file = copy_args

            # TODO: get rid of this quick fix for when network paths show up starting with '\' insted of '\\'
            if dest_file[0] == '\\' and dest_file[1] != '\\':
                dest_file = '\\' + dest_file

            file_events = []

            criteria = Criteria()
            criteria['hostname'] = process_event.state.hostname
            criteria['file_path'] = dest_file

            file_query = FileEvent.search('create', **criteria) & (
                (FieldQuery("pid") == process_event.state.pid) |
                (FieldQuery("pid") == 4))

            # Grab the first file event if exists
            for result in ctx.query(file_query, **window):
                file_event = FileEvent.update_existing('create', **result)
                # One per action is enough
                file_events.append(file_event)
                yield file_event

            # Create a fake one if none can be found
            if len(file_events) == 0:
                file_fields = dict(file_path=dest_file,
                                   file_name=dest_file.split('\\').pop())
                for field in ('fqdn', 'hostname', 'pid', 'exe', 'image_path',
                              'pid', 'ppid'):
                    if process_event.state[field]:
                        file_fields[field] = process_event.state[field]
                yield FileEvent.update_existing('create',
                                                time=process_event.time +
                                                datetime.timedelta(seconds=1),
                                                state=file_fields,
                                                metadata={'synthetic': True})
Beispiel #3
0
    def query(self, expression, start=None, end=None, session=None, **kwargs):
        if isinstance(expression, ExternalAnalytic):
            raise NotImplementedError()

        event_class, action = self.get_data_model(expression)
        field_query = self.parse_expression(expression)
        context_expression = (FieldQuery('action') == action)

        if session is not None:
            context_expression &= (FieldQuery('sessions') == session.id)

        else:
            if start is not None:
                if isinstance(start, datetime.datetime):
                    context_expression &= (FieldQuery('time') >= start)
            if end is not None:
                if isinstance(end, datetime.datetime):
                    context_expression &= (FieldQuery('time') <= end)

        context_query = self.parse_expression(context_expression)

        # mongo doesn't handle case insensitivity, so we actually have to make inefficient queries and perform
        # it client side
        # cursor = event_class.objects(__raw__={"$and": [field_query, context_query]})
        cursor = event_class.objects(__raw__=context_query)

        def wrap_cursor(input_cursor):
            for event in input_cursor:
                state = dict(event.state.to_mongo())
                if expression.compare(state):
                    yield {'id': event.id, 'uuid': event.uuid, 'state': state, 'time': event.time}
        return wrap_cursor(cursor)
Beispiel #4
0
    def _parse_expression(cls, expression, prefix=''):
        if isinstance(expression, (CascadeAnalytic, AnalyticReference)):
            return cls._parse_expression(expression.query, prefix=prefix)

        if isinstance(expression, DataModelQuery):
            if len(prefix):
                return cls._parse_expression(expression.query, prefix='data_model.fields.')
            else:
                return '{} AND {} AND (\n    {}\n)'.format(
                    cls._parse_expression(FieldQuery('data_model.object') == expression.object.object_name),
                    cls._parse_expression(FieldQuery('data_model.action') == expression.action),
                    cls._parse_expression(expression.query, prefix='data_model.fields.')
                )

        elif isinstance(expression, FieldComparison):
            # Elastic search doesn't handle field != value queries, so have to negate field == value
            if expression.comparator is FieldComparators.NotEquals:
                return 'NOT {}'.format(cls._parse_expression(FieldQuery(expression.field) == expression.value), prefix=prefix)

            return '{}{}{}'.format(
                prefix + expression.field,
                cls.comparator_lookup[expression.comparator],
                cls._escape_value(expression.value)
            )

        elif isinstance(expression, Operation):
            if expression.operator is QueryComparators.Not:
                return ('{}' if isinstance(expression.terms[0], FieldComparison) else '{} ({})').format(
                                        cls.comparator_lookup[QueryComparators.Not],
                                        cls._parse_expression(expression.terms[0], prefix=prefix))
            else:
                return ' {} '.format(cls.comparator_lookup[expression.operator]).join(
                    ('({})' if isinstance(term, Operation) else '{}').format(
                        cls._parse_expression(term, prefix=prefix)
                    ) for term in expression.terms
                )

        elif isinstance(expression, Sequence):
            raise NotImplementedError()

        else:
            raise TypeError("Unknown expression type {}".format(type(expression)))
def at(process_event, ctx):
    """ :type process_event: Process """

    if process_event.state.exe.lower() == "at.exe" and "/?" not in str(
            process_event.state.command_line):
        args = command_to_argv(process_event.state.command_line)
        lower_args = [_.lower() for _ in args]
        if len(args) == 1:
            return

        criteria = Criteria(
        )  # parent_exe="taskeng.exe". This may not be true in windows 8
        if len(args) > 2 and args[1].startswith("\\\\"):
            host_string = args[1]
            host_info = extract_host(host_string)
            if 'hostname' in host_info:
                criteria['hostname'] = host_info['hostname']

        else:
            # if the system is not directly defined, then the host stays the same
            criteria['hostname'] = process_event.state.hostname

        if lower_args[-1] in ('/delete', '/yes'):
            return

        # Get the case version of the command
        command = args[-1]
        logger.debug(
            "Pivoting onto AT command with command line {}".format(command))
        criteria['command_line'] = command

        query = ProcessEvent.search('create', **criteria) & (
            (FieldQuery("parent_image_path")
             == "C:\\Windows\\system32\\taskeng.exe") |
            (FieldQuery("parent_image_path")
             == "C:\\Windows\\system32\\svchost.exe"))
        for process_event in ctx.query(query, start=process_event.time):
            task_process = ProcessEvent.update_existing(
                'create', **process_event)
            yield task_process
def schtasks(process_event, ctx):
    """ :type process_event: Process """
    if process_event.state.exe.lower() == "schtasks.exe" and "/tr" in str(
            process_event.state.command_line).lower():
        args = command_to_argv(process_event.state.command_line)
        lower_args = [_.lower() for _ in args]
        criteria = Criteria()

        # if scheduling a task on a remote system, extract out fqdn or hostname (but could be IP)
        if "/s" in lower_args:
            host = next_arg(args, "/s")
            if host:
                host_info = extract_host(host)
                if 'hostname' in host_info:
                    criteria['hostname'] = host_info['hostname']

        else:
            # if the system is not directly defined, then the host stays the same
            criteria['hostname'] = process_event.state.hostname

        # now extract out the task to be run from the command line
        task_run = next_arg(args, "/tr")
        if task_run is None:
            return

        logger.debug(
            "Pivoting onto scheduled task with command line {}".format(
                task_run))

        query = ProcessEvent.search('create', **criteria) & FieldQuery(
            'command_line').wildcard('*' + task_run + '*') & (
                (FieldQuery("parent_image_path")
                 == "C:\\Windows\\System32\\taskeng.exe") |
                (FieldQuery("parent_image_path")
                 == "C:\\Windows\\System32\\svchost.exe"))

        for process_event in ctx.query(query, start=process_event.time):
            task_process = ProcessEvent.update_existing(
                'create', **process_event)
            yield task_process
Beispiel #7
0
    def get_query(self):
        """
        :rtype QueryTerm
        """
        query = EmptyQuery
        if self.key is not None:
            query &= (FieldQuery(self.key) == self.value)

        if len(self.children):
            children_query = EmptyQuery
            for child in self.children:
                children_query |= child.get_query()
            query &= children_query
        return query