├── .gitignore ├── README.md ├── Volatility_contest_2023 ├── Abyss_Watcher_Volatility_contest_2023.pdf ├── README.md └── plugins │ ├── README.md │ ├── check_ftrace.py │ ├── check_tracepoints.py │ └── check_unlinked_modules.py └── vol_ez_install ├── Dockerfile-vol2 ├── Dockerfile-vol3 ├── README.md └── vol_ez_install.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | generated_symbols/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Volatility scripts 2 | 3 | ## About the project 4 | 5 | This repository contains a variety of scripts related to the Volatility framework. 6 | 7 | ## Content 8 | 9 | Get full notices in the dedicated subdirectories ! 10 | 11 | ### vol_ez_install 12 | 13 | Install Volatility on your system effortlessly using Docker, and benefit from command aliases that streamline the experience ! 14 | 15 | ### Volatility_contest_2023 16 | 17 | Volatility contest 2023 plugins and submission document. 18 | 19 | --- 20 | 21 | ## Related work 22 | 23 | - Extended list of pre-generated Volatility2 profiles : https://github.com/Abyss-W4tcher/volatility2-profiles 24 | - Extended list of pre-generated Volatility3 symbols : https://github.com/Abyss-W4tcher/volatility3-symbols 25 | -------------------------------------------------------------------------------- /Volatility_contest_2023/Abyss_Watcher_Volatility_contest_2023.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abyss-W4tcher/volatility-scripts/4a55b99e11e59fb94f5f3a4ec3ebb1fdb8ba6bd5/Volatility_contest_2023/Abyss_Watcher_Volatility_contest_2023.pdf -------------------------------------------------------------------------------- /Volatility_contest_2023/README.md: -------------------------------------------------------------------------------- 1 | # Volatility Contest 2023 2 | 3 | This directory contains plugins and files related to the Volatility contest 2023. Here is the summary, taken from the official results : 4 | 5 | ### check_ftrace Plugin 6 | 7 | Function Tracer, ftrace, is a framework intended to help developers determine what is happening within the kernel. It is typically used for debugging and performance analysis. It has also been abused by rootkits to hide artifacts on a system. This plugin allows investigators to detect these hooks and provide further context for investigation. 8 | 9 | ### check_unlinked_modules Plugin 10 | 11 | Removing objects from linked lists has been a common technique leveraged by rootkits to hide resources from sysadmin tools on the live machine. By leveraging a “regex mask”, this plugin scans memory for unlinked modules. The documentation also describes how this technique could be expanded to other structures found in the symbol table. 12 | 13 | ### check_tracepoints Plugin 14 | 15 | Within the Linux kernel, a tracepoint provides a hooking mechanism to call a function that can be provided at runtime. They are typically used for tracing and performance analysis, but they can and have been abused by rootkits in the wild. This plugin enumerates the tracepoint arrays and looks for tracepoints with a probe attached. This plugin allows investigators to find tracepoint control flow changes that may have been added to the system. 16 | 17 | ## Usage 18 | 19 | Place all three plugins in `volatility3/volatility3/plugins/linux/` (not under `framework/plugins/linux`). You may need to create the `linux/` directory, if it did not exist beforehand. 20 | See `Abyss_Watcher_Volatility_contest_2023.pdf` for details and context. 21 | 22 | ## Results 23 | 24 | https://volatilityfoundation.org/the-2023-volatility-plugin-contest-results-are-in/ 25 | 26 | -------------------------------------------------------------------------------- /Volatility_contest_2023/plugins/README.md: -------------------------------------------------------------------------------- 1 | ## Plugins superseeded by Volatility3 itself 2 | 3 | - check_ftrace.py -> `linux.tracing.ftrace` at https://github.com/volatilityfoundation/volatility3/blob/develop/volatility3/framework/plugins/linux/tracing/ftrace.py 4 | - check_tracepoints.py -> `linux.tracing.tracepoints` at https://github.com/volatilityfoundation/volatility3/blob/develop/volatility3/framework/plugins/linux/tracing/tracepoints.py 5 | - check_unlinked_modules.py -> `linux.hidden_modules` at https://github.com/volatilityfoundation/volatility3/blob/develop/volatility3/framework/plugins/linux/hidden_modules.py 6 | -------------------------------------------------------------------------------- /Volatility_contest_2023/plugins/check_ftrace.py: -------------------------------------------------------------------------------- 1 | # This file is Copyright 2023 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 | # 4 | import logging 5 | from typing import List 6 | from enum import Enum 7 | from volatility3.plugins.linux import lsmod, check_unlinked_modules 8 | from volatility3.framework import constants, exceptions, interfaces 9 | from volatility3.framework.configuration import requirements 10 | from volatility3.framework.renderers import format_hints, TreeGrid 11 | from volatility3.framework.symbols import linux 12 | from volatility3.framework.objects import utility 13 | 14 | vollog = logging.getLogger(__name__) 15 | UNKNOWN = "UNKNOWN" 16 | 17 | 18 | class FTRACEFLAGS(Enum): 19 | FTRACE_OPS_FL_ENABLED = 1 << 0 20 | FTRACE_OPS_FL_DYNAMIC = 1 << 1 21 | FTRACE_OPS_FL_SAVE_REGS = 1 << 2 22 | FTRACE_OPS_FL_SAVE_REGS_IF_SUPPORTED = 1 << 3 23 | FTRACE_OPS_FL_RECURSION = 1 << 4 24 | FTRACE_OPS_FL_STUB = 1 << 5 25 | FTRACE_OPS_FL_INITIALIZED = 1 << 6 26 | FTRACE_OPS_FL_DELETED = 1 << 7 27 | FTRACE_OPS_FL_ADDING = 1 << 8 28 | FTRACE_OPS_FL_REMOVING = 1 << 9 29 | FTRACE_OPS_FL_MODIFYING = 1 << 10 30 | FTRACE_OPS_FL_ALLOC_TRAMP = 1 << 11 31 | FTRACE_OPS_FL_IPMODIFY = 1 << 12 32 | FTRACE_OPS_FL_PID = 1 << 13 33 | FTRACE_OPS_FL_RCU = 1 << 14 34 | FTRACE_OPS_FL_TRACE_ARRAY = 1 << 15 35 | FTRACE_OPS_FL_PERMANENT = 1 << 16 36 | FTRACE_OPS_FL_DIRECT = 1 << 17 37 | 38 | 39 | class Check_ftrace(interfaces.plugins.PluginInterface): 40 | """Detect ftrace hooking""" 41 | 42 | _version = (1, 0, 0) 43 | _required_framework_version = (2, 5, 2) 44 | 45 | @classmethod 46 | def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: 47 | return [ 48 | requirements.ModuleRequirement( 49 | name="kernel", 50 | description="Linux kernel", 51 | architectures=["Intel32", "Intel64", "AArch64"], 52 | ), 53 | requirements.PluginRequirement( 54 | name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) 55 | ), 56 | requirements.PluginRequirement( 57 | name="check_unlinked_modules", 58 | plugin=check_unlinked_modules.Check_unlinked_modules, 59 | version=(1, 0, 0), 60 | ), 61 | requirements.BooleanRequirement( 62 | name="show_ftrace_flags", 63 | description="Show ftrace flags associated with an ftrace_ops", 64 | optional=True, 65 | default=False, 66 | ), 67 | ] 68 | 69 | def run(self): 70 | """Plugin output format : 71 | 72 | ftrace_ops : hex("ftrace_ops struct offset") 73 | Callback : "callback offset" ["callback symbol" | "UNKNOWN"] 74 | Hooked symbol : "hooked symbol" | "UNKNOWN" 75 | Module : hex("module offset") ["associated module"] | "UNKNOWN" 76 | Callback out of kernel .text : True | False 77 | ftrace flags : list("ftrace_ops flags") 78 | """ 79 | 80 | # Naturally ordered by "ftrace_ops" (the same order as we walked the "ftrace_ops_list") 81 | columns = [ 82 | ("ftrace_ops", format_hints.Hex), 83 | ("Callback", str), 84 | ("Hooked symbol", str), 85 | ("Module", str), 86 | ("Callback out of kernel .text", bool), 87 | ] 88 | 89 | if self.config.get("show_ftrace_flags"): 90 | columns.append(("ftrace_ops flags", str)) 91 | 92 | return TreeGrid( 93 | columns, 94 | self._generator(), 95 | ) 96 | 97 | def _generator(self): 98 | """Iterate over ftrace_ops_list struct""" 99 | 100 | self.vmlinux = self.context.modules[self.config["kernel"]] 101 | if not self.vmlinux.has_symbol("ftrace_ops_list"): 102 | raise exceptions.SymbolError( 103 | "ftrace_ops_list", 104 | self.vmlinux.symbol_table_name, 105 | 'The provided symbol table does not include the "ftrace_ops_list" symbol. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt.', 106 | ) 107 | 108 | self.checked_callbacks = {} 109 | self.set_compiled_kernel_space_boundaries() 110 | self.setup_modules_and_handlers() 111 | 112 | # Access head of ftrace_ops_list 113 | ftrace_ops_head = self.vmlinux.object_from_symbol( 114 | "ftrace_ops_list" 115 | ).dereference() 116 | local_ftrace_ops_list = [] 117 | results = [] 118 | 119 | while True: 120 | local_ftrace_ops_list.append(ftrace_ops_head) 121 | if ftrace_ops_head.next.is_readable(): 122 | ftrace_ops_head = ftrace_ops_head.next.dereference() 123 | else: 124 | break 125 | 126 | for i, ftrace_ops in enumerate(local_ftrace_ops_list): 127 | self._progress_callback( 128 | (i / len(local_ftrace_ops_list)) * 100, f"Scanning ftrace_ops_list..." 129 | ) 130 | try: 131 | parse_result = self.parse_ftrace_ops(ftrace_ops=ftrace_ops) 132 | if parse_result: 133 | results.append((0, parse_result)) 134 | except Exception as e: 135 | vollog.exception(f"Unhandled exception : {e}") 136 | 137 | # Preferred to "yield", otherwise progress_callback and results overlap... 138 | return results 139 | 140 | def parse_ftrace_ops(self, ftrace_ops): 141 | """Main parser for an ftrace_ops struct""" 142 | ftrace_ops_addr = ftrace_ops.vol.offset 143 | ftrace_func_entries = self.walk_to_ftrace_func_entry(ftrace_ops) 144 | 145 | for ftrace_func_entry in ftrace_func_entries: 146 | callback = int(ftrace_ops.func) 147 | hook_symbols = wrapper_get_symbols_by_absolute_location( 148 | self.vmlinux, ftrace_func_entry.ip.cast("pointer") 149 | ) 150 | 151 | # Avoid running the aggressive module finder twice for an address, if it wasn't found previously 152 | if self.checked_callbacks.get(callback): 153 | module_name = self.checked_callbacks[callback] 154 | else: 155 | module_name = self.wrapper_lookup_module_address(callback) 156 | self.checked_callbacks[callback] = module_name 157 | 158 | # Useful information allowing to detect if a module was inserted dynamically or if it is part of the compiled kernel 159 | callback_out_of_kernel_range = ( 160 | callback < self.kernel_space_start or callback > self.kernel_space_end 161 | ) 162 | ftrace_flags = self.parse_ftrace_flags(ftrace_ops.flags) 163 | 164 | ### Format results ### 165 | callback_symbol = UNKNOWN 166 | f_module = UNKNOWN 167 | # Fetch more informations about the module 168 | if module_name != UNKNOWN: 169 | module_obj = get_module_object_from_name(module_name, self.modules) 170 | module_address = module_obj.vol.offset 171 | f_module = f"{hex(module_address)} [{module_name}]" 172 | callback_symbol = module_obj.get_symbol_by_address(callback) or UNKNOWN 173 | 174 | result = ( 175 | format_hints.Hex(ftrace_ops_addr), 176 | f"{hex(callback)} [{callback_symbol}]", 177 | str(hook_symbols), 178 | f_module, 179 | callback_out_of_kernel_range, 180 | ) 181 | 182 | if self.config.get("show_ftrace_flags"): 183 | result += (str(ftrace_flags),) 184 | 185 | return result 186 | 187 | def parse_ftrace_flags(self, ftrace_flags_value: int): 188 | """Parse flags set on a hook structure""" 189 | ret = [] 190 | for couple in FTRACEFLAGS: 191 | if ftrace_flags_value & couple.value: 192 | ret.append(couple.name) 193 | 194 | return ret 195 | 196 | def walk_to_ftrace_func_entry(self, ftrace_ops): 197 | """Function wrapping the process of walking to every ftrace_func_entry for an ftrace_ops""" 198 | 199 | # Decompose walk for better debugging 200 | try: 201 | func_hash = ftrace_ops.func_hash.dereference() 202 | except: 203 | vollog.debug( 204 | f"No func_hash for ftrace_ops@{hex(ftrace_ops.vol.offset)}, skipping..." 205 | ) 206 | return None 207 | 208 | try: 209 | filter_hash = func_hash.filter_hash.dereference() 210 | except: 211 | vollog.debug( 212 | f"No func_hash.filter_hash for ftrace_ops@{hex(ftrace_ops.vol.offset)}, skipping..." 213 | ) 214 | return None 215 | 216 | try: 217 | bucket_head = filter_hash.buckets.dereference().first.dereference() 218 | except exceptions.InvalidAddressException: 219 | vollog.debug( 220 | f"No func_hash.filter_hash.buckets for ftrace_ops@{hex(ftrace_ops.vol.offset)}, skipping..." 221 | ) 222 | return None 223 | 224 | while True: 225 | yield bucket_head.cast("ftrace_func_entry") 226 | if bucket_head.next.is_readable(): 227 | bucket_head = bucket_head.next.dereference() 228 | else: 229 | break 230 | 231 | def set_compiled_kernel_space_boundaries(self): 232 | """Set compiler kernel address spaces. Preferred to linux.LinuxUtilities.generate_kernel_handler_info()[0] for convenience""" 233 | self.kernel_space_start = self.vmlinux.get_absolute_symbol_address("_stext") 234 | self.kernel_space_end = self.vmlinux.get_absolute_symbol_address("_etext") 235 | 236 | def get_all_handlers(self): 237 | """Concatenate all handlers ("/proc/modules", "/sys/module/" and "unlinked kobject modules from sysfs hierarchy")""" 238 | return self.proc_handlers + self.sysfs_handlers + self.sysfs_unlinked_handlers 239 | 240 | def wrapper_lookup_module_address(self, leaked_address: int): 241 | # Detect module name based on leaked_address address 242 | module_name, _ = linux.LinuxUtilities.lookup_module_address( 243 | self.vmlinux, 244 | self.get_all_handlers(), 245 | leaked_address, 246 | ) 247 | # Aggressive module finder, for deeply hidden rootkits (try to detect usage of kobject_del) 248 | if module_name == UNKNOWN: 249 | sysfs_unlinked_modules = check_unlinked_modules.Check_unlinked_modules( 250 | self.context, self.config_path 251 | )._generator(self.sysfs_handlers, self.sysfs_modules, leaked_address) 252 | if sysfs_unlinked_modules: 253 | sysfs_unlinked_modules = [m[1] for m in sysfs_unlinked_modules] 254 | self.sysfs_unlinked_handlers = ( 255 | linux.LinuxUtilities.generate_kernel_handler_info( 256 | self.context, self.vmlinux.name, sysfs_unlinked_modules 257 | ) 258 | ) 259 | self.modules.extend(sysfs_unlinked_modules) 260 | 261 | # Search handlers again with the new informations, to see if leaked_address fits now 262 | module_name, _ = linux.LinuxUtilities.lookup_module_address( 263 | self.vmlinux, 264 | self.get_all_handlers(), 265 | leaked_address, 266 | ) 267 | 268 | return module_name 269 | 270 | def setup_modules_and_handlers(self): 271 | # Get /proc/modules and /sys/module/ listed modules 272 | self.proc_modules = list( 273 | lsmod.Lsmod.list_modules(self.context, self.vmlinux.name) 274 | ) 275 | self.sysfs_modules = list( 276 | check_unlinked_modules.Check_unlinked_modules.wrapper_get_sysfs_modules( 277 | self.context, self.config_path, self.vmlinux.name 278 | ) 279 | ) 280 | # Calculate boundaries for each module 281 | self.proc_handlers = linux.LinuxUtilities.generate_kernel_handler_info( 282 | self.context, self.vmlinux.name, self.proc_modules 283 | ) 284 | self.sysfs_handlers = linux.LinuxUtilities.generate_kernel_handler_info( 285 | self.context, self.vmlinux.name, self.sysfs_modules 286 | ) 287 | self.sysfs_unlinked_handlers = [] 288 | self.modules = self.proc_modules + self.sysfs_modules 289 | 290 | 291 | ## UTILITIES ## 292 | def wrapper_get_symbols_by_absolute_location( 293 | vmlinux: interfaces.context.ModuleInterface, target_address: int 294 | ): 295 | """List symbols related to a specified address""" 296 | 297 | symbols = list(vmlinux.get_symbols_by_absolute_location(target_address)) 298 | 299 | if len(symbols) == 0: 300 | return "UNKNOWN" 301 | else: 302 | return [ 303 | symbol if constants.BANG not in symbol else symbol.split(constants.BANG)[1] 304 | for symbol in symbols 305 | ] 306 | 307 | 308 | def get_module_object_from_name( 309 | wanted_module_name: str, modules: list 310 | ) -> linux.extensions.module: 311 | """Return a module object based on a module name""" 312 | for m in modules: 313 | if utility.array_to_string(m.name) == wanted_module_name: 314 | return m 315 | -------------------------------------------------------------------------------- /Volatility_contest_2023/plugins/check_tracepoints.py: -------------------------------------------------------------------------------- 1 | # This file is Copyright 2023 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 | # 4 | import logging 5 | from typing import List 6 | from volatility3.plugins.linux import lsmod, check_unlinked_modules 7 | from volatility3.framework import constants, exceptions, interfaces 8 | from volatility3.framework.configuration import requirements 9 | from volatility3.framework.renderers import TreeGrid 10 | from volatility3.framework.symbols import linux 11 | from volatility3.framework.objects import utility 12 | 13 | vollog = logging.getLogger(__name__) 14 | UNKNOWN = "UNKNOWN" 15 | 16 | 17 | class Check_tracepoints(interfaces.plugins.PluginInterface): 18 | """Detect tracepoints hooking""" 19 | 20 | _version = (1, 0, 0) 21 | _required_framework_version = (2, 5, 2) 22 | 23 | @classmethod 24 | def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: 25 | return [ 26 | requirements.ModuleRequirement( 27 | name="kernel", 28 | description="Linux kernel", 29 | architectures=["Intel32", "Intel64", "AArch64"], 30 | ), 31 | requirements.PluginRequirement( 32 | name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) 33 | ), 34 | requirements.PluginRequirement( 35 | name="check_unlinked_modules", 36 | plugin=check_unlinked_modules.Check_unlinked_modules, 37 | version=(1, 0, 0), 38 | ), 39 | requirements.PluginRequirement( 40 | name="check_modules", plugin=lsmod.Lsmod, version=(2, 0, 0) 41 | ), 42 | ] 43 | 44 | def run(self): 45 | """Plugin output format : 46 | 47 | Tracepoint : "tracepoint offset" ["tracepoint name"] 48 | Probe : "probe offset" ["probe name"] 49 | Module : hex("module offset") ["associated module"] | "UNKNOWN" 50 | Probe out of kernel .text : True | False 51 | """ 52 | 53 | columns = [ 54 | ("tracepoint", str), 55 | ("Probe", str), 56 | ("Module", str), 57 | ("Probe out of kernel .text", bool), 58 | ] 59 | 60 | return TreeGrid( 61 | columns, 62 | self._generator(), 63 | ) 64 | 65 | def _generator(self): 66 | self.vmlinux = self.context.modules[self.config["kernel"]] 67 | if not self.vmlinux.has_symbol("__start___tracepoints_ptrs"): 68 | raise exceptions.SymbolError( 69 | "__start___tracepoints_ptrs", 70 | self.vmlinux.symbol_table_name, 71 | 'The provided symbol table does not include the "__start___tracepoints_ptrs" symbol. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt.', 72 | ) 73 | 74 | self.checked_probes = {} 75 | self.set_compiled_kernel_space_boundaries() 76 | self.setup_modules_and_handlers() 77 | 78 | k_config_have_arch_prel32_relocations = False 79 | tracepoints = [] 80 | tracepoints_start = self.vmlinux.object_from_symbol( 81 | "__start___tracepoints_ptrs" 82 | ) 83 | tracepoints_end = self.vmlinux.object_from_symbol("__stop___tracepoints_ptrs") 84 | tracepoints_size_raw = tracepoints_end.vol.offset - tracepoints_start.vol.offset 85 | 86 | # Determine if tracepoints array contains a set of valid absolute pointers, or a set of relative pointers (represented by 32 bits integers). 87 | # See https://elixir.bootlin.com/linux/v6.6/source/include/linux/tracepoint.h#L113 for references. 88 | tracepoints_ptrs = utility.array_of_pointers( 89 | tracepoints_start, 90 | tracepoints_size_raw // 8, 91 | self.vmlinux.symbol_table_name + constants.BANG + "tracepoint", 92 | self.context, 93 | ) 94 | # Check two different pointers 95 | if ( 96 | not tracepoints_ptrs[0].is_readable() 97 | or not tracepoints_ptrs[1].is_readable() 98 | ): 99 | k_config_have_arch_prel32_relocations = True 100 | else: 101 | for tracepoint_ptr in tracepoints_ptrs: 102 | tracepoints.append(tracepoint_ptr.dereference()) 103 | 104 | vollog.debug( 105 | f"CONFIG_HAVE_ARCH_PREL32_RELOCATIONS was determined to be {k_config_have_arch_prel32_relocations}" 106 | ) 107 | 108 | if k_config_have_arch_prel32_relocations: 109 | subtype_int = self.vmlinux.context.symbol_space.get_type( 110 | self.vmlinux.symbol_table_name + constants.BANG + "int" 111 | ) 112 | tracepoints_relative_offsets = tracepoints_start.cast( 113 | "array", count=tracepoints_size_raw // 4, subtype=subtype_int 114 | ) 115 | # Based on "offset_to_ptr()". See https://elixir.bootlin.com/linux/v6.6/source/include/linux/compiler.h#L223 for references. 116 | for relative_offset in tracepoints_relative_offsets: 117 | tracepoint = self.vmlinux.object( 118 | "tracepoint", 119 | relative_offset + relative_offset.vol.offset, 120 | absolute=True, 121 | ) 122 | tracepoints.append(tracepoint) 123 | 124 | results = [] 125 | for i, tracepoint in enumerate(tracepoints): 126 | self._progress_callback( 127 | (i / len(tracepoints)) * 100, f"Iterating over tracepoints..." 128 | ) 129 | # Ignore tracepoints without attached probes 130 | if not tracepoint.funcs.is_readable(): 131 | continue 132 | 133 | try: 134 | parse_result = self.parse_tracepoint(tracepoint) 135 | results.append((0, parse_result)) 136 | except Exception as e: 137 | vollog.exception(f"Unhandled exception : {e}") 138 | 139 | # Preferred to "yield", otherwise progress_callback and results overlap... 140 | return results 141 | 142 | def parse_tracepoint(self, tracepoint): 143 | tracepoint_name = utility.pointer_to_string(tracepoint.name, count=512) 144 | tracepoint_offset = tracepoint.vol.offset 145 | probe_handler_address = tracepoint.funcs.dereference().func 146 | 147 | # Avoid running the aggressive module finder twice for an address, if it wasn't found previously 148 | if self.checked_probes.get(probe_handler_address): 149 | module_name = self.checked_probes[probe_handler_address] 150 | else: 151 | module_name = self.wrapper_lookup_module_address(probe_handler_address) 152 | self.checked_probes[probe_handler_address] = module_name 153 | 154 | # Useful information allowing to detect if a module was inserted dynamically or if it is part of the compiled kernel 155 | probe_out_of_kernel_range = ( 156 | probe_handler_address < self.kernel_space_start 157 | or probe_handler_address > self.kernel_space_end 158 | ) 159 | 160 | ### Format results ### 161 | probe_handler_address_symbol = UNKNOWN 162 | f_module = UNKNOWN 163 | # Fetch more informations about the module 164 | if module_name != UNKNOWN: 165 | module_obj = get_module_object_from_name(module_name, self.modules) 166 | module_address = module_obj.vol.offset 167 | f_module = f"{hex(module_address)} [{module_name}]" 168 | probe_handler_address_symbol = ( 169 | module_obj.get_symbol_by_address(probe_handler_address) or UNKNOWN 170 | ) 171 | 172 | results = ( 173 | f"{hex(tracepoint_offset)} [{tracepoint_name}]", 174 | f"{hex(probe_handler_address)} [{probe_handler_address_symbol}]", 175 | f_module, 176 | probe_out_of_kernel_range, 177 | ) 178 | return results 179 | 180 | def set_compiled_kernel_space_boundaries(self): 181 | """Set compiler kernel address spaces. Preferred to linux.LinuxUtilities.generate_kernel_handler_info()[0] for convenience""" 182 | self.kernel_space_start = self.vmlinux.get_absolute_symbol_address("_stext") 183 | self.kernel_space_end = self.vmlinux.get_absolute_symbol_address("_etext") 184 | 185 | def get_all_handlers(self): 186 | """Concatenate all handlers ("/proc/modules", "/sys/module/" and "unlinked kobject modules from sysfs hierarchy")""" 187 | return self.proc_handlers + self.sysfs_handlers + self.sysfs_unlinked_handlers 188 | 189 | def wrapper_lookup_module_address(self, leaked_address: int): 190 | # Detect module name based on leaked_address address 191 | module_name, _ = linux.LinuxUtilities.lookup_module_address( 192 | self.vmlinux, 193 | self.get_all_handlers(), 194 | leaked_address, 195 | ) 196 | # Aggressive module finder, for deeply hidden rootkits (try to detect usage of kobject_del) 197 | if module_name == UNKNOWN: 198 | sysfs_unlinked_modules = check_unlinked_modules.Check_unlinked_modules( 199 | self.context, self.config_path 200 | )._generator(self.sysfs_handlers, self.sysfs_modules, leaked_address) 201 | if sysfs_unlinked_modules: 202 | sysfs_unlinked_modules = [m[1] for m in sysfs_unlinked_modules] 203 | self.sysfs_unlinked_handlers = ( 204 | linux.LinuxUtilities.generate_kernel_handler_info( 205 | self.context, self.vmlinux.name, sysfs_unlinked_modules 206 | ) 207 | ) 208 | self.modules.extend(sysfs_unlinked_modules) 209 | 210 | # Search handlers again with the new informations, to see if leaked_address fits now 211 | module_name, _ = linux.LinuxUtilities.lookup_module_address( 212 | self.vmlinux, 213 | self.get_all_handlers(), 214 | leaked_address, 215 | ) 216 | 217 | return module_name 218 | 219 | def setup_modules_and_handlers(self): 220 | # Get /proc/modules and /sys/module/ listed modules 221 | self.proc_modules = list( 222 | lsmod.Lsmod.list_modules(self.context, self.vmlinux.name) 223 | ) 224 | self.sysfs_modules = list( 225 | check_unlinked_modules.Check_unlinked_modules.wrapper_get_sysfs_modules( 226 | self.context, self.config_path, self.vmlinux.name 227 | ) 228 | ) 229 | # Calculate boundaries for each module 230 | self.proc_handlers = linux.LinuxUtilities.generate_kernel_handler_info( 231 | self.context, self.vmlinux.name, self.proc_modules 232 | ) 233 | self.sysfs_handlers = linux.LinuxUtilities.generate_kernel_handler_info( 234 | self.context, self.vmlinux.name, self.sysfs_modules 235 | ) 236 | self.sysfs_unlinked_handlers = [] 237 | self.modules = self.proc_modules + self.sysfs_modules 238 | 239 | 240 | ## UTILITIES ## 241 | def get_module_object_from_name( 242 | wanted_module_name: str, modules: list 243 | ) -> linux.extensions.module: 244 | """Return a module object based on a module name""" 245 | for m in modules: 246 | if utility.array_to_string(m.name) == wanted_module_name: 247 | return m 248 | -------------------------------------------------------------------------------- /Volatility_contest_2023/plugins/check_unlinked_modules.py: -------------------------------------------------------------------------------- 1 | # This file is Copyright 2023 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 | # 4 | import logging 5 | from typing import List 6 | from volatility3.plugins.linux import check_modules 7 | from volatility3.framework import interfaces, exceptions 8 | from volatility3.framework.configuration import requirements 9 | from volatility3.framework.renderers import format_hints, TreeGrid 10 | from volatility3.framework.symbols import linux 11 | from volatility3.framework.objects import ( 12 | utility, 13 | PrimitiveObject, 14 | Boolean, 15 | Enumeration, 16 | templates, 17 | ) 18 | from enum import Enum 19 | from volatility3.framework.layers import scanners 20 | 21 | vollog = logging.getLogger(__name__) 22 | MAX_KERNEL_MEMORY_SEARCH_LIMIT = 2**20 # Arbitrary constant 23 | 24 | 25 | class Check_unlinked_modules(interfaces.plugins.PluginInterface): 26 | """Scan memory for unlinked modules""" 27 | 28 | _version = (1, 0, 1) 29 | _required_framework_version = (2, 5, 2) 30 | 31 | @classmethod 32 | def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: 33 | return [ 34 | requirements.ModuleRequirement( 35 | name="kernel", 36 | description="Linux kernel", 37 | architectures=["Intel32", "Intel64"], 38 | ), 39 | requirements.PluginRequirement( 40 | name="check_modules", 41 | plugin=check_modules.Check_modules, 42 | version=(0, 0, 0), 43 | ), 44 | requirements.IntRequirement( 45 | name="leaked_address", 46 | description="Optimized memory scan, around a leaked address from an hidden module (e.g. ftrace callback)", 47 | optional=True, 48 | default=0, 49 | ), 50 | ] 51 | 52 | @classmethod 53 | def wrapper_get_sysfs_modules( 54 | cls, 55 | context: interfaces.context.ContextInterface, 56 | config_path: str, 57 | vmlinux_name: str, 58 | ): 59 | """Wrapper for check_modules plugin, return a list of modules (similarly to the lsmod plugin)""" 60 | 61 | vmlinux = context.modules[vmlinux_name] 62 | sysfs_modules: dict = check_modules.Check_modules( 63 | context, config_path 64 | ).get_kset_modules(context, vmlinux_name) 65 | # Convert get_kset_modules() offsets back to module objects 66 | for m_offset in sysfs_modules.values(): 67 | yield vmlinux.object(object_type="module", offset=m_offset, absolute=True) 68 | 69 | @classmethod 70 | def lookup_sysfs_hidden_modules( 71 | cls, 72 | context: interfaces.context.ContextInterface, 73 | vmlinux_name: str, 74 | modules_handlers: list, 75 | leaked_address: int = 0, 76 | ): 77 | """ 78 | Some rootkits unlink themselves from list_head AND module_kobject. However, due to the use of a callback (e.g. via ftrace), they reveal an address inside their module memory. 79 | Doing so, we can scan the close memory range of the callback for any "module" structs. 80 | Most of the time, a hidden rootkit memory range will be located between two other modules, giving us some minimum and maximum range to search in. 81 | Note : callbacks with values like 0xffffffffffffffff will fail with DEBUG : "Scan Failure: Sections have no size, nothing to scan". This ensure that pottential OOB reads won't crash the plugin. 82 | 83 | Current framework implementation does not take into account modules with non-printable characters in their name. 84 | To avoid changing too much of the framework, we'll follow the same path. 85 | However, a module might have a name like "\xde\xad\xbe\xef", which would need manual investigation with volshell to uncover. 86 | In the case of multiple modules on a same memory dump nulling their name (e.g. rootkit "osom"), it could result in multiple modules with empty names, overlapping in the handers list. 87 | """ 88 | 89 | vmlinux = context.modules[vmlinux_name] 90 | flattened_handlers = flatten_modules_handlers(modules_handlers) 91 | flattened_handlers.sort() 92 | if leaked_address != 0: 93 | ( 94 | low_address, 95 | high_address, 96 | ) = calculate_closest_modules_from_address( 97 | target_address=leaked_address, 98 | flattened_handlers=flattened_handlers, 99 | ) 100 | if low_address == None: 101 | low_address = leaked_address - MAX_KERNEL_MEMORY_SEARCH_LIMIT 102 | if high_address == None: 103 | high_address = leaked_address + MAX_KERNEL_MEMORY_SEARCH_LIMIT 104 | 105 | # Search : between previous module and leaked address ; between leaked address and next module 106 | sections_to_scan = [ 107 | (low_address, leaked_address - low_address), 108 | (leaked_address, high_address - leaked_address), 109 | ] 110 | vollog.info( 111 | f"Searching modules structs from {hex(low_address)} to {hex(high_address)}, based on provided address {hex(leaked_address)}..." 112 | ) 113 | else: 114 | low_address = flattened_handlers[0] - MAX_KERNEL_MEMORY_SEARCH_LIMIT 115 | high_address = flattened_handlers[-1] + MAX_KERNEL_MEMORY_SEARCH_LIMIT 116 | # Search : between first and last modules ; before first module ; after last module 117 | sections_to_scan = [ 118 | (flattened_handlers[0], flattened_handlers[-1] - flattened_handlers[0]), 119 | (low_address, flattened_handlers[0] - low_address), 120 | (flattened_handlers[-1], high_address - flattened_handlers[-1]), 121 | ] 122 | vollog.info( 123 | f"Searching modules structs, in (start, size) : {[(hex(x[0]), x[1]) for x in sections_to_scan]}..." 124 | ) 125 | scanned_modules = scan_memory_for_modules( 126 | context=context, 127 | kernel_module_name=vmlinux.name, 128 | sections_to_scan=sections_to_scan, 129 | ) 130 | 131 | return_modules = [] 132 | for scanned_module in list(scanned_modules): 133 | scanned_module_handler = linux.LinuxUtilities.generate_kernel_handler_info( 134 | context, vmlinux.name, (scanned_module,) 135 | )[ 136 | 1 137 | ] # Skip __kernel__ 138 | 139 | # Check if scanned module already exists in our lists 140 | if not any( 141 | scanned_module_handler == existing_handler 142 | for existing_handler in modules_handlers 143 | ): 144 | return_modules.append(scanned_module) 145 | vollog.info( 146 | f'Found sysfs non-listed module "{utility.array_to_string(scanned_module.name)}" at {hex(scanned_module.vol.offset)}' 147 | ) 148 | 149 | return return_modules 150 | 151 | def _generator( 152 | self, sysfs_handlers: list, sysfs_modules: list, leaked_address: int = 0 153 | ): 154 | vmlinux = self.context.modules[self.config["kernel"]] 155 | 156 | # Detect unlinked modules 157 | sysfs_unlinked_modules = self.lookup_sysfs_hidden_modules( 158 | self.context, 159 | vmlinux.name, 160 | sysfs_handlers, 161 | leaked_address, 162 | ) 163 | 164 | dict_sysfs_modules = dict( 165 | (str(utility.array_to_string(module.name)), module) 166 | for module in sysfs_modules 167 | ) 168 | dict_sysfs_unlinked_modules = dict( 169 | (str(utility.array_to_string(module.name)), module) 170 | for module in sysfs_unlinked_modules 171 | ) 172 | for mod in set(dict_sysfs_unlinked_modules.items()).difference( 173 | set(dict_sysfs_modules.items()) 174 | ): 175 | yield mod 176 | 177 | def run(self): 178 | vmlinux = self.context.modules[self.config["kernel"]] 179 | 180 | # Get /sys/module/ listed modules 181 | sysfs_modules = list( 182 | self.wrapper_get_sysfs_modules(self.context, self.config_path, vmlinux.name) 183 | ) 184 | # Calculate boundaries for each module 185 | sysfs_handlers = linux.LinuxUtilities.generate_kernel_handler_info( 186 | self.context, vmlinux.name, sysfs_modules 187 | )[ 188 | 1: 189 | ] # Skip __kernel__, else the range to search in will be huge (_stext to _etext) 190 | 191 | detected_modules = self._generator( 192 | sysfs_handlers=sysfs_handlers, 193 | sysfs_modules=sysfs_modules, 194 | leaked_address=self.config.get("leaked_address"), 195 | ) 196 | return TreeGrid( 197 | [("Module Address", format_hints.Hex), ("Module Name", str)], 198 | [ 199 | (0, (format_hints.Hex(mod[1].vol.offset), mod[0])) 200 | for mod in detected_modules 201 | ], 202 | ) 203 | 204 | 205 | ## UTILITIES ## 206 | def regex_from_struct_members( 207 | context: interfaces.context.ContextInterface, 208 | kernel_module_name: str, 209 | vol_struct: templates.ObjectTemplate, 210 | path: list = [], 211 | overrides: dict[str, bytes] = {}, 212 | stop_key: str = "", 213 | ): 214 | """Walk an ObjectTemplate members to create a matching RegEx. 215 | This is useful to search for a struct artifacts in memory, while taking into account variants from one symbol table to another (one kernel to another). 216 | """ 217 | kernel = context.modules[kernel_module_name] 218 | 219 | # Sort struct members and flatten dict 220 | struct_members = dict( 221 | sorted(vol_struct.vol.members.items(), key=lambda item: item[1][0]) 222 | ) 223 | struct_members_list = [] 224 | for name, (offset, obj) in struct_members.items(): 225 | struct_members_list.append((name, offset, obj)) 226 | if name == stop_key and path == []: 227 | break 228 | struct_size = vol_struct.size 229 | struct_regex = [] 230 | consumed = 0 231 | 232 | # Iterate over every member of the struct 233 | for i, member in enumerate(struct_members_list): 234 | name, offset, obj = member 235 | object_class: PrimitiveObject | None = obj.vol.get("object_class") 236 | if i < len(struct_members_list) - 1: 237 | member_len = struct_members_list[i + 1][1] - offset 238 | else: 239 | member_len = struct_size - offset 240 | consumed += member_len 241 | # Keep track of the depth and path of this element, starting from the root object 242 | path.append(name) 243 | # Determine what regex to use, depending on the element type 244 | if ".".join(path) in overrides: 245 | round_regex = overrides[".".join(path)] 246 | elif object_class == Enumeration: 247 | choices = [] 248 | for choice in obj.vol.choices.values(): 249 | choices.append(int.to_bytes(choice, member_len, "little", signed=True)) 250 | round_regex = b"(?:" + b"|".join(choices) + b")" 251 | elif object_class == Boolean: 252 | possible_values = [] 253 | for byte_order in ["little", "big"]: 254 | for possible_value in [0, 1]: 255 | possible_values.append( 256 | int.to_bytes(possible_value, member_len, byte_order) 257 | ) 258 | # don't bother with endianness on 1 byte 259 | if member_len == 1: 260 | break 261 | 262 | round_regex = b"(?:" + b"|".join(possible_values) + b")" 263 | 264 | # # Possible alignement problem when not using lookbehind here 265 | # if member_len > 1: 266 | # round_regex = b"(?=" + round_regex + b")" 267 | else: 268 | type_name = obj.vol.type_name 269 | # Recursive introspection, call this function again to analyze a sub-element 270 | if kernel.has_type(type_name) and kernel.get_type(type_name).vol.get( 271 | "members" 272 | ): 273 | type = kernel.get_type(type_name) 274 | tmp_regex, ret_consumed = regex_from_struct_members( 275 | context, kernel_module_name, type, path, overrides 276 | ) 277 | # Detect missing bytes (padding) 278 | if member_len > ret_consumed: 279 | padding = member_len - ret_consumed 280 | tmp_regex += f".{{{padding}}}".encode() 281 | round_regex = tmp_regex 282 | # We can't make assumptions about this element 283 | else: 284 | round_regex = f".{{{member_len}}}".encode() 285 | 286 | # vollog.debug(f"path : {path} : {round_regex}") 287 | struct_regex.append(round_regex) 288 | # We are done with this element 289 | path.remove(name) 290 | 291 | # Use a lookahead to allow overlapping matches (only around final struct_regex) 292 | if len(path) == 0: 293 | return ( 294 | b"(?=" + b"".join(struct_regex) + b")", 295 | consumed, 296 | ) 297 | else: 298 | return b"".join(struct_regex), consumed 299 | 300 | 301 | class RegexOverrides(Enum): 302 | """Custom overrides""" 303 | 304 | # Match anything but \x00{ptr_size} 305 | NON_NULL_POINTER = ( 306 | lambda ptr_size: b"(?:(?!\x00" + f"{{{ptr_size}}}).{{{ptr_size}}})".encode() 307 | ) 308 | 309 | 310 | def scan_memory_for_modules( 311 | context: interfaces.context.ContextInterface, 312 | kernel_module_name: str, 313 | sections_to_scan: list, 314 | ): 315 | """Scan a memory region to uncover modules structs 316 | 317 | Args: 318 | context: The current context 319 | kernel_module_name: The name of the kernel module 320 | sections_to_scan: A list of tuples including a start address and a size 321 | Yields: 322 | A module object 323 | """ 324 | kernel = context.modules[kernel_module_name] 325 | m_struct = kernel.get_symbol("module").type.vol.subtype 326 | ptr_size = kernel.get_type("pointer").size 327 | 328 | # We assume module.mkobj.mod is a non-null pointer to the module itself, allowing us to drastically reduce regex candidates 329 | overrides = { 330 | "mkobj.mod": RegexOverrides.NON_NULL_POINTER(ptr_size), 331 | "init_layout.mnt.mod": RegexOverrides.NON_NULL_POINTER(ptr_size), 332 | } 333 | 334 | # Using a regex too long will eventually result in overlapping and alignements problems, so stop at mkobj 335 | stop_key = "mkobj" 336 | module_regex, s = regex_from_struct_members( 337 | context, kernel_module_name, m_struct, overrides=overrides, stop_key=stop_key 338 | ) 339 | scanner = scanners.RegExScanner(module_regex) 340 | scanned = context.layers[kernel.layer_name].scan( 341 | context=context, scanner=scanner, sections=sections_to_scan 342 | ) 343 | # Iterate over candidates structs 344 | for module_candidate_offset in scanned: 345 | # Use a try-except block to avoid crashing on OOB read 346 | try: 347 | m = kernel.object( 348 | object_type="module", offset=module_candidate_offset, absolute=True 349 | ) 350 | # Check if module mkobj.mod points to the candidate offset 351 | if m.mkobj.mod == module_candidate_offset: 352 | vollog.info( 353 | f'Found module "{utility.array_to_string(m.name)}" at {hex(m.vol.offset)}' 354 | ) 355 | yield m 356 | except exceptions.InvalidAddressException: 357 | continue 358 | 359 | 360 | def calculate_closest_modules_from_address( 361 | target_address: int, flattened_handlers: list 362 | ): 363 | """Determine closest modules for a given address. 364 | 365 | Args: 366 | target_address: The target address to search boundaries for 367 | handlers_set: A list containing an unordered list with flattened (module_start, module_end) from handlers. See flatten_modules_handlers() for reference. 368 | Returns: 369 | Tuple containing previous and next boundary 370 | """ 371 | # Insert target_address and sort 372 | flattened_handlers.append(target_address) 373 | flattened_handlers.sort() 374 | # Determine position of target_address in list 375 | target_address_index = flattened_handlers.index(target_address) 376 | # Check if target_address index in list is : top of the list > bottom of the list > any other case 377 | if target_address_index + 1 >= len(flattened_handlers): 378 | return ( 379 | flattened_handlers[target_address_index - 1], 380 | None, 381 | ) # No next boundary 382 | elif target_address_index - 1 < 0: 383 | return ( 384 | None, 385 | flattened_handlers[target_address_index + 1], 386 | ) # No previous boundary 387 | else: 388 | return ( 389 | flattened_handlers[target_address_index - 1], 390 | flattened_handlers[target_address_index + 1], 391 | ) 392 | 393 | 394 | def flatten_modules_handlers( 395 | handlers: list, 396 | ): 397 | """Flatten a list of previously calculated modules handlers boundaries (extract all "start" and "end")""" 398 | return list( 399 | sum( 400 | [(h[1], h[2]) for h in set(handlers)], 401 | (), 402 | ) 403 | ) 404 | -------------------------------------------------------------------------------- /vol_ez_install/Dockerfile-vol2: -------------------------------------------------------------------------------- 1 | FROM alpine:3.15 2 | 3 | RUN apk update 4 | 5 | RUN apk add pcre-dev pcre-tools python2 python2-dev gcc g++ zlib-dev jpeg-dev linux-headers openssl-dev dwarf-tools 6 | 7 | RUN python2 -m ensurepip 8 | 9 | RUN pip2 install --upgrade pip setuptools 10 | 11 | RUN pip2 install distorm3 yara-python pycryptodome pillow openpyxl ujson -------------------------------------------------------------------------------- /vol_ez_install/Dockerfile-vol3: -------------------------------------------------------------------------------- 1 | FROM alpine:3.17 2 | 3 | RUN apk update 4 | 5 | RUN apk add git python3 python3-dev py-pip snappy-dev make gcc capstone musl-dev linux-headers openssl-dev libusb-dev 6 | 7 | RUN pip3 install wheel 8 | 9 | WORKDIR /tmp 10 | # https://stackoverflow.com/a/58801213 11 | ADD "https://www.random.org/cgi-bin/randbyte?nbytes=10&format=h" skipcache 12 | RUN git clone https://github.com/volatilityfoundation/volatility3.git && cd volatility3/ && pip3 install .[full] 13 | RUN rm -rf volatility3 -------------------------------------------------------------------------------- /vol_ez_install/README.md: -------------------------------------------------------------------------------- 1 | # Volatility easy install 2 | 3 | Bored of spending more time installing volatility than actually using it ? Here is a small script that allows you to install it with all needed dependencies easily ! 4 | 5 | One container for each volatility version will be setup. The volatility code will be **hosted directly on your host**, in the home directory ("\~/vol2" and "\~/vol3"). Containers will be able to access it via a binded mount. 6 | 7 | ## Disclaimer 8 | 9 | The use of a dockerized setup for Volatility3 is not worth the Docker overhead, as Volatility3 dependencies integrate well with recent systems. It is still available for specific scenarios however. 10 | 11 | ## Setup 12 | 13 | **Requirements :** 14 | 15 | - `docker`, `sudo`, `git` 16 | 17 | Usage : `./vol_ez_install.sh`, do not use sudo to run directly. 18 | 19 | ```sh 20 | >>> Volatility easy install <<< 21 | Syntax: vol_ez_install.sh [option(s)] 22 | options: 23 | vol2_install Setup latest volatility2 github master on the system 24 | vol3_install Setup latest volatility3 github master on the system 25 | ``` 26 | 27 | The script adds two aliases `vol2d` and `vol3d` to your bashrc/zshrc, for smaller commands and better docker experience. 28 | 29 | ## Usage 30 | 31 | Example usage, **from the docker host** : 32 | 33 | ```sh 34 | # vol2 35 | vol2d -f "`wvol dump.raw`" imageinfo 36 | vol2d -f "`wvol dump.raw`" --profile [profile_name] pslist 37 | vol2d -f "`wvol dump.raw`" --profile [profile_name] procdump -D "`wvol ./dump_dir/`" -p [pid] 38 | 39 | # vol3 40 | vol3d -f "`wvol dump.raw`" windows.pslist 41 | vol3d -f "`wvol dump.raw`" -o "`wvol ./dump_dir/`" windows.dumpfiles --pid [pid] 42 | 43 | # vol3 volshell 44 | volshell3d -f "`wvol dump.raw`" -h 45 | ``` 46 | 47 | To reference files of your host inside the container, please use the ``` "`wvol [file_you_want_the_container_to_access]`" ``` syntax. Doing so, it translates to a path reachable by the container. 48 | -------------------------------------------------------------------------------- /vol_ez_install/vol_ez_install.sh: -------------------------------------------------------------------------------- 1 | Help() { 2 | # Display Help 3 | echo ">>> Volatility easy install <<<" 4 | echo "Syntax: vol_ez_install.sh [option(s)]" 5 | echo "options:" 6 | echo "vol2_install Setup latest volatility2 github master on the system" 7 | echo "vol3_install Setup latest volatility3 github master on the system" 8 | } 9 | 10 | add_rc() { 11 | 12 | check=false 13 | if [ -f ~/.zshrc ]; then 14 | echo "$1" >>~/.zshrc 15 | check=true 16 | fi 17 | if [ -f ~/.bashrc ]; then 18 | echo "$1" >>~/.bashrc 19 | check=true 20 | fi 21 | if [ ! "$check" ]; then 22 | echo "Shell is neither 'bash' nor 'zsh'. Cannot add alias." 23 | fi 24 | 25 | } 26 | 27 | vol2_install() { 28 | 29 | echo 'volatility2 setup...' 30 | 31 | # Create directories 32 | mkdir -p ~/vol2/ 33 | mkdir -p ~/vol2/custom_plugins 34 | # Fetch Dockerfile 35 | wget https://github.com/Abyss-W4tcher/volatility-scripts/raw/master/vol_ez_install/Dockerfile-vol2 -O ~/vol2/Dockerfile-vol2 36 | # Build container 37 | sudo docker build -t vol2_dck -f ~/vol2/Dockerfile-vol2 ~/vol2 38 | # Clone volatility2 39 | git clone https://github.com/volatilityfoundation/volatility.git ~/vol2/volatility2 40 | # Add aliases 41 | grep -q 'wvol' ~/.zshrc ~/.bashrc &>/dev/null || add_rc 'wvol() { echo "/bind"$(printf "%q" "$(realpath ""$1"")"); }' 42 | grep -q 'vol2d' ~/.zshrc ~/.bashrc &>/dev/null || add_rc 'alias vol2d="sudo docker run --rm -v /:/bind/ vol2_dck python2 $(wvol ~/vol2/volatility2/vol.py)"' 43 | 44 | echo 'volatility2 setup completed !' 45 | } 46 | 47 | vol3_install() { 48 | 49 | echo 'volatility3 setup...' 50 | 51 | # Create directories 52 | mkdir -p ~/vol3/ 53 | mkdir -p ~/vol3/custom_plugins 54 | # Fetch Dockerfile 55 | wget https://github.com/Abyss-W4tcher/volatility-scripts/raw/master/vol_ez_install/Dockerfile-vol3 -O ~/vol3/Dockerfile-vol3 56 | # Build container 57 | sudo docker build -t vol3_dck -f ~/vol3/Dockerfile-vol3 ~/vol3 58 | # Add volume for cache 59 | sudo docker volume create vol3-cache 60 | # Clone volatility3 61 | git clone https://github.com/volatilityfoundation/volatility3.git ~/vol3/volatility3 || (echo "Running git pull in ~/vol3/volatility3..." && cd ~/vol3/volatility3 && git pull) 62 | # Add aliases 63 | grep -q 'wvol' ~/.zshrc ~/.bashrc &>/dev/null || add_rc 'wvol() { echo "/bind"$(printf "%q" "$(realpath ""$1"")"); }' 64 | grep -q 'vol3d' ~/.zshrc ~/.bashrc &>/dev/null || add_rc 'alias vol3d="sudo docker run --rm -v vol3-cache:/root/.cache/volatility3/ -v /:/bind/ vol3_dck python3 $(wvol ~/vol3/volatility3/vol.py)"' 65 | grep -q 'volshell3d' ~/.zshrc ~/.bashrc &>/dev/null || add_rc 'alias volshell3d="sudo docker run --rm -it -v vol3-cache:/root/.cache/volatility3/ -v /:/bind/ vol3_dck python3 $(wvol ~/vol3/volatility3/volshell.py)"' 66 | 67 | echo 'volatility3 setup completed !' 68 | 69 | } 70 | 71 | for cmd in docker sudo git wget; do 72 | type $cmd &>/dev/null || { echo "Please install $cmd before proceeding"; exit 1; } 73 | done 74 | 75 | 76 | install=false 77 | for arg in "$@"; do 78 | if [ "$arg" = "vol2_install" ]; then 79 | vol2_install 80 | install=true 81 | fi 82 | if [ "$arg" = "vol3_install" ]; then 83 | vol3_install 84 | install=true 85 | fi 86 | done 87 | 88 | if [ "$install" = true ]; then 89 | echo "You can now run \"exec \$SHELL\" to reload the shell and get the commands ready !" 90 | else 91 | Help 92 | fi 93 | 94 | # Testing: 95 | # docker run --rm -it -v "${DOCKER_HOST#unix://}":/var/run/docker.sock -v $(pwd):/vol_dock ubuntu:latest bash 96 | # apt update && apt install -y wget sudo docker.io 97 | # cd /vol_dock/vol_ez_install/ && ./vol_ez_install.sh vol3_install --------------------------------------------------------------------------------