mirror of
https://github.com/WithSecureLabs/volatility-plugins
synced 2026-06-21 13:46:34 +00:00
Create gargoyle.py
This commit is contained in:
+407
@@ -0,0 +1,407 @@
|
||||
import struct
|
||||
import volatility
|
||||
import inspect
|
||||
import importlib
|
||||
import volatility.plugins.common as common
|
||||
from volatility.renderers import TreeGrid
|
||||
from volatility.renderers.basic import Address
|
||||
from volatility import utils, obj, win32
|
||||
from volatility.plugins.malware import malfind
|
||||
from volatility.plugins.malware.timers import Timers
|
||||
|
||||
from unicorn import *
|
||||
from unicorn.x86_const import *
|
||||
|
||||
class timerResult():
|
||||
def __init__(self, process, thread, timerRoutine):
|
||||
self.thread = thread
|
||||
self.process = process
|
||||
self.routine = timerRoutine
|
||||
self.didROP = "Unknown"
|
||||
self.didAdjustPerms = "Unknown"
|
||||
self.didJumpToAdjusted = "Unknown"
|
||||
self.adjustedAddresses = []
|
||||
self.probablePayload = 0
|
||||
self.prolog = "Unknown"
|
||||
|
||||
instrStream = process.get_process_address_space().read(timerRoutine, 16)
|
||||
if not instrStream:
|
||||
print "Process %s '%s': Can't read instruction stream at %s; perhaps it is paged out" % (hex(int(process.obj_offset)), process.ImageFileName, hex(timerRoutine))
|
||||
else:
|
||||
self.prolog = disAssembleInstr(instrStream, timerRoutine, 5)
|
||||
|
||||
def disAssembleInstr(instrStream, instrLocation, opcodeCount):
|
||||
toRet = []
|
||||
for _, i, _ in malfind.Disassemble(instrStream, instrLocation, True):
|
||||
toRet.append(i)
|
||||
opcodeCount = opcodeCount - 1
|
||||
if opcodeCount == 0:
|
||||
break
|
||||
return "; ".join(toRet)
|
||||
|
||||
# Unicorn doesn't support virtual memory properly (see unicorn bug #947) so there's not really much point setting up
|
||||
# GDT/etc.
|
||||
|
||||
def printUnicornContext(pas, nextIns, unicornEng):
|
||||
print "Unicorn context: "
|
||||
instrStream = pas.read(nextIns, 20)
|
||||
for _, i, _ in malfind.Disassemble(instrStream, nextIns):
|
||||
print "\t %s" % i
|
||||
print "ESP = %s" % hex(unicornEng.reg_read(UC_X86_REG_ESP))
|
||||
print "EAX = %s" % hex(unicornEng.reg_read(UC_X86_REG_EAX))
|
||||
print "EBX = %s" % hex(unicornEng.reg_read(UC_X86_REG_EBX))
|
||||
print "ECX = %s" % hex(unicornEng.reg_read(UC_X86_REG_ECX))
|
||||
print "EDX = %s" % hex(unicornEng.reg_read(UC_X86_REG_EDX))
|
||||
print "ESI = %s" % hex(unicornEng.reg_read(UC_X86_REG_ESI))
|
||||
print "EDI = %s" % hex(unicornEng.reg_read(UC_X86_REG_EDI))
|
||||
print "EFLAGS = %s" % hex(unicornEng.reg_read(UC_X86_REG_EFLAGS))
|
||||
print ""
|
||||
|
||||
def getWoW64Profile(process):
|
||||
"""
|
||||
Given a 64-bit Windows process, find the 32-bit profile which most closely matches the 64-bit proceesses profile.
|
||||
Hopefully, this will be close enough enable us to decode 32-bit objects running under WoW.
|
||||
"""
|
||||
pas = process.get_process_address_space()
|
||||
|
||||
wow64ProfileFull = str(pas.profile.__class__).strip('><\'')[len('class: '):]
|
||||
wow64ProfileNameSpace = '.'.join(wow64ProfileFull.split('.')[:-1])
|
||||
wow64ProfileName = str(wow64ProfileFull.split('.')[-1:][0].replace('x64', 'x86'))
|
||||
|
||||
module = importlib.import_module(wow64ProfileNameSpace)
|
||||
class_ = getattr(module, wow64ProfileName)
|
||||
return class_()
|
||||
|
||||
def isKernelSpace(process, DllBase):
|
||||
if process.get_process_address_space().profile.metadata.get("memory_model") == "32bit":
|
||||
# TODO: support 3GB address mode, if it's worth it
|
||||
return DllBase < 0x80000000
|
||||
else:
|
||||
return DllBase < 0x8000000000000000
|
||||
|
||||
# It seems that certain SPs of certain OSs (vista and certain versions of 2003/XP64) use a non-standard
|
||||
# APC format when running in WoW64 mode. We will apply this type only if we detect a version of windows
|
||||
# which uses it.
|
||||
WoW64UnusualAPC = {
|
||||
'_KAPC_WOW64' : [ 0x58, {
|
||||
'Type' : [ 0x0, ['unsigned char']],
|
||||
'SpareByte0' : [ 0x1, ['unsigned char']],
|
||||
'Size' : [ 0x2, ['unsigned char']],
|
||||
'SpareByte1' : [ 0x3, ['unsigned char']],
|
||||
'SpareLong0' : [ 0x4, ['unsigned long']],
|
||||
'Thread' : [ 0x8, ['pointer64', ['_KTHREAD']]],
|
||||
'ApcListEntry' : [ 0x10, ['_LIST_ENTRY']],
|
||||
'KernelRoutine' : [ 0x20, ['pointer64', ['void']]],
|
||||
'RundownRoutine' : [ 0x28, ['pointer64', ['void']]],
|
||||
'unknown' : [ 0x30, ['pointer64', ['void']]],
|
||||
'NormalContext' : [ 0x38, ['pointer32', ['void']]],
|
||||
'NormalRoutine' : [ 0x3C, ['pointer32', ['void']]],
|
||||
'SystemArgument1' : [ 0x40, ['pointer64', ['void']]],
|
||||
'SystemArgument2' : [ 0x48, ['pointer64', ['void']]],
|
||||
'ApcStateIndex' : [ 0x50, ['unsigned char']],
|
||||
'ApcMode' : [ 0x51, ['unsigned char']],
|
||||
'Inserted' : [ 0x52, ['unsigned char']],
|
||||
}]}
|
||||
|
||||
class APCVTypes(obj.ProfileModification):
|
||||
before = ['WindowsOverlay']
|
||||
conditions = {'os': lambda x: x == 'windows'}
|
||||
def modification(self, profile):
|
||||
if profile.metadata.get("memory_model", "32bit") == "64bit":
|
||||
profile.vtypes.update(WoW64UnusualAPC)
|
||||
|
||||
class gargoyle(common.AbstractWindowsCommand):
|
||||
|
||||
def __init__(self, config, *args, **kwargs):
|
||||
common.AbstractWindowsCommand.__init__(self, config, *args, **kwargs)
|
||||
|
||||
config.add_option("VERBOSE", default=0, action='count',
|
||||
short_option='v', help='Verbose information')
|
||||
config.add_option("ALLTIMERS", default = False, action = 'store_true',
|
||||
help = "Do not filter timers by valid EPROCESS")
|
||||
|
||||
def dbgMsg(self, *args):
|
||||
if self._config.VERBOSE > 0:
|
||||
print " ".join(map(str, args))
|
||||
|
||||
# This is called when Unicorn needs to access some memory that isn't mapped yet.
|
||||
# We simply map the memory, copy in its contents from the debuggee, and return.
|
||||
# Our main loop will retry. We signal errors by setting self.emulationFaulted.
|
||||
def badmemWrapped(self, uc, access, address, size, value, user_data):
|
||||
self.dbgMsg("Access to unmapped memory %s" % hex(address))
|
||||
|
||||
if self.pas == None:
|
||||
self.dbgMsg("Unable to handle memory mapping with no active process")
|
||||
raise MemoryError
|
||||
# Unicorn will only successfully map page-aligned addresses, so map the whole page.
|
||||
pageSize = 0x1000
|
||||
pageBase = address & (~(pageSize-1))
|
||||
uc.mem_map(pageBase, pageSize)
|
||||
# Read from the debuggee..
|
||||
pageCts = self.pas.read(pageBase, pageSize)
|
||||
if pageCts == None:
|
||||
self.dbgMsg ("Unable to read %s bytes at %s" % (hex(pageSize), hex(pageBase)))
|
||||
raise MemoryError
|
||||
# And write to Unicorn.
|
||||
uc.mem_write(pageBase, pageCts)
|
||||
self.dbgMsg( "Mapped %s bytes at base %s" % (hex(pageSize), hex(pageBase)))
|
||||
|
||||
return True
|
||||
|
||||
def badmem(self, uc, access, address, size, value, user_data):
|
||||
try:
|
||||
return self.badmemWrapped(uc, access, address, size, value, user_data)
|
||||
except Exception as e:
|
||||
self.emulationFaulted = e
|
||||
raise
|
||||
|
||||
def unified_output(self, data):
|
||||
return TreeGrid([("Process", str),
|
||||
("Handler", Address),
|
||||
("Prolog", str),
|
||||
("Adjusted page permissions", str),
|
||||
("Branched to code after altering page permission", str),
|
||||
("Probable payload", Address)],
|
||||
self.generator(data))
|
||||
|
||||
def generator(self, data):
|
||||
for timer in data:
|
||||
yield(0, [ str(timer.process.ImageFileName),
|
||||
Address(timer.routine),
|
||||
str(timer.prolog),
|
||||
str(timer.didAdjustPerms),
|
||||
str(timer.didJumpToAdjusted),
|
||||
Address(timer.probablePayload)])
|
||||
|
||||
def calculate(self):
|
||||
self.exportCache = {}
|
||||
addr_space = utils.load_as(self._config)
|
||||
|
||||
if self._config.ALLTIMERS:
|
||||
validProcesses = None
|
||||
else:
|
||||
validProcesses = []
|
||||
for p in win32.tasks.pslist(addr_space):
|
||||
validProcesses.append(int(p.obj_offset))
|
||||
|
||||
# Firstly, we must iterate over the timers found by the timers plugin. We can then find the owning process, the
|
||||
# associated APC, and the thread. We need the process because we will need to know what process context the APC's
|
||||
# NormalRoutine (if any) will run in, in order to do our emulation.
|
||||
for timeraddress, _ in Timers(self._config).calculate():
|
||||
timer = obj.Object("_ETIMER", offset=timeraddress.obj_offset, vm=timeraddress.obj_vm)
|
||||
self.dbgMsg("Timer at %s dpc %s" % (hex(int(timer.obj_offset)), hex(int(timeraddress.Dpc.DeferredRoutine))))
|
||||
etimer = timer.cast("_ETIMER")
|
||||
# Check for the weird APC format
|
||||
version = (addr_space.profile.metadata.get('major', 0),
|
||||
addr_space.profile.metadata.get('minor', 0))
|
||||
if addr_space.profile.metadata.get("memory_model") == "64bit" and ((version == (5, 2) ) or (version == (6, 0))):
|
||||
apc = obj.Object("_KAPC_WOW64", offset=etimer.TimerApc.obj_offset, vm=etimer.TimerApc.obj_vm)
|
||||
else:
|
||||
apc = obj.Object("_KAPC" , offset=etimer.TimerApc.obj_offset, vm=etimer.TimerApc.obj_vm)
|
||||
routine = int(apc.NormalRoutine)
|
||||
thread = obj.Object("_ETHREAD", offset=int(apc.Thread), vm=addr_space)
|
||||
if (thread == None) | (routine == 0):
|
||||
# This APC has no user-mode payload.
|
||||
continue
|
||||
process = thread.owning_process()
|
||||
if process == None:
|
||||
# This usually happens when a timer is not pointing to a valid thread. I'm not sure why this happens -
|
||||
# I guess there's some flag in the timer which states that it isn't valid, or the timer/timer list is
|
||||
# # being manipulated when we dump.
|
||||
self.dbgMsg('Timer %s : warning: Thread ID %s has no owning process, skipping' % (hex(int(timeraddress.obj_offset)), hex(int(thread.Cid.UniqueThread))))
|
||||
continue
|
||||
|
||||
# If this is a WoW64 APC - ie, an APC queued by a 32-bit thread on a 64-bit windows install - then we must
|
||||
# 'decode' the NormalRoutine by shifting and negating it.
|
||||
# We detect these WoW64-style APCs by comparing the top half of the 64bit address, except bit zero, to 0xffffffff. I'm not
|
||||
# sure if this is reliable, but it seems to work.
|
||||
if (((routine >> 32) | 0x01) == 0xffffffff):
|
||||
routine32bit = (-(routine >> 2)) & 0xffffffff
|
||||
self.dbgMsg("WoW64-style APC routine decoded %s to %s" % (hex(routine), hex(routine32bit)))
|
||||
routine = routine32bit
|
||||
|
||||
if (validProcesses != None) and (not process in validProcesses):
|
||||
continue
|
||||
|
||||
for result in self.examine(addr_space, process, thread, routine, apc, timer):
|
||||
yield result
|
||||
|
||||
def getWoW64Modules(self, process):
|
||||
pas = process.get_process_address_space()
|
||||
|
||||
# Get a pointer to the 32-bit PEB, which is stored as part of the _EWOW64PROCESS. This is of type nt!_PEB32.
|
||||
peb32 = process.Peb32
|
||||
ldr = int(peb32.Ldr)
|
||||
|
||||
# Since this PEB is 32bit, we must use definitions from the 32bit version of windows which corresponds to this
|
||||
# build.
|
||||
profile32 = getWoW64Profile(process)
|
||||
pas32 = pas
|
||||
pas32.profile = profile32
|
||||
pebLdrData = obj.Object("_PEB_LDR_DATA", offset=ldr, vm=pas32)
|
||||
modules = pebLdrData.InMemoryOrderModuleList.cast("_LIST_ENTRY").list_of_type("_LDR_DATA_TABLE_ENTRY", "InMemoryOrderLinks")
|
||||
return modules
|
||||
|
||||
def findExport(self, process, addr_space, moduleName, exportName):
|
||||
# mind the case! 'WoW' vs 'Wow'.
|
||||
if hasattr(process.m, 'Wow64Process'):
|
||||
wow64Process = process.Wow64Process
|
||||
elif hasattr(process, 'WoW64Process'):
|
||||
wow64Process = process.WoW64Process
|
||||
else:
|
||||
wow64Process = None
|
||||
|
||||
isWoW64 = False
|
||||
if wow64Process != None and wow64Process.v() != 0:
|
||||
isWoW64 = True
|
||||
if isWoW64:
|
||||
# WoW64 processes are treated specially, since we must get 32bit modules via the 32bit PEB.
|
||||
pas = process.get_process_address_space()
|
||||
if pas == None:
|
||||
# Probably not a real EPROCESS.
|
||||
return None
|
||||
modList = self.getWoW64Modules(process)
|
||||
else:
|
||||
# Not a WoW64 process, so just get the modules normally.
|
||||
modList = process.get_mem_modules()
|
||||
|
||||
exp = self.findExportInModuleList(modList, moduleName, exportName, process, isWoW64)
|
||||
if exp == None:
|
||||
print "Unable to find export %s!%s in process %s" % (moduleName, exportName, process.ImageFileName)
|
||||
return exp
|
||||
|
||||
def findExportInModuleList(self, moduleList, moduleName, exportName, process, isWoW64):
|
||||
moduleNameLowercase = moduleName.lower()
|
||||
exportNameLowercase = exportName.lower()
|
||||
|
||||
for m in moduleList:
|
||||
dllName = str(m.BaseDllName).lower()
|
||||
if dllName == moduleNameLowercase:
|
||||
# Cache per-process (since a module may appear in a different process at a different base). For kernel modules
|
||||
# there's no need to be per-process, so don't bother.
|
||||
if isKernelSpace(process, m.DllBase):
|
||||
cacheKey = "%s!%s (kernel)" % (moduleNameLowercase, exportNameLowercase)
|
||||
else:
|
||||
cacheKey = "%s!%s %s (wow64 %s)" % (moduleNameLowercase, exportNameLowercase, hex(m.DllBase), isWoW64)
|
||||
if cacheKey in self.exportCache.keys():
|
||||
return self.exportCache[cacheKey]
|
||||
|
||||
for _, expAddress, expName in m.exports():
|
||||
if str(expName).lower() == exportNameLowercase:
|
||||
toReturn = m.DllBase + expAddress
|
||||
self.dbgMsg("Found %s ! %s at %s (%s)" % (m.BaseDllName, expName, hex(toReturn), hex(m.DllBase)))
|
||||
self.exportCache[cacheKey] = toReturn
|
||||
return toReturn
|
||||
|
||||
return None
|
||||
|
||||
def examine(self, addr_space, process, thread, routine, apc, timer):
|
||||
# We will now emulate through the instruction stream, starting at the APC handler, and see if anything fishy
|
||||
# goes on. Specifically, we will see if the APC calls VirtualProtect. If it does, we will see if it also
|
||||
# tries to jump to the newly-VirtualProtect'ed memory - a sure sign of Gargoyle-ness.
|
||||
VirtualProtect = self.findExport(process, addr_space, "KERNEL32.DLL", "VirtualProtect")
|
||||
VirtualProtectEx = self.findExport(process, addr_space, "KERNEL32.DLL", "VirtualProtectEx")
|
||||
# We'll need to set the process address space so that our badmem callback can use it later on.
|
||||
self.pas = process.get_process_address_space()
|
||||
self.emulationFaulted = None
|
||||
|
||||
result = timerResult(process, thread, routine)
|
||||
self.dbgMsg("Timer %s APC %s routine %s in process %s ('%s') thread %s" % (hex(int(timer.obj_offset)), hex(int(apc.obj_offset)), hex(routine), hex(int(process.obj_offset)), process.ImageFileName, hex(thread.StartAddress)))
|
||||
|
||||
unicornEng = Uc(UC_ARCH_X86, UC_MODE_32)
|
||||
# Populate the context from which to start emulating.
|
||||
# We use an arbitrary ESP, with a magic value to signify that the APC handler has returned.
|
||||
initialStackBase = 0x00000000f0000000
|
||||
unicornEng.mem_map(initialStackBase, 2 * 1024 * 1024)
|
||||
unicornEng.mem_write(initialStackBase + 0x100 + 0, "\xbe\xba\xde\xc0")
|
||||
# We push the argument which the APC handler is given
|
||||
unicornEng.mem_write(initialStackBase + 0x100 + 4, apc.NormalContext.obj_vm.read(apc.NormalContext.obj_offset, 4))
|
||||
unicornEng.reg_write(UC_X86_REG_ESP, initialStackBase + 0x100)
|
||||
|
||||
# Set up our handlers, which will map memory on-demand from the debuggee
|
||||
unicornEng.hook_add(UC_HOOK_MEM_READ_UNMAPPED, self.badmem)
|
||||
unicornEng.hook_add(UC_HOOK_MEM_WRITE_INVALID, self.badmem)
|
||||
unicornEng.hook_add(UC_HOOK_MEM_FETCH_UNMAPPED, self.badmem)
|
||||
|
||||
# There's not really much point mapping the GDT, since Unicorn doesn't properly support paging.
|
||||
# See Unicorn's bug #947, "(x86) Emulated CPU is not translating virtual memory addresses".
|
||||
|
||||
# Now, lets emulate some instructions! We won't get many, because Unicorn can't emulate a lot of things (like
|
||||
# segment-prefixed instructions, as used by wow64) but we'll get enough to detect most ROP chains.
|
||||
instrEmulated = 0
|
||||
nextIns = routine
|
||||
memoryRange = None
|
||||
while instrEmulated < 10000:
|
||||
|
||||
if self._config.VERBOSE > 0:
|
||||
print "Before instruction %d at %s:" % (instrEmulated, hex(nextIns))
|
||||
print "CS:IP = %s:%s SS:SP = %s:%s" % (
|
||||
hex(unicornEng.reg_read(UC_X86_REG_CS)), hex(unicornEng.reg_read(UC_X86_REG_EIP)),
|
||||
hex(unicornEng.reg_read(UC_X86_REG_SS)), hex(unicornEng.reg_read(UC_X86_REG_ESP)))
|
||||
|
||||
instrStream = self.pas.read(nextIns, 15)
|
||||
for _, i, _ in malfind.Disassemble(instrStream, nextIns):
|
||||
print "\t%s\t%s" % (hex(nextIns), i)
|
||||
break
|
||||
|
||||
# Attempt to emulate a single instruction
|
||||
try:
|
||||
unicornEng.emu_start(nextIns, nextIns + 0x10, count = 1)
|
||||
except unicorn.UcError as e1:
|
||||
break
|
||||
if self.emulationFaulted != None:
|
||||
break
|
||||
|
||||
# Great, we emulated an instruction. Move on to the next instruction.
|
||||
nextIns = unicornEng.reg_read(UC_X86_REG_EIP)
|
||||
instrEmulated = instrEmulated + 1
|
||||
|
||||
# If we're now at our magic address, then our APC has completed executing entirely. That's all, folks.
|
||||
if nextIns == 0xc0debabe:
|
||||
break
|
||||
|
||||
# Now we can check for some suspicious circumstance.
|
||||
esp = unicornEng.reg_read(UC_X86_REG_ESP)
|
||||
if esp == int(apc.NormalContext):
|
||||
result.didROP = "True"
|
||||
self.dbgMsg("APC has performed stack pivot; new stack is its context pointer")
|
||||
if VirtualProtect == None:
|
||||
# If we didn't find VirtualProtect, we can't go any further. I guess a stack pivot is a pretty big
|
||||
# red flag anyway.
|
||||
break
|
||||
if VirtualProtectEx != None:
|
||||
if nextIns == VirtualProtectEx:
|
||||
result.didAdjustPerms = "True"
|
||||
|
||||
# Read the arguments to VirtualProtect, and the return address, from the stack
|
||||
returnAddress = struct.unpack("I", unicornEng.mem_read(esp - 0, 4))[0]
|
||||
memoryRange = struct.unpack("I", unicornEng.mem_read(esp + 8, 4))[0]
|
||||
|
||||
result.adjustedAddresses.append(memoryRange)
|
||||
self.dbgMsg("VirtualProtectEx: Timer routine is adjusting memory permissions of range %s" % hex(memoryRange))
|
||||
# Set the return address to whatever VirtualProtect would've returned to
|
||||
nextIns = returnAddress
|
||||
unicornEng.reg_write(UC_X86_REG_EIP, returnAddress)
|
||||
# Pop five args plus the return address off the (32bit) stack
|
||||
unicornEng.reg_write(UC_X86_REG_ESP, esp + (6*4))
|
||||
if VirtualProtect != None:
|
||||
if nextIns == VirtualProtect:
|
||||
result.didAdjustPerms = "True"
|
||||
|
||||
# Read the arguments to VirtualProtect, and the return address, from the stack
|
||||
returnAddress = struct.unpack("I", unicornEng.mem_read(esp - 0, 4))[0]
|
||||
memoryRange = struct.unpack("I", unicornEng.mem_read(esp + 4, 4))[0]
|
||||
|
||||
result.adjustedAddresses.append(memoryRange)
|
||||
self.dbgMsg("VirtualProtect: Timer routine is adjusting memory permissions of range %s" % hex(memoryRange))
|
||||
# Set the return address to whatever VirtualProtect would've returned to
|
||||
nextIns = returnAddress
|
||||
unicornEng.reg_write(UC_X86_REG_EIP, returnAddress)
|
||||
# Pop four args plus the return address off the (32bit) stack
|
||||
unicornEng.reg_write(UC_X86_REG_ESP, esp + (5 * 4))
|
||||
if nextIns in result.adjustedAddresses:
|
||||
result.didJumpToAdjusted = "True"
|
||||
result.probablePayload = nextIns
|
||||
self.dbgMsg( "Timer routine is jumping to newly-executable code at %s!" % hex(memoryRange))
|
||||
break
|
||||
yield result
|
||||
Reference in New Issue
Block a user