├── .gitignore ├── README.txt ├── exportfile.py ├── exportstack.py ├── symbols.py └── volshell ├── linux ├── __init__.py └── volshell.py ├── mac ├── __init__.py └── volshell.py ├── volshell.py └── windows ├── __init__.py └── volshell.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010, 2013 Carl Pulley 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or (at 6 | # your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, but 9 | # WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 16 | 17 | VOLATILITY PLUGINS 18 | ================== 19 | 20 | Honeynet Plugins 21 | ================ 22 | 23 | The code for the following plugins were originally written for Challenge 3 of 24 | the Honeynet Forensic Challenge 2010 (Banking Troubles - see https://github.com/carlpulley/volatility/tree/v1.3 25 | and http://honeynet.org/challenges/2010_3_banking_troubles for more 26 | information). 27 | 28 | exportfile.py [DEPRECIATED: replaced by dumpfiles in Volatility 2.3] - this 29 | plugin implements the exporting and saving of _FILE_OBJECT's. 30 | In addition, file reconstruction functionality is offered by 31 | the plugin. 32 | exportstack.py - this plugin displays information regarding an _EPROCESS'es 33 | thread data structures. 34 | 35 | To install these plugins simply include them (e.g. with the --plugins command 36 | line option), when running Volatility. 37 | 38 | For documentation on using these plugins, please use the Volatility --help 39 | option to the plugin command. 40 | 41 | CURRENT LIMITATIONS: exportfile.py should work with Volatility 2.0 whilst 42 | exportstack.py should work with Volatility 2.3. 43 | 44 | 45 | Other Plugins 46 | ============= 47 | 48 | symbols.py - this plugin is designed to resolve: 49 | * Windows addresses to the nearest function/method name within a symbol 50 | table 51 | * symbol names to addresses. 52 | Including this plugin will ensure that _EPROCESS object classes are 53 | injected with a symbol_table and lookup method. 54 | 55 | When symbols_table is called with build_symbols True, SQLite DB symbol 56 | tables are built (these include Microsoft's debugging symbol information). 57 | The symbol PDB files are downloaded and, after processing, their contents 58 | are inserted into the underlying SQLite DB (which is located within 59 | Volatility's caching directories). Brendan Dolan-Gavitt's pdbparse is used 60 | here. 61 | 62 | When lookup is called with use_symbols True, then Microsoft's debugging 63 | symbol information is used during resolution. Otherwise, module exports 64 | information is used for resolution. 65 | 66 | Example usage (from a volshell command prompt): 67 | 68 | # Case 1: SQLite Symbols DB not built: 69 | volshell> self.proc.symbol_table(build_symbols=True) 70 | 71 | # Case 2: SQLite Symbols DB already built: 72 | volshell> self.proc.symbol_table() 73 | 74 | Example queries: 75 | 76 | # lookup nearest symbol to an address 77 | volshell> self.proc.lookup(0xb25fc838) 78 | [ 'sysaudio.sys/PAGE!CClockInstance::ClockGetCorrelatedPhysicalTime' ] 79 | 80 | # lookup nearest module export symbol to an address 81 | volshell> self.proc.lookup(0x71ab3076, use_symbols=False) 82 | [ 'ws2_32.dll/????!WSALookupServiceNextW+0x1dd' ] 83 | 84 | # get stack cookie address for ntoskrnl.exe 85 | volshell> self.proc.lookup("ntoskrnl.exe/.data!___security_cookie") 86 | [ 2153029696L ] 87 | 88 | # "all" (known) stack cookie addresses within self.proc's address space 89 | volshell> self.proc.lookup(".data!___security_cookie") 90 | [ 2153029696L, 2154673632L, 4166547756L, ... ] 91 | 92 | # wininet.dll stack cookie and cookie complement addresses ('%' matches 93 | # anything) 94 | volshell> self.proc.lookup("wininet%/.data!%security_cookie%") 95 | [ 1998821912, 1998822580 ] 96 | 97 | # view all wshtcpip.dll global variables 98 | volshell> [ self.proc.lookup(a) for a in self.proc.lookup("wshtcpip.dll/.data!%") ] 99 | [ 'wshtcpip.dll/.data!__security_cookie', 100 | 'wshtcpip.dll/.data!__security_cookie_complement', 101 | 'wshtcpip.dll/.data!TcpMappingTriples', 102 | 'wshtcpip.dll/.data!UdpMappingTriples', 103 | 'wshtcpip.dll/.data!RawMappingTriples', 104 | 'wshtcpip.dll/.data!Winsock2Protocols', 105 | 'wshtcpip.dll/.data!TcpipProviderGuid', 106 | 'wshtcpip.dll/.data!_NLG_Destination' 107 | ] 108 | 109 | NOTE: due to a bug in pdbparse's src/undname.c code, it is currently 110 | necessary to hand patch this file prior building pdbparse. For more 111 | details, see: 112 | https://code.google.com/p/pdbparse/issues/detail?id=13 113 | 114 | volshell.py - this plugin is a reworking of the existing Volatility volshell 115 | plugin. Major changes are as follows: 116 | + hh has been deleted. All help information is now available as Python 117 | documentation strings. For example, help(self) and dir(self) give 118 | general command help, whilst help() provides help on a 119 | specific command. 120 | TODO: when using the IPython command line prompt, __builtin__.help 121 | currently overwrites the defined help alias (to self.help), so it is 122 | necessary to manually correct this by entering 'help = self.help' 123 | after the IPython shell starts. Failing to do this means that 124 | individual plugin help will be limited. 125 | + the type of volshell instance launched (i.e. WinVolshell, 126 | LinuxVolshell, MacVolshell, etc.) is chosen using the profile metadata 127 | (specifically the os attribute). When the OS is unknown, a base 128 | Volshell is launched - so just load the image and go! 129 | + all Volatility plugins are potentially available as commands. These 130 | are filtered using the image's profile. Any plugin without a 131 | render_text is additionally filtered out. Plugin commands can produce 132 | three types of output: 133 | * with render=True, the plugin prints to stdout 134 | * with render=False and table_data=True, the plugin hooks the 135 | table_header and table_row methods and returns a list of hashes 136 | representing the displayed tabular data 137 | * with render=False and table_data=False, the plugin returns the 138 | plugin's calculate result. 139 | Plugin arguments are scraped by hooking the singleton class conf. 140 | ConfObject and grabbing command line options. These are used (after 141 | filtering out generic options from commands.Command) to generate valid 142 | keyword arguments with defaults (if specified). Plugin commands are 143 | dynamically added to the Volshell class and are accessed via 144 | self.. For convenience, aliases are generated using 145 | ' = self.'. 146 | + it is now possible to override exiting commands in BaseVolshell (e.g. 147 | see ps in WinVolshell, LinuxVolshell and MacVolshell) and to add in 148 | commands that are OS specific (e.g. see WinVolshell for list_entry). 149 | + a source command has been added to ease loading Volshell scripts into 150 | the current session. Any function in the loaded file matching the 151 | pattern: 152 | 153 | def func(self, ..): 154 | .. 155 | 156 | is blindly bound to the current Volshell instance and made available 157 | as self.func(..) or func(self, ..). If this code was located in 158 | /path/to/func.py then it can be sourced using the Volshell command ( 159 | for convenience, sys.path is also searched): 160 | 161 | source("/path/to/func.py") 162 | 163 | TODO: implement code to assign 'func = self.func' in the Volshell 164 | session. 165 | + [EXPERIMENTAL] it is possible to use the Volshell plugin in a 166 | Volatility as a library like manner [1]. The following simple code 167 | demonstrates the idea by printing out a (sorted) process tree: 168 | 169 | from volatility.plugins.volshell import Volshell 170 | from itertools import groupby 171 | 172 | def analyse(mem_image): 173 | shell = Volshell(filename=mem_image) 174 | data = groupby(sorted(shell.pslist(), key=lambda x: x['PPID']), lambda x: x['PPID']) 175 | for ppid, pids in data: 176 | print "PPID: {0}".format(ppid) 177 | for pid in pids: 178 | print " PID: {0}".format(pid['PID']) 179 | 180 | In library mode, the Volshell plugin related methods (i.e. the help, 181 | calculate and render_* methods) are disabled. 182 | TODO: generate examples demonstrating the potential uses for Volshell 183 | script and library code. 184 | + [EXPERIMENTAL] based on [2] and [3], there appears to be a longer term 185 | preference for IPython being the default command line experience (+1 186 | from myself!). So, when we failover to a basic Python Volshell, an 187 | IPython "nag" banner is displayed on startup. 188 | 189 | INSTALLATION: run the following commands to install (WARNING: the existing 190 | Volshell code is deleted): 191 | 192 | rm $VOLATILITY_SRC/volatility/plugins/linux/linux_volshell.py 193 | rm $VOLATILITY_SRC/volatility/plugins/mac/mac_volshell.py 194 | cp -f volshell/volshell.py $VOLATILITY_SRC/volatility/plugins/ 195 | cp -fr volshell/linux $VOLATILITY_SRC/volatility/plugins/ 196 | cp -fr volshell/mac $VOLATILITY_SRC/volatility/plugins/ 197 | cp -fr volshell/windows $VOLATILITY_SRC/volatility/plugins/ 198 | 199 | REFERENCES: 200 | =========== 201 | 202 | [1] Using Volatility as a Library (accessed 24/Mar/2013): 203 | https://code.google.com/p/volatility/wiki/VolatilityUsage23#Using_Volatility_as_a_Library 204 | [2] Volatility Roadmap: Volatility 3.0 (Official Tech Preview Merge) (accessed 24/Mar/2013): 205 | https://code.google.com/p/volatility/wiki/VolatilityRoadmap#Volatility_3.0_(Official_Tech_Preview_Merge) 206 | [3] Volatility Technology Preview Documentation: Tutorial (accessed 24/Mar/2013): 207 | https://volatility.googlecode.com/svn/branches/scudette/docs/tutorial.html 208 | -------------------------------------------------------------------------------- /exportfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # exportfile.py 4 | # Copyright (C) 2010 Carl Pulley 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or (at 9 | # your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | # 20 | 21 | """ 22 | This plugin implements the exporting and saving of _FILE_OBJECT's 23 | 24 | @author: Carl Pulley 25 | @license: GNU General Public License 2.0 or later 26 | @contact: c.j.pulley@hud.ac.uk 27 | @organization: University of Huddersfield 28 | """ 29 | 30 | import re 31 | import commands as exe 32 | import os 33 | import os.path 34 | import math 35 | import struct 36 | import hashlib 37 | import volatility.utils as utils 38 | import volatility.obj as obj 39 | import volatility.win32.tasks as tasks 40 | import volatility.debug as debug 41 | import volatility.commands as commands 42 | import volatility.plugins.filescan as filescan 43 | 44 | class ExportException(Exception): 45 | """General exception for handling warnings and errors during exporting of files""" 46 | pass 47 | 48 | class ExportFile(filescan.FileScan): 49 | """ 50 | Given a PID, _EPROCESS or _FILE_OBJECT, extract the associated (or given) 51 | _FILE_OBJECT's from memory. 52 | 53 | Exported files are written to a user-defined dump directory (--dir). 54 | Contiguous retrievable pages are written to a file named using the 55 | retrieved virtual addresses: 56 | 57 | pages from _SHARED_CACHE_MAP are saved in files named cache.0xXX-0xXX.dmp.MD5 58 | 59 | pages from _CONTROL_AREA are saved in files named direct.0xXX-0xXX.dmp.MD5 60 | 61 | where MD5 stands for the hash of the files contents. 62 | 63 | Pages that can not be retrieved from memory are saved as pages filled in 64 | with a given fill byte (--fill). 65 | 66 | In addition, a "this" file is created (a sector "copy" of the file on disk) 67 | - this is an aggregated reconstruction based on the retrievable pages above 68 | and, with non-retrievable pages substitued by fill-byte pages (--fill). Some 69 | slack space editing may still be necessary to retrieve the original (user 70 | view) file contents. All exported files are placed into a common directory 71 | (base of which is determined by --dir) whose name and path agree with that 72 | located in _FILE_OBJECT (modulo a unix/linux path naming convention). 73 | 74 | This plugin is particularly useful when one expects memory to be fragmented, 75 | and so, linear carving techniques (e.g. using scalpel or foremost) might be 76 | expected to fail. See reference [5] (below) for more information regarding 77 | some of the forensic benefits of accessing files via the _FILE_OBJECT data 78 | structure. 79 | 80 | EXAMPLE: 81 | 82 | _FILE_OBJECT at 0x81CE4868 is a file named: 83 | \\\\Program Files\\\\Mozilla Firefox\\\\chrome\\\\en-US.jar 84 | with file size 20992 B and has recoverable pages in memory covering file 85 | offsets: 86 | 0x43000 -> 0x45FFF 87 | 0x47000 -> 0x47FFF 88 | 0x51000 -> 0x52FFF 89 | Then: 90 | volatility exportfile -f SAMPLE --fobj 0x82106940 --dir EXAMPLE1 --fill 0xff 91 | would produce: 92 | EXAMPLE1/ 93 | Program Files/ 94 | Mozilla Firefox/ 95 | chrome/ 96 | en-US.jar/ 97 | this 98 | cache.0x43000-0x45FFF.dmp.MD5 99 | cache.0x47000-0x47FFF.dmp.MD5 100 | cache.0x51000-0x52FFF.dmp.MD5 101 | Where this = fillpages(0xFF) 102 | + cache.0x43000-0x45FFF.dmp.MD5 103 | + fillPages(0xFF) 104 | + cache.0x47000-0x47FFF.dmp.MD5 105 | + fillPages(0xFF) 106 | + cache.0x51000-0x52FFF.dmp.MD5 107 | and fillPages(0xFF) is a collection of pages filled with the byte 0xFF. 108 | 109 | REFERENCES: 110 | [1] Russinovich, M., Solomon, D.A. & Ionescu, A., 2009. Windows Internals: 111 | Including Windows Server 2008 and Windows Vista, Fifth Edition (Pro 112 | Developer) 5th ed. Microsoft Press. 113 | [2] Information on mapping _FILE_OBJECT's to _EPROCESS (accessed 6/Oct/2011): 114 | http://computer.forensikblog.de/en/2009/04/linking_file_objects_to_processes.html 115 | [3] OSR Online article: Finding File Contents in Memory (accessed 7/Oct/2011): 116 | http://www.osronline.com/article.cfm?article=280 117 | [4] Extracting Event Logs or Other Memory Mapped Files from Memory Dumps by 118 | J. McCash (accessed 7/Oct/2011): 119 | http://computer-forensics.sans.org/blog/2011/02/01/digital-forensics-extracting-event-logs-memory-mapped-files-memory-dumps 120 | [5] Physical Memory Forensics for Files and Cache by J. Butler and J. 121 | Murdock (accessed 8/Oct/2011): 122 | https://media.blackhat.com/bh-us-11/Butler/BH_US_11_ButlerMurdock_Physical_Memory_Forensics-WP.pdf 123 | [6] CodeMachine article on Prototype PTEs (accessed 8/Oct/2011): 124 | http://www.codemachine.com/article_protopte.html 125 | [7] OSR Online article: Cache Me If You Can: Using the NT Chace Manager (accessed 14/Oct/2011): 126 | http://www.osronline.com/article.cfm?id=167 127 | """ 128 | 129 | meta_info = dict( 130 | author = 'Carl Pulley', 131 | copyright = 'Copyright (c) 2010 Carl Pulley', 132 | contact = 'c.j.pulley@hud.ac.uk', 133 | license = 'GNU General Public License 2.0 or later', 134 | url = 'http://compeng.hud.ac.uk/scomcjp', 135 | os = ['WinXPSP2x86', 'WinXPSP3x86'], 136 | version = '1.0', 137 | ) 138 | 139 | def __init__(self, config, *args): 140 | filescan.FileScan.__init__(self, config, *args) 141 | config.add_option("fill", default = 0, type = 'int', action = 'store', help = "Fill character (byte) for padding out missing pages") 142 | config.add_option("dir", short_option = 'D', type = 'str', action = 'store', help = "Directory in which to save exported files") 143 | config.add_option("pid", type = 'int', action = 'store', help = "Extract all associated _FILE_OBJECT's from a PID") 144 | config.add_option("eproc", type = 'int', action = 'store', help = "Extract all associated _FILE_OBJECT's from an _EPROCESS offset (kernel address)") 145 | config.add_option("fobj", type = 'int', action = 'store', help = "Extract a given _FILE_OBJECT offset (physical address)") 146 | config.add_option("pool", default = False, action = 'store_true', help = "Extract all _FILE_OBJECT's found by searching the pool") 147 | config.add_option("reconstruct", default = False, action = 'store_true', help = "Do not export file objects, perform file reconstruction on previously extracted file pages") 148 | 149 | def calculate(self): 150 | self.kernel_address_space = utils.load_as(self._config) 151 | self.flat_address_space = utils.load_as(self._config, astype = 'physical') 152 | if not(bool(self._config.DIR)): 153 | debug.error("--dir needs to be present") 154 | if not(bool(self._config.pid) ^ bool(self._config.eproc) ^ bool(self._config.fobj) ^ bool(self._config.pool)): 155 | if not(bool(self._config.pid) or bool(self._config.eproc) or bool(self._config.fobj) or bool(self._config.pool)): 156 | debug.error("exactly *ONE* of the options --pid, --eproc, --fobj or --pool must be specified (you have not specified _any_ of these options)") 157 | else: 158 | debug.error("exactly *ONE* of the options --pid, --eproc, --fobj or --pool must be specified (you have used _multiple_ such options)") 159 | if bool(self._config.pid): 160 | # --pid 161 | eproc_matches = [ eproc for eproc in tasks.pslist(self.kernel_address_space) if eproc.UniqueProcessId == self._config.pid ] 162 | if len(eproc_matches) != 1: 163 | debug.error("--pid needs to take a *VALID* PID argument (could not find PID {0} in the process listing for this memory image)".format(self._config.pid)) 164 | return self.dump_from_eproc(eproc_matches[0]) 165 | elif bool(self._config.eproc): 166 | # --eproc 167 | return self.dump_from_eproc(obj.Object("_EPROCESS", offset = self._config.eproc, vm = self.kernel_address_space)) 168 | elif bool(self._config.fobj): 169 | # --fobj 170 | try: 171 | file_object = obj.Object("_FILE_OBJECT", offset = self._config.fobj, vm = self.flat_address_space) 172 | if bool(self._config.reconstruct): 173 | # --reconstruct 174 | return [ (file_object, self.parse_string(file_object.FileName)) ] 175 | else: 176 | return filter(None, [ self.dump_file_object(file_object) ]) 177 | except ExportException as exn: 178 | debug.error(exn) 179 | else: 180 | # --pool 181 | return self.dump_from_pool() 182 | 183 | def dump_from_eproc(self, eproc): 184 | result = [] 185 | if eproc.ObjectTable.HandleTableList: 186 | for h in eproc.ObjectTable.handles(): 187 | h.kas = self.kernel_address_space 188 | if h.get_object_type() == "File": 189 | file_obj = obj.Object("_FILE_OBJECT", h.Body.obj_offset, h.obj_vm) 190 | try: 191 | if bool(self._config.reconstruct): 192 | # --reconstruct 193 | result += [ (file_obj, self.parse_string(file_obj.FileName)) ] 194 | else: 195 | result += [ self.dump_file_object(file_obj) ] 196 | except ExportException as exn: 197 | debug.warning(exn) 198 | return filter(None, result) 199 | 200 | def dump_from_pool(self): 201 | result = [] 202 | for object_obj, file_obj, name in filescan.FileScan.calculate(self): 203 | try: 204 | if bool(self._config.reconstruct): 205 | # --reconstruct 206 | result += [ (file_obj, name) ] 207 | else: 208 | result += [ self.dump_file_object(file_obj) ] 209 | except ExportException as exn: 210 | debug.warning(exn) 211 | return filter(None, result) 212 | 213 | def render_text(self, outfd, data): 214 | base_dir = os.path.abspath(self._config.dir) 215 | if bool(self._config.reconstruct): 216 | # Perform no file extraction, simply reconstruct existing extracted file pages 217 | for file_object, file_name in data: 218 | file_name_path = re.sub(r'//', '/', re.sub(r'[\\:]', '/', base_dir + "/" + file_name)) 219 | self.reconstruct_file(outfd, file_object, file_name_path) 220 | else: 221 | # Process extracted file page data and then perform file reconstruction upon the results 222 | for file_data in data: 223 | for data_type, fobj_inst, file_name, file_size, extracted_file_data in file_data: 224 | # FIXME: fix this hacky way of creating directory structures 225 | file_name_path = re.sub(r'//', '/', re.sub(r'[\\:]', '/', base_dir + "/" + file_name)) 226 | if not(os.path.exists(file_name_path)): 227 | exe.getoutput("mkdir -p {0}".format(re.escape(file_name_path))) 228 | outfd.write("#"*20) 229 | outfd.write("\nExporting {0} [_FILE_OBJECT @ 0x{1:08X}]:\n {2}\n\n".format(data_type, fobj_inst.v(), file_name)) 230 | if data_type == "_SHARED_CACHE_MAP": 231 | # _SHARED_CACHE_MAP processing 232 | self.dump_shared_pages(outfd, data, fobj_inst, file_name_path, file_size, extracted_file_data) 233 | elif data_type == "_CONTROL_AREA": 234 | # _CONTROL_AREA processing 235 | self.dump_control_pages(outfd, data, fobj_inst, file_name_path, extracted_file_data) 236 | self.reconstruct_file(outfd, fobj_inst, file_name_path) 237 | 238 | def render_sql(self, outfd, data): 239 | debug.error("TODO: not implemented yet!") 240 | 241 | KB = 0x400 242 | _1KB = KB 243 | MB = _1KB**2 244 | _1MB = MB 245 | GB = _1KB**3 246 | _1GB = GB 247 | 248 | # TODO: monitor issue 151 on Volatility trunk (following code is copied from FileScan) 249 | def parse_string(self, unicode_obj): 250 | """Unicode string parser""" 251 | # We need to do this because the unicode_obj buffer is in 252 | # kernel_address_space 253 | string_length = unicode_obj.Length 254 | string_offset = unicode_obj.Buffer 255 | 256 | string = self.kernel_address_space.read(string_offset, string_length) 257 | if not string: 258 | return '' 259 | return string[:260].decode("utf16", "ignore").encode("utf8", "xmlcharrefreplace") 260 | 261 | def read_large_integer(self, large_integer): 262 | return large_integer.HighPart.v() * pow(2, 4 *8) + large_integer.LowPart.v() 263 | 264 | def walk_vacb_tree(self, depth, ptr): 265 | if depth < 1: 266 | raise ExportException("consistency check failed (expected VACB tree to have a positive depth)") 267 | if depth == 1: 268 | return [ (obj.Object("_VACB", offset = ptr + 4*index, vm = self.kernel_address_space), index) for index in range(0, 128) ] 269 | return [ (vacb, 128*index + section) for index in range(0, 128) for (vacb, section) in self.walk_vacb_tree(depth-1, ptr+4*index) ] 270 | 271 | def read_vacbs_from_cache_map(self, shared_cache_map, file_size, depth = None, ptr = None): 272 | if file_size < self._1MB: 273 | return [ (shared_cache_map.InitialVacbs[index], index) for index in range(0, 4) ] 274 | else: 275 | tree_depth = math.ceil((math.ceil(math.log(file_size, 2)) - 18)/7) 276 | return self.walk_vacb_tree(tree_depth, shared_cache_map.Vacbs.v()) 277 | 278 | def dump_file_object(self, file_object): 279 | if not file_object.is_valid(): 280 | raise ExportException("consistency check failed (expected [_FILE_OBJECT @ 0x{0:08X}] to be a valid physical address)".format(file_object.v())) 281 | file_name = self.parse_string(file_object.FileName) 282 | if file_name == None or file_name == '': 283 | debug.warning("expected file name to be non-null and non-empty [_FILE_OBJECT @ 0x{0:08X}] (using _FILE_OBJECT address instead to distinguish file)".format(file_object.v())) 284 | file_name = "FILE_OBJECT.0x{0:08X}".format(file_object.v()) 285 | section_object_ptr = obj.Object('_SECTION_OBJECT_POINTERS', offset = file_object.SectionObjectPointer, vm = self.kernel_address_space) 286 | result = [] 287 | if section_object_ptr.DataSectionObject != 0 and section_object_ptr.DataSectionObject != None: 288 | # Shared Cache Map 289 | try: 290 | shared_cache_map = obj.Object('_SHARED_CACHE_MAP', offset = section_object_ptr.SharedCacheMap, vm = self.kernel_address_space) 291 | if shared_cache_map == None: 292 | raise ExportException("consistency check failed [_FILE_OBJECT @ 0x{0:08X}] (expected _SHARED_CACHE_MAP to be non-null for file name '{1}')".format(file_object.v(), file_name)) 293 | if shared_cache_map.FileSize == None: 294 | raise ExportException("consistency check failed [_FILE_OBJECT @ 0x{0:08X}] (expected FileSize to be non-null for file name '{1}')".format(file_object.v(), file_name)) 295 | file_size = self.read_large_integer(shared_cache_map.FileSize) 296 | if self.read_large_integer(shared_cache_map.ValidDataLength) > file_size: 297 | raise ExportException("consistency check failed [_FILE_OBJECT @ 0x{0:08X}] (expected ValidDataLength to be bounded by file size for file name '{1}')".format(file_object.v(), file_name)) 298 | result += [ ("_SHARED_CACHE_MAP", file_object, file_name, file_size, self.dump_shared_cache_map(shared_cache_map, file_size)) ] 299 | except ExportException as exn: 300 | debug.warning(exn) 301 | if section_object_ptr.DataSectionObject != 0 and section_object_ptr.DataSectionObject != None: 302 | # Data Section Object 303 | try: 304 | control_area = obj.Object("_CONTROL_AREA", offset = section_object_ptr.DataSectionObject, vm = self.kernel_address_space) 305 | result += [ ("_CONTROL_AREA", file_object, file_name, None, self.dump_control_area(control_area)) ] 306 | except ExportException as exn: 307 | debug.warning(exn) 308 | if section_object_ptr.ImageSectionObject != 0 and section_object_ptr.ImageSectionObject != None: 309 | # Image Section Object 310 | try: 311 | control_area = obj.Object("_CONTROL_AREA", offset = section_object_ptr.ImageSectionObject, vm = self.kernel_address_space) 312 | result += [ ("_CONTROL_AREA", file_object, file_name, None, self.dump_control_area(control_area)) ] 313 | except ExportException as exn: 314 | debug.warning(exn) 315 | return result 316 | 317 | def walk_subsections(self, ptr): 318 | if ptr == 0 or ptr == None: 319 | return [] 320 | return [ ptr ] + self.walk_subsections(ptr.NextSubsection) 321 | 322 | # TODO: take into account the PTE flags when reading pages (output via debug.warning?) 323 | def read_pte_array(self, subsection, base_addr, num_of_ptes): 324 | result = [] 325 | for pte in range(0, num_of_ptes): 326 | if not(self.kernel_address_space.is_valid_address(base_addr + 4*pte)): 327 | raise ExportException("consistency check failed (expected PTE to be at a valid address)") 328 | (pte_addr, ) = struct.unpack('=I', self.kernel_address_space.read(base_addr + 4*pte, 4)) 329 | page_phy_addr = pte_addr & 0xFFFFF000 330 | if page_phy_addr != 0 and self.flat_address_space.is_valid_address(page_phy_addr) and page_phy_addr != subsection.v(): 331 | result += [ (self.flat_address_space.read(page_phy_addr, 4*self.KB), pte) ] 332 | elif page_phy_addr != 0: 333 | result += [ (None, pte) ] 334 | return result 335 | 336 | def dump_shared_cache_map(self, shared_cache_map, file_size): 337 | # _VACB points to a 256KB area of paged memory. Parts of this memory may be paged out, so we perform a linear search, 338 | # through our set of _VACB page addresses, looking for valid in-memory pages. 339 | result = [] 340 | for vacb, section in self.read_vacbs_from_cache_map(shared_cache_map, file_size): 341 | if vacb != 0 and vacb != None: 342 | vacb_base_addr = vacb.BaseAddress.v() 343 | for page in range(0, 64): 344 | if self.kernel_address_space.is_valid_address(vacb_base_addr + page*(4*self.KB)): 345 | result += [ (self.kernel_address_space.read(vacb_base_addr + page*(4*self.KB), (4*self.KB)), section*(256*self.KB) + page*(4*self.KB), section*(256*self.KB) + (page + 1)*(4*self.KB) - 1) ] 346 | return result 347 | 348 | def dump_control_area(self, control_area): 349 | result = [] 350 | start_subsection = obj.Object("_SUBSECTION", offset = control_area.v() + control_area.size(), vm = self.kernel_address_space) 351 | subsection_list = self.walk_subsections(start_subsection) 352 | sector = None 353 | for subsection, index in zip(subsection_list, range(0, len(subsection_list))): 354 | start_sector = subsection.StartingSector 355 | num_of_sectors = subsection.NumberOfFullSectors 356 | if sector == None: 357 | sector = start_sector 358 | sector_data = [ (page, pte_index) for page, pte_index in self.read_pte_array(subsection, subsection.SubsectionBase.v(), subsection.PtesInSubsection) if pte_index <= num_of_sectors and page != None ] 359 | result += map(lambda ((page, pte_index), position): (page, sector + position*8), zip(sector_data, range(0, len(sector_data)))) 360 | sector += len(sector_data)*8 361 | return result 362 | 363 | def dump_data(self, outfd, data, dump_file, start_addr, end_addr): 364 | if not os.path.exists(dump_file): 365 | with open(dump_file, 'wb') as fobj: 366 | fobj.write(data) 367 | outfd.write("Dumped File Offset Range: 0x{0:08X} -> 0x{1:08X}\n".format(start_addr, end_addr)) 368 | else: 369 | outfd.write("..Skipping File Offset Range: 0x{0:08X} -> 0x{1:08X}\n".format(start_addr, end_addr)) 370 | 371 | def dump_shared_pages(self, outfd, data, file_object, file_name_path, file_size, extracted_file_data): 372 | for vacb, start_addr, end_addr in extracted_file_data: 373 | vacb_hash = hashlib.md5(vacb).hexdigest() 374 | vacb_path = "{0}/cache.0x{1:08X}-0x{2:08X}.dmp.{3}".format(file_name_path, start_addr, end_addr, vacb_hash) 375 | self.dump_data(outfd, vacb, vacb_path, start_addr, end_addr) 376 | 377 | def dump_control_pages(self, outfd, data, file_object, file_name_path, extracted_file_data): 378 | sorted_extracted_fobjs = sorted(extracted_file_data, key = lambda tup: tup[1]) 379 | for page, start_sector in sorted_extracted_fobjs: 380 | if page != None: 381 | start_addr = start_sector*512 382 | end_addr = (start_sector + 8)*512 - 1 383 | page_hash = hashlib.md5(page).hexdigest() 384 | page_path = "{0}/direct.0x{1:08X}-0x{2:08X}.dmp.{3}".format(file_name_path, start_addr, end_addr, page_hash) 385 | self.dump_data(outfd, page, page_path, start_addr, end_addr) 386 | 387 | def reconstruct_file(self, outfd, file_object, file_name_path): 388 | def check_for_overlaps(data): 389 | # ASSUME: data is sorted by (start_addr, end_addr, md5) 390 | while len(data) >= 2: 391 | data_type1, start_addr1, end_addr1, data1, hash1 = data[0] 392 | data_type2, start_addr2, end_addr2, data2, hash2 = data[1] 393 | if start_addr2 <= end_addr1: 394 | # Check if the overlap provides a data conflict 395 | if start_addr1 == start_addr2 and end_addr1 == end_addr2 and hash1 == hash2: 396 | # data overlap provides no conflict 397 | pass 398 | else: 399 | # data conflict exists 400 | if os.path.exists("{0}/this".format(file_name_path)): 401 | # We're about to fail with reconstruction, eliminate any old "this" files from previous reconstruction attempts 402 | os.remove("{0}/this".format(file_name_path)) 403 | raise ExportException("File Reconstruction Failed: overlapping page conflict (for address range 0x{0:08X}-0x{1:08X}) detected during file reconstruction - please manually fix and use --reconstruct to attempt rebuilding the file".format(start_addr2, end_addr1)) 404 | else: 405 | yield data[0] 406 | data = data[1:] 407 | if len(data) > 0: 408 | yield data[0] 409 | 410 | outfd.write("[_FILE_OBJECT @ 0x{0:08X}] Reconstructing extracted memory pages from:\n {1}\n".format(file_object.v(), file_name_path)) 411 | fill_char = chr(self._config.fill % 256) 412 | if os.path.exists(file_name_path): 413 | # list files in file_name_path 414 | raw_page_dumps = [ f for f in os.listdir(file_name_path) if re.search("(cache|direct)\.0x[a-fA-F0-9]{8}\-0x[a-fA-F0-9]{8}\.dmp(\.[a-fA-F0-9]{32})?$", f) ] 415 | # map file list to (dump_type, start_addr, end_addr, data, md5) tuple list 416 | page_dumps = [] 417 | for dump in raw_page_dumps: 418 | dump_type, addr_range, ignore, md5 = dump.split(".") 419 | start_addr, end_addr = map(lambda x: x[2:], addr_range.split("-")) 420 | data = "" 421 | with open("{0}/{1}".format(file_name_path, dump), 'r') as fobj: 422 | data = fobj.read() 423 | if hashlib.md5(data).hexdigest() != md5: 424 | debug.error("consistency check failed (MD5 checksum check for {0} failed!)".format(dump)) 425 | page_dumps += [ (dump_type, int(start_addr, 16), int(end_addr, 16), data, md5) ] 426 | try: 427 | # check for page overlaps 428 | extracted_file_data = check_for_overlaps(sorted(page_dumps, key = lambda x: (x[1], x[2], x[3]))) 429 | # glue remaining file pages together and save reconstructed file as "this" 430 | with open("{0}/this".format(file_name_path), 'wb') as fobj: 431 | offset = 0 432 | for data_type, start_addr, end_addr, data, data_hash in extracted_file_data: 433 | if offset > end_addr: 434 | break 435 | if offset < start_addr: 436 | fobj.write(fill_char*(start_addr - offset)) 437 | fobj.write(data) 438 | offset = end_addr + 1 439 | outfd.write("Successfully Reconstructed File\n") 440 | except ExportException as exn: 441 | debug.warning(exn) 442 | else: 443 | outfd.write("..Skipping file reconstruction due to a lack of extracted pages\n") 444 | -------------------------------------------------------------------------------- /exportstack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # exportstack.py 4 | # Copyright (C) 2010, 2013 Carl Pulley 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or (at 9 | # your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | # 20 | 21 | """ 22 | Code to parse and search _ETHREAD stacks. 23 | 24 | Based on preliminary work done by Carl Pulley for Honeynet Challenge 3 (Banking Troubles). 25 | 26 | @author: Carl Pulley 27 | @license: GNU General Public License 2.0 or later 28 | @contact: c.j.pulley@hud.ac.uk 29 | @organization: University of Huddersfield 30 | 31 | DEPENDENCIES: 32 | distorm3 33 | yara 34 | 35 | REFERENCES: 36 | [1] Russinovich, M., Solomon, D.A. & Ionescu, A. (2009). Windows Internals: 37 | Including Windows Server 2008 and Windows Vista, Fifth Edition (Pro 38 | Developer) 5th ed. Microsoft Press. 39 | [2] Hewardt, M. & Pravat, D. (2007). Advanced Windows Debugging 1st ed. 40 | Addison-Wesley Professional. 41 | [3] Ligh, M., H. (2011). Investigating Windows Threads with Volatility. Retrieved 42 | from: 43 | http://mnin.blogspot.com/2011/04/investigating-windows-threads-with.html 44 | [4] Ionescu, A. (2008). Part 1: Processes, Threads, Fibers and Jobs. Retrieved 45 | from: 46 | http://www.alex-ionescu.com/BH08-AlexIonescu.pdf 47 | [5] Windows Data Alignment on IPF, x86, and x64 (2006). Retrieved from: 48 | http://msdn.microsoft.com/en-us/library/aa290049(v=vs.71).aspx 49 | [6] Deyblue, A. (2009). Grabbing Kernel Thread Call Stacks the Process Explorer Way. 50 | Retrived from: 51 | http://blog.airesoft.co.uk/2009/02/grabbing-kernel-thread-contexts-the-process-explorer-way/ 52 | [7] Litchfield, D. (2003). Defeating the Stack Based Buffer Overflow Prevention 53 | Mechanism of Microsoft Windows 2003 Server. Retrieved from: 54 | http://www.blackhat.com/presentations/bh-asia-03/bh-asia-03-litchfield.pdf 55 | [8] Bray, B. (2002). Compiler Security Checks In Depth. Microsoft. Retrieved from: 56 | http://msdn.microsoft.com/en-GB/library/aa290051(v=vs.71).aspx 57 | [9] Miller, M. (2007). Reducing the Effective Entropy of GS Cookies. Uninformed Vol. 7. 58 | Retrieved from: 59 | http://uninformed.org/index.cgi?v=7&a=2&p=5 60 | [10] Jurczyk, M. and Coldwind, G. (2011). Exploiting the otherwise non-exploitable: 61 | Windows Kernel-mode GS Cookies subverted. Retrieved from: 62 | http://vexillium.org/dl.php?/Windows_Kernel-mode_GS_Cookies_subverted.pdf 63 | """ 64 | 65 | import bisect 66 | try: 67 | import distorm3 68 | distorm3_installed = True 69 | except ImportError: 70 | distorm3_installed = False 71 | import functools 72 | from inspect import isfunction 73 | import itertools 74 | import os.path 75 | import pydoc 76 | import sys 77 | import volatility.debug as debug 78 | import volatility.obj as obj 79 | import volatility.plugins.malware.threads as threads 80 | import volatility.plugins.overlays.windows.pe_vtypes as pe_vtypes 81 | import volatility.plugins.overlays.windows.windows as windows 82 | import volatility.plugins.symbols as symbols 83 | import volatility.registry as registry 84 | import volatility.utils as utils 85 | import volatility.plugins.vadinfo as vadinfo 86 | import volatility.win32.modules as sysmods 87 | import volatility.win32.tasks as tasks 88 | try: 89 | import yara 90 | yara_installed = True 91 | except ImportError: 92 | yara_installed = False 93 | 94 | DEFAULT_UNWIND = "EBP,CALL_RET,EXCEPTION_REGISTRATION_RECORD" 95 | 96 | #-------------------------------------------------------------------------------- 97 | # unwind classes 98 | #-------------------------------------------------------------------------------- 99 | 100 | class StackTop(object): 101 | """Abstract class that represents the top of the stack""" 102 | def __init__(self, start, stack_base, stack_limit, eproc, *args, **kwargs): 103 | if isfunction(start): 104 | self.start = start(self.__class__.__name__) 105 | else: 106 | self.start = start 107 | self.stack_base = stack_base 108 | self.stack_limit = stack_limit 109 | self.eproc = eproc 110 | self.addrspace = eproc.get_process_address_space() 111 | self.alignment = 4 if self.addrspace.profile.metadata.get('memory_model', '32bit') == '32bit' else 16 112 | self.caller = None 113 | 114 | def up(self): 115 | """Stack iterator that retrieves stack frames above this one""" 116 | return [] 117 | 118 | def down(self): 119 | """Stack iterator that retrieves stack frames below and including this one""" 120 | return [self] 121 | 122 | def __repr__(self): 123 | return "{0:#010x}".format(self.start) 124 | 125 | def exec_addr(self): 126 | return "RET: {0:#010x}".format(self.caller) if self.caller != None else "-"*10 127 | 128 | class UserFrame(StackTop): 129 | """User stack unwind strategy - user code should extend this class""" 130 | pass 131 | 132 | class KernelFrame(StackTop): 133 | """Kernel stack unwind strategy - user code should extend this class""" 134 | pass 135 | 136 | # TODO: add in 64bit stack unwinding 137 | 138 | class EBP(UserFrame, KernelFrame): 139 | """ 140 | EBP (-> *EBP) RET: *(EBP+4) symbol_lookup(*(EBP+4)) 141 | Represents an x86 EBP/EIP stack frame unwind 142 | """ 143 | def __init__(self, start, stack_base, stack_limit, eproc, *args, **kwargs): 144 | StackTop.__init__(self, start, stack_base, stack_limit, eproc, *args, **kwargs) 145 | self.ebp = self.start 146 | if self.addrspace.is_valid_address(self.ebp): 147 | self.next_ebp = self.addrspace.read_long_phys(self.addrspace.vtop(self.ebp)) 148 | else: 149 | self.next_ebp = obj.NoneObject("Invalid stack frame address: EBP={0:#x}".format(self.ebp)) 150 | if self.addrspace.is_valid_address(self.ebp + self.alignment): 151 | self.eip = self.addrspace.read_long_phys(self.addrspace.vtop(self.ebp + self.alignment)) 152 | else: 153 | self.eip = obj.NoneObject("Invalid stack frame address: EBP{1:+#x}={0:#x}".format(self.ebp + self.alignment, self.alignment)) 154 | self.caller = self.eip 155 | 156 | def up(self): 157 | # Perform a linear search looking for potential (old) EBP chain values in remnants of old stack frames 158 | frame = self 159 | # From [5], we assume an alignment for stack frames based on the memory model's bit size 160 | ebp_ptr = frame.ebp - self.alignment 161 | ebp_bases = { frame.ebp } 162 | while self.stack_limit < ebp_ptr and ebp_ptr <= self.stack_base: 163 | if self.addrspace.is_valid_address(ebp_ptr): 164 | ebp_value = self.addrspace.read_long_phys(self.addrspace.vtop(ebp_ptr)) 165 | if ebp_value in ebp_bases: 166 | frame = EBP(ebp_ptr, self.stack_base, self.stack_limit, self.eproc) 167 | yield frame 168 | ebp_bases.add(frame.ebp) 169 | ebp_ptr -= self.alignment 170 | raise StopIteration 171 | 172 | def down(self): 173 | # Follow the EBP chain downloads and so locate stack frames 174 | frame = self 175 | yield frame 176 | while self.stack_limit < frame.next_ebp and frame.next_ebp <= self.stack_base: 177 | if not self.addrspace.is_valid_address(frame.next_ebp): 178 | raise StopIteration 179 | frame = EBP(frame.next_ebp, self.stack_base, self.stack_limit, self.eproc) 180 | yield frame 181 | raise StopIteration 182 | 183 | def __repr__(self): 184 | ebp_str = "{:#010x}".format(self.ebp) if self.ebp != None else "-"*10 185 | next_ebp_str = "{:#010x}".format(self.next_ebp) if self.next_ebp != None else "-"*10 186 | return "{0} (-> {1})".format(ebp_str, next_ebp_str) 187 | 188 | # FIXME: following still needs more work 189 | class GS(EBP): 190 | """ 191 | EBP (-> *(EBP)) /GS [security_cookie]: *(EBP+4) symbol_lookup(*(EBP+4)) [*(EBP-4) == security_cookie ^ EBP] 192 | Detect EBP stack frames with /GS security cookies (originating module determined using EIP) 193 | """ 194 | # Security cookie calculations prior to cookie saving: 195 | # WinXP: *(EBP-4) == security_cookie 196 | # Vista, 2008 and Win7: *(EBP-4) == security_cookie ^ EBP 197 | def __init__(self, start, stack_base, stack_limit, eproc, modules=None, module_addrs=None, *args, **kwargs): 198 | EBP.__init__(self, start, stack_base, stack_limit, eproc, *args, **kwargs) 199 | if modules == None: 200 | self.modules = dict( (m.DllBase, m) for m in list(sysmods.lsmod(eproc.get_process_address_space())) + list(eproc.get_load_modules()) ) 201 | self.module_addrs = sorted(self.modules.keys()) 202 | else: 203 | self.modules = modules 204 | self.module_addrs = module_addrs 205 | mod = tasks.find_module(self.modules, self.module_addrs, self.eip) 206 | self.security_cookie = None 207 | self.cookie = None 208 | security_cookie_addr = None 209 | if mod != None: 210 | load_config = mod.get_load_config_directory() 211 | if load_config == None: 212 | # Attempt to use PDB symbols to locate this module's ___security_cookie 213 | addrs = eproc.lookup("{0}/.data!___security_cookie".format(str(mod.BaseDllName))) 214 | if len(addrs) > 0: 215 | security_cookie_addr = addrs[0] 216 | else: 217 | # Use _IMAGE_LOAD_CONFIG_DIRECTORY to locate this module's ___security_cookie 218 | security_cookie_addr = load_config.SecurityCookie 219 | if security_cookie_addr != None and self.addrspace.is_valid_address(security_cookie_addr): 220 | self.security_cookie = self.addrspace.read_long_phys(self.addrspace.vtop(security_cookie_addr)) 221 | if self.addrspace.is_valid_address(self.ebp - self.alignment): 222 | self.cookie = self.addrspace.read_long_phys(self.addrspace.vtop(self.ebp - self.alignment)) 223 | 224 | def cookie_check(self): 225 | return self.ebp ^ self.cookie == self.security_cookie 226 | 227 | def up(self): 228 | for frame in EBP.up(self): 229 | yield GS(frame.start, frame.stack_base, frame.stack_limit, frame.eproc, modules=self.modules, module_addrs=self.module_addrs) 230 | raise StopIteration 231 | 232 | def down(self): 233 | for frame in EBP.down(self): 234 | yield GS(frame.start, frame.stack_base, frame.stack_limit, frame.eproc, modules=self.modules, module_addrs=self.module_addrs) 235 | raise StopIteration 236 | 237 | def __repr__(self): 238 | if self.security_cookie != None: 239 | if self.cookie != None: 240 | if self.cookie_check(): 241 | return "/GS [True]" 242 | else: 243 | return "/GS False [{0:#010x} ^ {1:#010x} != {2:#010x}]".format(self.ebp, self.cookie, self.security_cookie) 244 | else: 245 | return "/GS ???? [UNREADABLE]".format(self.security_cookie) 246 | else: 247 | return "no /GS" 248 | 249 | class EXCEPTION_REGISTRATION_RECORD(UserFrame): 250 | """ 251 | EXN (-> EXN.Next) Handler: EXN.Handler symbol_lookup(EXN.Handler) 252 | Represents an _EXCEPTION_REGISTRATION_RECORD stack frame unwind 253 | """ 254 | def __init__(self, start, stack_base, stack_limit, eproc, *args, **kwargs): 255 | UserFrame.__init__(self, start, stack_base, stack_limit, eproc, *args, **kwargs) 256 | if self.addrspace.is_valid_address(self.start): 257 | self.exn = obj.Object('_EXCEPTION_REGISTRATION_RECORD', self.start, self.addrspace) 258 | else: 259 | self.exn = obj.NoneObject("Invalid exception frame address: {0:#x}".format(self.start)) 260 | self.caller = self.exn.Handler.v() 261 | 262 | def up(self): 263 | # Perform a linear search looking for potential (old) _EXCEPTION_REGISTRATION_RECORD Next values in remnants of old exception frames 264 | frame = self 265 | exn_ptr = self.exn.v() - self.alignment 266 | exn_bases = { frame.exn.v() } 267 | while self.stack_limit < exn_ptr and exn_ptr <= self.stack_base: 268 | if self.addrspace.is_valid_address(exn_ptr): 269 | exn = obj.Object('_EXCEPTION_REGISTRATION_RECORD', exn_ptr, self.addrspace) 270 | if exn.Next.v() in exn_bases: 271 | frame = EXCEPTION_REGISTRATION_RECORD(exn.v(), self.stack_base, self.stack_limit, self.eproc) 272 | yield frame 273 | exn_bases.add(frame.exn.v()) 274 | exn_ptr -= self.alignment 275 | raise StopIteration 276 | 277 | def down(self): 278 | # Follow _EXCEPTION_REGISTRATION_RECORD Next pointers and so locate exception frames 279 | frame = self 280 | yield frame 281 | while self.stack_limit < frame.exn.Next and frame.exn.Next <= self.stack_base: 282 | if not self.addrspace.is_valid_address(frame.exn.Next): 283 | raise StopIteration 284 | frame = EXCEPTION_REGISTRATION_RECORD(frame.exn.Next.v(), self.stack_base, self.stack_limit, self.eproc) 285 | yield frame 286 | raise StopIteration 287 | 288 | def __repr__(self): 289 | exn_str = "{0:#010x}".format(self.exn.v()) if self.exn != None else "-" 290 | exn_next_str = "{0:#010x}".format(self.exn.Next.v()) if self.exn != None else "-" 291 | return "{0} (-> {1})".format(exn_str, exn_next_str) 292 | 293 | def exec_addr(self): 294 | return "Handler: " + ("{0:#010x}".format(self.caller) if self.caller != None else "-"*10) 295 | 296 | def get_page_nx_bit(addrspace, addr): 297 | pdpte = addrspace.get_pdpte(addr) 298 | pde = addrspace.get_pde(addr, pdpte) 299 | pte = addrspace.get_pte(addr, pde) 300 | return pte >> 63 301 | 302 | class PAGE_EXECUTE(UserFrame, KernelFrame): 303 | """ 304 | ADDR RET: *(ADDR) symbol_lookup(*(ADDR)) [NX not in page_permissions(*(ADDR))] 305 | Stack addresses belonging to memory pages with NX bit unset 306 | """ 307 | def __init__(self, start, stack_base, stack_limit, eproc, ret_addr=None, *args, **kwargs): 308 | StackTop.__init__(self, start, stack_base, stack_limit, eproc, *args, **kwargs) 309 | assert self.addrspace.pae 310 | if ret_addr == None: 311 | self.caller = obj.NoneObject("No return address specified") 312 | else: 313 | self.caller = ret_addr 314 | 315 | def check(self, addr): 316 | return get_page_nx_bit(self.addrspace, addr) == 0 317 | 318 | def up(self): 319 | addr_ptr = self.start - self.alignment 320 | while self.stack_limit < addr_ptr and addr_ptr <= self.stack_base: 321 | if self.addrspace.is_valid_address(addr_ptr): 322 | ret_addr = self.addrspace.read_long_phys(self.addrspace.vtop(addr_ptr)) 323 | if self.check(ret_addr): 324 | yield PAGE_EXECUTE(addr_ptr, self.stack_base, self.stack_limit, self.eproc, ret_addr) 325 | addr_ptr -= self.alignment 326 | raise StopIteration 327 | 328 | def down(self): 329 | addr_ptr = self.start 330 | while self.stack_limit < addr_ptr and addr_ptr <= self.stack_base: 331 | if self.addrspace.is_valid_address(addr_ptr): 332 | ret_addr = self.addrspace.read_long_phys(self.addrspace.vtop(addr_ptr)) 333 | if self.check(ret_addr): 334 | yield PAGE_EXECUTE(addr_ptr, self.stack_base, self.stack_limit, self.eproc, ret_addr) 335 | addr_ptr += self.alignment 336 | raise StopIteration 337 | 338 | # How far to search for a straight-line code/RET instruction sequence (i.e. ROP gadget) 339 | MAX_GADGET_SIZE = 256 340 | 341 | # FIXME: following needs some testing 342 | class Gadget(PAGE_EXECUTE): 343 | """ 344 | ADDR ROP [len(GADGET)]: *(ADDR) symbol_lookup(*(ADDR)) [PAGE_EXECUTE(*(ADDR)); GADGET == "instr1; ..; RET"] 345 | Gagets are stack addresses belonging to PAGE_EXECUTE memory pages, with return addresses that are 346 | non-trivial code blocks ending in a RET instruction. 347 | """ 348 | def __init__(self, start, stack_base, stack_limit, eproc, rop_size=0, *args, **kwargs): 349 | if not distorm3_installed: 350 | debug.error("In order to work with ROP Gadget's, you need to install distorm3") 351 | PAGE_EXECUTE.__init__(self, start, stack_base, stack_limit, eproc, *args, **kwargs) 352 | 353 | memory_model = self.addrspace.profile.metadata.get('memory_model', '32bit') 354 | if memory_model == '32bit': 355 | self.mode = distorm3.Decode32Bits 356 | else: 357 | self.mode = distorm3.Decode64Bits 358 | self.rop_size = rop_size 359 | 360 | def check(self, addr): 361 | max_mem_addr = (2**32 if self.mode == distorm3.Decode32Bits else 2**64) - 1 362 | max_gadget_size = min(MAX_GADGET_SIZE, max_mem_addr - addr) 363 | if max_gadget_size == 0: 364 | return False 365 | code = self.addrspace.zread(addr, max_gadget_size) 366 | for op in distorm3.DecomposeGenerator(addr, code, self.mode, features=distorm3.DF_RETURN_FC_ONLY): 367 | if not op.valid: 368 | continue 369 | if self.rop_size > 0 and op.flowControl == 'FC_RET': 370 | self.rop_size = addr - op.address 371 | return True 372 | else: 373 | return False 374 | return False 375 | 376 | def up(self): 377 | for frame in PAGE_EXECUTE.up(self): 378 | if self.check(frame.caller): 379 | yield Gadget(frame.start, frame.stack_base, frame.stack_limit, frame.eproc, rop_size=self.rop_size) 380 | raise StopIteration 381 | 382 | def down(self): 383 | for frame in PAGE_EXECUTE.down(self): 384 | if self.check(frame.caller): 385 | yield Gadget(frame.start, frame.stack_base, frame.stack_limit, frame.eproc, rop_size=self.rop_size) 386 | raise StopIteration 387 | 388 | def exec_addr(self): 389 | return "ROP [{0:#x} B]: ".format(self.rop_size) + ("{0:#010x}".format(self.caller) if self.caller != None else "-"*10) 390 | 391 | class CALL_RET(PAGE_EXECUTE): 392 | """ 393 | ADDR (CALL xxx) RET: xxx symbol_lookup(xxx) [PAGE_EXECUTE(ADDR); *(ADDR-size(CALL xxx)) == CALL xxx] 394 | Any stack address that belongs to an executable page (i.e. a RET address) and has a CALL 395 | instruction as its prior instruction is taken to be a CALL/RET frame. 396 | """ 397 | def __init__(self, start, stack_base, stack_limit, eproc, instr=None, caller=None, *args, **kwargs): 398 | if not distorm3_installed: 399 | debug.error("In order to detect CALL/RET frames, you need to install distorm3") 400 | PAGE_EXECUTE.__init__(self, start, stack_base, stack_limit, eproc, *args, **kwargs) 401 | 402 | memory_model = self.addrspace.profile.metadata.get('memory_model', '32bit') 403 | if memory_model == '32bit': 404 | self.mode = distorm3.Decode32Bits 405 | else: 406 | self.mode = distorm3.Decode64Bits 407 | self.caller = caller 408 | self.instr = instr 409 | 410 | def check(self, addr): 411 | self.caller = None 412 | self.instr = None 413 | if addr == None: 414 | return False 415 | # We only need the last 16 bytes, since this is the largest size the (prior) x86 instruction can be 416 | code = self.addrspace.zread(addr-16, 16) 417 | for size in range(0, 16): 418 | instrs = distorm3.Decompose(addr-size-1, code[-(size+1):], self.mode) 419 | op = instrs[-1] 420 | if not op.valid: 421 | continue 422 | if op.flowControl == 'FC_NONE': 423 | return False 424 | if op.flowControl == 'FC_CALL': 425 | self.instr = op 426 | # if the CALL instruction has a determinate target, set caller to it 427 | if [ o.type for o in op.operands ] == ["Immediate"]: 428 | self.caller = op.operands[0].value 429 | elif [ o.type for o in op.operands ] == ["AbsoluteMemoryAddress"]: 430 | self.caller = self.start + op.disp 431 | return True 432 | return False 433 | 434 | def up(self): 435 | for frame in PAGE_EXECUTE.up(self): 436 | if self.check(frame.caller): 437 | yield CALL_RET(frame.start, frame.stack_base, frame.stack_limit, frame.eproc, instr=self.instr, caller=self.caller) 438 | raise StopIteration 439 | 440 | def down(self): 441 | for frame in PAGE_EXECUTE.down(self): 442 | if self.check(frame.caller): 443 | yield CALL_RET(frame.start, frame.stack_base, frame.stack_limit, frame.eproc, instr=self.instr, caller=self.caller) 444 | raise StopIteration 445 | 446 | def exec_addr(self): 447 | if self.caller != None: 448 | return "CALL: {0:#010x}".format(self.caller) 449 | else: 450 | return "-" 451 | 452 | def __repr__(self): 453 | if self.instr != None: 454 | return "{0:#010x} ({1})".format(self.start, self.instr) 455 | else: 456 | return PAGE_EXECUTE.__repr__(self) 457 | 458 | #-------------------------------------------------------------------------------- 459 | # profile modifications 460 | #-------------------------------------------------------------------------------- 461 | 462 | # Extension methods for the profiles EPROCESS (injected by StackModification) 463 | class StackEPROCESS(windows._EPROCESS): 464 | def search_stack_frames(self, start, stack_base, stack_limit, yara_rules, frame_delta=32, unwind=DEFAULT_UNWIND): 465 | """ 466 | Use Yara to search kernel/user stack frames within +/- frame_delta of the frame's start 467 | address. 468 | 469 | Frames to search are chosen by using the strategies specifed by the unwind parameter. 470 | 471 | yara_rules - compiled Yara rules, built for example with: 472 | 1. yara.compile("/path/to/yara.rules") 473 | or 2. yara.compile(source="rule dummy { condition: true }") 474 | """ 475 | 476 | if not yara_installed: 477 | debug.error("In order to search the stack frames, it is necessary to install yara") 478 | 479 | stack_registry = registry.get_plugin_classes(StackTop) 480 | 481 | for unwind_strategy_nm in unwind.split(","): 482 | if unwind_strategy_nm not in stack_registry: 483 | raise ValueError("{0} is not a known stack unwind strategy".format(unwind_strategy_nm)) 484 | unwind_strategy = stack_registry[unwind_strategy_nm](start, stack_base, stack_limit, self) 485 | for frame in itertools.chain(unwind_strategy.up(), unwind_strategy.down()): 486 | search_data = self.get_process_address_space().zread(frame.start - frame_delta, 2* frame_delta) 487 | for match in yara_rules.match(data = search_data): 488 | for moffset, name, value in match.strings: 489 | # Match offset here is converted into frame start address and a +/- frame_delta 490 | yield match, name, value, frame.start, moffset-frame_delta 491 | 492 | raise StopIteration 493 | 494 | class StackLDR_DATA_TABLE_ENTRY(pe_vtypes._LDR_DATA_TABLE_ENTRY): 495 | def load_config(self): 496 | """Return the IMAGE_LOAD_CONFIG_DIRECTORY for load config""" 497 | return self._directory(10) # IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 498 | 499 | def get_load_config_directory(self): 500 | """Return the load config object for this PE""" 501 | try: 502 | load_config = self.load_config() 503 | except ValueError, why: 504 | return obj.NoneObject(str(why)) 505 | 506 | return obj.Object("_IMAGE_LOAD_CONFIG_DIRECTORY", offset=self.DllBase + load_config.VirtualAddress, vm=self.obj_native_vm) 507 | 508 | class StackModification(obj.ProfileModification): 509 | before = ["WindowsObjectClasses"] 510 | 511 | conditions = {'os': lambda x: x == 'windows'} 512 | 513 | def modification(self, profile): 514 | for func in StackEPROCESS.__dict__: 515 | if isfunction(StackEPROCESS.__dict__[func]): 516 | setattr(profile.object_classes['_EPROCESS'], func, StackEPROCESS.__dict__[func]) 517 | for func in StackLDR_DATA_TABLE_ENTRY.__dict__: 518 | if isfunction(StackLDR_DATA_TABLE_ENTRY.__dict__[func]): 519 | setattr(profile.object_classes['_LDR_DATA_TABLE_ENTRY'], func, StackLDR_DATA_TABLE_ENTRY.__dict__[func]) 520 | 521 | class StackModification32bit(obj.ProfileModification): 522 | conditions = {'os': lambda x: x == 'windows', 'memory_model': lambda x: x == '32bit'} 523 | 524 | def modification(self, profile): 525 | profile.vtypes.update({ 526 | '_IMAGE_LOAD_CONFIG_DIRECTORY': [ 0x48, { 527 | 'Size': [ 0x0, ['unsigned int']], 528 | 'TimeDateStamp': [ 0x4, ['unsigned int']], 529 | 'MajorVersion': [ 0x8, ['unsigned short']], 530 | 'MinorVersion': [ 0xa, ['unsigned short']], 531 | 'GlobalFlagsClear': [ 0xc, ['unsigned int']], 532 | 'GlobalFlagsSet': [ 0x10, ['unsigned int']], 533 | 'CriticalSectionDefaultTimeout': [ 0x14, ['unsigned int']], 534 | 'DeCommitFreeBlockThreshold': [ 0x18, ['unsigned int']], 535 | 'DeCommitTotalFreeThreshold': [ 0x1c, ['unsigned int']], 536 | 'LockPrefixTable': [ 0x20, ['unsigned int']], 537 | 'MaximumAllocationSize': [ 0x24, ['unsigned int']], 538 | 'VirtualMemoryThreshold': [ 0x28, ['unsigned int']], 539 | 'ProcessHeapFlags': [ 0x2c, ['unsigned int']], 540 | 'ProcessAffinityMask': [ 0x30, ['unsigned int']], 541 | 'CSDVersion': [ 0x34, ['unsigned short']], 542 | 'Reserved1': [ 0x36, ['unsigned short']], 543 | 'EditList': [ 0x38, ['unsigned int']], 544 | 'SecurityCookie': [ 0x3c, ['unsigned int']], # == ___security_cookie 545 | 'SEHandlerTable': [ 0x40, ['unsigned int']], 546 | 'SEHandlerCount': [ 0x44, ['unsigned int']], 547 | }], 548 | }) 549 | 550 | class StackModification64bit(obj.ProfileModification): 551 | conditions = {'os': lambda x: x == 'windows', 'memory_model': lambda x: x == '64bit'} 552 | 553 | def modification(self, profile): 554 | profile.vtypes.update({ 555 | '_IMAGE_LOAD_CONFIG_DIRECTORY': [ 0x74, { 556 | 'Size': [ 0x0, ['unsigned int']], 557 | 'TimeDateStamp': [ 0x4, ['unsigned int']], 558 | 'MajorVersion': [ 0x8, ['unsigned short']], 559 | 'MinorVersion': [ 0xa, ['unsigned short']], 560 | 'GlobalFlagsClear': [ 0xc, ['unsigned int']], 561 | 'GlobalFlagsSet': [ 0x10, ['unsigned int']], 562 | 'CriticalSectionDefaultTimeout': [ 0x14, ['unsigned int']], 563 | 'DeCommitFreeBlockThreshold': [ 0x1c, ['unsigned long long']], 564 | 'DeCommitTotalFreeThreshold': [ 0x24, ['unsigned long long']], 565 | 'LockPrefixTable': [ 0x2c, ['unsigned long long']], 566 | 'MaximumAllocationSize': [ 0x34, ['unsigned long long']], 567 | 'VirtualMemoryThreshold': [ 0x3c, ['unsigned long long']], 568 | 'ProcessAffinityMask': [ 0x44, ['unsigned long long']], 569 | 'ProcessHeapFlags': [ 0x4c, ['unsigned int']], 570 | 'CSDVersion': [ 0x50, ['unsigned short']], 571 | 'Reserved1': [ 0x52, ['unsigned short']], 572 | 'EditList': [ 0x54, ['unsigned long long']], 573 | 'SecurityCookie': [ 0x5c, ['unsigned long long']], # == ___security_cookie 574 | 'SEHandlerTable': [ 0x64, ['unsigned long long']], 575 | 'SEHandlerCount': [ 0x6c, ['unsigned long long']], 576 | }], 577 | }) 578 | 579 | #-------------------------------------------------------------------------------- 580 | # main plugin code 581 | #-------------------------------------------------------------------------------- 582 | 583 | class ExportStack(threads.Threads): 584 | """ 585 | This plugin is an extension of Michael Hale Ligh's threads plugin (see 586 | http://code.google.com/p/volatility/wiki/CommandReference). 587 | 588 | Given a PID or _EPROCESS object (kernel address), display information 589 | regarding the user and kernel stacks, etc. for the processes threads. 590 | 591 | Support is provided for: EBP chains; PAGE_EXECUTE; ROP Gadgets; EBP frames 592 | with /GS information; CALL/RET frames; and _EXCEPTION_REGISTRATION_RECORD 593 | unwinds. Alternative unwind strategies are easily added. 594 | 595 | By default, EBP chains, CALL/RET frames and _EXCEPTION_REGISTRATION_RECORD 596 | unwinds are performed. 597 | 598 | Where possible, executable stack addresses are resolved to module and function 599 | names using the symbols.Symbols plugin. 600 | """ 601 | 602 | def __init__(self, config, *args, **kwargs): 603 | threads.Threads.__init__(self, config, *args, **kwargs) 604 | 605 | if not yara_installed: 606 | debug.warning("In order to search the stack frames, it is necessary to install yara - searching is disabled") 607 | 608 | config.add_option('UNWIND', default = DEFAULT_UNWIND, help = 'List of frame unwinding strategies (comma-separated)', action = 'store', type = 'str') 609 | config.add_option('LISTUNWINDS', default = False, help = 'List all known frame unwinding strategies', action = 'store_true') 610 | config.add_option("SYMBOLS", default = False, action = 'store_true', cache_invalidator = False, help = "Use symbol servers to resolve process addresses to module names (we assume symbol tables have already been built)") 611 | 612 | stack_registry = registry.get_plugin_classes(StackTop) 613 | 614 | if getattr(config, 'LISTUNWINDS', False): 615 | print "Stack Frame Unwind Strategies:\n" 616 | for cls_name, cls in sorted(stack_registry.items(), key=lambda v: v[0]): 617 | if cls_name not in ["UserFrame", "KernelFrame"]: 618 | print "{0:<20}: {1}\n".format(cls_name, pydoc.getdoc(cls)) 619 | sys.exit(0) 620 | 621 | self.kernel_strategies = [] 622 | self.user_strategies = [] 623 | for strategy in getattr(config, 'UNWIND', DEFAULT_UNWIND).split(","): 624 | if ":" in strategy: 625 | if strategy.startswith("kernel:"): 626 | strategy = strategy[len("kernel:"):] 627 | if strategy not in stack_registry or not issubclass(stack_registry[strategy], KernelFrame): 628 | debug.error("{0} is not a valid kernel stack unwinding strategy".format(strategy)) 629 | self.kernel_strategies.append(stack_registry[strategy]) 630 | elif strategy.startswith("user:"): 631 | strategy = strategy[len("user:"):] 632 | if strategy not in stack_registry or not issubclass(stack_registry[strategy], UserFrame): 633 | debug.error("{0} is not a valid user stack unwinding strategy".format(strategy)) 634 | self.user_strategies.append(stack_registry[strategy]) 635 | else: 636 | debug.error("{0} is an unrecognised stack".format(strategy.split(":")[0])) 637 | elif strategy not in stack_registry: 638 | debug.error("{0} is neither a valid kernel nor user stack unwinding strategy".format(strategy)) 639 | elif not issubclass(stack_registry[strategy], KernelFrame) and not issubclass(stack_registry[strategy], UserFrame): 640 | debug.error("{0} is neither a valid kernel nor stack unwinding strategy".format(strategy)) 641 | else: 642 | if issubclass(stack_registry[strategy], KernelFrame): 643 | self.kernel_strategies.append(stack_registry[strategy]) 644 | if issubclass(stack_registry[strategy], UserFrame): 645 | self.user_strategies.append(stack_registry[strategy]) 646 | 647 | self.use_symbols = getattr(config, 'SYMBOLS', False) 648 | 649 | # Determine which filters the user wants to see 650 | if getattr(config, 'FILTER', None): 651 | self.filters = set(config.FILTER.split(',')) 652 | else: 653 | self.filters = set() 654 | 655 | def render_text(self, outfd, data): 656 | kernel_start = 0 #self.kdbg.MmSystemRangeStart.dereference_as("Pointer") 657 | 658 | for thread, addr_space, system_mods, system_mod_addrs, instances, hooked_tables, system_range, owner_name in data: 659 | # If the user didn't set filters, display all results. If 660 | # the user set one or more filters, only show threads 661 | # with matching results. 662 | tags = set([t for t, v in instances.items() if v.check()]) 663 | 664 | if len(self.filters) > 0 and not self.filters & tags: 665 | continue 666 | 667 | threads.Threads.render_text(self, outfd, [(thread, addr_space, system_mods, system_mod_addrs, instances, hooked_tables, system_range, owner_name)]) 668 | self.carve_thread(outfd, thread, kernel_start) 669 | 670 | def check_for_exec_stack(self, stack_base, stack_limit, addrspace): 671 | exec_pages = [] 672 | for page in range(stack_base, stack_limit, 0x1000): 673 | if get_page_nx_bit(addrspace, page) == 0: 674 | exec_pages.append(page) 675 | return exec_pages 676 | 677 | def unwind_stack(self, outfd, sym_lookup, stack_frames): 678 | self.table_header(outfd, [ 679 | ('Frame', '<14'), 680 | ("Type", "<12"), 681 | ('Representation', '<50'), 682 | ('Executable Address', '<32'), 683 | ("Module", "<") 684 | ]) 685 | for number, (addr, frames) in stack_frames: 686 | if number == 0: 687 | frame_number = "current" 688 | else: 689 | frame_number = "current{0:+}".format(number) 690 | for frame in frames: 691 | module_name = sym_lookup(frame.caller) 692 | self.table_row(outfd, frame_number, frame.__class__.__name__, repr(frame), frame.exec_addr(), module_name) 693 | 694 | def unwind_strategy(self, unwind_strategies, start, stack_base, stack_limit, eproc): 695 | frames_up = list((a, list(fs)) for a, fs in itertools.groupby(sorted([ frame for strategy in unwind_strategies for frame in strategy(start, stack_base, stack_limit, eproc).up() ], key=lambda f: f.start, reverse=True), lambda f: f.start)) 696 | frames_down = list((a, list(fs)) for a, fs in itertools.groupby(sorted([ frame for strategy in unwind_strategies for frame in strategy(start, stack_base, stack_limit, eproc).down() ], key=lambda f: f.start), lambda f: f.start)) 697 | if len(frames_down) > 0 and frames_down[0][0] == start: 698 | frames_down = [ (start, [StackTop(start, stack_base, stack_limit, eproc)] + frames_down[0][1]) ] + frames_down[1:] 699 | else: 700 | frames_down = [ (start, [StackTop(start, stack_base, stack_limit, eproc)]) ] + frames_down 701 | return list(reversed(list(enumerate(frames_up, start=1)))) + [ (-n, f) for n, f in enumerate(frames_down, start=0) ] 702 | 703 | def carve_thread(self, outfd, thread, kernel_start): 704 | def lookup(eproc, addr): 705 | names = eproc.lookup(addr, use_symbols=self.use_symbols) 706 | if names == None: 707 | return "-" 708 | if len(names) == 0: 709 | return "UNKNOWN" 710 | ambiguity = "" 711 | if len(names) > 1: 712 | ambiguity = "[AMBIGUOUS: 1st of {0}] ".format(len(names)) 713 | return "{0}{1}".format(ambiguity, names[0]) 714 | 715 | trap_frame = thread.Tcb.TrapFrame.dereference_as("_KTRAP_FRAME") 716 | process_addrspace = thread.owning_process().get_process_address_space() 717 | eproc = thread.owning_process() 718 | 719 | #outfd.write("\n") 720 | 721 | #outfd.write("Kernel Start Address: {0:#010x}\n".format(kernel_start)) 722 | 723 | outfd.write("\n") 724 | kstack_base = thread.Tcb.InitialStack.v() 725 | kstack_limit = thread.Tcb.StackLimit.v() 726 | outfd.write("Kernel Stack:\n") 727 | outfd.write(" Range: {0:#08x}-{1:#08x}\n".format(kstack_limit, kstack_base)) 728 | outfd.write(" Size: {0:#x}\n".format(kstack_base - kstack_limit)) 729 | exec_pages = self.check_for_exec_stack(kstack_base, kstack_limit, process_addrspace) 730 | if len(exec_pages) > 0: 731 | outfd.write("Kernel Stack has pages marked as EXECUTABLE\n") 732 | esp = thread.Tcb.KernelStack.v() 733 | if thread.Tcb.KernelStackResident and process_addrspace.is_valid_address(esp+12) and kstack_limit < kstack_base: 734 | outfd.write("<== Kernel Stack Unwind ==>\n") 735 | # See [6] for how EBP is saved on the kernel stack and the reason for a magic value of 12 736 | ebp = process_addrspace.read_long_phys(process_addrspace.vtop(esp+12)) 737 | self.unwind_stack(outfd, functools.partial(lookup, eproc), self.unwind_strategy(self.kernel_strategies, ebp, kstack_base, kstack_limit, eproc)) 738 | outfd.write("<== End Kernel Stack Unwind ==>\n") 739 | elif not thread.Tcb.KernelStackResident: 740 | outfd.write("Kernel stack is not resident\n") 741 | 742 | teb = obj.Object('_TEB', offset = thread.Tcb.Teb.v(), vm = process_addrspace) 743 | 744 | outfd.write("\n") 745 | if teb.is_valid(): 746 | ustack_base = teb.NtTib.StackBase.v() 747 | ustack_limit = teb.NtTib.StackLimit.v() 748 | outfd.write("User Stack:\n") 749 | outfd.write(" Range: {0:#08x}-{1:#08x}\n".format(ustack_limit, ustack_base)) 750 | outfd.write(" Size: {0:#x}\n".format(ustack_base - ustack_limit)) 751 | exec_pages = self.check_for_exec_stack(ustack_base, ustack_limit, process_addrspace) 752 | if len(exec_pages) > 0: 753 | outfd.write("User Stack has pages marked as EXECUTABLE\n") 754 | if trap_frame.is_valid() and ustack_limit < ustack_base: 755 | outfd.write("<== User Stack Unwind ==>\n") 756 | def start_func(frame_class_name): 757 | if frame_class_name == "EXCEPTION_REGISTRATION_RECORD": 758 | teb = obj.Object('_TEB', thread.Tcb.Teb.v(), process_addrspace) 759 | return teb.NtTib.ExceptionList.v() 760 | else: 761 | return trap_frame.Ebp 762 | self.unwind_stack(outfd, functools.partial(lookup, eproc), self.unwind_strategy(self.user_strategies, start_func, ustack_base, ustack_limit, eproc)) 763 | outfd.write("<== End User Stack Unwind ==>\n") 764 | elif not trap_frame.is_valid(): 765 | outfd.write("User Trap Frame is Invalid\n") 766 | else: 767 | outfd.write("User Stack:\n") 768 | outfd.write(" TEB has been paged out?\n") 769 | -------------------------------------------------------------------------------- /symbols.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # symbols.py 4 | # Copyright (C) 2013 Carl Pulley 5 | # 6 | # Donated under Volatility Foundation, Inc. Individual Contributor Licensing 7 | # Agreement 8 | # 9 | # This program is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or (at 12 | # your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, but 15 | # WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | # General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program; if not, write to the Free Software 21 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 22 | # 23 | 24 | """ 25 | This code is designed to resolve addresses or symbol names. 26 | 27 | Use --build_symbols to build the *initial* SQLite database for the current profile. 28 | 29 | When the --use_symbols option is specified, then Microsoft's debugging symbol 30 | information is used to perform lookups. The symbol PDB files are downloaded and 31 | processed into SQLite3 DB entries that are saved within Volatility's caching 32 | directories. 33 | 34 | If --use_symbols is *not* specified, then module exports information is used to 35 | resolve lookups. 36 | 37 | @author: Carl Pulley 38 | @license: GNU General Public License 2.0 or later 39 | @contact: c.j.pulley@hud.ac.uk 40 | 41 | DEPENDENCIES: 42 | construct (pdbparse dependency) 43 | pdbparse (along with cabextract or 7z) - need to install >= 1.1 [>= r104] 44 | pefile (pdbparse dependency) 45 | sqlite3 46 | 47 | REFERENCES: 48 | [1] Fog, A. (2012). Calling conventions for different C++ compilers and operating systems. 49 | Retrieved from: 50 | http://www.agner.org/optimize/calling_conventions.pdf 51 | [2] Using the Mozilla symbol server. Retrieved 3/Apr/2013 from: 52 | https://developer.mozilla.org/en/docs/Using_the_Mozilla_symbol_server 53 | [3] Dolan-Gavitt, B. (2007). pdbparse - Open-source parser for Microsoft debug symbols. 54 | Retrieved 31/Mar/2013 from: 55 | https://code.google.com/p/pdbparse/ 56 | [4] Starodumov, O. (2004). Matching debug information. Retrieved from: 57 | http://www.debuginfo.com/articles/debuginfomatch.html 58 | [5] Mozilla (2007) symbolstore - Runs dump_syms over debug info files. Retrieved 59 | 7/Apr/2013 from: 60 | http://mxr.mozilla.org/mozilla-central/source/toolkit/crashreporter/tools/symbolstore.py 61 | [6] Wine (2006). pdb.c source file from the winedump tool. Retrieved 12/Aug/2013 from: 62 | http://source.winehq.org/source/tools/winedump/pdb.c 63 | [7] Schreiber, S. B. (2001). Undocumented Windows 2000 Secrets: A Programmers Cookbook. 64 | Addison-Wesley. ISBN 0-201-72187-2. 65 | """ 66 | 67 | try: 68 | import construct 69 | except ImportError: 70 | debug.error("construct (a pdbparse dependency) needs to be installed in order to use the symbols plugin/code") 71 | try: 72 | import pdbparse 73 | import pdbparse.peinfo 74 | from pdbparse.undecorate import undecorate as msvc_undecorate 75 | from pdbparse.undname import undname as msvcpp_undecorate 76 | except ImportError: 77 | debug.error("pdbparse (>= 1.1 [r104]) needs to be installed in order to use the symbols plugin/code") 78 | from inspect import isfunction 79 | import ntpath 80 | import os 81 | import re 82 | import shutil 83 | import sqlite3 84 | import sys 85 | import time 86 | import tempfile 87 | from urllib import FancyURLopener 88 | import urllib2 89 | import volatility.commands as commands 90 | import volatility.debug as debug 91 | import volatility.obj as obj 92 | import volatility.plugins.overlays.windows.windows as windows 93 | import volatility.utils as utils 94 | import volatility.win32.tasks as tasks 95 | 96 | # URLs to query when searching for debugging symbols 97 | SYM_URLS = [ 'http://msdl.microsoft.com/download/symbols', 'http://symbols.mozilla.org/firefox', 'http://chromium-browser-symsrv.commondatastorage.googleapis.com' ] 98 | 99 | class NameParser(object): 100 | def undecorate(self, dname): 101 | if dname.startswith("?"): 102 | # TODO: calculate stack space 103 | calling_conv = "UNDEFINED" 104 | parsed_calling_conv = filter(lambda x: x in ["__cdecl", "__pascal", "__thiscall", "__stdcall", "__fastcall", "__clrcall"], msvcpp_undecorate(dname, flags=0x0).split(" ")) 105 | if len(parsed_calling_conv) == 1: 106 | calling_conv = parsed_calling_conv[0] 107 | return (msvcpp_undecorate(dname), -1, calling_conv) 108 | else: 109 | return msvc_undecorate(dname) 110 | 111 | # Following is based on code taken from [3]: 112 | # https://code.google.com/p/pdbparse/source/browse/trunk/pdbparse/symchk.py 113 | class PDBOpener(FancyURLopener): 114 | version = "Microsoft-Symbol-Server/6.6.0007.5" 115 | def http_error_default(self, url, fp, errcode, errmsg, headers): 116 | if errcode == 404: 117 | raise urllib2.HTTPError(url, errcode, errmsg, headers, fp) 118 | else: 119 | FancyURLopener.http_error_default(url, fp, errcode, errmsg, headers) 120 | 121 | class DummyOmap(object): 122 | def remap(self, addr): 123 | return addr 124 | 125 | #-------------------------------------------------------------------------------- 126 | # profile modifications 127 | #-------------------------------------------------------------------------------- 128 | 129 | class SymbolTable(object): 130 | def __init__(self, addr_space, build_symbols): 131 | self.parser = NameParser() 132 | 133 | # Used to cache symbol/export/module information in an SQLite3 DB 134 | # NB: Volatility cache storeage area used, but we manage things manually. 135 | cache_sym_db_path = "{0}/symbols".format(addr_space._config.CACHE_DIRECTORY) 136 | if not os.path.exists(cache_sym_db_path): 137 | os.makedirs(cache_sym_db_path) 138 | self._sym_db_conn = sqlite3.connect("{0}/{1}.db".format(cache_sym_db_path, addr_space.profile.__class__.__name__)) 139 | db = self.get_cursor() 140 | db.execute("attach ':memory:' as volatility") 141 | self._sym_db_conn.commit() 142 | 143 | kdbg = tasks.get_kdbg(addr_space) 144 | 145 | # Build tables and indexes 146 | self.create_tables(db) 147 | 148 | # Insert kernel space module information 149 | debug.info("Building function symbol tables for kernel space...") 150 | for mod in kdbg.modules(): 151 | if build_symbols: 152 | # Module row created by add_exports if it doesn't already exist 153 | self.add_exports(db, mod) 154 | self.add_debug_symbols(db, kdbg.obj_vm, mod) 155 | # Link in system modules to each process 156 | for eproc in kdbg.processes(): 157 | self.add_module(db, eproc, mod) 158 | 159 | # Insert (per process) user space module information 160 | debug.info("Building function symbol tables for each processes user space...") 161 | for eproc in kdbg.processes(): 162 | debug.info("Processing PID {0} ...".format(int(eproc.UniqueProcessId))) 163 | for mod in eproc.get_load_modules(): 164 | if build_symbols: 165 | # Module row created by add_exports if it doesn't already exist 166 | self.add_exports(db, mod) 167 | self.add_debug_symbols(db, eproc.get_process_address_space(), mod) 168 | # Link in user module to current process 169 | self.add_module(db, eproc, mod) 170 | 171 | debug.info("Symbol tables successfully built") 172 | 173 | def __del__(self): 174 | self._sym_db_conn.close() 175 | 176 | def create_tables(self, db): 177 | """ 178 | Class Diagram 179 | ============= 180 | 181 | .................... 182 | : : 183 | : process *---------* module *-------* pdb -------* symbol 184 | : | : | | | 185 | : base : * mod_pdb * 186 | .................... export frame 187 | In-memory tables ^ 188 | | 189 | +---+---+ 190 | | | 191 | fpo fpov2 192 | """ 193 | 194 | db.execute("""CREATE TABLE IF NOT EXISTS module ( 195 | id INTEGER PRIMARY KEY, 196 | name TEXT NOT NULL, 197 | mpath TEXT NOT NULL, 198 | mlimit INTEGER NOT NULL, 199 | UNIQUE(mpath, name) 200 | )""") 201 | db.execute("CREATE INDEX IF NOT EXISTS module_name ON module(name)") 202 | 203 | db.execute("""CREATE TABLE IF NOT EXISTS export ( 204 | id INTEGER PRIMARY KEY, 205 | module_id INTEGER NOT NULL, 206 | ordinal INTEGER, 207 | name TEXT, 208 | rva INTEGER NOT NULL, 209 | FOREIGN KEY(module_id) REFERENCES module(id) 210 | )""") 211 | db.execute("CREATE INDEX IF NOT EXISTS export_name ON export(name)") 212 | db.execute("CREATE INDEX IF NOT EXISTS export_rva ON export(rva)") 213 | db.execute("CREATE INDEX IF NOT EXISTS export_mod_rva ON export(module_id, rva)") 214 | 215 | db.execute("""CREATE TABLE IF NOT EXISTS mod_pdb ( 216 | id INTEGER PRIMARY KEY, 217 | module_id INTEGER NOT NULL, 218 | pdb_id INTEGER NOT NULL, 219 | FOREIGN KEY(module_id) REFERENCES module(id), 220 | FOREIGN KEY(pdb_id) REFERENCES pdb(id) 221 | )""") 222 | db.execute("CREATE INDEX IF NOT EXISTS mod_pdb_link ON mod_pdb(module_id, pdb_id)") 223 | 224 | db.execute("""CREATE TABLE IF NOT EXISTS pdb ( 225 | id INTEGER PRIMARY KEY, 226 | file TEXT NOT NULL, 227 | guid TEXT, 228 | src TEXT, 229 | downloaded_at TEXT, 230 | UNIQUE(guid, file, src) 231 | )""") 232 | db.execute("CREATE INDEX IF NOT EXISTS pdb_file_guid ON pdb(file, guid)") 233 | 234 | db.execute("""CREATE TABLE IF NOT EXISTS symbol ( 235 | id INTEGER PRIMARY KEY, 236 | pdb_id INTEGER NOT NULL, 237 | type INTEGER NOT NULL, 238 | section TEXT NOT NULL, 239 | name TEXT NOT NULL, 240 | rva INTEGER NOT NULL, 241 | FOREIGN KEY(pdb_id) REFERENCES pdb(id) 242 | )""") 243 | db.execute("CREATE INDEX IF NOT EXISTS symbol_name ON symbol(section, name)") 244 | db.execute("CREATE INDEX IF NOT EXISTS symbol_rva ON symbol(rva)") 245 | db.execute("CREATE INDEX IF NOT EXISTS symbol_pdb_rva ON symbol(pdb_id, rva)") 246 | 247 | # Single table inheritance usage: 248 | # 249 | # table_name == "fpo": 250 | # NULL entries: max_stack; flags; program_string 251 | # non-NULL entries: frame; has_seh; use_bp 252 | # table_name == "fpov2": 253 | # NULL entries: frame; has_seh; use_bp 254 | # non-NULL entries: max_stack; flags; program_string 255 | db.execute("""CREATE TABLE IF NOT EXISTS frame ( 256 | id INTEGER PRIMARY KEY, 257 | pdb_id INTEGER NOT NULL, 258 | table_name TEXT NOT NULL, 259 | off_start INTEGER NOT NULL, 260 | proc_size INTEGER NOT NULL, 261 | locals INTEGER NOT NULL, 262 | params INTEGER NOT NULL, 263 | prolog INTEGER NOT NULL, 264 | saved_regs INTEGER NOT NULL, 265 | frame TEXT, 266 | has_seh INTEGER, 267 | use_bp INTEGER, 268 | max_stack INTEGER, 269 | flags INTEGER, 270 | program_string TEXT, 271 | FOREIGN KEY(pdb_id) REFERENCES pdb(id) 272 | )""") 273 | db.execute("CREATE INDEX IF NOT EXISTS frame_child ON frame(table_name)") 274 | 275 | db.execute("""CREATE TABLE IF NOT EXISTS volatility.process ( 276 | id INTEGER PRIMARY KEY, 277 | eproc INTEGER NOT NULL, 278 | UNIQUE (eproc) 279 | )""") 280 | db.execute("CREATE INDEX IF NOT EXISTS volatility.eproc_addr ON process(eproc)") 281 | 282 | db.execute("""CREATE TABLE IF NOT EXISTS volatility.base ( 283 | id INTEGER PRIMARY KEY, 284 | process_id INTEGER NOT NULL, 285 | module_id INTEGER NOT NULL, 286 | addr INTEGER NOT NULL, 287 | FOREIGN KEY(process_id) REFERENCES process(id), 288 | FOREIGN KEY(module_id) REFERENCES module(id) 289 | )""") 290 | db.execute("CREATE INDEX IF NOT EXISTS volatility.base_link ON base(process_id, module_id)") 291 | 292 | self._sym_db_conn.commit() 293 | 294 | def get_cursor(self): 295 | return self._sym_db_conn.cursor() 296 | 297 | def module_name(self, mod): 298 | # Normalised module name (without base directory) 299 | return ntpath.normpath(ntpath.normcase(ntpath.basename(str(mod.BaseDllName)))) 300 | 301 | def module_path(self, mod): 302 | # Normalised directory path holding module 303 | # FIXME: should we be performing any shell expansion here (e.g. SystemRoot == c:\\WINDOWS)? 304 | return ntpath.normpath(ntpath.normcase(ntpath.dirname(str(mod.FullDllName)))) 305 | 306 | def module_id(self, db, mod): 307 | mod_path = self.module_path(mod) 308 | mod_name = self.module_name(mod) 309 | 310 | db.execute("SELECT id FROM module WHERE mpath=? AND name=?", (mod_path, mod_name)) 311 | row = db.fetchone() 312 | if row == None: 313 | mod_name = self.module_name(mod) 314 | mod_path = self.module_path(mod) 315 | mod_limit = int(mod.SizeOfImage) 316 | db.execute("INSERT INTO module(name, mpath, mlimit) VALUES (?, ?, ?)", (mod_name, mod_path, mod_limit)) 317 | self._sym_db_conn.commit() 318 | db.execute("SELECT LAST_INSERT_ROWID() FROM module") 319 | row = db.fetchone() 320 | 321 | return row[0] 322 | 323 | def process_id(self, db, eproc): 324 | eproc_addr = int(eproc.v()) 325 | db.execute("SELECT id FROM volatility.process WHERE eproc=?", (eproc_addr,)) 326 | row = db.fetchone() 327 | if row == None: 328 | db.execute("INSERT INTO volatility.process(eproc) VALUES (?)", (eproc_addr,)) 329 | self._sym_db_conn.commit() 330 | db.execute("SELECT LAST_INSERT_ROWID() FROM volatility.process") 331 | row = db.fetchone() 332 | 333 | return row[0] 334 | 335 | def do_download(self, db, module_id, guid, filename): 336 | db.execute("""SELECT * 337 | FROM mod_pdb 338 | INNER JOIN pdb ON pdb_id=pdb.id 339 | WHERE module_id=? 340 | AND guid=? 341 | AND file=? 342 | AND downloaded_at IS NOT NULL 343 | """, (module_id, str(guid.upper()).rstrip('\0'), str(filename).rstrip('\0'))) 344 | row = db.fetchone() 345 | if row == None: 346 | return True 347 | return False 348 | 349 | def lookup_name(self, eproc, addr, use_symbols): 350 | def export_lookup(db, module_name, module_id, module_base, section_pad): 351 | db.execute("""SELECT rva 352 | FROM export 353 | WHERE module_id = :module_id 354 | AND rva <= :addr 355 | """, { "module_id": module_id, "addr": addr - module_base }) 356 | row = db.fetchone() 357 | if row == None or row[0] == None: 358 | return [ (module_name, section_pad, None, addr - module_base) ] 359 | minimal_rva = row[0] 360 | 361 | db.execute("""SELECT MAX(rva) 362 | FROM export 363 | WHERE module_id = :module_id 364 | AND :minimal_rva <= rva 365 | AND rva <= :addr 366 | """, { "module_id": module_id, "minimal_rva": minimal_rva, "addr": addr - module_base }) 367 | row = db.fetchone() 368 | max_rva = row[0] 369 | assert(max_rva != None) 370 | 371 | db.execute("""SELECT DISTINCT ordinal, name, :addr - rva 372 | FROM export 373 | WHERE module_id = :module_id 374 | AND rva = :max_rva 375 | """, { "module_id": module_id, "addr": addr - module_base, "max_rva": max_rva }) 376 | row = db.fetchall() 377 | assert(len(row) > 0) 378 | result = [] 379 | for ordinal, func_name, diff in row: 380 | if func_name == "": 381 | func_name = str(ordinal or '') 382 | result += [ (module_name, section_pad, func_name, diff) ] 383 | return result 384 | 385 | db = self.get_cursor() 386 | eproc_addr = int(eproc.v()) 387 | 388 | # Locate the module our address may reside in 389 | db.execute("""SELECT DISTINCT module.id, module.name, base.addr 390 | FROM volatility.process AS process 391 | INNER JOIN volatility.base AS base ON base.process_id = process.id 392 | INNER JOIN module ON base.module_id = module.id 393 | WHERE process.eproc = :eproc 394 | AND base.addr <= :addr 395 | AND :addr < base.addr + module.mlimit 396 | """, { "eproc": eproc_addr, "addr": addr }) 397 | row = db.fetchall() 398 | if len(row) == 0: 399 | return [] 400 | assert(len(row) == 1) 401 | module_id, module_name, module_base = row[0] 402 | 403 | # Locate the module's symbol range our address may reside in 404 | if use_symbols: 405 | db.execute("""SELECT pdb.id 406 | FROM mod_pdb 407 | INNER JOIN pdb ON mod_pdb.pdb_id = pdb.id 408 | INNER JOIN symbol ON pdb.id = symbol.pdb_id 409 | WHERE module_id = :module_id 410 | AND rva <= :addr 411 | """, { "module_id": module_id, "addr": addr - module_base }) 412 | row = db.fetchone() 413 | if row == None or row[0] == None: 414 | return export_lookup(db, module_name, module_id, module_base, "") 415 | pdb_id = row[0] 416 | 417 | db.execute("""SELECT MAX(rva) 418 | FROM pdb 419 | INNER JOIN symbol ON pdb.id = symbol.pdb_id 420 | WHERE pdb.id = :pdb_id 421 | AND rva <= :addr 422 | """, { "pdb_id": pdb_id, "addr": addr - module_base }) 423 | row = db.fetchone() 424 | max_rva = row[0] 425 | assert(max_rva != None) 426 | 427 | db.execute("""SELECT DISTINCT section, name, :addr - rva 428 | FROM pdb 429 | INNER JOIN symbol ON pdb.id = symbol.pdb_id 430 | WHERE pdb.id = :pdb_id 431 | AND rva = :max_rva 432 | """, { "pdb_id": pdb_id, "addr": addr - module_base, "max_rva": max_rva }) 433 | row = db.fetchall() 434 | assert(len(row) > 0) 435 | 436 | return [ (module_name, section_name, func_name, diff) for section_name, func_name, diff in row ] 437 | else: 438 | return export_lookup(db, module_name, module_id, module_base, "????") 439 | 440 | # TODO: implement mapping from name to decorated form? 441 | def lookup_addr(self, eproc, name, section, module, use_symbols): 442 | db = self.get_cursor() 443 | 444 | if use_symbols: 445 | if section != None and module != None: 446 | sql_query = """SELECT base.addr + rva 447 | FROM symbol 448 | INNER JOIN pdb ON symbol.pdb_id = pdb.id 449 | INNER JOIN mod_pdb ON mod_pdb.pdb_id = pdb.id 450 | INNER JOIN module ON mod_pdb.module_id = module.id 451 | INNER JOIN volatility.base AS base ON base.module_id = module.id 452 | INNER JOIN volatility.process AS process ON base.process_id = process.id 453 | WHERE section LIKE :section 454 | AND module.name LIKE :module 455 | AND symbol.name LIKE :name 456 | AND process.eproc = :eproc 457 | """ 458 | sql_vars = { "eproc": int(eproc.v()), "name": name, "section": section, "module": module } 459 | elif section == None and module != None: 460 | sql_query = """SELECT base.addr + rva 461 | FROM symbol 462 | INNER JOIN pdb ON symbol.pdb_id = pdb.id 463 | INNER JOIN mod_pdb ON mod_pdb.pdb_id = pdb.id 464 | INNER JOIN module ON mod_pdb.module_id = module.id 465 | INNER JOIN volatility.base AS base ON base.module_id = module.id 466 | INNER JOIN volatility.process AS process ON base.process_id = process.id 467 | WHERE module.name LIKE :module 468 | AND symbol.name LIKE :name 469 | AND process.eproc = :eproc 470 | """ 471 | sql_vars = { "eproc": int(eproc.v()), "name": name, "module": module } 472 | elif section != None and module == None: 473 | sql_query = """SELECT base.addr + rva 474 | FROM symbol 475 | INNER JOIN pdb ON symbol.pdb_id = pdb.id 476 | INNER JOIN mod_pdb ON mod_pdb.pdb_id = pdb.id 477 | INNER JOIN module ON mod_pdb.module_id = module.id 478 | INNER JOIN volatility.base AS base ON base.module_id = module.id 479 | INNER JOIN volatility.process AS process ON base.process_id = process.id 480 | WHERE section LIKE :section 481 | AND symbol.name LIKE :name 482 | AND process.eproc = :eproc 483 | """ 484 | sql_vars = { "eproc": int(eproc.v()), "name": name, "section": section } 485 | else: 486 | sql_query = """SELECT base.addr + rva 487 | FROM symbol 488 | INNER JOIN pdb ON symbol.pdb_id = pdb.id 489 | INNER JOIN mod_pdb ON mod_pdb.pdb_id = pdb.id 490 | INNER JOIN module ON mod_pdb.module_id = module.id 491 | INNER JOIN volatility.base AS base ON base.module_id = module.id 492 | INNER JOIN volatility.process AS process ON base.process_id = process.id 493 | WHERE symbol.name LIKE :name 494 | AND process.eproc = :eproc 495 | """ 496 | sql_vars = { "eproc": int(eproc.v()), "name": name } 497 | else: 498 | if module != None: 499 | sql_query = """SELECT base.addr + rva 500 | FROM export 501 | INNER JOIN module ON export.module_id = module.id 502 | INNER JOIN volatility.base AS base ON base.module_id = module.id 503 | INNER JOIN volatility.process AS process ON base.process_id = process.id 504 | WHERE export.name LIKE :name 505 | AND module.name LIKE :module 506 | AND process.eproc = :eproc 507 | """ 508 | sql_vars = { "eproc": int(eproc.v()), "name": name, "module": module } 509 | else: 510 | sql_query = """SELECT base.addr + rva 511 | FROM export 512 | INNER JOIN module ON export.module_id = module.id 513 | INNER JOIN volatility.base AS base ON base.module_id = module.id 514 | INNER JOIN volatility.process AS process ON base.process_id = process.id 515 | WHERE export.name LIKE :name 516 | AND process.eproc = :eproc 517 | """ 518 | sql_vars = { "eproc": int(eproc.v()), "name": name } 519 | 520 | return [ row[0] for row in db.execute(sql_query, sql_vars) ] 521 | 522 | def add_module(self, db, eproc, mod): 523 | process_id = self.process_id(db, eproc) 524 | module_id = self.module_id(db, mod) 525 | mod_base = int(mod.DllBase) 526 | db.execute("SELECT * FROM volatility.base WHERE addr=? AND process_id=? AND module_id=?", (mod_base, process_id, module_id)) 527 | row = db.fetchone() 528 | if row == None: 529 | # Not previously seen this process/module addressing relationship 530 | db.execute("INSERT INTO volatility.base(addr, process_id, module_id) VALUES (?, ?, ?)", (mod_base, process_id, module_id)) 531 | self._sym_db_conn.commit() 532 | 533 | def add_exports(self, db, mod): 534 | module_id = self.module_id(db, mod) 535 | 536 | for ordinal, func_addr, func_name in mod.exports(): 537 | # FIXME: when mapping names to addresses, is this correct behaviour? 538 | # Here we ignore forwarded exports as no symbol address should resolve to them! 539 | if func_addr != None: 540 | db.execute("SELECT name FROM export WHERE module_id=? AND rva=?", (module_id, int(func_addr))) 541 | row = db.fetchone() 542 | if row == None: 543 | # We've not previously seen this function export 544 | db.execute("INSERT INTO export(module_id, ordinal, name, rva) VALUES (?, ?, ?, ?)", (module_id, int(ordinal), str(func_name or '').rstrip('\0'), int(func_addr))) 545 | else: 546 | # Only update if the function name is currently unknown (i.e. it was previously referred to via an ordinal) 547 | old_func_name = row[0] 548 | if old_func_name == '' and func_name != None: 549 | db.execute("UPDATE export SET name=? WHERE module_id=? AND rva=?", (str(func_name).rstrip('\0'), module_id, int(func_addr))) 550 | 551 | self._sym_db_conn.commit() 552 | 553 | # Following is based on code taken from [3]: 554 | # https://code.google.com/p/pdbparse/source/browse/trunk/pdbparse/symlookup.py 555 | def add_debug_symbols(self, db, addr_space, mod): 556 | def is_valid_debug_dir(debug_dir, image_base, addr_space): 557 | return debug_dir != None and debug_dir.AddressOfRawData != 0 and addr_space.is_valid_address(image_base + debug_dir.AddressOfRawData) and addr_space.is_valid_address(image_base + debug_dir.AddressOfRawData + debug_dir.SizeOfData - 1) 558 | 559 | mod_path = self.module_path(mod) 560 | mod_name = self.module_name(mod) 561 | module_id = self.module_id(db, mod) 562 | image_base = mod.DllBase 563 | debug_dir = mod.get_debug_directory() 564 | if not is_valid_debug_dir(debug_dir, image_base, addr_space): 565 | # No joy in this process space 566 | return 567 | if debug_dir.Type != 2: # IMAGE_DEBUG_TYPE_CODEVIEW 568 | return 569 | debug_data = addr_space.zread(image_base + debug_dir.AddressOfRawData, debug_dir.SizeOfData) 570 | if debug_data[:4] == 'RSDS': 571 | guid, filename = pdbparse.peinfo.get_rsds(debug_data) 572 | elif debug_data[:4] == 'NB10': 573 | guid, filename = pdbparse.peinfo.get_nb10(debug_data) 574 | else: 575 | debug.warning("'{0}' is an unknown CodeView section".format(debug_data[:4])) 576 | return 577 | if filename == '': 578 | return 579 | # We have valid CodeView debugging information, so try and download PDB file from a symbol server 580 | saved_mod_path = tempfile.gettempdir() + os.sep + guid 581 | if self.do_download(db, module_id, guid, filename): 582 | if not os.path.exists(saved_mod_path): 583 | os.makedirs(saved_mod_path) 584 | self.download_pdbfile(db, guid.upper(), module_id, filename, saved_mod_path) 585 | # At this point, and if all has gone well, known co-ords should be: mod; image_base; guid; filename 586 | pdbname = saved_mod_path + os.sep + filename 587 | if not os.path.exists(pdbname): 588 | shutil.rmtree(saved_mod_path) 589 | debug.warning("Symbols for module {0} (in {1}) not found".format(mod.BaseDllName, filename)) 590 | return 591 | 592 | try: 593 | # Do this the hard way to avoid having to load 594 | # the types stream in mammoth PDB files 595 | pdb = pdbparse.parse(pdbname, fast_load=True) 596 | pdb.STREAM_DBI.load() 597 | # As pdbparse doesn't add STREAM_FPO_STRINGS to parent, do it manually 598 | if pdb.STREAM_DBI.DBIDbgHeader.snNewFPO != -1: 599 | pdb.add_supported_stream("STREAM_FPO_STRINGS", pdb.STREAM_DBI.DBIDbgHeader.snNewFPO+1, pdbparse.PDBFPOStrings) 600 | except AttributeError: 601 | pass 602 | 603 | try: 604 | pdb._update_names() 605 | pdb.STREAM_GSYM = pdb.STREAM_GSYM.reload() 606 | pdb.STREAM_GSYM.load() 607 | pdb.STREAM_SECT_HDR = pdb.STREAM_SECT_HDR.reload() 608 | pdb.STREAM_SECT_HDR.load() 609 | # These are the dicey ones 610 | pdb.STREAM_OMAP_FROM_SRC = pdb.STREAM_OMAP_FROM_SRC.reload() 611 | pdb.STREAM_OMAP_FROM_SRC.load() 612 | pdb.STREAM_SECT_HDR_ORIG = pdb.STREAM_SECT_HDR_ORIG.reload() 613 | pdb.STREAM_SECT_HDR_ORIG.load() 614 | except AttributeError: 615 | pass 616 | 617 | try: 618 | # Ensure FPO streams are loaded 619 | pdb.STREAM_FPO = pdb.STREAM_FPO.reload() 620 | pdb.STREAM_FPO.load() 621 | pdb.STREAM_FPO_NEW = pdb.STREAM_FPO_NEW.reload() 622 | pdb.STREAM_FPO_NEW.load() 623 | pdb.STREAM_FPO_STRINGS = pdb.STREAM_FPO_STRINGS.reload() 624 | pdb.STREAM_FPO_STRINGS.load() 625 | pdb.STREAM_FPO_NEW.load2() # inject program strings 626 | except AttributeError: 627 | pass 628 | except construct.adapters.ConstError as err: 629 | # TODO: fix actual PDB parsing failure issue! 630 | debug.warning("Ignoring PDB parsing failure of {0}/{1}: {2}".format(guid.upper(), filename, err.message)) 631 | 632 | db.execute("""SELECT pdb.id 633 | FROM mod_pdb 634 | INNER JOIN pdb ON pdb_id=pdb.id 635 | WHERE module_id=? 636 | AND guid=? 637 | AND file=? 638 | """, (module_id, str(guid.upper()).rstrip('\0'), str(filename).rstrip('\0'))) 639 | row = db.fetchone() 640 | assert(row != None) 641 | pdb_id = row[0] 642 | 643 | self.process_gsyms(db, pdb, pdb_id, module_id, guid, filename) 644 | try: 645 | self.process_fpo(db, pdb, pdb_id, module_id, guid, filename) 646 | except AttributeError: 647 | pass 648 | try: 649 | self.process_fpov2(db, pdb, pdb_id, module_id, guid, filename) 650 | except AttributeError: 651 | pass 652 | 653 | shutil.rmtree(saved_mod_path) 654 | debug.info("Removed directory {0} and its contents".format(saved_mod_path)) 655 | self._sym_db_conn.commit() 656 | 657 | def process_gsyms(self, db, pdb, pdb_id, module_id, guid, filename): 658 | try: 659 | sects = pdb.STREAM_SECT_HDR_ORIG.sections 660 | omap = pdb.STREAM_OMAP_FROM_SRC 661 | except AttributeError: 662 | # In this case there is no OMAP, so we use the given section 663 | # headers and use the identity function for omap.remap 664 | sects = pdb.STREAM_SECT_HDR.sections 665 | omap = DummyOmap() 666 | gsyms = pdb.STREAM_GSYM 667 | 668 | for sym in gsyms.globals: 669 | if not hasattr(sym, 'offset'): 670 | continue 671 | off = sym.offset 672 | try: 673 | virt_base = sects[sym.segment-1].VirtualAddress 674 | section = sects[sym.segment-1].Name 675 | except IndexError: 676 | continue 677 | 678 | sym_rva = omap.remap(off+virt_base) 679 | 680 | db.execute("INSERT INTO symbol(pdb_id, type, section, name, rva) VALUES (?, ?, ?, ?, ?)", (pdb_id, int(sym.symtype), str(section).rstrip('\0'), str(sym.name).rstrip('\0'), int(sym_rva))) 681 | 682 | def process_fpo(self, db, pdb, pdb_id, module_id, guid, filename): 683 | data_stream = pdb.STREAM_FPO 684 | for fpo in data_stream.fpo: 685 | db.execute("""INSERT INTO frame(pdb_id, table_name, off_start, proc_size, locals, params, prolog, saved_regs, frame, has_seh, use_bp) 686 | VALUES (:pdb_id, 'fpo', :off_start, :proc_size, :locals, :params, :prolog, :saved_regs, :frame, :has_seh, :use_bp) 687 | """, { 688 | "pdb_id": pdb_id, 689 | "off_start": int(fpo.ulOffStart), 690 | "proc_size": int(fpo.cbProcSize), 691 | "locals": int(fpo.cdwLocals), 692 | "params": int(fpo.cdwParams), 693 | "prolog": int(fpo.cbProlog), 694 | "saved_regs": int(fpo.cbRegs), 695 | "frame": int(fpo.cbFrame), 696 | "has_seh": int(fpo.fHasSEH), 697 | "use_bp": int(fpo.fUseBP) 698 | }) 699 | 700 | def process_fpov2(self, db, pdb, pdb_id, module_id, guid, filename): 701 | def flags_to_int(flags): 702 | return 1*int(flags.SEH) + 2*int(flags.CPPEH) + 4*int(flags.fnStart) 703 | 704 | data_stream = pdb.STREAM_FPO_NEW 705 | for fpo in data_stream.fpo: 706 | db.execute("""INSERT INTO frame(pdb_id, table_name, off_start, proc_size, locals, params, prolog, saved_regs, max_stack, flags, program_string) 707 | VALUES (:pdb_id, 'fpov2', :off_start, :proc_size, :locals, :params, :prolog, :saved_regs, :max_stack, :flags, :program_string) 708 | """, { 709 | "pdb_id": pdb_id, 710 | "off_start": int(fpo.ulOffStart), 711 | "proc_size": int(fpo.cbProcSize), 712 | "locals": int(fpo.cbLocals), 713 | "params": int(fpo.cbParams), 714 | "prolog": int(fpo.cbProlog), 715 | "saved_regs": int(fpo.cbSavedRegs), 716 | "max_stack": int(fpo.maxStack), 717 | "flags": flags_to_int(fpo.flags), 718 | "program_string": str(fpo.ProgramString) 719 | }) 720 | 721 | lastprog = None 722 | def progress(self, blocks, blocksz, totalsz): 723 | if self.lastprog == None: 724 | debug.info("Connected. Downloading data...") 725 | percent = int((100*(blocks*blocksz)/float(totalsz))) 726 | if self.lastprog != percent and percent % 5 == 0: 727 | debug.info("{0}%".format(percent)) 728 | self.lastprog = percent 729 | 730 | def download_pdbfile(self, db, guid, module_id, filename, path): 731 | db.execute("SELECT id FROM pdb WHERE guid=? AND file=?", (str(guid.upper()).rstrip('\0'), str(filename).rstrip('\0'))) 732 | row = db.fetchone() 733 | if row == None: 734 | db.execute("INSERT INTO pdb(guid, file) VALUES (?, ?)", (str(guid.upper()).rstrip('\0'), str(filename).rstrip('\0'))) 735 | db.execute("SELECT LAST_INSERT_ROWID() FROM pdb") 736 | row = db.fetchone() 737 | pdb_id = row[0] 738 | db.execute("SELECT * FROM mod_pdb WHERE module_id=? AND pdb_id=?", (module_id, pdb_id)) 739 | row = db.fetchone() 740 | if row == None: 741 | db.execute("INSERT INTO mod_pdb(module_id, pdb_id) VALUES (?, ?)", (module_id, pdb_id)) 742 | self._sym_db_conn.commit() 743 | 744 | for sym_url in SYM_URLS: 745 | url = "{0}/{1}/{2}/".format(sym_url, filename, guid) 746 | proxy = urllib2.ProxyHandler() 747 | opener = urllib2.build_opener(proxy) 748 | tries = [ filename[:-1] + '_', filename ] 749 | for t in tries: 750 | debug.info("Trying {0}".format(url+t)) 751 | outfile = os.path.join(path, t) 752 | try: 753 | PDBOpener().retrieve(url+t, outfile, reporthook=self.progress) 754 | debug.info("Downloaded symbols and cached at {0}".format(outfile)) 755 | if t.endswith("_"): 756 | self.cabextract(outfile, path) 757 | debug.info("Unpacked download into {0}".format(path)) 758 | os.remove(outfile) 759 | db.execute("UPDATE pdb SET downloaded_at=DATETIME('now'), src=? WHERE id=? AND guid=? AND file=?", (sym_url, pdb_id, str(guid.upper()).rstrip('\0'), str(filename).rstrip('\0'))) 760 | self._sym_db_conn.commit() 761 | return 762 | except urllib2.HTTPError, e: 763 | debug.warning("HTTP error {0}".format(e.code)) 764 | debug.warning("Failed to download debugging symbols for {0}".format(filename)) 765 | 766 | cabsetup = None 767 | def cabextract(self, filename, path): 768 | def _cabextract(filename, path): 769 | os.system("cabextract -d{0} {1}".format(path, filename)) 770 | def _7z(filename, path): 771 | os.system("7z -o{0} e {1}".format(path, filename)) 772 | 773 | if self.cabsetup != None: 774 | self.cabsetup(filename, path) 775 | return 776 | for cabfunc in ["_cabextract", "_7z"]: 777 | try: 778 | self.cabsetup = locals()[cabfunc] 779 | self.cabsetup(filename, path) 780 | return 781 | except: 782 | pass 783 | debug.error("could not unpack the downloaded CAB file - please install cabextract or 7z") 784 | 785 | class SymbolsEPROCESS(windows._EPROCESS): 786 | _symbol_table = None 787 | def symbol_table(self, build_symbols=False): 788 | """ 789 | Returns the SymbolTable object instance used to access symbol information from the underlying 790 | SQLite3 DB. 791 | 792 | If the build_symbols option is True, then the SQLite3 DB will be rebuilt (if it doesn't exist) 793 | or updated (if it exists). 794 | """ 795 | if SymbolsEPROCESS._symbol_table == None: 796 | SymbolsEPROCESS._symbol_table = SymbolTable(self.get_process_address_space(), build_symbols) 797 | 798 | return SymbolsEPROCESS._symbol_table 799 | 800 | def lookup(self, addr_or_name, use_symbols=True): 801 | """ 802 | When an address is given, resolve to the nearest symbol names within this processes 803 | symbol table. If use_symbols is specified, addresses can resolve to names with the 804 | format: 805 | 806 | module.pdb/section!name+0x0FF 807 | 808 | If use_symbols is *not* specified, addresses resolve to names with the format: 809 | 810 | module/????!name+0x0FF 811 | 812 | Here, ???? indicates that exports are being used (i.e. an unknown section). 813 | 814 | When a name is given, resolve to the matching symbol name within this processes symbol 815 | table and return matching addresses. Symbols/names may be specified using a string 816 | matching the format (N.B. '%' can be used as a wild card): 817 | 818 | MODULE/SECTION!NAME 819 | 820 | If use_symbols is True, then resolving occurs using Microsoft's debugging symbol information. 821 | Otherwise, resolving occurs using the module exports information. 822 | """ 823 | if addr_or_name == None: 824 | return None 825 | elif type(addr_or_name) == int or type(addr_or_name) == long: 826 | names = self.symbol_table().lookup_name(self, int(addr_or_name), use_symbols) 827 | result = [] 828 | for module_name, section_name, func_name, diff in names: 829 | if func_name == None: 830 | result += [ "{0}/{1}!{2:+#x}".format(module_name, section_name, diff) ] 831 | else: 832 | if diff == 0: 833 | diff = "" 834 | else: 835 | diff = "{0:+#x}".format(diff) 836 | func_name = str(self.symbol_table().parser.undecorate(str(func_name))[0]) 837 | result += [ "{0}/{1}!{2}{3}".format(module_name, section_name, func_name, diff) ] 838 | return result 839 | elif type(addr_or_name) == str: 840 | pattern = re.compile("\A(((?P{0}+)/)?(?P
{0}*)!)?(?P{0}+)\Z".format("[a-zA-Z0-9_@\?\$\.%]")) 841 | mobj = pattern.match(addr_or_name) 842 | if mobj == None: 843 | raise ValueError("lookup: symbol name needs to match the format MODULE/SECTION!NAME") 844 | name = mobj.group("name") 845 | section = mobj.group("section") 846 | module = mobj.group("module") 847 | return self.symbol_table().lookup_addr(self, name, section, module, use_symbols) 848 | else: 849 | raise TypeError("lookup: primary argument should be an integer (i.e. an address) or a string (i.e. a symbol name), not of type {0}".format(type(addr_or_name))) 850 | 851 | class SymbolsModification(obj.ProfileModification): 852 | before = ["WindowsObjectClasses"] 853 | 854 | conditions = {'os': lambda x: x == 'windows'} 855 | 856 | def modification(self, profile): 857 | for func in SymbolsEPROCESS.__dict__: 858 | if isfunction(SymbolsEPROCESS.__dict__[func]): 859 | setattr(profile.object_classes['_EPROCESS'], func, SymbolsEPROCESS.__dict__[func]) 860 | 861 | #-------------------------------------------------------------------------------- 862 | # main plugin code 863 | #-------------------------------------------------------------------------------- 864 | 865 | class Symbols(commands.Command): 866 | """ 867 | Symbols objects are used to resolve addresses (e.g. functions, methods, etc.) typically 868 | using Microsoft's debugging symbols (i.e. PDB files). Name resolution is performed (via 869 | the lookup method) relative to a given processes address space. 870 | 871 | When --use_symbols is specified, addresses are resolved using Microsoft's debugging symbols. 872 | 873 | When --use_symbols is *not* specified, address resolution uses the module export symbols. 874 | 875 | NOTE: before using this plugin, it is necessary to first ensure that the symbols table (i.e. an 876 | SQLite3 DB) has been built. Use the --build_symbols option for this purpose. 877 | """ 878 | 879 | def __init__(self, config, *args, **kwargs): 880 | commands.Command.__init__(self, config, *args, **kwargs) 881 | config.add_option("BUILD_SYMBOLS", default = False, action = 'store_true', cache_invalidator = False, help = "Build symbol table information") 882 | config.add_option("USE_SYMBOLS", default = False, action = 'store_true', cache_invalidator = False, help = "Use symbol servers to resolve process addresses to module names") 883 | 884 | self.kdbg = tasks.get_kdbg(utils.load_as(config)) 885 | self.build_symbols = getattr(config, 'BUILD_SYMBOLS', False) 886 | self.use_symbols = getattr(config, 'USE_SYMBOLS', False) 887 | 888 | def render_text(self, outfd, data): 889 | self.table_header(outfd, [ 890 | ('Process ID', '<16'), 891 | ('Module', '<16'), 892 | ('Section', '<16'), 893 | ("Symbol Address", '<16'), 894 | ("Symbol Name", "<") 895 | ]) 896 | parser = NameParser() 897 | for eproc in self.kdbg.processes(): 898 | pid = int(eproc.UniqueProcessId) 899 | eproc_addr = int(eproc.v()) 900 | sym_obj = eproc.symbol_table(build_symbols=self.build_symbols) 901 | db = sym_obj.get_cursor() 902 | 903 | if self.use_symbols: 904 | sql_query = """SELECT module.name, symbol.section, base.addr + rva, symbol.name 905 | FROM volatility.process AS process 906 | INNER JOIN volatility.base AS base ON process.id = base.process_id 907 | INNER JOIN module ON module.id = base.module_id 908 | INNER JOIN mod_pdb ON module.id = mod_pdb.module_id 909 | INNER JOIN pdb ON pdb.id = mod_pdb.pdb_id 910 | INNER JOIN symbol ON pdb.id = symbol.pdb_id 911 | WHERE eproc = ? 912 | ORDER BY base.addr + rva ASC 913 | """ 914 | else: 915 | sql_query = """SELECT module.name, export.ordinal, base.addr + rva, export.name 916 | FROM volatility.process AS process 917 | INNER JOIN volatility.base AS base ON process.id = base.process_id 918 | INNER JOIN module ON module.id = base.module_id 919 | INNER JOIN export ON module.id = export.module_id 920 | WHERE eproc = ? 921 | ORDER BY base.addr + rva ASC 922 | """ 923 | 924 | for module, section_or_ordinal, sym_addr, raw_sym_name in db.execute(sql_query, (eproc_addr,)): 925 | sym_name = parser.undecorate(raw_sym_name)[0] 926 | if sym_name == "?": 927 | sym_name = raw_sym_name 928 | if self.use_symbols: 929 | section = section_or_ordinal 930 | else: 931 | section = "????" 932 | if sym_name == "": 933 | sym_name = str(section_or_ordinal) 934 | self.table_row(outfd, pid, module, section, "{0:#010x}".format(sym_addr), sym_name) 935 | -------------------------------------------------------------------------------- /volshell/linux/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlpulley/volatility/eef6c3b21524a91c91758609526e8c41b34a5cfe/volshell/linux/__init__.py -------------------------------------------------------------------------------- /volshell/linux/volshell.py: -------------------------------------------------------------------------------- 1 | # Volatility 2 | # Copyright (C) 2008 Volatile Systems 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or (at 7 | # your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 17 | # 18 | 19 | import volatility.plugins.linux.pslist as pslist 20 | import volatility.plugins.volshell as volshell 21 | 22 | class LinuxVolshell(volshell.BaseVolshell): 23 | """Shell in the (Linux) memory image""" 24 | 25 | @staticmethod 26 | def is_valid_profile(profile): 27 | return profile.metadata.get('os', 'Unknown').lower() == 'linux' 28 | 29 | def getPidList(self): 30 | return pslist.linux_pslist(self._config).calculate() 31 | 32 | def getPid(self, proc): 33 | return proc.pid 34 | 35 | def getPPid(self, proc): 36 | return proc.p_pptr.pid 37 | 38 | def getImageName(self, proc): 39 | return proc.comm 40 | 41 | def getDTB(self, proc): 42 | return self.addrspace.vtop(proc.mm.pgd) or proc.mm.pgd 43 | 44 | def ps(self, render=True, pid=None, **kwargs): 45 | """Print a process listing. 46 | 47 | Prints a process listing with PID, PPID, image name, and offset. 48 | """ 49 | if pid: 50 | if type(pid).__name__ == 'int': 51 | pid = str(pid) 52 | if type(pid).__name__ == 'list': 53 | if len(pid) == 0: 54 | pid = None 55 | else: 56 | pid = ",".join([ str(p) for p in pid ]) 57 | return self.linux_pslist(render=render, pid=pid, **kwargs) 58 | -------------------------------------------------------------------------------- /volshell/mac/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlpulley/volatility/eef6c3b21524a91c91758609526e8c41b34a5cfe/volshell/mac/__init__.py -------------------------------------------------------------------------------- /volshell/mac/volshell.py: -------------------------------------------------------------------------------- 1 | # Volatility 2 | # Copyright (C) 2008 Volatile Systems 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or (at 7 | # your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 17 | # 18 | 19 | import volatility.plugins.mac.pslist as pslist 20 | import volatility.plugins.volshell as volshell 21 | 22 | class MacVolshell(volshell.BaseVolshell): 23 | """Shell in the (Mac) memory image""" 24 | 25 | @staticmethod 26 | def is_valid_profile(profile): 27 | return profile.metadata.get('os', 'Unknown').lower() == 'mac' 28 | 29 | def getPidList(self): 30 | return pslist.mac_pslist(self._config).calculate() 31 | 32 | def getPid(self, proc): 33 | return proc.p_pid 34 | 35 | def getPPid(self, proc): 36 | return proc.p_pptr.p_pid 37 | 38 | def getImageName(self, proc): 39 | return proc.p_comm 40 | 41 | def getDTB(self, proc): 42 | return proc.task.dereference_as("task").map.pmap.pm_cr3 43 | 44 | def ps(self, render=True, pid=None, **kwargs): 45 | """Print a process listing. 46 | 47 | Prints a process listing with PID, PPID, image name, and offset. 48 | """ 49 | if pid: 50 | if type(pid).__name__ == 'int': 51 | pid = str(pid) 52 | if type(pid).__name__ == 'list': 53 | if len(pid) == 0: 54 | pid = None 55 | else: 56 | pid = ",".join([ str(p) for p in pid ]) 57 | return self.mac_pslist(render=render, pid=pid, **kwargs) 58 | -------------------------------------------------------------------------------- /volshell/volshell.py: -------------------------------------------------------------------------------- 1 | # Volatility 2 | # Copyright (C) 2008 Volatile Systems 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or (at 7 | # your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 17 | # 18 | 19 | """ 20 | @author: AAron Walters, Brendan Dolan-Gavitt and Carl Pulley 21 | @license: GNU General Public License 2.0 or later 22 | @contact: awalters@volatilesystems.com, bdolangavitt@wesleyan.edu, c.j.pulley@hud.ac.uk 23 | @organization: Volatile Systems 24 | """ 25 | 26 | from inspect import currentframe, getargspec, isfunction, ismethod 27 | import os 28 | import pydoc 29 | import struct 30 | import sys 31 | import volatility.addrspace as addrspace 32 | import volatility.commands as commands 33 | import volatility.conf as conf 34 | import volatility.debug as debug 35 | import volatility.exceptions as exceptions 36 | import volatility.obj as obj 37 | import volatility.plugins as plugins 38 | import volatility.registry as registry 39 | import volatility.utils as utils 40 | 41 | try: 42 | import distorm3 #pylint: disable-msg=F0401 43 | except ImportError: 44 | pass 45 | 46 | class VolshellException(exceptions.VolatilityException): 47 | """General exception for handling errors whilst processing shell commands""" 48 | pass 49 | 50 | class Config(conf.ConfObject): 51 | def __init__(self): 52 | conf.ConfObject.__init__(self) 53 | self.option_hook = {} 54 | 55 | def add_option(self, option, **args): 56 | option = option.lower().replace("-", "_") 57 | self.option_hook[option] = { 'default': str(args['default']) if 'default' in args else "", 'help': args['help'] if 'help' in args else "", 'type': args['type'] if 'type' in args else '' } 58 | if 'type' in args and args['type'] == 'choice': 59 | self.option_hook[option]['choices'] = args['choices'] if 'choices' in args else [] 60 | 61 | class Plugin(object): 62 | def __init__(self, shell, module_name, module_class): 63 | if "render_text" not in dir(module_class): 64 | raise VolshellException("{0} does not support render_text".format(module_name)) 65 | if module_name in dir(shell): 66 | raise VolshellException("{0} is already registered".format(module_name)) 67 | 68 | self.module = module_class 69 | self.base_config = shell.base_config 70 | 71 | # Setup plugin instance documentation string 72 | config = Config() 73 | self.module(config) 74 | args = [ arg for arg in config.option_hook if arg not in self.base_config ] 75 | arg_help = [ 76 | "render[=False] (bool): determines if output is printed or a data structure is returned", 77 | "table_data[=True] (bool): determines if returned data should be from table_header/table_row hooking or from the output of {0}.calculate()".format(self.module.__name__) 78 | ] + [ 79 | "{0}{1}{2}: {3}".format(opt, "[={0}]".format(config.option_hook[opt]['default']) if config.option_hook[opt]['default'] != "" else "", 80 | " ({0})".format(config.option_hook[opt]['type']) if config.option_hook[opt]['type'] != "" else "", 81 | config.option_hook[opt]['help']) for opt in sorted(args) 82 | ] 83 | self.__name__ = module_name 84 | self.__doc__ = "{0}\n\nArguments:\n {1}\n".format(pydoc.getdoc(self.module), "\n ".join(arg_help)) 85 | 86 | def __call__(self, *args, **kwargs): 87 | if "render" not in kwargs: 88 | kwargs["render"] = False 89 | if "table_data" not in kwargs: 90 | kwargs["table_data"] = True 91 | 92 | config = Config() 93 | self.module(config) 94 | default_config = config.option_hook 95 | plugin_options = default_config.keys() + ["render", "table_data"] 96 | 97 | config_arg = conf.ConfObject() 98 | for key in kwargs: 99 | if key not in plugin_options: 100 | raise VolshellException("{0} is not a valid keyword argument".format(key)) 101 | if key in self.base_config: 102 | raise VolshellException("{0} is not usable within volshell".format(key)) 103 | config_arg.update(key, kwargs[key]) 104 | 105 | module_class = self.module 106 | class HookTable(module_class): 107 | def __init__(self, config, *_args, **_kwargs): 108 | self.table_title = None 109 | self.table_data = [] 110 | module_class.__init__(self, config, *_args, **_kwargs) 111 | 112 | def format_value(self, value, fmt): 113 | return value 114 | 115 | def table_header(self, outfd, title_format_list = None): 116 | self.table_title = [ title for title, spec in title_format_list ] 117 | module_class.table_header(self, outfd, title_format_list) 118 | 119 | def table_row(self, outfd, *args): 120 | if self.table_title == None: 121 | raise VolshellException("no table header has been set and the plugin is adding rows!") 122 | row = dict(zip(self.table_title, args)) 123 | self.table_data += [row] 124 | module_class.table_row(self, outfd, *args) 125 | 126 | module = HookTable(config_arg) 127 | data = module.calculate() 128 | if "render" in kwargs and kwargs["render"]: 129 | module.render_text(sys.stdout, data) 130 | result = None 131 | elif "table_data" in kwargs and kwargs["table_data"]: 132 | with open(os.devnull, 'w') as null: 133 | module.render_text(null, data) 134 | result = module.table_data 135 | else: 136 | result = data 137 | # conf.ConfObject is a singleton, so first we remove any config 138 | # options we may have set 139 | for key in plugin_options: 140 | try: 141 | config_arg.remove_option(key) 142 | except ValueError: 143 | pass 144 | return result 145 | 146 | 147 | class BaseVolshell(commands.Command): 148 | def __init__(self, config): 149 | commands.Command.__init__(self, config) 150 | 151 | config.add_option('OFFSET', short_option = 'o', default = None, 152 | help = 'EPROCESS Offset (in hex) in kernel address space', 153 | action = 'store', type = 'int') 154 | config.add_option('IMNAME', short_option = 'n', default = None, 155 | help = 'Process executable image name', 156 | action = 'store', type = 'str') 157 | config.add_option('PID', short_option = 'p', default = None, 158 | help = 'Operate on these Process IDs (comma-separated)', 159 | action = 'store', type = 'str') 160 | 161 | self.addrspace = utils.load_as(config) 162 | 163 | hooked_config = Config() 164 | commands.Command(hooked_config) 165 | self.base_config = hooked_config.option_hook 166 | 167 | # Add shell plugins to object instance 168 | self.shell_plugins = set() 169 | plugins = registry.get_plugin_classes(commands.Command, lower = True) 170 | # Filter plugins to those that are valid for the current profile (and remove the .*volshell plugins!) 171 | cmds = dict((cmd, plugin) for cmd, plugin in plugins.items() if plugin.is_valid_profile(self.addrspace.profile) and not cmd.endswith("volshell")) 172 | for module in cmds.keys(): 173 | try: 174 | plugin = Plugin(self, module, cmds[module]) 175 | setattr(self.__class__, module, plugin) 176 | self.shell_plugins.add(module) 177 | except VolshellException, exn: 178 | print "WARNING: {0} not loaded as a volshell command ({1})".format(module, exn) 179 | 180 | self.proc = None 181 | 182 | def cc(self, offset = None, pid = None, name = None): 183 | """Change current shell context. 184 | 185 | This function changes the current shell context to to the process 186 | specified. The process specification can be given as a virtual address 187 | (option: offset), PID (option: pid), or process name (option: name). 188 | 189 | If multiple processes match the given PID or name, you will be shown a 190 | list of matching processes, and will have to specify by offset. 191 | 192 | Calling this function with no arguments simply prints out the current 193 | context. 194 | """ 195 | if pid is not None: 196 | offsets = [] 197 | for p in self.getPidList(): 198 | if self.getPid(p).v() == pid: 199 | offsets.append(p) 200 | if not offsets: 201 | raise VolshellException("Unable to find process matching pid {0}".format(pid)) 202 | elif len(offsets) > 1: 203 | err = "Multiple processes match {0}, please specify by offset\n".format(pid) 204 | err += "Matching process offsets: {0}".format("; ".join([ "{0:#08X}".format(off.v()) for off in offsets ])) 205 | raise VolshellException(err) 206 | else: 207 | offset = offsets[0].v() 208 | elif name is not None: 209 | offsets = [] 210 | for p in self.getPidList(): 211 | if self.getImageName(p).find(name) >= 0: 212 | offsets.append(p) 213 | if not offsets: 214 | raise VolshellException("Unable to find process matching name {0}".format(name)) 215 | elif len(offsets) > 1: 216 | err = "Multiple processes match name {0}, please specify by PID or offset\n".format(name) 217 | err += "Matching process offsets: {0}".format("; ".join([ "{0:#08X}".format(off.v()) for off in offsets ])) 218 | raise VolshellException(err) 219 | else: 220 | offset = offsets[0].v() 221 | 222 | if pid is not None or name is not None or offset is not None: 223 | self.proc = obj.Object("_EPROCESS", offset = offset, vm = self.addrspace) 224 | 225 | print "Current context: process {0}, pid={1}, ppid={2}, DTB={3:#x}".format(self.getImageName(self.proc), self.getPid(self.proc).v(), self.getPPid(self.proc), self.getDTB(self.proc).v()) 226 | 227 | def db(self, address, length = 0x80, space = None): 228 | """Print bytes as canonical hexdump. 229 | 230 | This function prints bytes at the given virtual address as a canonical 231 | hexdump. The address will be translated in the current process context 232 | (see help on cc for information on how to change contexts). 233 | 234 | The length parameter (default: 0x80) specifies how many bytes to print, 235 | the width parameter (default: 16) allows you to change how many bytes per 236 | line should be displayed, and the space parameter allows you to 237 | optionally specify the address space to read the data from. 238 | """ 239 | if space is None: 240 | space = self.proc.get_process_address_space() 241 | data = space.read(address, length) 242 | if data is None: 243 | raise VolshellException("Memory unreadable at {0:#08x}".format(address)) 244 | 245 | for offset, hexchars, chars in utils.Hexdump(data): 246 | print "{0:#010x} {1:<48} {2}".format(address + offset, hexchars, ''.join(chars)) 247 | 248 | def dd(self, address, length = 0x80, space = None): 249 | """Print dwords at address. 250 | 251 | This function prints the data at the given address, interpreted as 252 | a series of dwords (unsigned four-byte integers) in hexadecimal. 253 | The address will be translated in the current process context 254 | (see help on cc for information on how to change contexts). 255 | 256 | The optional length parameter (default: 0x80) controls how many bytes 257 | to display, and space allows you to optionally specify the address space 258 | to read the data from. 259 | """ 260 | if space is None: 261 | space = self.proc.get_process_address_space() 262 | # round up to multiple of 4 263 | if length % 4 != 0: 264 | length = (length + 4) - (length % 4) 265 | data = space.read(address, length) 266 | if data is None: 267 | raise VolshellException("Memory unreadable at {0:#08x}".format(address)) 268 | dwords = [] 269 | for i in range(0, length, 4): 270 | (dw,) = struct.unpack(" 0 and getargspec(cmd.__getattribute__(func)).args[0] == 'self': 420 | setattr(self.__class__, func, cmd.__getattribute__(func)) 421 | loaded +=1 422 | print "{0} successfully loaded".format(func) 423 | if loaded > 0: 424 | print "Finished processing {0} ({1} commands loaded)".format(script, loaded) 425 | else: 426 | raise VolshellException("no commands loaded") 427 | except IOError, exn: 428 | raise VolshellException("failed to load {0} ({1})".format(script, exn)) 429 | except SyntaxError, exn: 430 | raise VolshellException("failed to load {0} ({1})".format(script, exn)) 431 | 432 | # TODO: catch and print out VolshellException's 433 | def render_text(self, _outfd, _data): 434 | # With Plugin object instances, dynamically setup the Plugin class doc strings (as there's 435 | # only one __doc__ per object/class function) 436 | class VolshellHelper(object): 437 | """Define the builtin 'help'. 438 | 439 | This is a wrapper around pydoc.help (with a twist of lemon). 440 | """ 441 | def __repr__(self): 442 | return "To get help, type 'help(self)', 'dir(self)' or 'help()" 443 | 444 | def __call__(self, obj=None): 445 | if isinstance(obj, Plugin): 446 | obj.__call__.__func__.__doc__ = obj.__doc__ 447 | return pydoc.help(obj or self) 448 | 449 | self.help = VolshellHelper() # Ensure the Volshell help function is defined 450 | help = self.help 451 | 452 | # Build alises for shell functions and plugins 453 | for nm in self.shell_plugins: 454 | exec "{0} = self.{0}".format(nm) in globals(), locals() 455 | for nm in dir(self): 456 | if nm not in dir(commands.Command) and ismethod(self.__getattribute__(nm)): 457 | exec "{0} = self.{0}".format(nm) in globals(), locals() 458 | 459 | # Break into shell 460 | print "Welcome to Volshell!" 461 | print 462 | print "Metadata:" 463 | print " Image: {0} ({1} bytes [{2}])".format(self._config.LOCATION, self.addrspace.base.fsize, self.addrspace.base.mode.upper()) 464 | print " Address Space: {0} ({1})".format(type(self.addrspace).__name__, type(self.addrspace.base).__name__) 465 | print " Profile: {0}".format(type(self.addrspace.profile).__name__) 466 | print " OS: {0} ({1} {2})".format(self.addrspace.profile.metadata.get("os", "UNKOWN"), self.addrspace.profile.metadata.get("arch", "x86"), self.addrspace.profile.metadata.get("memory_model", "32bit")) 467 | print 468 | print help 469 | print 470 | 471 | if self._config.OFFSET is not None: 472 | cc(offset = self._config.OFFSET) 473 | elif self._config.PID is not None: 474 | cc(pid = self._config.PID) 475 | elif self._config.IMNAME is not None: 476 | cc(name = self._config.IMNAME) 477 | else: 478 | # Just use the first process, whatever it is 479 | for p in self.getPidList(): 480 | cc(offset = p.v()) 481 | break 482 | 483 | try: 484 | from IPython.frontend.terminal.embed import InteractiveShellEmbed #pylint: disable-msg=W0611,F0401 485 | shell = InteractiveShellEmbed(banner1 = "TODO: manually enter 'help = self.help' to enable full help functionality") 486 | shell() 487 | except ImportError, exn: 488 | import code 489 | 490 | frame = currentframe() 491 | 492 | # Try to enable tab completion 493 | try: 494 | import rlcompleter, readline #pylint: disable-msg=W0612 495 | readline.parse_and_bind("tab: complete") 496 | except ImportError: 497 | pass 498 | 499 | # evaluate commands in current namespace 500 | namespace = frame.f_globals.copy() 501 | namespace.update(frame.f_locals) 502 | 503 | code.interact(banner = "Note: for an enhanced command line experience, upgrade to IPython (see http://ipython.org)", local = namespace) 504 | 505 | class Volshell(BaseVolshell): 506 | """Shell in the memory image (supports *arbitrary* images). 507 | 508 | In the current shell session: 509 | self.proc has the current context's _EPROCESS object; 510 | the images Kernel/Virtual address space is in self.addrspace (use self.proc.get_process_address_space() for Process address space); 511 | and all shell command are self. methods (aliased using = self.). 512 | 513 | Creating an instance of Volshell *without* specifying a config object causes Volshell to 514 | operate in library mode. So, the Volshell help, calculate and render_* functions are all 515 | disabled. 516 | """ 517 | 518 | def __init__(self, config=None, **kwargs): 519 | # If no config is argument present, we are in library mode - so build a config object using the kwargs dict 520 | if config is None: 521 | self.library_mode = True 522 | self.__init__.__func__.__doc__ = "INITIALISED IN LIBRARY MODE" 523 | # Use kwargs to fake command line arguments 524 | if "plugins" in kwargs: 525 | plugins.__path__ += kwargs["plugins"].split(plugins.plugin_separator) 526 | if "debug" in kwargs: 527 | debug.setup(min(int(kwargs["debug"]), debug.MAX_DEBUG)) 528 | else: 529 | debug.setup() 530 | sys.argv = ["volshell"] + [ "--{0}={1}".format(key, val) for key, val in kwargs.items() if key not in ["plugins", "debug"] ] 531 | # (Singleton) config object is basically empty (this is the first time it is created) 532 | config = conf.ConfObject() 533 | # Load up plugin modules now (they may set config options) 534 | registry.PluginImporter() 535 | registry.register_global_options(config, addrspace.BaseAddressSpace) 536 | registry.register_global_options(config, commands.Command) 537 | # Finish setting up the conf.ConfObject() singleton 538 | config.parse_options(False) 539 | BaseVolshell.__init__(self, config) 540 | if not config.LOCATION: 541 | raise VolshellException("please specify a location or filename keyword") 542 | # Disable Volshell help, calculate and render_* methods 543 | def disable_method(self, *args, **kwargs): 544 | """LIBRARY MODE: METHOD DISABLED""" 545 | raise VolshellException("LIBRARY MODE: METHOD DISABLED") 546 | for method in dir(self): 547 | if (method in ["help", "calculate"] or method.startswith("render_")) and ismethod(self.__getattribute__(method)): 548 | setattr(self.__class__, method, disable_method) 549 | else: 550 | self.library_mode = False 551 | self.__init__.__func__.__doc__ = "INITIALISED IN PLUGIN/SHELL MODE" 552 | BaseVolshell.__init__(self, config) 553 | 554 | image_os = self.addrspace.profile.metadata.get("os", "UNKNOWN") 555 | if image_os == "windows": 556 | from volatility.plugins.windows.volshell import WinVolshell 557 | Volshell.__bases__ = (WinVolshell,) 558 | elif image_os == "linux": 559 | from volatility.plugins.linux.volshell import LinuxVolshell 560 | Volshell.__bases__ = (LinuxVolshell,) 561 | elif image_os == "mac": 562 | from volatility.plugins.mac.volshell import MacVolshell 563 | Volshell.__bases__ = (MacVolshell,) 564 | else: 565 | print "WARNING: {0} is an unknown or unsupported OS - using a base Volshell".format(image_os) 566 | -------------------------------------------------------------------------------- /volshell/windows/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlpulley/volatility/eef6c3b21524a91c91758609526e8c41b34a5cfe/volshell/windows/__init__.py -------------------------------------------------------------------------------- /volshell/windows/volshell.py: -------------------------------------------------------------------------------- 1 | # Volatility 2 | # Copyright (C) 2008 Volatile Systems 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or (at 7 | # your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 17 | # 18 | 19 | import volatility.win32.tasks as win32 20 | import volatility.plugins.volshell as volshell 21 | 22 | class WinVolshell(volshell.BaseVolshell): 23 | """Shell in the (Windows) memory image""" 24 | 25 | def getPidList(self): 26 | return win32.pslist(self.addrspace) 27 | 28 | def getPid(self, proc): 29 | return proc.UniqueProcessId 30 | 31 | def getPPid(self, proc): 32 | return proc.InheritedFromUniqueProcessId.v() 33 | 34 | def getImageName(self, proc): 35 | return proc.ImageFileName 36 | 37 | def getDTB(self, proc): 38 | return proc.Pcb.DirectoryTableBase 39 | 40 | def ps(self, render=True, table_data=False, pid=None): 41 | """Print a process listing. 42 | 43 | Prints a process listing with PID, PPID, image name, and offset. 44 | """ 45 | if pid: 46 | if type(pid).__name__ == 'int': 47 | pid = str(pid) 48 | if type(pid).__name__ == 'list': 49 | if len(pid) == 0: 50 | pid = None 51 | else: 52 | pid = ",".join([ str(p) for p in pid ]) 53 | return self.pslist(render=render, table_data=table_data, pid=pid) 54 | 55 | def list_entry(self, head, objname, offset = -1, fieldname = None, forward = True): 56 | """Traverse a _LIST_ENTRY. 57 | 58 | Traverses a _LIST_ENTRY starting at virtual address head made up of 59 | objects of type objname. The value of offset should be set to the 60 | offset of the _LIST_ENTRY within the desired object. 61 | """ 62 | vm = self.proc.get_process_address_space() 63 | seen = set() 64 | 65 | if fieldname: 66 | offset = vm.profile.get_obj_offset(objname, fieldname) 67 | 68 | lst = obj.Object("_LIST_ENTRY", head, vm) 69 | seen.add(lst) 70 | if not lst.is_valid(): 71 | return 72 | while True: 73 | if forward: 74 | lst = lst.Flink 75 | else: 76 | lst = lst.Blink 77 | 78 | if not lst.is_valid(): 79 | return 80 | 81 | if lst in seen: 82 | break 83 | else: 84 | seen.add(lst) 85 | 86 | nobj = obj.Object(objname, lst.obj_offset - offset, vm) 87 | yield nobj 88 | --------------------------------------------------------------------------------