Skip to content

Commit 93bdcb3

Browse files
committed
Plugins: determine process exe deletion structurally
1 parent da47c40 commit 93bdcb3

File tree

1 file changed

+57
-40
lines changed

1 file changed

+57
-40
lines changed

volatility3/framework/plugins/linux/malware/process_spoofing.py

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ class ProcessSpoofing(plugins.PluginInterface):
2222

2323
_required_framework_version = (2, 0, 0)
2424
_version = (1, 1, 0)
25-
deleted = " (deleted)"
2625

2726
@classmethod
2827
def get_requirements(cls):
@@ -51,7 +50,7 @@ def get_executable_path(
5150
cls,
5251
context: interfaces.context.ContextInterface,
5352
task: interfaces.objects.ObjectInterface,
54-
) -> Optional[str]:
53+
) -> Tuple[Optional[str], bool]:
5554
"""
5655
Extract the executable path from task_struct.mm.exe_file
5756
@@ -60,33 +59,54 @@ def get_executable_path(
6059
task: task_struct object of the process
6160
6261
Returns:
63-
Executable path or None if not available
62+
Tuple of (basename, is_deleted) or (None, False) if not available
6463
"""
64+
is_deleted = False
65+
6566
try:
6667
mm = task.mm
67-
if not mm or not mm.is_readable():
68-
# Kernel threads don't have mm struct
69-
return None
68+
except (exceptions.InvalidAddressException, AttributeError) as e:
69+
vollog.debug(f"Unable to access mm for task at {task.vol.offset:#x}: {e}")
70+
return None, is_deleted
7071

72+
if not mm or not mm.is_readable():
73+
# Kernel threads don't have mm struct
74+
return None, is_deleted
75+
76+
try:
7177
exe_file = mm.exe_file
78+
except (exceptions.InvalidAddressException, AttributeError) as e:
79+
vollog.debug(
80+
f"Unable to access exe_file for task at {task.vol.offset:#x}: {e}"
81+
)
82+
return None, is_deleted
7283

73-
if not exe_file or not exe_file.is_readable():
74-
return None
84+
if not exe_file or not exe_file.is_readable():
85+
return None, is_deleted
7586

87+
try:
7688
exe_inode = exe_file.f_path.dentry.d_inode
7789
exe_path = linux.LinuxUtilities.path_for_file(context, task, exe_file)
90+
except (exceptions.InvalidAddressException, AttributeError) as e:
91+
vollog.debug(
92+
f"Unable to read exe_file path for task at {task.vol.offset:#x}: {e}"
93+
)
94+
return None, is_deleted
7895

79-
# If the inode link count is 0, the process image has been deleted
80-
if exe_inode.i_nlink == 0:
81-
exe_path += cls.deleted
82-
83-
return exe_path if exe_path else None
96+
if not exe_path:
97+
return None, is_deleted
8498

99+
try:
100+
# Check if the inode link count is 0 (process image has been deleted)
101+
is_deleted = exe_inode.i_nlink == 0
85102
except (exceptions.InvalidAddressException, AttributeError) as e:
86103
vollog.debug(
87-
f"Unable to read executable path for task at {task.vol.offset:#x}: {e}"
104+
f"Unable to check inode link count for task at {task.vol.offset:#x}: {e}"
88105
)
89-
return None
106+
# Continue without deletion info - we still have the path
107+
108+
basename = PurePosixPath(exe_path).name
109+
return basename, is_deleted
90110

91111
@classmethod
92112
def get_cmdline_basename(
@@ -155,29 +175,28 @@ def get_comm(cls, task: interfaces.objects.ObjectInterface) -> Optional[str]:
155175

156176
def _extract_process_names(
157177
self, task: interfaces.objects.ObjectInterface
158-
) -> Tuple[Optional[str], Optional[str], Optional[str]]:
178+
) -> Tuple[Optional[str], Optional[str], Optional[str], bool]:
159179
"""
160180
Extract all three process name sources for comparison
161181
162182
Args:
163183
task: task_struct object of the process
164184
165185
Returns:
166-
Tuple of (exe_path_basename, cmdline_basename, comm)
186+
Tuple of (exe_basename, cmdline_basename, comm, is_deleted)
167187
"""
168-
exe_path = self.get_executable_path(self.context, task)
169-
exe_basename = PurePosixPath(exe_path).name if exe_path else None
188+
exe_basename, is_deleted = self.get_executable_path(self.context, task)
170189
cmdline_basename = self.get_cmdline_basename(self.context, task)
171190
comm = self.get_comm(task)
172191

