# Vortessence  Memory Forensics
#
# Copyright 2007-2015 Volatility Foundation
# Copyright 2010, 2011, 2012 Michael Ligh <michael.ligh@mnin.org>
#
# Copyright 2015 Bern University of Applied Sciences
#
# 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 json
import sys, pydoc
import volatility.utils as utils
import volatility.registry as registry
import volatility.obj as obj
import volatility.win32.modules as modules
import volatility.win32.tasks as tasks
import volatility.plugins.ssdt as ssdt
import volatility.plugins.taskmods as taskmods
import volatility.plugins.modscan as modscan
import volatility.plugins.malware.malfind as malfind
import volatility.debug as debug

try:
    import distorm3 #pylint: disable-msg=W0611
    has_distorm3 = True
except ImportError:
    has_distorm3 = False

#--------------------------------------------------------------------------------
# vtypes
#--------------------------------------------------------------------------------

thread_types = {
    '_KTHREAD' : [ None , {
    'State' : [ None, ['Enumeration', dict(target = 'unsigned char', choices = {
        0: 'Initialized', 1: 'Ready', 2: 'Running', 3: 'Standby', 4: 'Terminated',
        5: 'Waiting', 6: 'Transition', 7: 'DeferredReady', 8: 'GateWait'})]],
    'WaitReason' : [ None, ['Enumeration', dict(target = 'unsigned char', choices = {
        0: 'Executive', 1: 'FreePage', 2: 'PageIn', 3: 'PoolAllocation',
        4: 'DelayExecution', 5: 'Suspended', 6: 'UserRequest', 7: 'WrExecutive',
        8: 'WrFreePage', 9: 'WrPageIn', 10: 'WrPoolAllocation', 11: 'WrDelayExecution',
        12: 'WrSuspended', 13: 'WrUserRequest', 14: 'WrEventPair', 15: 'WrQueue',
        16: 'WrLpcReceive', 17: 'WrLpcReply', 18: 'WrVirtualMemory', 19: 'WrPageOut',
        20: 'WrRendezvous', 21: 'Spare2', 22: 'Spare3', 23: 'Spare4', 24: 'Spare5',
        25: 'Spare6', 26: 'WrKernel', 27: 'WrResource', 28: 'WrPushLock', 29: 'WrMutex',
        30: 'WrQuantumEnd', 31: 'WrDispatchInt', 32: 'WrPreempted',
        33: 'WrYieldExecution', 34: 'WrFastMutex', 35: 'WrGuardedMutex',
        36: 'WrRundown', 37: 'MaximumWaitReason'})]],
    }],
    '_ETHREAD': [ None, {
    'CrossThreadFlags': [ None, ['Flags', {'bitmap': {
        'PS_CROSS_THREAD_FLAGS_TERMINATED': 0,
        'PS_CROSS_THREAD_FLAGS_DEADTHREAD': 1,
        'PS_CROSS_THREAD_FLAGS_HIDEFROMDBG': 2,
        'PS_CROSS_THREAD_FLAGS_IMPERSONATING': 3,
        'PS_CROSS_THREAD_FLAGS_SYSTEM': 4,
        'PS_CROSS_THREAD_FLAGS_HARD_ERRORS_DISABLED': 5,
        'PS_CROSS_THREAD_FLAGS_BREAK_ON_TERMINATION': 6,
        'PS_CROSS_THREAD_FLAGS_SKIP_CREATION_MSG': 7,
        'PS_CROSS_THREAD_FLAGS_SKIP_TERMINATION_MSG': 8,
        }}]],
    }],
}

#--------------------------------------------------------------------------------
# profile modifications  
#--------------------------------------------------------------------------------

class MalwareKthread(obj.ProfileModification):
    before = ['WindowsObjectClasses', 'WindowsOverlay']
    conditions = {'os': lambda x: x == 'windows'}
    def modification(self, profile):
        profile.merge_overlay(thread_types)

#--------------------------------------------------------------------------------
# thread checks
#--------------------------------------------------------------------------------

