# 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 . # #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 import json #-------------------------------------------------------------------------------- # 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...[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...[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 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()