Files
2015-05-25 20:31:56 -04:00

716 lines
29 KiB
Python

# Volatility
# Copyright (C) 2007-2013 Volatility Foundation
# Copyright (c) 2010, 2011, 2012 Michael Ligh <michael.ligh@mnin.org>
#
# This file is part of Volatility.
#
# Volatility 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 2 of the License, or
# (at your option) any later version.
#
# Volatility 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 Volatility. If not, see <http://www.gnu.org/licenses/>.
#
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
from volatility.renderers import TreeGrid
from volatility.renderers.basic import Address, Hex
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 unified_output(self, data):
return TreeGrid([("Offset", Address),
("PID", int),
("TID", int),
("Tags", str),
("Create Time", str),
("Exit Time", str),
("Owning Process", str),
("Attached Process", str),
("State", str),
("State Reason", str),
("Base Priority", int),
("Priority", int),
("TEB", Address),
("Start Address", Address),
("Owner Name", str),
("Win32 Start Address", Address),
("Win32 Thread", Address),
("Cross Thread Flags", str),
("EIP", Hex),
("EAX", Hex),
("EBX", Hex),
("ECX", Hex),
("EDX", Hex),
("ESI", Hex),
("EDI", Hex),
("ESP", Hex),
("EBP", Hex),
("ErrCode", Hex),
("SegCS", Hex),
("SegSS", Hex),
("SegDS", Hex),
("SegES", Hex),
("SegGS", Hex),
("SegFS", Hex),
("EFlags", Hex),
("dr0", Hex),
("dr1", Hex),
("dr2", Hex),
("dr3", Hex),
("dr6", Hex),
("dr7", Hex),
("SSDT", Address),
("Entry Number", int),
("Descriptor Service Table", Address),
("Hook Number", int),
("Function Name", str),
("Function Address", Address),
("Module Name", str),
("Disassembly", str),
],
self.generator(data))
def generator(self, 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
values = []
values.append(Address(thread.obj_offset))
values.append(int(thread.Cid.UniqueProcess))
values.append(int(thread.Cid.UniqueThread))
values.append(','.join(tags))
values.append(str(thread.CreateTime))
if thread.ExitTime > 0:
values.append(str(thread.ExitTime))
else:
values.append('')
values.append(str(thread.owning_process().ImageFileName))
values.append(str(thread.attached_process().ImageFileName))
# Lookup the thread's state
state = str(thread.Tcb.State)
# Find the wait reason
if state == 'Waiting':
state_reason = str(thread.Tcb.WaitReason)
else:
state_reason = ''
values.append(state)
values.append(state_reason)
values.append(int(thread.Tcb.BasePriority))
values.append(int(thread.Tcb.Priority))
values.append(Address(thread.Tcb.Teb))
values.append(Address(thread.StartAddress))
values.append(owner_name)
# Check the flag which indicates whether Win32StartAddress is valid
if thread.SameThreadApcFlags & 1:
values.append(Address(thread.Win32StartAddress))
else:
values.append(Address(-1))
values.append(Address(thread.Tcb.Win32Thread))
values.append(str(thread.CrossThreadFlags))
# Disasemble the start address if possible
dis = ''
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"
dis += "\n".join(["{0:#x} {1:<16} {2}".format(o, h, i)
for o, i, h in malfind.Disassemble(buf, thread.StartAddress.v(), mode)])
if self.bits32:
# Print the registers if possible
trapframe = thread.Tcb.TrapFrame.dereference_as("_KTRAP_FRAME")
if trapframe:
for r in trapframe.Eip, trapframe.Eax, trapframe.Ebx, \
trapframe.Ecx, trapframe.Edx, trapframe.Esi, \
trapframe.Edi, trapframe.HardwareEsp, \
trapframe.Ebp, trapframe.ErrCode, trapframe.SegCs, \
trapframe.HardwareSegSs, trapframe.SegDs, \
trapframe.SegEs, trapframe.SegGs, trapframe.SegFs, \
trapframe.EFlags, trapframe.Dr0, trapframe.Dr1, \
trapframe.Dr2, trapframe.Dr3, trapframe.Dr6, \
trapframe.Dr7 :
values.append(Hex(r))
else:
values.extend( [Hex(-1)] * 23 )
values.append(Address(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():
service_table = Address(desc.KiServiceTable.v())
else:
service_table = Address(-1)
# Show exactly which functions are hooked
table = desc.KiServiceTable.v()
if table not in hooked_tables.keys():
yield (0, values + [i, service_table, -1, '',
Address(-1), '', dis])
continue
yielded=False
for (j, func_name, func_addr, mod_name) in hooked_tables[table]:
yielded=True
yield(0, values + [i, service_table, j, func_name,
Address(func_addr), mod_name, dis])
if not yielded:
yield (0, values + [i, service_table, -1, '',
Address(-1), '', dis])
else:
values.extend([ -1, Address(-1), -1, '', Address(-1), '', dis ])
yield (0, values)
else:
# registers
values.extend( [Hex(-1)] * 23 )
# ssdt
values.extend([ Address(-1), -1, Address(-1), -1, '',
Address(-1), '', dis ])
yield (0, values)
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 (j, func_name, func_addr, mod_name) in hooked_tables[table]:
s += " [{0:#x}] {1} {2:#x} {3}\n".format(
j, 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))