class AbstractThreadCheck(object):
    """Base thread check class"""

    def __init__(self, thread, mods, mod_addrs, \
                    hooked_tables, found_by_scanner):
        """
        @param thread: the _ETHREAD object

        @param mods: a dictionary with module bases as 
        keys and _LDR_DATA_TABLE_ENTRY as values. 

        @param mod_addrs: a sorted list of module base
        addresses 

        @param hooked_tables: a list of SSDTs that have
        one or more hooked functions. 

        @param found_by_scanner: True/False if the _ETHREAD
        passed as the thread parameter was found via 
        list walking or pool scanning. 
        """
        self.thread = thread
        self.mods = mods
        self.mod_addrs = mod_addrs
        self.hooked_tables = hooked_tables
        self.found_by_scanner = found_by_scanner
        self.flags = str(thread.CrossThreadFlags)

    def check(self):
        """Return True or False from this method"""

class OrphanThread(AbstractThreadCheck):
    """Detect orphan threads"""

    def check(self):
        """This check is True for system threads whose start address
        do not map back to known/loaded kernel drivers."""

        # Take the address space from any module object
        addr_space = self.mods.values()[0].obj_vm

        module = tasks.find_module(self.mods,
            self.mod_addrs, addr_space.address_mask(self.thread.StartAddress))

        return ('PS_CROSS_THREAD_FLAGS_SYSTEM' in self.flags and
                    module == None)

class DkomExit(AbstractThreadCheck):
    """Detect inconsistencies wrt exit times and termination"""

    def check(self):
        """This check is True when a thread's ExitTime is non-zero
        (indicating it has exited) but the state and flags 
        indicate that it is still active."""

        return (self.thread.ExitTime != 0 and
                    str(self.thread.Tcb.State) != 'Terminated' and
                    not 'PS_CROSS_THREAD_FLAGS_TERMINATED' in self.flags)

class HideFromDebug(AbstractThreadCheck):
    """Detect threads hidden from debuggers"""

    def check(self):
        """This check is True when a thread's flags report that
        it is being hidden from a debugger."""

        return 'PS_CROSS_THREAD_FLAGS_HIDEFROMDBG' in self.flags

class SystemThread(AbstractThreadCheck):
    """Detect system threads"""

    def check(self):
        """This check is True when a thread's flags report that 
        it is a system thread (i.e. PsCreateSystemThread)."""

        return 'PS_CROSS_THREAD_FLAGS_SYSTEM' in self.flags

class Impersonation(AbstractThreadCheck):
    """Detect impersonating threads"""

    def check(self):
        """This check is True when a thread's flags indicate that 
        it is impersonating another thread's security context."""

        return 'PS_CROSS_THREAD_FLAGS_IMPERSONATING' in self.flags

class HwBreakpoint(AbstractThreadCheck):
    """Detect threads with hardware breakpoints"""

    def check(self):
        """This check is True when a thread's trap frame shows
        usage of the Dr* registers in a manner consistent with
        hardware breakpoints."""

        # Don't check threads that appear to have exited
        if self.found_by_scanner:
            return False

        if 'PS_CROSS_THREAD_FLAGS_TERMINATED' in self.flags:
            return False

        trap = self.thread.Tcb.TrapFrame.dereference_as("_KTRAP_FRAME")

        if not trap:
            return False

        if ((trap.Dr0 != 0 or trap.Dr1 != 0 or trap.Dr2 != 0
                or trap.Dr3 != 0) and
                (trap.Dr6 != 0 and trap.Dr7 != 0)):
            return True

        return False

class AttachedProcess(AbstractThreadCheck):
    """Detect threads attached to another process"""

    def check(self):
        """This check is True when a thread is currently attached
        to a process other than the process that owns the thread."""

        return (self.thread.ExitTime == 0 and
                self.thread.owning_process().obj_offset !=
                self.thread.attached_process().obj_offset)

class HookedSSDT(AbstractThreadCheck):
    """Check if a thread is using a hooked SSDT"""

    def check(self):
        """This check is True if any of the thread's SSDTs have 
        hooked functions. If its True and the SSDT hooking module
        is legit, you can filter them out with --allow-hook."""

        # Check doesn't apply to x64
        if self.hooked_tables == None:
            return False

        ssdt_obj = self.thread.Tcb.ServiceTable.\
            dereference_as('_SERVICE_DESCRIPTOR_TABLE')

        for _, desc in enumerate(ssdt_obj.Descriptors):
            table = desc.KiServiceTable.v()
            if table in self.hooked_tables.keys():
                return True
        return False

