Skip to content

Commit 3ff4f01

Browse files
authored
Merge pull request #899 from cstation/feature/linux_elf_dump
Dump ELFs to file
2 parents c8a85e1 + d53714f commit 3ff4f01

File tree

3 files changed

+180
-34
lines changed

3 files changed

+180
-34
lines changed

volatility3/framework/constants/linux/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,5 @@
279279
"bpf",
280280
"checkpoint_restore",
281281
)
282+
283+
ELF_MAX_EXTRACTION_SIZE = 1024 * 1024 * 1024 * 4 - 1

volatility3/framework/plugins/linux/elfs.py

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,26 @@
44
"""A module containing a collection of plugins that produce data typically
55
found in Linux's /proc file system."""
66

7-
from typing import List
7+
import logging
8+
from typing import List, Optional, Type
89

9-
from volatility3.framework import renderers, interfaces
10+
from volatility3.framework import constants, interfaces, renderers
1011
from volatility3.framework.configuration import requirements
1112
from volatility3.framework.interfaces import plugins
1213
from volatility3.framework.objects import utility
1314
from volatility3.framework.renderers import format_hints
15+
from volatility3.framework.symbols import intermed
16+
from volatility3.framework.symbols.linux.extensions import elf
1417
from volatility3.plugins.linux import pslist
1518

19+
vollog = logging.getLogger(__name__)
20+
1621

1722
class Elfs(plugins.PluginInterface):
1823
"""Lists all memory mapped ELF files for all processes."""
1924

2025
_required_framework_version = (2, 0, 0)
26+
_version = (2, 0, 0)
2127

2228
@classmethod
2329
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -36,9 +42,93 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]
3642
element_type=int,
3743
optional=True,
3844
),
45+
requirements.BooleanRequirement(
46+
name="dump",
47+
description="Extract listed processes",
48+
default=False,
49+
optional=True,
50+
),
3951
]
4052

53+
@classmethod
54+
def elf_dump(
55+
cls,
56+
context: interfaces.context.ContextInterface,
57+
layer_name: str,
58+
elf_table_name: str,
59+
vma: interfaces.objects.ObjectInterface,
60+
task: interfaces.objects.ObjectInterface,
61+
open_method: Type[interfaces.plugins.FileHandlerInterface],
62+
) -> Optional[interfaces.plugins.FileHandlerInterface]:
63+
"""Extracts an ELF as a FileHandlerInterface
64+
Args:
65+
context: the context to operate upon
66+
layer_name: The name of the layer on which to operate
67+
elf_table_name: the name for the symbol table containing the symbols for ELF-files
68+
vma: virtual memory allocation of ELF
69+
task: the task object whose memory should be output
70+
open_method: class to provide context manager for opening the file
71+
Returns:
72+
An open FileHandlerInterface object containing the complete data for the task or None in the case of failure
73+
"""
74+
75+
proc_layer = context.layers[layer_name]
76+
file_handle = None
77+
78+
elf_object = context.object(
79+
elf_table_name + constants.BANG + "Elf",
80+
offset=vma.vm_start,
81+
layer_name=layer_name,
82+
)
83+
84+
if not elf_object.is_valid():
85+
return None
86+
87+
sections = {}
88+
# TODO: Apply more effort to reconstruct ELF, e.g.: https://github.com/enbarberis/core2ELF64 ?
89+
for phdr in elf_object.get_program_headers():
90+
if phdr.p_type != 1: # PT_LOAD = 1
91+
continue
92+
93+
start = phdr.p_vaddr
94+
size = phdr.p_memsz
95+
end = start + size
96+
97+
# Use complete memory pages for dumping
98+
# If start isn't a multiple of 4096, stick to the highest multiple < start
99+
# If end isn't a multiple of 4096, stick to the lowest multiple > end
100+
if start % 4096:
101+
start = start & ~0xFFF
102+
103+
if end % 4096:
104+
end = (end & ~0xFFF) + 4096
105+
106+
real_size = end - start
107+
108+
# Check if ELF has a legitimate size
109+
if real_size < 0 or real_size > constants.linux.ELF_MAX_EXTRACTION_SIZE:
110+
raise ValueError(f"The claimed size of the ELF is invalid: {real_size}")
111+
112+
sections[start] = real_size
113+
114+
elf_data = b""
115+
for section_start in sorted(sections.keys()):
116+
read_size = sections[section_start]
117+
118+
buf = proc_layer.read(vma.vm_start + section_start, read_size, pad=True)
119+
elf_data = elf_data + buf
120+
121+
file_handle = open_method(
122+
f"pid.{task.pid}.{utility.array_to_string(task.comm)}.{vma.vm_start:#x}.dmp"
123+
)
124+
file_handle.write(elf_data)
125+
126+
return file_handle
127+
41128
def _generator(self, tasks):
129+
elf_table_name = intermed.IntermediateSymbolTable.create(
130+
self.context, self.config_path, "linux", "elf", class_types=elf.class_types
131+
)
42132
for task in tasks:
43133
proc_layer_name = task.add_process_layer()
44134
if not proc_layer_name:
@@ -60,6 +150,21 @@ def _generator(self, tasks):
60150

