├── Plugins documentation.pdf ├── README.md ├── pfn.py ├── volexp.py └── winobj.py /Plugins documentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memoryforensics1/Vol3xp/216e936ce560c78704e467ab90d1863e881ea381/Plugins documentation.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vol3xp, Volatility 3 Explorer Plugins 2 | 3 | ### WinObj -> Windows Kernel Objects Explorer an improve of for volatility 3 (winobj.py) 4 | WinObj (very similar to WinObj [sysinternals]) Also supports Struct Analyzer and [WinObjGui](#11) from VolExp. 5 | 6 | ### RAMMap -> Physical Address Mapping (pfn.py) 7 | RAMMap (very similar to Rammap [SysInternals]), but additonally it marks any suspicious pages (for more information read the pdf). 8 | This module contains 3 plugins: 9 | 1. P2V - Converts physical address to virtual address using PfnDatabase and finds the owning process of a page (if any). 10 | 2. PFNInfo - Gives information about a physical page from the PfnDatabase, the use of the page, file name, and much more. 11 | 3. RAMMap - Uses both of the plugins above. Displays a RamMap-like UI for all the physical pages, and colors suspicious pages. 12 | [You can see far more detailed information about the plugins in the pdf] 13 | 14 | ### And the main event -> Volatilty Explorer (volexp.py) 15 | 16 | This program allows the user to upload a memory dump and navigate through it with ease using a graphical interface. 17 | It can also function as a plugin to the Volatility Framework (). 18 | This program functions similarly to Process Explorer/Hacker, but allows the user to analyze a Memory Dump. 19 | This program can run from Windows, Linux and MacOS machines, but only accepts Windows memory images. 20 | 21 | ## note: volatility explorer for volatility2 -> 22 | 23 | ### Quick Start 24 | 1. Download the volexp.py file (download the ). 25 | 26 | 2. Run as a standalone program or as a plugin to Volatility: 27 | - As a standalone program: 28 | ```shell 29 | python3 volexp 30 | ``` 31 | - As a Volatility plugin: 32 | ```shell 33 | python3 vol.py -f windows.volexp.volexp 34 | ``` 35 | 36 | 37 | ### Some Features: 38 | ```shell 39 | python3 volexp.py 40 | ``` 41 | - Some of the information display will not update in real time (except Processes info(update slowly), real time functions like struct analyzer, PE properties, run real time plugin, etc.). 42 | ![example vol3xp, the colors used to identify special processes (serviceses, protected)](https://github.com/memoryforensics1/info/blob/master/Win10Example.GIF) 43 | 44 | 45 | 46 | - The program also allows to view Loaded dll's, open handles and network connections of each process (Access to a dll's properties is also optional). 47 | 48 | ![Lower Pane](https://github.com/memoryforensics1/info/blob/master/Win10Handles.png) 49 | 50 | 51 | 52 | - To present more information of a process, Double-Click (or Left-Click and select Properties) to bring up an information window. 53 | 54 | ![Process properties](https://github.com/memoryforensics1/info/blob/master/ImageProperties.png) 55 | 56 | 57 | - Or present more information on any PE. 58 | 59 | ![PE properties](https://github.com/memoryforensics1/info/blob/master/PeProeprties.png) 60 | 61 | 62 | 63 | - The program allows the user to view the files in the Memory Dump as well as their information. Additionally it allows the user to extract those files (HexDump/strings view is also optional). 64 | 65 | ![File Explorer](https://github.com/memoryforensics1/info/blob/master/FilesExplorer.png) 66 | 67 | 68 | 69 | - The program supports viewing of the Windows Objects and files's matadata (MFT). 70 | 71 | ![Other Explorers (Winobj and MFT explorer)](https://github.com/memoryforensics1/info/blob/master/Explorers.png) 72 | 73 | 74 | 75 | - The program also support viewing a regview of the memory dump 76 | 77 | ![RegView](https://github.com/memoryforensics1/info/blob/master/RegView.png) 78 | 79 | 80 | 81 | - Additionally, the program supports struct analysis. (writing on the memory's struct, running Volatility functions on a struct is available). 82 | Example of getting all the load modules inside _EPROCESS struct in another struct analyzer window: 83 | 84 | ![Struct Analyzer](https://github.com/memoryforensics1/info/blob/master/StructAnalyzer.png) 85 | 86 | 87 | 88 | - The Program is also capable of automatically marking suspicious processes found by another plugin. 89 | Example of a running threadmap plugin: 90 | 91 | ![Cmd Plugin run threadmap](https://github.com/memoryforensics1/info/blob/master/threadmapExample.GIF) 92 | 93 | 94 | 95 | - View memory use of a process. 96 | 97 | ![Vad Information](https://github.com/memoryforensics1/info/blob/master/VadInformation.png) 98 | 99 | 100 | - Manually marking a certain process and adding a sidenote on it. 101 | 102 | - User's actions can be saved on a seperate file for later usage. 103 | 104 | ### get help: https://github.com/memoryforensics1/VolExp/wiki/VolExp-help: 105 | ![volexp help](https://github.com/memoryforensics1/info/blob/master/help.gif) 106 | -------------------------------------------------------------------------------- /winobj.py: -------------------------------------------------------------------------------- 1 | # This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 2 | # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 3 | # Creator: Aviel Zohar (memoryforensicsanalysis@gmail.com) 4 | import logging 5 | from typing import List 6 | 7 | from volatility3.framework import renderers, interfaces, objects, exceptions, constants 8 | from volatility3.framework.configuration import requirements 9 | from volatility3.framework.objects import utility 10 | from volatility3.framework.renderers import format_hints 11 | import volatility3.plugins.windows.info as info 12 | import volatility3.plugins.windows.handles as handles 13 | from volatility3.plugins.windows import pslist 14 | 15 | vollog = logging.getLogger(__name__) 16 | 17 | #Globals 18 | NAME = 0x1 19 | ADDR = 0x0 20 | HEADER = 0x2 21 | VALUES = 0x1 22 | ADDITIONAL_INFO = 0x3 23 | 24 | 25 | class WinObj(interfaces.plugins.PluginInterface): 26 | """ 27 | Object Manager Enumeration 28 | """ 29 | 30 | _required_framework_version = (2, 0, 0) 31 | _version = (2, 0, 0) 32 | 33 | def __init__(self, *args, **kwargs): 34 | super().__init__(*args, **kwargs) 35 | 36 | self.config['primary'] = self.context.modules[self.config['kernel']].layer_name 37 | self.config['nt_symbols'] = self.context.modules[self.config['kernel']].symbol_table_name 38 | self.kaddr_space = self.config['primary'] 39 | self.kvo = self.context.layers[self.config['primary']].config["kernel_virtual_offset"] 40 | self.ntkrnlmp = self._context.module(self.config['nt_symbols'], 41 | layer_name=self.kaddr_space, 42 | offset=self.kvo) 43 | 44 | # Get the cookie (or none if this version dont use cookie). 45 | try: 46 | offset = self.context.symbol_space.get_symbol(self.config["nt_symbols"] + constants.BANG + "ObHeaderCookie").address 47 | kernel = self.context.modules[self.config["kernel"]] 48 | physical_layer_name = self.context.layers[kernel.layer_name] 49 | kvo = self.context.layers[kernel.layer_name].config['kernel_virtual_offset'] 50 | 51 | self.cookie = self.context.object(self.config["nt_symbols"] + constants.BANG + "unsigned int" , self.config["primary"], offset=kvo + offset) 52 | except exceptions.SymbolError: 53 | self.cookie = None 54 | 55 | self._protect_values = None 56 | self.root_obj_list = [] 57 | self.tables = {} 58 | self.exlude_types = [] 59 | 60 | # Sets default values of a 64 bit machine, 61 | #the values will be updated according to the profile 62 | self.POINTER_SIZE = 0x8 63 | self.OBJECT_HEADER_QUOTA_INFO_SIZE = 0x20 64 | self.OBJECT_HEADER_PROCESS_INFO_SIZE = 0x10 65 | self.OBJECT_HEADER_HANDLE_INFO_SIZE = 0x10 66 | self.OBJECT_HEADER_NAME_INFO_SIZE = 0x20 67 | self.OBJECT_HEADER_CREATOR_INFO_SIZE = 0x20 68 | self.OBJECT_HEADER_NAME_INFO_ID = 0x2 69 | self.OBJECT_HEADER_CREATOR_INFO_ID = 0x1 70 | self.OBJECT_HEADER_HANDLE_INFO_ID = 0x4 71 | self.OBJECT_HEADER_QUOTA_INFO_ID = 0x8 72 | self.OBJECT_HEADER_PROCESS_INFO_ID = 0x10 73 | self.OBJECT_HEADER_SIZE = 0x30 74 | self.OBJECT_POOL_HEADER = 0x10 75 | self.OBJECT_INFO_HEADERS_LIST = [self.OBJECT_HEADER_CREATOR_INFO_ID, 76 | self.OBJECT_HEADER_HANDLE_INFO_ID, 77 | self.OBJECT_HEADER_QUOTA_INFO_ID, 78 | self.OBJECT_HEADER_NAME_INFO_ID, 79 | self.OBJECT_HEADER_PROCESS_INFO_ID] 80 | 81 | self.OBJECT_INFO_HEADERS_ID_TO_SIZE ={self.OBJECT_HEADER_NAME_INFO_ID: self.OBJECT_HEADER_NAME_INFO_SIZE, 82 | self.OBJECT_HEADER_CREATOR_INFO_ID: self.OBJECT_HEADER_CREATOR_INFO_SIZE, 83 | self.OBJECT_HEADER_HANDLE_INFO_ID : self.OBJECT_HEADER_HANDLE_INFO_SIZE, 84 | self.OBJECT_HEADER_QUOTA_INFO_ID : self.OBJECT_HEADER_QUOTA_INFO_SIZE, 85 | self.OBJECT_HEADER_PROCESS_INFO_ID: self.OBJECT_HEADER_PROCESS_INFO_SIZE} 86 | self.type_map = handles.Handles.get_type_map(self.context, self.config["primary"], self.config["nt_symbols"]) 87 | 88 | @classmethod 89 | def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: 90 | # Since we're calling the plugin, make sure we have the plugin's requirements 91 | return [requirements.ModuleRequirement(name='kernel', description='Windows kernel', 92 | architectures=["Intel32", "Intel64"]), 93 | requirements.SymbolTableRequirement(name="nt_symbols", description="Windows kernel symbols"), 94 | requirements.BooleanRequirement(name='PARSE_ALL', 95 | description='Parse every directory under the root dir', 96 | optional=True), 97 | requirements.StringRequirement(name='SUPPLY_ADDR', 98 | description='Parse directories under specific addresses', 99 | optional=True), 100 | requirements.StringRequirement(name='FULL_PATH', 101 | description='Parse a directory found by full path location', 102 | optional=True), 103 | ] 104 | 105 | def get_root_directory(self): 106 | """ 107 | :return : a pointer to the root directory 108 | """ 109 | # gets the pointer 110 | # if for some reason ObpRootDirectoryObject not exist lets take the value from ObpRootDirectoryObject 111 | try: 112 | import struct 113 | _pointer_struct = struct.Struct(" _SECTION 199 | myObj = self.ntkrnlmp.object("pointer", offset=myObj.vol.offset - kvo).cast( 200 | "_EX_FAST_REF").dereference().cast('_SECTION').u1.ControlArea 201 | except: 202 | return "(parse object failed on address: {}".format(myObj) 203 | # the default is "_SEGMENT_OBJECT", and we need _SEGMENT 204 | try: 205 | fileName = myObj.FilePointer.dereference().cast("_FILE_OBJECT").file_name_with_device() 206 | except: 207 | return "(parse file name failed on address: {}".format(myObj) 208 | 209 | return "FileObj: {}".format(fileName) 210 | 211 | # additional information about Driver 212 | elif obj_type == "Driver": 213 | driver = self.ntkrnlmp.object("pointer", offset=myObj.vol.offset - kvo).cast("_EX_FAST_REF").dereference().cast( 214 | '_DRIVER_OBJECT') 215 | try: 216 | return "Full Name: {}".format(driver.DriverName.String) 217 | except: 218 | return "(parse name failed on address: {}".format(driver) 219 | 220 | # additional information about Device 221 | elif obj_type == "Device": 222 | device = self.ntkrnlmp.object("pointer", offset=myObj.vol.offset - kvo).cast("_EX_FAST_REF").dereference().cast( 223 | '_DEVICE_OBJECT') 224 | try: 225 | return "Driver: {}".format(device.DriverObject.DriverName.String) 226 | except: 227 | return "(parse name failed on address: {}".format(device) 228 | 229 | # additional information about Type 230 | elif obj_type == "Type": 231 | myType = self.ntkrnlmp.object("pointer", offset=myObj.vol.offset - kvo).cast("_EX_FAST_REF").dereference().cast( 232 | '_OBJECT_TYPE') 233 | key = self.ntkrnlmp.object("string", offset=myType.Key.vol.offset - kvo, max_length=4, errors="replace") 234 | return "Key: {}".format(key) 235 | 236 | # additional information about Window Station (Not supported yet..) 237 | elif obj_type == "WindowStation" and False: 238 | win_sta = self.ntkrnlmp.object("pointer", offset=myObj.vol.offset - kvo).cast("_EX_FAST_REF").dereference().cast( 239 | 'tagWINDOWSTATION') 240 | names = "".join("{} ".format(Desktop.Name) for Desktop in win_sta.desktops()).strip() 241 | session_id = win_sta.dwSessionId 242 | atom_table = hex(win_sta.pGlobalAtomTable)[:-1] 243 | return "Desktop Names:{},Session Id:{},Atoms:{}".format(names,session_id,atom_table) 244 | 245 | # additional information about all the others 246 | else: 247 | return "Handle Count - {}, Pointer Count {}".format(obj_header.HandleCount,obj_header.PointerCount) 248 | 249 | def GetName(self, obj_header): 250 | """ 251 | :param obj_header: "_OBJECT_HEADER" 252 | :return : string 253 | the function will return the name of the object 254 | """ 255 | 256 | # When this work in volatility for all version just replace the function with this 257 | #try: 258 | # name_info = obj_header.NameInfo() 259 | # return name_info.Name.get_string() 260 | #except: 261 | # return '' 262 | 263 | # checks if this is an old version 264 | if self.ntkrnlmp.get_type("_OBJECT_HEADER").has_member('NameInfoOffset'): 265 | size = obj_header.NameInfoOffset 266 | 267 | # new version 268 | else: 269 | try: 270 | info_headers = self.get_all_object_headers(obj_header.InfoMask) 271 | except: 272 | return "" 273 | 274 | # calculates the size according to the info headers 275 | if self.OBJECT_HEADER_CREATOR_INFO_ID in info_headers: 276 | size = self.OBJECT_HEADER_NAME_INFO_SIZE + self.OBJECT_HEADER_CREATOR_INFO_SIZE 277 | else: 278 | size = self.OBJECT_HEADER_NAME_INFO_SIZE 279 | 280 | layer_name = self.config['primary'] 281 | kvo = self.context.layers[layer_name].config["kernel_virtual_offset"] 282 | name_info = self.ntkrnlmp.object("_OBJECT_HEADER_NAME_INFO", offset=obj_header.vol.offset - kvo - size) 283 | 284 | # checks that the name is not empty 285 | if name_info.Name: 286 | # validates the name 287 | #if name_info.Name.Buffer and name_info.Name.Length <= name_info.Name.MaximumLength: 288 | try: 289 | return name_info.Name.get_string() 290 | except: 291 | return "" 292 | return "" 293 | 294 | def AddToList(self, myObj, l): 295 | """ 296 | :param myObj : pointer object 297 | :param l : list 298 | :return : None 299 | the function will add the object to the received list after a validation 300 | """ 301 | layer_name = self.config['primary'] 302 | kvo = self.context.layers[layer_name].config["kernel_virtual_offset"] 303 | obj_header = self.ntkrnlmp.object("_OBJECT_HEADER", offset=myObj.cast('pointer').real - self.OBJECT_HEADER_SIZE - kvo) 304 | 305 | # Make sure that there is no duplicated, and validate the pointer. 306 | for item in l: 307 | try: 308 | if item[0] == myObj: 309 | return 310 | elif (not obj_header.is_valid()) or (obj_header.PointerCount < 1 and obj_header.HandleCount < 1) or \ 311 | (obj_header.PointerCount < 0 or obj_header.HandleCount < 0): 312 | return 313 | except: 314 | return 315 | name = self.GetName(obj_header) 316 | # validates the object 317 | if name: 318 | obj_type = obj_header.get_object_type(self.type_map, self.cookie) 319 | if obj_type in self.exlude_types: 320 | return 321 | add_info = self.get_additional_info(myObj, obj_type, obj_header) 322 | l.append((myObj,name,obj_header,add_info)) 323 | 324 | def parse_directory(self, addr, l): 325 | """ 326 | :param addr : long, pointer the the driectory 327 | :param l : list 328 | :return : None 329 | the function will parse the directory and add every valid object to the received list 330 | """ 331 | seen = set() 332 | layer_name = self.config['primary'] 333 | kvo = self.context.layers[layer_name].config["kernel_virtual_offset"] 334 | directory_array = self.ntkrnlmp.object('_OBJECT_DIRECTORY', addr - self.ntkrnlmp.offset) 335 | for pointer_addr in directory_array.HashBuckets: 336 | if not pointer_addr or pointer_addr == 0xffffffff: 337 | continue 338 | 339 | # Walk the ChainLink foreach item inside the directory. 340 | while pointer_addr not in seen: 341 | try: 342 | myObj = self.ntkrnlmp.object("pointer", offset=pointer_addr+self.POINTER_SIZE - kvo) 343 | self.AddToList(myObj, l) 344 | except exceptions.InvalidAddressException: 345 | pass 346 | 347 | seen.add(pointer_addr) 348 | try: 349 | pointer_addr = pointer_addr.ChainLink 350 | except exceptions.InvalidAddressException: 351 | break 352 | if not pointer_addr: 353 | break 354 | 355 | def get_directory(self, name="", root_dir=[]): 356 | """ 357 | :param name : string 358 | :param root_dir : list of tuples 359 | :return : None 360 | the function will parse the root directory object and add every directory/given name, 361 | to the tables dictionary 362 | """ 363 | l = [] 364 | name = str(name) 365 | 366 | # checks whether a root dir was given or not 367 | if not root_dir: 368 | 369 | # default option 370 | root_dir = self.root_obj_list 371 | 372 | # parses the root directory 373 | for obj,obj_name,obj_header,add_info in root_dir: 374 | # if there is a specific name 375 | if name: 376 | # if this is the name that was received 377 | if name.lower() == obj_name.lower(): 378 | self.parse_directory(obj, l) 379 | self.tables[obj_name] = (obj.vol.offset, l) 380 | break 381 | 382 | # parse all 383 | else: 384 | # checks if object is a directory 385 | if obj_header.get_object_type(self.type_map) == "Directory": 386 | self.parse_directory(obj, l) 387 | self.tables[obj_name] = (obj.vol.offset,l) 388 | l = [] 389 | 390 | def SaveByPath(self, path): 391 | """ 392 | This function get a path to directory append all the data in this directory to self.tables 393 | :param path: path in the object directory to get all the object information from. 394 | :return: 395 | """ 396 | # validation 397 | try: 398 | 399 | # takes a copy in order to remove all stages from the final parser 400 | save = self.tables.copy() 401 | 402 | stages = path.split("/")[1:] 403 | 404 | # allow backslashes as well 405 | if len(stages) == 0: 406 | stages = path.split("\\")[1:] 407 | 408 | self.get_directory(stages[0]) 409 | 410 | 411 | addr,current_dir = self.tables[stages[0]] 412 | 413 | 414 | for place,stage in enumerate(stages[1:]): 415 | self.get_directory(stage,current_dir) 416 | addr,current_dir = self.tables[stage] 417 | 418 | # removes all stages 419 | save_list = current_dir 420 | self.tables = save 421 | 422 | #sets the full path in the dictionary 423 | self.tables[path] = (addr,current_dir) 424 | 425 | except KeyError: 426 | raise KeyError("Invalid Path -> {}".format(path)) 427 | 428 | def get_object_information(self): 429 | """ 430 | Check user parameters and start to get the information 431 | :return: None 432 | """ 433 | # updates objects size 434 | self.update_sizes() 435 | 436 | # Get root directory 437 | root_dir = self.get_root_directory() 438 | self.parse_directory(root_dir, self.root_obj_list) 439 | 440 | # checks for the SUPPLY_ADDR option 441 | if self.config.get('SUPPLY_ADDR', None): 442 | addrs = self.config.get('SUPPLY_ADDR', None).split(",") 443 | for addr in addrs: 444 | l = [] 445 | 446 | # validates the address 447 | try: 448 | addr = eval(addr) 449 | 450 | # addr is not valid 451 | except (SyntaxError,NameError): 452 | continue 453 | 454 | obj_header = self.ntkrnlmp.object("_OBJECT_HEADER", offset=addr-self.OBJECT_HEADER_SIZE - self.ntkrnlmp.offset) 455 | name = self.GetName(obj_header) 456 | 457 | # validates the directory 458 | if name: 459 | self.parse_directory(addr, l) 460 | self.tables[name] = (addr, l) 461 | 462 | # checks for the FULL_PATH option 463 | elif self.config.get('FULL_PATH', None): 464 | 465 | # gets all dirs 466 | dirs = self.config.get('FULL_PATH', None).split(",") 467 | for path in dirs: 468 | self.SaveByPath(path) 469 | 470 | # default option 471 | else: 472 | self.tables["/"] = (root_dir,self.root_obj_list) 473 | 474 | # checks for the PARSE_ALL option 475 | if self.config.get('PARSE_ALL', None): 476 | self.get_directory() 477 | 478 | def _generator(self): 479 | self.get_object_information() 480 | for table in self.tables: 481 | l = self.tables[table][VALUES] 482 | for obj in l: 483 | yield (0,[hex(obj[ADDR]), str(obj[NAME]), str(obj[HEADER].get_object_type(self.type_map)), str(obj[ADDITIONAL_INFO])]) 484 | 485 | def run(self): 486 | return renderers.TreeGrid([("Object Address(V)", str), ("Name", str), ("str", str), ("Additional Info", str)], 487 | self._generator()) 488 | --------------------------------------------------------------------------------