Files
Jamie Levy dc389a370c dumpfiles:
fixes a crash that occurs when dumping a specific file
at a given physical address and using the output renderers
2018-07-03 10:45:33 -05:00

1281 lines
58 KiB
Python

# Volatility
# Copyright (C) 2012-13 Volatility Foundation
#
# 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 Version 2 as
# published by the Free Software Foundation. You may not use, modify or
# distribute this program under any other version of the GNU General
# Public License.
#
# 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/>.
#
#pylint: disable-msg=C0111
import os
import re
import math
import volatility.obj as obj
import volatility.utils as utils
import volatility.debug as debug
import volatility.win32.tasks as tasks_mod
import volatility.win32.modules as modules
import volatility.plugins.common as common
import volatility.plugins.taskmods as taskmods
from volatility.renderers import TreeGrid
from volatility.renderers.basic import Address, Bytes
import json
from io import BytesIO
#--------------------------------------------------------------------------------
# Constants
#--------------------------------------------------------------------------------
PAGE_SIZE = 0x1000
PAGE_MASK = PAGE_SIZE - 1
IMAGE_EXT = "img"
DATA_EXT = "dat"
FILEOFFSET_MASK = 0xFFFFFFFFFFFF0000
VACB_BLOCK = 0x40000
VACB_ARRAY = 0x80
VACB_OFFSET_SHIFT = 18
VACB_LEVEL_SHIFT = 7
VACB_SIZE_OF_FIRST_LEVEL = 1 << (VACB_OFFSET_SHIFT + VACB_LEVEL_SHIFT)
class _CONTROL_AREA(obj.CType):
def extract_ca_file(self, unsafe = False):
""" Extracts a file from a specified CONTROL_AREA
Attempts to extract the memory resident pages pertaining to a
particular CONTROL_AREA object.
Args:
control_area: Instance of a CONTROL_AREA object
unsafe: Relax safety constraints for more data
Returns:
mdata: List of pages, (physoffset, fileoffset, size) tuples, that are memory resident
zpad: List of pages, (offset, size) tuples, that not memory resident
Raises:
"""
zpad = []
mdata = []
# Depending on the particular address space being used we need to
# determine if the MMPTE will be either 4 or 8 bytes. The x64
# and IA32_PAE both use 8 byte PTEs. Whereas, IA32 uses 4 byte
# PTE entries.
memory_model = self.obj_vm.profile.metadata.get('memory_model', '32bit')
pae = self.obj_vm.pae
if pae:
mmpte_size = self.obj_vm.profile.get_obj_size("_MMPTEPA")
else:
mmpte_size = self.obj_vm.profile.get_obj_size("_MMPTE")
# Calculate the size of the _CONTROL_AREA object. It is used to find
# the correct offset for the SUBSECTION object and the size of the
# CONTROL_AREA can differ between versions of Windows.
control_area_size = self.size()
# The segment is used to describe the physical view of the
# file. We also use this as a semantic check to see if
# the processing should continue. If the Segment address
# is invalid, then we return.
Segment = self.Segment
if not Segment.is_valid():
return mdata, zpad
# The next semantic check validates that the _SEGMENT object
# points back to the appropriate _CONTROL_AREA object. If the
# check is invalid, then we return.
if (self.obj_offset != Segment.ControlArea):
return mdata, zpad
# This is a semantic check added to make sure the Segment.SizeOfSegment value
# is consistant with the Segment.TotalNumberOfPtes. This occurs fequently
# when traversing through CONTROL_AREA Objects (~5%), often leading to
# impossible values. Thus, to be conservative we do not proceed if the
# Segment does not seem sound.
if Segment.SizeOfSegment != (Segment.TotalNumberOfPtes * PAGE_SIZE):
return mdata, zpad
# The _SUBSECTION object is typically found immediately following
# the CONTROL_AREA object. For Image Section Objects, the SUBSECTIONS
# typically correspond with the sections found in the PE. On the otherhand,
# for Data Section Objects, there is typically only a single valid SUBSECTION.
subsection_offset = self.obj_offset + control_area_size
#subsection = obj.Object("_SUBSECTION", subsection_offset, self.kaddr_space)
subsection = obj.Object("_SUBSECTION", subsection_offset, self.obj_vm)
# This was another check which was inspired by Ruud's code. It
# verifies that the first SubsectionBaase (Mmst) never starts
# at the beginning of a page. The UNSAFE option allows us to
# ignore this constraint. This was necessary for dumping file data
# for file objects found with filescan (ie $Mft)
SubsectionBase = subsection.SubsectionBase
if (SubsectionBase & PAGE_MASK == 0x0) and not unsafe:
return mdata, zpad
# We obtain the Subsections associated with this file
# by traversing the singly linked list. Ideally, this
# list should be null (0) terminated. Upon occasion we
# we have seen instances where the link pointers are
# undefined (XXX). If we hit an invalid pointer, the we
# we exit the traversal.
while subsection.is_valid() and subsection.v() != 0x0:
if not subsection:
break
# This constraint makes sure that the _SUBSECTION object
# points back to the associated CONTROL_AREA object. Otherwise,
# we exit the traversal.
if (self.obj_offset != subsection.ControlArea):
break
# Extract subsection meta-data into local variables
# this helps with performance and not having to do
# repetitive lookups.
PtesInSubsection = subsection.PtesInSubsection
SubsectionBase = subsection.SubsectionBase
NextSubsection = subsection.NextSubsection
# The offset into the file is stored implicitely
# based on the PTE's location within the Subsection.
StartingSector = subsection.StartingSector
SubsectionOffset = StartingSector * 0x200
# This was another check based on something Ruud
# had done. We also so instances where DataSectionObjects
# would hit a SubsectionBase that was paged aligned
# and hit strange data. In those instances, the
# MMPTE SubsectionAddress would not point to the associated
# Subsection. (XXX)
if (SubsectionBase & PAGE_MASK == 0x0) and not unsafe:
break
ptecount = 0
while (ptecount < PtesInSubsection):
pteoffset = SubsectionBase + (mmpte_size * ptecount)
FileOffset = SubsectionOffset + ptecount * 0x1000
# The size of MMPTE changes depending on if it is IA32 (4 bytes)
# or IA32_PAE/AMD64 (8 bytes).
objname = "_MMPTE"
if pae:
objname = "_MMPTEPA"
mmpte = obj.Object(objname, offset = pteoffset, vm = \
subsection.obj_vm)
if not mmpte:
ptecount += 1
continue
# First we check if the entry is valid. If the entry is valid
# then we get the physical offset. The valid entries are actually
# handled by the hardware.
if mmpte.u.Hard.Valid == 0x1:
# There are some valid Page Table entries where bit 63
# is used to specify if the page is executable. This is
# maintained by the processor. If it is not executable,
# then the bit is set. Within the Intel documentation,
# this is known as the Execute-disable (XD) flag. Regardless,
# we will use the get_phys_addr method from the address space
# to obtain the physical address.
### Should we check the size of the PAGE? Haven't seen
# a hit for LargePage.
#if mmpte.u.Hard.LargePage == 0x1:
# print "LargePage"
physoffset = mmpte.u.Hard.PageFrameNumber << 12
mdata.append([physoffset, FileOffset, PAGE_SIZE])
ptecount += 1
continue
elif mmpte.u.Soft.Prototype == 0x1:
# If the entry is not a valid physical address then
# we check if it contains a pointer back to the SUBSECTION
# object. If so, the page is in the backing file and we will
# need to pad to maintain spacial integrity of the file. This
# check needs to be performed for looking for the transition flag.
# The prototype PTEs are initialized as MMPTE_SUBSECTION with the
# SubsectionAddress.
# On x86 systems that use 4 byte MMPTE , the MMPTE_SUBSECTION
# stores an "encoded" version of the SUBSECTION object address.
# The data is relative to global variable (MmSubsectionBase or
# MmNonPagedPoolEnd) depending on the WhichPool member of
# _SUBSECTION. This applies to x86 systems running ntoskrnl.exe.
# If bit 10 is set then it is prototype/subsection
if (memory_model == "32bit") and not pae:
SubsectionOffset = \
((mmpte.u.Subsect.SubsectionAddressHigh << 7) |
(mmpte.u.Subsect.SubsectionAddressLow << 3))
#WhichPool = mmpte.u.Subsect.WhichPool
#print "mmpte 0x%x ptecount 0x%x sub-32 0x%x pteoffset 0x%x which 0x%x subdelta 0x%x"%(mmpte.u.Long,ptecount,subsection_offset,pteoffset,WhichPool,SubsectionOffset)
zpad.append([FileOffset, PAGE_SIZE])
ptecount += 1
continue
if memory_model == "64bit" or pae:
SubsectionAddress = mmpte.u.Subsect.SubsectionAddress
else:
SubsectionAddress = mmpte.u.Long
if SubsectionAddress == subsection.obj_offset:
# sub proto/prot 4c0 420
#print "mmpte 0x%x ptecount 0x%x sub 0x%x offset 0x%x"%(mmpte.u.Long,ptecount,SubsectionAddress,pteoffset)
zpad.append([FileOffset, PAGE_SIZE])
ptecount += 1
continue
elif (SubsectionAddress == (subsection.obj_offset + 4)):
# This was a special case seen on IA32_PAE systems where
# the SubsectionAddress pointed to subsection.obj_offset+4
# (0x420, 0x460, 0x4a0)
#print "mmpte 0x%x ptecount 0x%x sub+4 0x%x offset 0x%x"%(mmpte.u.Long,ptecount,SubsectionAddress,pteoffset)
zpad.append([FileOffset, PAGE_SIZE])
ptecount += 1
continue
else:
#print "mmpte 0x%x ptecount 0x%x sub_unk 0x%x offset 0x%x suboffset 0x%x"%(mmpte.u.Long,ptecount,SubsectionAddress,pteoffset,subsection.obj_offset)
zpad.append([FileOffset, PAGE_SIZE])
ptecount += 1
continue
# Check if the entry is a DemandZero entry.
elif (mmpte.u.Soft.Transition == 0x0):
if ((mmpte.u.Soft.PageFileLow == 0x0) and
(mmpte.u.Soft.PageFileHigh == 0x0)):
# Example entries include: a0,e0
#print "mmpte 0x%x ptecount 0x%x zero offset 0x%x subsec 0x%x"%(mmpte.u.Long,ptecount,pteoffset,subsection.obj_offset)
zpad.append([FileOffset, PAGE_SIZE])
ptecount += 1
else:
#print "mmpte 0x%x ptecount 0x%x paged offset 0x%x subsec 0x%x file 0x%x offset 0x%x"%(mmpte.u.Long,ptecount,pteoffset,subsection.obj_offset,mmpte.u.Soft.PageFileLow,mmpte.u.Soft.PageFileHigh)
zpad.append([FileOffset, PAGE_SIZE])
ptecount += 1
# If the entry is not a valid physical address then
# we also check to see if it is in transition.
elif mmpte.u.Trans.Transition == 0x1:
physoffset = mmpte.u.Trans.PageFrameNumber << 12
#print "mmpte 0x%x ptecount 0x%x transition 0x%x offset 0x%x"%(mmpte.u.Long,ptecount,physoffset,pteoffset)
mdata.append([physoffset, FileOffset, PAGE_SIZE])
ptecount += 1
continue
else:
# This is a catch all for all the other entry types.
# sub proto/pro 420,4e0,460,4a0 (x64 +0x28)(x32 +4)
# other a0,e0,0, (20,60)
# 0x80000000
#print "mmpte 0x%x ptecount 0x%x other offset 0x%x subsec 0x%x"%(mmpte.u.Long,ptecount,pteoffset,subsection.obj_offset)
zpad.append([FileOffset, PAGE_SIZE])
ptecount += 1
# Traverse the singly linked list to its next member.
subsection = NextSubsection
return (mdata, zpad)
class _SHARED_CACHE_MAP(obj.CType):
def is_valid(self):
if not obj.CType.is_valid(self):
return False
# Added a semantic check to make sure the data is in a sound state. It's better
# to catch it early.
FileSize = self.FileSize.QuadPart
ValidDataLength = self.ValidDataLength.QuadPart
SectionSize = self.SectionSize.QuadPart
# Corrupted values: Win2003SP0x86.vmem
if FileSize <= 0 or ValidDataLength <= 0:
return False
#print "SectionSize 0x%x < 0 or FileSize < 0x%x ValidDataLength 0x%x"%(SectionSize,FileSize,ValidDataLength)
#if SectionSize < 0 or (FileSize < ValidDataLength):
if SectionSize < 0 or ((FileSize < ValidDataLength) and (ValidDataLength != 0x7fffffffffffffff)):
return False
return True
def process_index_array(self, array_pointer, level, limit, vacbary = None):
""" Recursively process the sparse multilevel VACB index array
Args:
array_pointer: The address of a possible index array
shared_cache_map: The associated SHARED_CACHE_MAP object
level: The current level
limit: The level where we abandon all hope. Ideally this is 7
vacbary: An array of collected VACBs
Returns:
vacbary: Collected VACBs
"""
if vacbary is None:
vacbary = []
if level > limit:
return []
# Create an array of VACB entries
VacbArray = obj.Object("Array", offset = array_pointer, \
vm = self.obj_vm, count = VACB_ARRAY, \
targetType = "address", parent = self)
# Iterate through the entries
for _i in range(0, VACB_ARRAY):
# Check if the VACB entry is in use
if VacbArray[_i] == 0x0:
continue
Vacbs = obj.Object("_VACB", offset = int(VacbArray[_i]), vm = self.obj_vm)
# Check if this is a valid VACB entry by verifying
# the SharedCacheMap member.
if Vacbs.SharedCacheMap == self.obj_offset:
# This is a VACB associated with this cache map
vacbinfo = self.extract_vacb(Vacbs, VACB_BLOCK)
if vacbinfo:
vacbary.append(vacbinfo)
else:
#Process the next level of the multi-level array
vacbary = self.process_index_array(VacbArray[_i], level + 1, limit, vacbary)
#vacbary = vacbary + _vacbary
return vacbary
def extract_vacb(self, vacbs, size):
""" Extracts data from a specified VACB
Attempts to extract the memory resident data from a specified
VACB.
Args:
vacbs: The VACB object
size: How much data should be read from the VACB
shared_cache_map: The associated SHARED_CACHE_MAP object
Returns:
vacbinfo: Extracted VACB meta-information
"""
# This is used to collect summary information. We will eventually leverage this
# when creating the externally exposed APIs.
vacbinfo = {}
# Check if the Overlay member of _VACB is resident
# The Overlay member stores information about the FileOffset
# and the ActiveCount. This is just another proactive check
# to make sure the objects are seemingly sound.
if not vacbs.Overlay:
return vacbinfo
# We should add another check to make sure that
# the SharedCacheMap member of the VACB points back
# to the corresponding SHARED_CACHE_MAP
if vacbs.SharedCacheMap != self.v():
return vacbinfo
# The FileOffset member of VACB is used to denote the
# offset within the file where the view begins. Since all
# views are 256 KB in size, the bottom 16 bits are used to
# store the number of references to the view.
FileOffset = vacbs.Overlay.FileOffset.QuadPart
if not FileOffset:
return vacbinfo
ActiveCount = vacbs.Overlay.ActiveCount
FileOffset = FileOffset & FILEOFFSET_MASK
BaseAddress = vacbs.BaseAddress.v()
vacbinfo['foffset'] = int(FileOffset)
vacbinfo['acount'] = int(ActiveCount)
vacbinfo['voffset'] = int(vacbs.obj_offset)
vacbinfo['baseaddr'] = int(BaseAddress)
vacbinfo['size'] = int(size)
return vacbinfo
def extract_scm_file(self):
""" Extracts a file from a specified _SHARED_CACHE_MAP
Attempts to extract the memory resident pages pertaining to a
particular _SHARED_CACHE_MAP object.
Args:
shared_cache_map: Instance of a _SHARED_CACHE_MAP object
Returns:
vacbary: List of collected VACB meta information.
Raises:
"""
vacbary = []
if self.obj_offset == 0x0:
return
# Added a semantic check to make sure the data is in a sound state.
#FileSize = shared_cache_map.FileSize.QuadPart
#ValidDataLength = shared_cache_map.ValidDataLength.QuadPart
SectionSize = self.SectionSize.QuadPart
# Let's begin by determining the number of Virtual Address Control
# Blocks (VACB) that are stored within the cache (nonpaged). A VACB
# represents one 256-KB view in the system cache. There a are a couple
# options to use for the data size: ValidDataLength, FileSize,
# and SectionSize.
full_blocks = SectionSize / VACB_BLOCK
left_over = SectionSize % VACB_BLOCK
# As an optimization, the shared cache map object contains a VACB index
# array of four entries. The VACB index arrays are arrays of pointers
# to VACBs, that track which views of a given file are mapped in the cache.
# For example, the first entry in the VACB index array refers to the first
# 256 KB of the file. The InitialVacbs can describe a file up to 1 MB (4xVACB).
iterval = 0
while (iterval < full_blocks) and (full_blocks <= 4):
Vacbs = self.InitialVacbs[iterval]
vacbinfo = self.extract_vacb(Vacbs, VACB_BLOCK)
if vacbinfo: vacbary.append(vacbinfo)
iterval += 1
# We also have to account for the spill over data
# that is not found in the full blocks. The first case to
# consider is when the spill over is still in InitialVacbs.
if (left_over > 0) and (full_blocks < 4):
Vacbs = self.InitialVacbs[iterval]
vacbinfo = self.extract_vacb(Vacbs, left_over)
if vacbinfo: vacbary.append(vacbinfo)
# If the file is larger than 1 MB, a seperate VACB index array
# needs to be allocated. This is based on how many 256 KB blocks
# would be required for the size of the file. This newly allocated
# VACB index array is found through the Vacbs member of
# SHARED_CACHE_MAP.
Vacbs = self.Vacbs
if not Vacbs or (Vacbs.v() == 0):
return vacbary
# There are a number of instances where the initial value in
# InitialVacb will also be the fist entry in Vacbs. Thus we
# ignore, since it was already processed. It is possible to just
# process again as the file offset is specified for each VACB.
if self.InitialVacbs[0].obj_offset == Vacbs.v():
return vacbary
# If the file is less than 32 MB than it can be found in
# a single level VACB index array.
size_of_pointer = self.obj_vm.profile.get_obj_size("address")
if not SectionSize > VACB_SIZE_OF_FIRST_LEVEL:
ArrayHead = Vacbs.v()
_i = 0
for _i in range(0, full_blocks):
vacb_addr = ArrayHead + (_i * size_of_pointer)
vacb_entry = obj.Object("address", offset = vacb_addr, vm = Vacbs.obj_vm)
# If we find a zero entry, then we proceed to the next one.
# If the entry is zero, then the view is not mapped and we
# skip. We do not pad because we use the FileOffset to seek
# to the correct offset in the file.
if not vacb_entry or (vacb_entry.v() == 0x0):
continue
Vacb = obj.Object("_VACB", offset = vacb_entry.v(), vm = self.obj_vm)
vacbinfo = self.extract_vacb(Vacb, VACB_BLOCK)
if vacbinfo:
vacbary.append(vacbinfo)
if left_over > 0:
vacb_addr = ArrayHead + ((_i + 1) * size_of_pointer)
vacb_entry = obj.Object("address", offset = vacb_addr, vm = Vacbs.obj_vm)
if not vacb_entry or (vacb_entry.v() == 0x0):
return vacbary
Vacb = obj.Object("_VACB", offset = vacb_entry.v(), vm = self.obj_vm)
vacbinfo = self.extract_vacb(Vacb, left_over)
if vacbinfo:
vacbary.append(vacbinfo)
# The file is less than 32 MB, so we can
# stop processing.
return vacbary
# If we get to this point, then we know that the SectionSize is greator than
# VACB_SIZE_OF_FIRST_LEVEL (32 MB). Then we have a "sparse multilevel index
# array where each VACB index array is made up of 128 entries. We no
# longer assume the data is sequential. (Log2 (32 MB) - 18)/7
#tree_depth = math.ceil((math.ceil(math.log(file_size, 2)) - 18)/7)
level_depth = math.ceil(math.log(SectionSize, 2))
level_depth = (level_depth - VACB_OFFSET_SHIFT) / VACB_LEVEL_SHIFT
level_depth = math.ceil(level_depth)
limit_depth = level_depth
if SectionSize > VACB_SIZE_OF_FIRST_LEVEL:
# Create an array of 128 entries for the VACB index array
VacbArray = obj.Object("Array", offset = Vacbs.v(), \
vm = self.obj_vm, count = VACB_ARRAY, \
targetType = "address", parent = self)
# We use a bit of a brute force method. We walk the
# array and if any entry points to the shared cache map
# object then we extract it. Otherwise, if it is non-zero
# we attempt to traverse to the next level.
for _i in range(0, VACB_ARRAY):
if VacbArray[_i] == 0x0:
continue
Vacb = obj.Object("_VACB", offset = int(VacbArray[_i]), vm = self.obj_vm)
if Vacb.SharedCacheMap == self.obj_offset:
vacbinfo = self.extract_vacb(Vacb, VACB_BLOCK)
if vacbinfo:
vacbary.append(vacbinfo)
else:
# The Index is a pointer
#Process the next level of the multi-level array
# We set the limit_depth to be the depth of the tree
# as determined from the size and we initialize the
# current level to 2.
vacbary = self.process_index_array(VacbArray[_i], 2, limit_depth, vacbary)
#vacbary = vacbary + _vacbary
return vacbary
class ControlAreaModification(obj.ProfileModification):
conditions = {'os': lambda x: x == 'windows'}
def modification(self, profile):
profile.object_classes.update({
'_CONTROL_AREA': _CONTROL_AREA,
'_SHARED_CACHE_MAP': _SHARED_CACHE_MAP,
})
#--------------------------------------------------------------------------------
# VTypes
#--------------------------------------------------------------------------------
# Windows x86 symbols for ntkrnlpa
ntkrnlpa_types_x86 = {
'__ntkrnlpa' : [ 0x8, {
'Long' : [ 0x0, ['unsigned long long']],
'VolatileLong' : [ 0x0, ['unsigned long long']],
'Hard' : [ 0x0, ['_MMPTE_HARDWARE_64']],
'Flush' : [ 0x0, ['_HARDWARE_PTE']],
'Proto' : [ 0x0, ['_MMPTE_PROTOTYPE']],
'Soft' : [ 0x0, ['_MMPTE_SOFTWARE_64']],
'TimeStamp' : [ 0x0, ['_MMPTE_TIMESTAMP']],
'Trans' : [ 0x0, ['_MMPTE_TRANSITION_64']],
'Subsect' : [ 0x0, ['_MMPTE_SUBSECTION_64']],
'List' : [ 0x0, ['_MMPTE_LIST']],
} ],
'_MMPTEPA' : [ 0x8, {
'u' : [ 0x0, ['__ntkrnlpa']],
} ],
'_MMPTE_SUBSECTION_64' : [ 0x8, {
'Valid' : [ 0x0, ['BitField', dict(start_bit = 0, end_bit = 1, native_type = 'unsigned long long')]],
'Unused0' : [ 0x0, ['BitField', dict(start_bit = 1, end_bit = 5, native_type = 'unsigned long long')]],
'Protection' : [ 0x0, ['BitField', dict(start_bit = 5, end_bit = 10, native_type = 'unsigned long long')]],
'Prototype' : [ 0x0, ['BitField', dict(start_bit = 10, end_bit = 11, native_type = 'unsigned long long')]],
'Unused1' : [ 0x0, ['BitField', dict(start_bit = 11, end_bit = 32, native_type = 'unsigned long long')]],
'SubsectionAddress' : [ 0x0, ['BitField', dict(start_bit = 32, end_bit = 64, native_type = 'long long')]],
} ],
'_MMPTE_TRANSITION_64' : [ 0x8, {
'Valid' : [ 0x0, ['BitField', dict(start_bit = 0, end_bit = 1, native_type = 'unsigned long long')]],
'Write' : [ 0x0, ['BitField', dict(start_bit = 1, end_bit = 2, native_type = 'unsigned long long')]],
'Owner' : [ 0x0, ['BitField', dict(start_bit = 2, end_bit = 3, native_type = 'unsigned long long')]],
'WriteThrough' : [ 0x0, ['BitField', dict(start_bit = 3, end_bit = 4, native_type = 'unsigned long long')]],
'CacheDisable' : [ 0x0, ['BitField', dict(start_bit = 4, end_bit = 5, native_type = 'unsigned long long')]],
'Protection' : [ 0x0, ['BitField', dict(start_bit = 5, end_bit = 10, native_type = 'unsigned long long')]],
'Prototype' : [ 0x0, ['BitField', dict(start_bit = 10, end_bit = 11, native_type = 'unsigned long long')]],
'Transition' : [ 0x0, ['BitField', dict(start_bit = 11, end_bit = 12, native_type = 'unsigned long long')]],
'PageFrameNumber' : [ 0x0, ['BitField', dict(start_bit = 12, end_bit = 48, native_type = 'unsigned long long')]],
'Unused' : [ 0x0, ['BitField', dict(start_bit = 48, end_bit = 64, native_type = 'unsigned long long')]],
}],
'_MMPTE_HARDWARE_64' : [ 0x8, {
'Valid' : [ 0x0, ['BitField', dict(start_bit = 0, end_bit = 1, native_type = 'unsigned long long')]],
'Dirty1' : [ 0x0, ['BitField', dict(start_bit = 1, end_bit = 2, native_type = 'unsigned long long')]],
'Owner' : [ 0x0, ['BitField', dict(start_bit = 2, end_bit = 3, native_type = 'unsigned long long')]],
'WriteThrough' : [ 0x0, ['BitField', dict(start_bit = 3, end_bit = 4, native_type = 'unsigned long long')]],
'CacheDisable' : [ 0x0, ['BitField', dict(start_bit = 4, end_bit = 5, native_type = 'unsigned long long')]],
'Accessed' : [ 0x0, ['BitField', dict(start_bit = 5, end_bit = 6, native_type = 'unsigned long long')]],
'Dirty' : [ 0x0, ['BitField', dict(start_bit = 6, end_bit = 7, native_type = 'unsigned long long')]],
'LargePage' : [ 0x0, ['BitField', dict(start_bit = 7, end_bit = 8, native_type = 'unsigned long long')]],
'Global' : [ 0x0, ['BitField', dict(start_bit = 8, end_bit = 9, native_type = 'unsigned long long')]],
'CopyOnWrite' : [ 0x0, ['BitField', dict(start_bit = 9, end_bit = 10, native_type = 'unsigned long long')]],
'Unused' : [ 0x0, ['BitField', dict(start_bit = 10, end_bit = 11, native_type = 'unsigned long long')]],
'Write' : [ 0x0, ['BitField', dict(start_bit = 11, end_bit = 12, native_type = 'unsigned long long')]],
'PageFrameNumber' : [ 0x0, ['BitField', dict(start_bit = 12, end_bit = 48, native_type = 'unsigned long long')]],
'reserved1' : [ 0x0, ['BitField', dict(start_bit = 48, end_bit = 52, native_type = 'unsigned long long')]],
'SoftwareWsIndex' : [ 0x0, ['BitField', dict(start_bit = 52, end_bit = 63, native_type = 'unsigned long long')]],
'NoExecute' : [ 0x0, ['BitField', dict(start_bit = 63, end_bit = 64, native_type = 'unsigned long long')]],
} ],
'_MMPTE_SOFTWARE_64' : [ 0x8, {
'Valid' : [ 0x0, ['BitField', dict(start_bit = 0, end_bit = 1, native_type = 'unsigned long long')]],
'PageFileLow' : [ 0x0, ['BitField', dict(start_bit = 1, end_bit = 5, native_type = 'unsigned long long')]],
'Protection' : [ 0x0, ['BitField', dict(start_bit = 5, end_bit = 10, native_type = 'unsigned long long')]],
'Prototype' : [ 0x0, ['BitField', dict(start_bit = 10, end_bit = 11, native_type = 'unsigned long long')]],
'Transition' : [ 0x0, ['BitField', dict(start_bit = 11, end_bit = 12, native_type = 'unsigned long long')]],
'UsedPageTableEntries' : [ 0x0, ['BitField', dict(start_bit = 12, end_bit = 22, native_type = 'unsigned long long')]],
'InStore' : [ 0x0, ['BitField', dict(start_bit = 22, end_bit = 23, native_type = 'unsigned long long')]],
'Reserved' : [ 0x0, ['BitField', dict(start_bit = 23, end_bit = 32, native_type = 'unsigned long long')]],
'PageFileHigh' : [ 0x0, ['BitField', dict(start_bit = 32, end_bit = 64, native_type = 'unsigned long long')]],
} ],
}
class DumpFilesVTypesx86(obj.ProfileModification):
"""This modification applies the vtypes for all
versions of 32bit Windows."""
before = ['WindowsObjectClasses']
conditions = {'os': lambda x: x == 'windows',
'memory_model': lambda x : x == '32bit'}
def modification(self, profile):
profile.vtypes.update(ntkrnlpa_types_x86)
class DumpFiles(common.AbstractWindowsCommand):
"""Extract memory mapped and cached files"""
def __init__(self, config, *args, **kwargs):
common.AbstractWindowsCommand.__init__(self, config, *args, **kwargs)
self.kaddr_space = None
self.filters = []
config.add_option('REGEX', short_option = 'r',
help = 'Dump files matching REGEX',
action = 'store', type = 'string')
config.add_option('IGNORE-CASE', short_option = 'i',
help = 'Ignore case in pattern match',
action = 'store_true', default = False)
config.add_option('OFFSET', short_option = 'o', default = None,
help = 'Dump files for Process with physical address OFFSET',
action = 'store', type = 'int')
config.add_option('PHYSOFFSET', short_option = 'Q', default = None,
help = 'Dump File Object at physical address PHYSOFFSETs (comma delimited)',
action = 'store', type = 'str')
config.add_option('DUMP-DIR', short_option = 'D', default = None,
cache_invalidator = False,
help = 'Directory in which to dump extracted files')
config.add_option('SUMMARY-FILE', short_option = 'S', default = None,
cache_invalidator = False,
help = 'File where to store summary information')
config.add_option('PID', short_option = 'p', default = None,
help = 'Operate on these Process IDs (comma-separated)',
action = 'store', type = 'str')
config.add_option('NAME', short_option = 'n',
help = 'Include extracted filename in output file path',
action = 'store_true', default = False)
config.add_option('UNSAFE', short_option = 'u',
help = 'Relax safety constraints for more data',
action = 'store_true', default = False)
# Possible filters include:
# SharedCacheMap,DataSectionObject,ImageSectionObject,HandleTable,VAD
config.add_option("FILTER", short_option = 'F', default = None,
help = 'Filters to apply (comma-separated). Possible values:\n\nSharedCacheMap,DataSectionObject,ImageSectionObject,HandleTable,VAD')
def filter_tasks(self, tasks):
""" Reduce the tasks based on the user selectable PIDS parameter.
Returns a reduced list or the full list if config.PIDS not specified.
"""
if self._config.PID is None:
return tasks
try:
pidlist = [int(p) for p in self._config.PID.split(',')]
except ValueError:
debug.error("Invalid PID {0}".format(self._config.PID))
return [t for t in tasks if t.UniqueProcessId in pidlist]
def audited_read_bytes(self, vm, vaddr, length, pad):
""" This function provides an audited zread capability
It performs a similar function to zread, in that it will
pad "invalid" pages. The main difference is that it allows
us to collect auditing information about which pages were actually
present and which ones were padded.
Args:
vm: The address space to read the data from.
vaddr: The virtual address to start reading the data from.
length: How many bytes to read
pad: This argument controls if the unavailable bytes are padded.
Returns:
ret: Data that was read
mdata: List of pages that are memory resident
zpad: List of pages that not memory resident
Raises:
"""
zpad = []
mdata = []
vaddr, length = int(vaddr), int(length)
ret = ''
while length > 0:
chunk_len = min(length, PAGE_SIZE - (vaddr % PAGE_SIZE))
buf = vm.read(vaddr, chunk_len)
if vm.vtop(vaddr) is None:
zpad.append([vaddr, chunk_len])
if pad:
buf = '\x00' * chunk_len
else:
buf = ''
else:
mdata.append([vaddr, chunk_len])
ret += buf
vaddr += chunk_len
length -= chunk_len
return ret, mdata, zpad
def calculate(self):
""" Finds all the requested FILE_OBJECTS
Traverses the VAD and HandleTable to find all requested
FILE_OBJECTS
"""
# Initialize containers for collecting artifacts.
control_area_list = []
shared_maps = []
procfiles = []
# These lists are used for object collecting files from
# both the VAD and handle tables
vadfiles = []
handlefiles = []
# Determine which filters the user wants to see
self.filters = []
if self._config.FILTER:
self.filters = self._config.FILTER.split(',')
# Instantiate the kernel address space
self.kaddr_space = utils.load_as(self._config)
# Check to see if the physical address offset was passed for a
# particular process. Otherwise, use the whole task list.
if self._config.OFFSET != None:
tasks_list = [taskmods.DllList.virtual_process_from_physical_offset(
self.kaddr_space, self._config.OFFSET)]
else:
# Filter for the specified processes
tasks_list = self.filter_tasks(tasks_mod.pslist(self.kaddr_space))
# If a regex is specified, build it.
if self._config.REGEX:
try:
if self._config.IGNORE_CASE:
file_re = re.compile(self._config.REGEX, re.I)
else:
file_re = re.compile(self._config.REGEX)
except re.error, e:
debug.error('Error parsing regular expression: {0:s}'.format(e))
# Check to see if a specific physical address was specified for a
# FILE_OBJECT. In particular, this is useful for FILE_OBJECTS that
# are found with filescan that are not associated with a process
# For example, $Mft.
if self._config.PHYSOFFSET:
try:
phys = []
for p in self._config.PHYSOFFSET.split(","):
file_obj = obj.Object("_FILE_OBJECT", int(p, 16), self.kaddr_space.base, native_vm = self.kaddr_space)
phys.append(file_obj)
procfiles.append((None, phys))
except ValueError:
debug.error("Invalid PHYSOFFSET {0}".format(self._config.PHYSOFFSET))
# Iterate through the process list and collect all references to
# FILE_OBJECTS from both the VAD and HandleTable. Each open handle to a file
# has a corresponding FILE_OBJECT.
if not self._config.PHYSOFFSET:
for task in tasks_list:
pid = task.UniqueProcessId
# Extract FILE_OBJECTS from the VAD
if not self.filters or "VAD" in self.filters:
for vad in task.VadRoot.traverse():
if vad != None:
try:
control_area = vad.ControlArea
if not control_area:
continue
file_object = vad.FileObject
if file_object:
# Filter for specific FILE_OBJECTS based on user defined
# regular expression. (Performance optimization)
if self._config.REGEX:
name = None
if file_object.FileName:
name = str(file_object.file_name_with_device())
if not name:
continue
if not file_re.search(name):
continue
vadfiles.append(file_object)
except AttributeError:
pass
if not self.filters or "HandleTable" in self.filters:
# Extract the FILE_OBJECTS from the handle table
if task.ObjectTable.HandleTableList:
for handle in task.ObjectTable.handles():
otype = handle.get_object_type()
if otype == "File":
file_obj = handle.dereference_as("_FILE_OBJECT")
if file_obj:
# Filter for specific FILE_OBJECTS based on user defined
# regular expression. (Performance Optimization)
if self._config.REGEX:
name = None
if file_obj.FileName:
name = str(file_obj.file_name_with_device())
if not name:
continue
if not file_re.search(name):
continue
handlefiles.append(file_obj)
# Append the lists of file objects
#allfiles = handlefiles + vadfiles
procfiles.append((pid, handlefiles + vadfiles))
for pid, allfiles in procfiles:
for file_obj in allfiles:
# XXX TODO: remove these comments when accepted
#if not self._config.PHYSOFFSET:
offset = file_obj.obj_offset
#else:
# I'm not sure why we need to specify PHYSOFFSET here,
# shouldn't we have a valid _FILE_OBJECT?
# offset = self._config.PHYSOFFSET
name = None
if file_obj.FileName:
name = str(file_obj.file_name_with_device())
# The SECTION_OBJECT_POINTERS structure is used by the memory
# manager and cache manager to store file-mapping and cache information
# for a particular file stream. We will use it to determine what type
# of FILE_OBJECT we have and how it should be parsed.
if file_obj.SectionObjectPointer:
DataSectionObject = \
file_obj.SectionObjectPointer.DataSectionObject
SharedCacheMap = \
file_obj.SectionObjectPointer.SharedCacheMap
ImageSectionObject = \
file_obj.SectionObjectPointer.ImageSectionObject
# The ImageSectionObject is used to track state information for
# an executable file stream. We will use it to extract memory
# mapped binaries.
if not self.filters or "ImageSectionObject" in self.filters:
if ImageSectionObject and ImageSectionObject != 0:
summaryinfo = {}
# It points to a image section object( CONTROL_AREA )
control_area = \
ImageSectionObject.dereference_as('_CONTROL_AREA')
if not control_area in control_area_list:
control_area_list.append(control_area)
# The format of the filenames: file.<pid>.<control_area>.[img|dat]
ca_offset_string = "0x{0:x}".format(control_area.obj_offset)
if self._config.NAME and name != None:
fname = name.split("\\")
ca_offset_string += "." + fname[-1]
file_string = ".".join(["file", str(pid), ca_offset_string, IMAGE_EXT])
of_path = os.path.join(self._config.DUMP_DIR, file_string)
(mdata, zpad) = control_area.extract_ca_file(self._config.UNSAFE)
summaryinfo['name'] = name
summaryinfo['type'] = "ImageSectionObject"
if pid:
summaryinfo['pid'] = int(pid)
else:
summaryinfo['pid'] = None
summaryinfo['present'] = mdata
summaryinfo['pad'] = zpad
summaryinfo['fobj'] = int(offset)
summaryinfo['ofpath'] = of_path
yield summaryinfo
# The DataSectionObject is used to track state information for
# a data file stream. We will use it to extract artifacts of
# memory mapped data files.
if not self.filters or "DataSectionObject" in self.filters:
if DataSectionObject and DataSectionObject != 0:
summaryinfo = {}
# It points to a data section object (CONTROL_AREA)
control_area = DataSectionObject.dereference_as('_CONTROL_AREA')
if not control_area in control_area_list:
control_area_list.append(control_area)
# The format of the filenames: file.<pid>.<control_area>.[img|dat]
ca_offset_string = "0x{0:x}".format(control_area.obj_offset)
if self._config.NAME and name != None:
fname = name.split("\\")
ca_offset_string += "." + fname[-1]
file_string = ".".join(["file", str(pid), ca_offset_string, DATA_EXT])
of_path = os.path.join(self._config.DUMP_DIR, file_string)
(mdata, zpad) = control_area.extract_ca_file(self._config.UNSAFE)
summaryinfo['name'] = name
summaryinfo['type'] = "DataSectionObject"
if pid:
summaryinfo['pid'] = int(pid)
else:
summaryinfo['pid'] = None
summaryinfo['present'] = mdata
summaryinfo['pad'] = zpad
summaryinfo['fobj'] = int(offset)
summaryinfo['ofpath'] = of_path
yield summaryinfo
# The SharedCacheMap is used to track views that are mapped to the
# data file stream. Each cached file has a single SHARED_CACHE_MAP object,
# which has pointers to slots in the system cache which contain views of the file.
# The shared cache map is used to describe the state of the cached file.
if self.filters and "SharedCacheMap" not in self.filters:
continue
if SharedCacheMap:
vacbary = []
summaryinfo = {}
#The SharedCacheMap member points to a SHARED_CACHE_MAP object.
shared_cache_map = SharedCacheMap.dereference_as('_SHARED_CACHE_MAP')
if shared_cache_map.obj_offset == 0x0:
continue
# Added a semantic check to make sure the data is in a sound state. It's better
# to catch it early.
if not shared_cache_map.is_valid():
continue
if not shared_cache_map.obj_offset in shared_maps:
shared_maps.append(shared_cache_map.obj_offset)
else:
continue
shared_cache_map_string = ".0x{0:x}".format(shared_cache_map.obj_offset)
if self._config.NAME and name != None:
fname = name.split("\\")
shared_cache_map_string = shared_cache_map_string + "." + fname[-1]
of_path = os.path.join(self._config.DUMP_DIR, "file." + str(pid) + shared_cache_map_string + ".vacb")
vacbary = shared_cache_map.extract_scm_file()
summaryinfo['name'] = name
summaryinfo['type'] = "SharedCacheMap"
if pid:
summaryinfo['pid'] = int(pid)
else:
summaryinfo['pid'] = None
summaryinfo['fobj'] = int(offset)
summaryinfo['ofpath'] = of_path
summaryinfo['vacbary'] = vacbary
yield summaryinfo
def unified_output(self, data):
return TreeGrid([("Source", str),
("Address", Address),
("PID", int),
("Name", str),
("OutputPath", str),
("Data", Bytes)],
self.generator(data))
def generator(self, data):
summaryfo = None
summaryinfo = data
if self._config.DUMP_DIR == None:
debug.error("Please specify a dump directory (--dump-dir)")
if not os.path.isdir(self._config.DUMP_DIR):
debug.error(self._config.DUMP_DIR + " is not a directory")
if self._config.SUMMARY_FILE:
summaryfo = open(self._config.SUMMARY_FILE, 'wb')
for summaryinfo in data:
if summaryinfo['type'] == "DataSectionObject":
if len(summaryinfo['present']) == 0:
continue
of = BytesIO()
for mdata in summaryinfo['present']:
rdata = None
if not mdata[0]:
continue
try:
rdata = self.kaddr_space.base.read(mdata[0], mdata[2])
except (IOError, OverflowError):
debug.debug("IOError: Pid: {0} File: {1} PhysAddr: {2} Size: {3}".format(summaryinfo['pid'], summaryinfo['name'], mdata[0], mdata[2]))
if not rdata:
continue
of.seek(mdata[1])
of.write(rdata)
continue
# XXX Verify FileOffsets
#for zpad in summaryinfo['pad']:
# of.seek(zpad[0])
# of.write("\0" * zpad[1])
if self._config.SUMMARY_FILE:
json.dump(summaryinfo, summaryfo)
summaryfo.write("\n")
yield(0, ["DataSectionObject",
Address(summaryinfo['fobj']),
int(summaryinfo['pid']) if summaryinfo['pid'] else -1,
str(summaryinfo['name']),
str(summaryinfo['ofpath']),
Bytes(of.getvalue())])
of.close()
elif summaryinfo['type'] == "ImageSectionObject":
if len(summaryinfo['present']) == 0:
continue
of = BytesIO()
for mdata in summaryinfo['present']:
rdata = None
if not mdata[0]:
continue
try:
rdata = self.kaddr_space.base.read(mdata[0], mdata[2])
except (IOError, OverflowError):
debug.debug("IOError: Pid: {0} File: {1} PhysAddr: {2} Size: {3}".format(summaryinfo['pid'], summaryinfo['name'], mdata[0], mdata[2]))
if not rdata:
continue
of.seek(mdata[1])
of.write(rdata)
continue
# XXX Verify FileOffsets
#for zpad in summaryinfo['pad']:
# print "ZPAD 0x%x"%(zpad[0])
# of.seek(zpad[0])
# of.write("\0" * zpad[1])
if self._config.SUMMARY_FILE:
json.dump(summaryinfo, summaryfo)
summaryfo.write("\n")
yield(0, ["ImageSectionObject",
Address(summaryinfo['fobj']),
int(summaryinfo['pid']),
str(summaryinfo['name']),
str(summaryinfo['ofpath']),
Bytes(of.getvalue())])
of.close()
elif summaryinfo['type'] == "SharedCacheMap":
of = BytesIO()
for vacb in summaryinfo['vacbary']:
if not vacb:
continue
(rdata, mdata, zpad) = self.audited_read_bytes(self.kaddr_space, vacb['baseaddr'], vacb['size'], True)
### We need to update the mdata,zpad
if rdata:
try:
of.seek(vacb['foffset'])
of.write(rdata)
except IOError:
# TODO: Handle things like write errors (not enough disk space, etc)
continue
vacb['present'] = mdata
vacb['pad'] = zpad
if self._config.SUMMARY_FILE:
json.dump(summaryinfo, summaryfo)
summaryfo.write("\n")
yield(0, ["SharedCacheMap",
Address(summaryinfo['fobj']),
int(summaryinfo['pid']) if summaryinfo['pid'] else -1,
str(summaryinfo['name']),
str(summaryinfo['ofpath']),
Bytes(of.getvalue())])
of.close()
else:
return
if self._config.SUMMARY_FILE:
summaryfo.close()
def render_text(self, outfd, data):
"""Renders output for the dumpfiles plugin.
This includes extracting the file artifacts from memory
to the specified dump directory.
Args:
outfd: The file descriptor to write the text to.
data: (summaryinfo)
"""
# Summary file object
summaryfo = None
summaryinfo = data
if self._config.DUMP_DIR == None:
debug.error("Please specify a dump directory (--dump-dir)")
if not os.path.isdir(self._config.DUMP_DIR):
debug.error(self._config.DUMP_DIR + " is not a directory")
if self._config.SUMMARY_FILE:
summaryfo = open(self._config.SUMMARY_FILE, 'wb')
for summaryinfo in data:
if summaryinfo['type'] == "DataSectionObject":
outfd.write("DataSectionObject {0:#010x} {1:<6} {2}\n".format(summaryinfo['fobj'], summaryinfo['pid'], summaryinfo['name']))
if len(summaryinfo['present']) == 0:
continue
of = open(summaryinfo['ofpath'], 'wb')
for mdata in summaryinfo['present']:
rdata = None
if not mdata[0]:
continue
try:
rdata = self.kaddr_space.base.read(mdata[0], mdata[2])
except (IOError, OverflowError):
debug.debug("IOError: Pid: {0} File: {1} PhysAddr: {2} Size: {3}".format(summaryinfo['pid'], summaryinfo['name'], mdata[0], mdata[2]))
if not rdata:
continue
of.seek(mdata[1])
of.write(rdata)
continue
# XXX Verify FileOffsets
#for zpad in summaryinfo['pad']:
# of.seek(zpad[0])
# of.write("\0" * zpad[1])
if self._config.SUMMARY_FILE:
json.dump(summaryinfo, summaryfo)
summaryfo.write("\n")
of.close()
elif summaryinfo['type'] == "ImageSectionObject":
outfd.write("ImageSectionObject {0:#010x} {1:<6} {2}\n".format(summaryinfo['fobj'], summaryinfo['pid'], summaryinfo['name']))
if len(summaryinfo['present']) == 0:
continue
of = open(summaryinfo['ofpath'], 'wb')
for mdata in summaryinfo['present']:
rdata = None
if not mdata[0]:
continue
try:
rdata = self.kaddr_space.base.read(mdata[0], mdata[2])
except (IOError, OverflowError):
debug.debug("IOError: Pid: {0} File: {1} PhysAddr: {2} Size: {3}".format(summaryinfo['pid'], summaryinfo['name'], mdata[0], mdata[2]))
if not rdata:
continue
of.seek(mdata[1])
of.write(rdata)
continue
# XXX Verify FileOffsets
#for zpad in summaryinfo['pad']:
# print "ZPAD 0x%x"%(zpad[0])
# of.seek(zpad[0])
# of.write("\0" * zpad[1])
if self._config.SUMMARY_FILE:
json.dump(summaryinfo, summaryfo)
summaryfo.write("\n")
of.close()
elif summaryinfo['type'] == "SharedCacheMap":
outfd.write("SharedCacheMap {0:#010x} {1:<6} {2}\n".format(summaryinfo['fobj'], summaryinfo['pid'], summaryinfo['name']))
of = open(summaryinfo['ofpath'], 'wb')
for vacb in summaryinfo['vacbary']:
if not vacb:
continue
(rdata, mdata, zpad) = self.audited_read_bytes(self.kaddr_space, vacb['baseaddr'], vacb['size'], True)
### We need to update the mdata,zpad
if rdata:
try:
of.seek(vacb['foffset'])
of.write(rdata)
except IOError:
# TODO: Handle things like write errors (not enough disk space, etc)
continue
vacb['present'] = mdata
vacb['pad'] = zpad
if self._config.SUMMARY_FILE:
json.dump(summaryinfo, summaryfo)
summaryfo.write("\n")
of.close()
else:
return
if self._config.SUMMARY_FILE:
summaryfo.close()