class ScannerOnly(AbstractThreadCheck):
    """Detect threads no longer in a linked list"""

    def check(self):
        """This check is True when a thread is found by pool tag
        scanning but not in list traversal."""

        return self.found_by_scanner

#--------------------------------------------------------------------------------
# threads plugin 
#--------------------------------------------------------------------------------

class Threads(taskmods.DllList):
    "Investigate _ETHREAD and _KTHREADs"

    def __init__(self, config, *args, **kwargs):
        taskmods.DllList.__init__(self, config, *args, **kwargs)
        self.bits32 = None
        config.add_option("FILTER", short_option = 'F', default = None,
                            help = 'Tags to filter (comma-separated)')

        config.add_option("LISTTAGS", short_option = 'L', default = False,
                          action = 'store_true', help = 'List all available tags')

    def get_hooked_tables(self, addr_space):
        """This function finds SSDTs in an address space, checks
        if there are any hooked functions in the SSDTs, and returns
        a dictionary where SSDT base addresses are the keys and the
        values are lists of hooked function names.

        @param addr_space: a kernel address space. 
        """

        # Names of the legit executive modules for SSDT tables 
        executive_modules = [
            # SSDT 0
            ["ntoskrnl.exe", "ntkrnlpa.exe", "ntkrnlmp.exe", "ntkrpamp.exe"],
            # SSDT 1 
            ["win32k.sys"],
            # SSDT 2
            ["spud.sys"],
            # SSDT 3
            []]

        syscalls = addr_space.profile.syscalls

        hooked_tables = {}

        for info in ssdt.SSDT(self._config).calculate():
            idx, table, n, vm, mods, mod_addrs = info
            # This is straight out of ssdt.py. Too bad there's no better way 
            # to not duplicate code?
            for i in range(n):
                if self.bits32:
                    # These are absolute function addresses in kernel memory. 
                    syscall_addr = obj.Object('address', table + (i * 4), vm).v()
                else:
                    # These must be signed long for x64 because they are RVAs 
                    # relative to the base of the table and can be negative. 
                    offset = obj.Object('long', table + (i * 4), vm).v()
                    # The offset is the top 20 bits of the 32 bit number. 
                    syscall_addr = table + (offset >> 4)
                try:
                    syscall_name = syscalls[idx][i]
                except IndexError:
                    syscall_name = "UNKNOWN"

                syscall_mod = tasks.find_module(mods, mod_addrs, syscall_addr)
                if syscall_mod:
                    syscall_modname = syscall_mod.BaseDllName
                else:
                    syscall_modname = "UNKNOWN"

                if str(syscall_modname).lower() not in executive_modules[idx]:
                    fields = (i, syscall_name, syscall_addr, syscall_modname)
                    if hooked_tables.has_key(table):
                        hooked_tables[table].append(fields)
                    else:
                        hooked_tables[table] = [(fields)]

        return hooked_tables

    def calculate(self):

        if not has_distorm3:
            debug.warning("For best results please install distorm3")

        # Checks that subclass AbstractThreadCheck
        checks = registry.get_plugin_classes(AbstractThreadCheck)

        # If --listtags is chosen, just print the tags and return 
        if self._config.LISTTAGS:
            for cls_name, cls in checks.items():
                sys.stdout.write("{0:<20} {1}\n".format(cls_name, pydoc.getdoc(cls)))
            return

        addr_space = utils.load_as(self._config)
        system_range = tasks.get_kdbg(addr_space).MmSystemRangeStart.dereference_as("Pointer")

        # Only show threads owned by particular processes
        pidlist = []
        if self._config.PID:
            pidlist = [int(p) for p in self._config.PID.split(',')]
        elif self._config.OFFSET:
            process = self.virtual_process_from_physical_offset(addr_space, self._config.OFFSET)
            if process:
                pidlist = [int(process.UniqueProcessId)]

        # Get sorted list of kernel modules 
        mods = dict((addr_space.address_mask(mod.DllBase), mod) for mod in modules.lsmod(addr_space))
        mod_addrs = sorted(mods.keys())

        # Are we on x86 or x64. Save this for render_text 
        self.bits32 = addr_space.profile.metadata.\
            get("memory_model", "32bit") == "32bit"

        # Get a list of hooked SSDTs but only on x86
        if self.bits32:
            hooked_tables = self.get_hooked_tables(addr_space)
        else:
            hooked_tables = None

        # Dictionary to store threads. Keys are physical offsets of
        # ETHREAD objects. Values are tuples, where the first item is
        # a boolean specifying if the object was found by scanning and
        # the second item is the actual ETHREAD object. 
        seen_threads = dict()

        # Gather threads by list traversal of active/linked processes 
        for task in taskmods.DllList(self._config).calculate():
            for thread in task.ThreadListHead.\
                    list_of_type("_ETHREAD", "ThreadListEntry"):
                seen_threads[thread.obj_vm.vtop(thread.obj_offset)] = (False, thread)

        # Now scan for threads and save any that haven't been seen
        for thread in modscan.ThrdScan(self._config).calculate():
            if not seen_threads.has_key(thread.obj_offset):
                seen_threads[thread.obj_offset] = (True, thread)

        # Keep a record of processes whose DLLs we've already enumerated
        process_dll_info = {}

        for _offset, (found_by_scanner, thread) in seen_threads.items():

            # Skip processes the user doesn't want to see
            if ((self._config.PID or self._config.OFFSET) and not pidlist) or (pidlist and thread.Cid.UniqueProcess not in pidlist):
                continue

            # Do we need to gather DLLs for module resolution 
            if addr_space.address_compare(thread.StartAddress, system_range) != -1:
                owner = tasks.find_module(mods, 
                                          mod_addrs, 
                                          addr_space.address_mask(thread.StartAddress))
            else:
                owning_process = thread.owning_process() 
                if not owning_process.is_valid(): 
                    owner = None
                else:
                    try:
                        user_mod_addrs, user_mods = process_dll_info[owning_process.obj_offset]
                    except KeyError:
                        user_mods = dict((addr_space.address_mask(mod.DllBase), mod) 
                                            for mod in owning_process.get_load_modules())
                        user_mod_addrs = sorted(user_mods.keys())
                        process_dll_info[owning_process.obj_offset] = (user_mod_addrs, user_mods)
                    owner = tasks.find_module(user_mods, 
                                              user_mod_addrs, 
                                              addr_space.address_mask(thread.StartAddress))
            
            if owner:
                owner_name = str(owner.BaseDllName or '')
            else:
                owner_name = "UNKNOWN"

            # Replace the dummy class with an instance 
            instances = dict(
                        (cls_name, cls(thread, mods, mod_addrs,
                            hooked_tables, found_by_scanner))
                        for cls_name, cls in checks.items()
                        )

            yield thread, addr_space, mods, mod_addrs, \
                        instances, hooked_tables, system_range, owner_name

    def render_text(self, outfd, data):

        # Determine which filters the user wants to see
        if self._config.FILTER:
            filters = set(self._config.FILTER.split(','))
        else:
            filters = set()

        for thread, addr_space, mods, mod_addrs, \
                     instances, hooked_tables, system_range, owner_name in data:
            # If the user didn't set filters, display all results. If 
            # the user set one or more filters, only show threads 
            # with matching results. 
            tags = set([t for t, v in instances.items() if v.check()])

            if filters and not filters & tags:
                continue

            s = "------\n"

            s += "ETHREAD: {0:#010x} Pid: {1} Tid: {2}\n".format(
                thread.obj_offset,
                thread.Cid.UniqueProcess, thread.Cid.UniqueThread)

            s += "Tags: {0}\n".format(','.join(tags))
            s += "Created: {0}\n".format(thread.CreateTime)
            s += "Exited: {0}\n".format(thread.ExitTime)

            s += "Owning Process: {0}\n".format(
                thread.owning_process().ImageFileName)

            s += "Attached Process: {0}\n".format(
                thread.attached_process().ImageFileName)

            # Lookup the thread's state
            state = str(thread.Tcb.State)

            # Append the wait reason 
            if state == 'Waiting':
                state = state + ':' + str(thread.Tcb.WaitReason)

            s += "State: {0}\n".format(state)
            s += "BasePriority: {0:#x}\n".format(thread.Tcb.BasePriority)
            s += "Priority: {0:#x}\n".format(thread.Tcb.Priority)
            s += "TEB: {0:#010x}\n".format(thread.Tcb.Teb)

            s += "StartAddress: {0:#010x} {1}\n".format(
                thread.StartAddress, owner_name)

            # Check the flag which indicates whether Win32StartAddress is valid
            if thread.SameThreadApcFlags & 1:
                s += "Win32StartAddress: {0:#010x}\n".format(
                    thread.Win32StartAddress)

            if self.bits32:
                s += "ServiceTable: {0:#010x}\n".format(thread.Tcb.ServiceTable)

                ssdt_obj = obj.Object("_SERVICE_DESCRIPTOR_TABLE",
                    offset = thread.Tcb.ServiceTable,
                    vm = addr_space
                    )

                if ssdt_obj != None:
                    for i, desc in enumerate(ssdt_obj.Descriptors):
                        if desc.is_valid():
                            s += "  [{0}] {1:#010x}\n".format(i, desc.KiServiceTable.v())
                        else:
                            s += "  [{0}] -\n".format(i)
                        # Show exactly which functions are hooked 
                        table = desc.KiServiceTable.v()
                        if table not in hooked_tables.keys():
                            continue
                        for (i, func_name, func_addr, mod_name) in hooked_tables[table]:
                            s += "      [{0:#x}] {1} {2:#x} {3}\n".format(
                                i, func_name, func_addr, mod_name)

            s += "Win32Thread: {0:#010x}\n".format(thread.Tcb.Win32Thread)
            s += "CrossThreadFlags: {0}\n".format(thread.CrossThreadFlags)

            # Print the registers if possible 
            trapframe = thread.Tcb.TrapFrame.dereference_as("_KTRAP_FRAME")

            if trapframe and self.bits32:
                s += "Eip: {0:#10x}\n".format(trapframe.Eip)
                s += "  eax={0:#010x} ebx={1:#010x} ecx={2:#010x}".format(
                    trapframe.Eax, trapframe.Ebx, trapframe.Ecx)
                s += " edx={0:#010x} esi={1:#010x} edi={2:#010x}\n".format(
                    trapframe.Edx, trapframe.Esi, trapframe.Edi)
                s += "  eip={0:#010x} esp={1:#010x} ebp={2:#010x} err={3:#010x}\n".format(
                    trapframe.Eip, trapframe.HardwareEsp, trapframe.Ebp, trapframe.ErrCode)
                s += "  cs={0:#04x} ss={1:#04x} ds={2:#04x}".format(
                    trapframe.SegCs, trapframe.HardwareSegSs, trapframe.SegDs)
                s += " es={0:#04x} gs={1:#04x} fs={2:#04x} efl={3:#010x}\n".format(
                    trapframe.SegEs, trapframe.SegGs, trapframe.SegFs, trapframe.EFlags)
                s += "  dr0={0:#010x} dr1={1:#010x} dr2={2:#010x}".format(
                    trapframe.Dr0, trapframe.Dr1, trapframe.Dr2)
                s += " dr3={0:#010x} dr6={1:#010x} dr7={2:#010x}\n".format(
                    trapframe.Dr3, trapframe.Dr6, trapframe.Dr7)

            # Disasemble the start address if possible 
            process_space = thread.owning_process().get_process_address_space()
            
            if process_space.is_valid_address(thread.StartAddress):
                buf = process_space.zread(thread.StartAddress, 24)

                mode = "32bit" if self.bits32 else "64bit"

                s += "\n".join(["{0:#x} {1:<16} {2}".format(o, h, i)
                    for o, i, h in malfind.Disassemble(buf, thread.StartAddress.v(), mode)])

            outfd.write("{0}\n".format(s))

    def get_json(self, data):
        results = {}

        # Determine which filters the user wants to see
        if self._config.FILTER:
            filters = set(self._config.FILTER.split(','))
        else:
            filters = set()

        for thread, addr_space, mods, mod_addrs, \
            instances, hooked_tables, system_range, owner_name in data:
            # If the user didn't set filters, display all results. If
            # the user set one or more filters, only show threads
            # with matching results.
            tags = set([t for t, v in instances.items() if v.check()])

            if filters and not filters & tags:
                continue

            row = {'offset': int(thread.obj_offset),
                   'UniqueProcess': int(thread.Cid.UniqueProcess),
                   'UniqueThread': int(thread.Cid.UniqueThread),
                   'Tags': [str(i) for i in tags],
                   'CreateTime': str(thread.CreateTime),
                   'ExitTime': str(thread.ExitTime),
                   'ImageFileName': str(thread.owning_process().ImageFileName),
                   'Attached': str(thread.attached_process().ImageFileName),
                   'BasePriority': int(thread.Tcb.BasePriority),
                   'Priority': int(thread.Tcb.Priority),
                   'Teb': int(thread.Tcb.Teb),
                   'State': str(thread.Tcb.State),
                   'WaitReason': str(thread.Tcb.WaitReason),
                   'StartAddress': int(thread.StartAddress),
                   "SystemThread": bool("SystemThread" in tags),
                   "OwnerName": "",
                   "Win32StartAddressValid": thread.SameThreadApcFlags & 1,
                   "Win32StartAddress": int(thread.Win32StartAddress),
                   'ServiceTable': int(thread.Tcb.ServiceTable) if self.bits32 else "",
                   "Win32Thread": int(thread.Tcb.Win32Thread),
                   "CrossThreadFlags": int(thread.CrossThreadFlags),
                   "TrapFrame": {},
                   'SSDT': {},
                   "Disassembly": []
            }

            # Print the registers if possible
            trapframe = thread.Tcb.TrapFrame.dereference_as("_KTRAP_FRAME")

            if trapframe and self.bits32:
                row["TrapFrame"] = {key: int(eval('trapframe.%s' % key, {'trapframe': trapframe})) for key in
                                    trapframe.members.keys()}

            # Disasemble the start address if possible
            if addr_space.is_valid_address(thread.StartAddress):
                buf = addr_space.zread(thread.StartAddress, 24)
                row["Disassembly"] = [{'Address': int(o), "Bytes": str(h), "Instruction": str(i)}
                                      for o, i, h in
                                      malfind.Disassemble(buf, thread.StartAddress.v())
                ]

            # If its a system thread, get the owning module
            if row["SystemThread"]:
                owner = tasks.find_module(mods, mod_addrs, thread.StartAddress)
                if not owner is None:
                    row["OwnerName"] = str(owner.BaseDllName or owner_name)

            if self.bits32:
                ssdt_obj = obj.Object("_SERVICE_DESCRIPTOR_TABLE",
                                      offset=thread.Tcb.ServiceTable,
                                      vm=addr_space
                )

                if ssdt_obj is not None:
                    for i, desc in enumerate(ssdt_obj.Descriptors):
                        row['SSDT'][str(i)] = {'index': i,
                                               'KiServiceTable': '',
                                               'HookedSSDT': []}
                        if desc.is_valid():
                            row['SSDT'][str(i)]['KiServiceTable'] = desc.KiServiceTable.v()

                        table = desc.KiServiceTable.v()
                        if table not in hooked_tables.keys():
                            continue

                        # no use for this data so far
                        #
                        # for (j, func_name, func_addr, mod_name) in hooked_tables[table]:
                        #     row['SSDT'][str(i)]['HookedSSDT'].append({'index': j,
                        #                                               'FunctionName': func_name,
                        #                                               'FunctionAddress': func_addr,
                        #                                               'ModuleName': str(mod_name)})

            results[str(row['offset'])] = row

        return results

    def render_json(self, outfd, data):
        outfd.write(json.dumps(self.get_json(data), indent=4) + '\n')