61151
path = vma.get_name(self.context, task)
62152

153+
file_output = "Disabled"
154+
if self.config["dump"]:
155+
file_handle = self.elf_dump(
156+
self.context,
157+
proc_layer_name,
158+
elf_table_name,
159+
vma,
160+
task,
161+
self.open,
162+
)
163+
file_output = "Error outputting file"
164+
if file_handle:
165+
file_handle.close()
166+
file_output = str(file_handle.preferred_filename)
167+
63168
yield (
64169
0,
65170
(
@@ -68,6 +173,7 @@ def _generator(self, tasks):
68173
format_hints.Hex(vma.vm_start),
69174
format_hints.Hex(vma.vm_end),
70175
path,
176+
file_output,
71177
),
72178
)
73179

@@ -81,6 +187,7 @@ def run(self):
81187
("Start", format_hints.Hex),
82188
("End", format_hints.Hex),
83189
("File Path", str),
190+
("File Output", str),
84191
],
85192
self._generator(
86193
pslist.PsList.list_tasks(

volatility3/framework/plugins/linux/pslist.py

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0
22
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
33
#
4-
from typing import Callable, Iterable, List, Any, Tuple
4+
from typing import Any, Callable, Iterable, List
55

6-
from volatility3.framework import renderers, interfaces
6+
from volatility3.framework import interfaces, renderers
77
from volatility3.framework.configuration import requirements
88
from volatility3.framework.objects import utility
99
from volatility3.framework.renderers import format_hints
10+
from volatility3.framework.symbols import intermed
11+
from volatility3.framework.symbols.linux.extensions import elf
12+
from volatility3.plugins.linux import elfs
1013

1114

1215
class PsList(interfaces.plugins.PluginInterface):
@@ -24,6 +27,9 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]
2427
description="Linux kernel",
2528
architectures=["Intel32", "Intel64"],
2629
),
30+
requirements.PluginRequirement(
31+
name="elfs", plugin=elfs.Elfs, version=(2, 0, 0)
32+
),
2733
requirements.ListRequirement(
2834
name="pid",
2935
description="Filter on specific process IDs",
@@ -42,6 +48,12 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]
4248
optional=True,
4349
default=False,
4450
),
51+
requirements.BooleanRequirement(
52+
name="dump",
53+
description="Extract listed processes",
54+
optional=True,
55+
default=False,
56+
),
4557
]
4658

4759
@classmethod
@@ -66,38 +78,12 @@ def filter_func(x):
6678
else:
6779
return lambda _: False
6880