173-
return exe_basename, cmdline_basename, comm
192+
return exe_basename, cmdline_basename, comm, is_deleted
174193

175194
def _detect_spoofing(
176195
self,
177196
exe_basename: Optional[str],
178197
cmdline_basename: Optional[str],
179198
comm: Optional[str],
180-
) -> Tuple[bool, bool, bool]:
199+
) -> Tuple[bool, bool]:
181200
"""
182201
Analyze the three name sources to detect potential spoofing
183202
@@ -187,34 +206,26 @@ def _detect_spoofing(
187206
comm: Name from comm field
188207
189208
Returns:
190-
Tuple of (is_deleted, cmdline_spoofed, comm_spoofed) boolean flags
209+
Tuple of (cmdline_spoofed, comm_spoofed) boolean flags
191210
"""
192-
# Check if process image has been deleted
193-
is_deleted = exe_basename and exe_basename.endswith(self.deleted)
194-
195-
# Get clean basename for comparison (without " (deleted)" suffix)
196-
clean_exe_basename = exe_basename
197-
if is_deleted:
198-
clean_exe_basename = exe_basename[: len(self.deleted) * -1]
199-
200211
# Skip kernel threads - need at least 2 sources for comparison
201212
available_sources = sum(
202-
1 for name in [clean_exe_basename, cmdline_basename, comm] if name
213+
1 for name in [exe_basename, cmdline_basename, comm] if name
203214
)
204215
if available_sources < 2:
205-
return False, False, False
216+
return False, False
206217

207218
# Check for cmdline spoofing
208219
cmdline_spoofed = False
209-
if clean_exe_basename and cmdline_basename:
210-
cmdline_spoofed = clean_exe_basename != cmdline_basename
220+
if exe_basename and cmdline_basename:
221+
cmdline_spoofed = exe_basename != cmdline_basename
211222

212223
# Check for comm spoofing (comm is truncated to 15 characters)
213224
comm_spoofed = False
214-
if clean_exe_basename and comm:
215-
comm_spoofed = clean_exe_basename[:15] != comm
225+
if exe_basename and comm:
226+
comm_spoofed = exe_basename[:15] != comm
216227

217-
return is_deleted, cmdline_spoofed, comm_spoofed
228+
return cmdline_spoofed, comm_spoofed
218229

219230
def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]:
220231
"""
@@ -231,13 +242,19 @@ def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]:
231242
pid = task.pid
232243
ppid = task.get_parent_pid()
233244

234-
exe_basename, cmdline_basename, comm = self._extract_process_names(task)
245+
exe_basename, cmdline_basename, comm, is_deleted = (
246+
self._extract_process_names(task)
247+
)
235248

236-
is_deleted, cmdline_spoofed, comm_spoofed = self._detect_spoofing(
249+
cmdline_spoofed, comm_spoofed = self._detect_spoofing(
237250
exe_basename, cmdline_basename, comm
238251
)
239252

253+
# Prepare display values
240254
exe_render = exe_basename if exe_basename else "N/A"
255+
if is_deleted and exe_basename:
256+
exe_render += " (deleted)"
257+
241258
cmdline_render = cmdline_basename if cmdline_basename else "N/A"
242259
comm_render = comm if comm else "N/A"
243260

@@ -273,7 +290,7 @@ def run(self):
273290
("Comm", str),
274291
("Cmdline_Spoofed", bool),
275292
("Comm_Spoofed", bool),
276-
("Deleted", bool),
293+
("Exe_Deleted", bool),
277294
],
278295
self._generator(
279296
pslist.PsList.list_tasks(

0 commit comments

Comments
 (0)