69-
def _get_task_fields(
70-
self, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False
71-
) -> Tuple[int, int, int, str]:
72-
"""Extract the fields needed for the final output
73-
74-
Args:
75-
task: A task object from where to get the fields.
76-
decorate_comm: If True, it decorates the comm string of
77-
- User threads: in curly brackets,
78-
- Kernel threads: in square brackets
79-
Defaults to False.
80-
Returns:
81-
A tuple with the fields to show in the plugin output.
82-
"""
83-
pid = task.tgid
84-
tid = task.pid
85-
ppid = task.parent.tgid if task.parent else 0
86-
name = utility.array_to_string(task.comm)
87-
if decorate_comm:
88-
if task.is_kernel_thread:
89-
name = f"[{name}]"
90-
elif task.is_user_thread:
91-
name = f"{{{name}}}"
92-
93-
task_fields = (format_hints.Hex(task.vol.offset), pid, tid, ppid, name)
94-
return task_fields
95-
9681
def _generator(
9782
self,
9883
pid_filter: Callable[[Any], bool],
9984
include_threads: bool = False,
10085
decorate_comm: bool = False,
86+
dump: bool = False,
10187
):
10288
"""Generates the tasks list.
10389
@@ -110,14 +96,63 @@ def _generator(
11096
- User threads: in curly brackets,
11197
- Kernel threads: in square brackets
11298
Defaults to False.
99+
dump: If True, the main executable of the process is written to a file
100+
Defaults to False.
113101
Yields:
114102
Each rows
115103
"""
116104
for task in self.list_tasks(
117105
self.context, self.config["kernel"], pid_filter, include_threads
118106
):
119-
row = self._get_task_fields(task, decorate_comm)
120-
yield (0, row)
107+
elf_table_name = intermed.IntermediateSymbolTable.create(
108+
self.context,
109+
self.config_path,
110+
"linux",
111+
"elf",
112+
class_types=elf.class_types,
113+
)
114+
file_output = "Disabled"
115+
if dump:
116+
proc_layer_name = task.add_process_layer()
117+
if not proc_layer_name:
118+
continue
119+
120+
# Find the vma that belongs to the main ELF of the process
121+
file_output = "Error outputting file"
122+
123+
for v in task.mm.get_mmap_iter():
124+
if v.vm_start == task.mm.start_code:
125+
file_handle = elfs.Elfs.elf_dump(
126+
self.context,
127+
proc_layer_name,
128+
elf_table_name,
129+
v,
130+
task,
131+
self.open,
132+
)
133+
if file_handle:
134+
file_output = str(file_handle.preferred_filename)
135+
file_handle.close()
136+
break
137+
138+
pid = task.tgid
139+
tid = task.pid
140+
ppid = task.parent.tgid if task.parent else 0
141+
name = utility.array_to_string(task.comm)
142+
if decorate_comm:
143+
if task.is_kernel_thread:
144+
name = f"[{name}]"
145+
elif task.is_user_thread:
146+
name = f"{{{name}}}"
147+
148+
yield 0, (
149+
format_hints.Hex(task.vol.offset),
150+
pid,
151+
tid,
152+
ppid,
153+
name,
154+
file_output,
155+
)
121156

122157
@classmethod
123158
def list_tasks(
@@ -155,6 +190,7 @@ def run(self):
155190
pids = self.config.get("pid")
156191
include_threads = self.config.get("threads")
157192
decorate_comm = self.config.get("decorate_comm")
193+
dump = self.config.get("dump")
158194
filter_func = self.create_pid_filter(pids)
159195

160196
columns = [
@@ -163,7 +199,8 @@ def run(self):
163199
("TID", int),
164200
("PPID", int),
165201
("COMM", str),
202+
("File output", str),
166203
]
167204
return renderers.TreeGrid(
168-
columns, self._generator(filter_func, include_threads, decorate_comm)
205+
columns, self._generator(filter_func, include_threads, decorate_comm, dump)
169206
)

0 commit comments

Comments
 (0)