Skip to content

Commit b881fa1

Browse files
authored
Merge pull request #1731 from volatilityfoundation/process_ghosting_update_with_delete_on_close
Add delete on close detection to process ghosting. Update plugin to c…
2 parents d0e32e5 + 3a4e622 commit b881fa1

File tree

1 file changed

+155
-40
lines changed

1 file changed

+155
-40
lines changed

volatility3/framework/plugins/windows/processghosting.py

Lines changed: 155 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@
22
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
33
#
44
import logging
5-
import contextlib
5+
6+
from typing import Optional, Tuple, Generator, Dict
67

78
from volatility3.framework import interfaces, exceptions
89
from volatility3.framework import renderers
910
from volatility3.framework.configuration import requirements
1011
from volatility3.framework.objects import utility
1112
from volatility3.framework.renderers import format_hints
12-
from volatility3.plugins.windows import pslist
13+
from volatility3.plugins.windows import pslist, vadinfo
1314

1415
vollog = logging.getLogger(__name__)
1516

1617

1718
class ProcessGhosting(interfaces.plugins.PluginInterface):
18-
"""Lists processes whose DeletePending bit is set or whose FILE_OBJECT is set to 0"""
19+
"""Lists processes whose DeletePending bit is set or whose FILE_OBJECT is set to 0 or Vads that are DeleteOnClose"""
1920

21+
_version = (1, 0, 0)
2022
_required_framework_version = (2, 4, 0)
2123

2224
@classmethod
@@ -33,52 +35,163 @@ def get_requirements(cls):
3335
),
3436
]
3537

36-
def _generator(self, procs):
37-
kernel = self.context.modules[self.config["kernel"]]
38+
@classmethod
39+
def _process_checks(
40+
cls,
41+
proc: interfaces.objects.ObjectInterface,
42+
mapped_files: Dict[int, Tuple[str, interfaces.objects.ObjectInterface]],
43+
) -> Generator[
44+
Tuple[int, Optional[int], Optional[int], int, Optional[str]], None, None
45+
]:
46+
"""
47+
Checks the EPROCESS for signs of ghosting
48+
"""
49+
if not proc.has_member("ImageFilePointer"):
50+
return
3851

39-
if not kernel.get_type("_EPROCESS").has_member("ImageFilePointer"):
40-
vollog.warning(
41-
"This plugin only supports Windows 10 builds when the ImageFilePointer member of _EPROCESS is present"
52+
delete_pending = None
53+
54+
# if it is 0 then its a side effect of process ghosting
55+
if proc.ImageFilePointer.vol.offset != 0:
56+
try:
57+
file_object = proc.ImageFilePointer
58+
delete_pending = file_object.DeletePending
59+
file_object = file_object.dereference().vol.offset
60+
except exceptions.InvalidAddressException:
61+
file_object = 0
62+
63+
# ImageFilePointer equal to 0 means process ghosting or similar techniques were used
64+
else:
65+
file_object = 0
66+
67+
# delete_pending besides 0 or 1 = smear
68+
if isinstance(delete_pending, int) and delete_pending not in [0, 1]:
69+
vollog.debug(
70+
f"Invalid delete_pending value {delete_pending} found for process {proc.UniqueProcessId}"
4271
)
72+
delete_pending = None
73+
74+
if file_object == 0 or delete_pending == 1:
75+
yield file_object, delete_pending, None, proc.SectionBaseAddress
76+
77+
@classmethod
78+
def _vad_checks(
79+
cls, control_area: interfaces.objects.ObjectInterface, vad_path: str
80+
) -> Generator[Tuple[int, Optional[int], Optional[int]], None, None]:
81+
"""
82+
Checks the control area for delete on close or delete pending being set
83+
"""
84+
try:
85+
file_object = control_area.FilePointer.dereference().cast("_FILE_OBJECT")
86+
except exceptions.InvalidAddressException:
4387
return
4488

45-
for proc in procs:
46-
delete_pending = renderers.UnreadableValue()
47-
process_name = utility.array_to_string(proc.ImageFileName)
89+
try:
90+
delete_on_close = control_area.u.Flags.DeleteOnClose
91+
except exceptions.InvalidAddressException:
92+
delete_on_close = None
4893

49-
# if it is 0 then its a side effect of process ghosting
50-
if proc.ImageFilePointer.vol.offset != 0:
51-
try:
52-
file_object = proc.ImageFilePointer
53-
delete_pending = file_object.DeletePending
54-
except exceptions.InvalidAddressException:
55-
file_object = 0
94+
if delete_on_close and vad_path.lower().endswith((".exe", ".dll")):
95+
yield file_object.vol.offset, None, delete_on_close
5696

57-
# ImageFilePointer equal to 0 means process ghosting or similar techniques were used
58-
else:
59-
file_object = 0
97+
try:
98+
delete_pending = file_object.DeletePending
99+
except exceptions.InvalidAddressException:
100+
delete_pending = None
60101

61-
if isinstance(delete_pending, int) and delete_pending not in [0, 1]:
102+
if delete_pending == 1:
103+
yield file_object.vol.offset, delete_pending, None
104+
105+
@classmethod
106+
def check_for_ghosting(
107+
cls,
108+
proc: interfaces.objects.ObjectInterface,
109+
mapped_files: Dict[int, Tuple[str, interfaces.objects.ObjectInterface]],
110+
) -> Generator[
111+
Tuple[int, Optional[int], Optional[int], int, Optional[str]], None, None
112+
]:
113+
"""
114+
Returns process or vad info for ghosting files
115+
116+
Args:
117+
proc:
118+
mapped_files: A dictionary mapping vad base addreses to the path and vad instance for the process
119+
120+
Return:
121+
A Generator of tuples of the file object address, the delete pending state, delete on close state, base address of the VAD, and the path
122+
"""
123+
# check the direct file object of the process
124+
yield from cls._process_checks(proc, mapped_files)
125+
126+
# walk each vad, check if it is pending delete or has its delete on close bit set
127+
for vad_base, (path, vad) in mapped_files.items():
128+
# these checks have no meaning for private memory areas
129+
if vad.get_private_memory() == 1:
130+
continue
131+
132+
try:
133+
if vad.has_member("ControlArea"):
134+
control_area = vad.ControlArea
135+
elif vad.has_member("Subsection"):
136+
control_area = vad.Subsection.ControlArea
137+
# We got here from a short vad, likely smear
138+
else:
139+
continue
140+
except exceptions.InvalidAddressException:
62141
vollog.debug(
63-
f"Invalid delete_pending value {delete_pending} found for {process_name} {proc.UniqueProcessId}"
142+
f"Unable to get control area for vad at base {vad_base:#x} for process with pid {proc.UniqueProcessId}"
64143
)
144+
continue
145+
146+
for file_object_address, delete_pending, delete_on_close in cls._vad_checks(
147+
control_area, path
148+
):
149+
yield format_hints.Hex(
150+
file_object_address
151+
), delete_pending, delete_on_close, vad_base
152+
153+
def _generator(self, procs):
154+
kernel = self.context.modules[self.config["kernel"]]
65155

66-
# delete_pending besides 0 or 1 = smear
67-
if file_object == 0 or delete_pending == 1:
68-
path = renderers.UnreadableValue()
69-
if file_object:
70-
with contextlib.suppress(exceptions.InvalidAddressException):
71-
path = file_object.FileName.String
72-
73-
yield (
74-
0,
75-
(
76-
proc.UniqueProcessId,
77-
process_name,
78-
format_hints.Hex(file_object),
79-
delete_pending,
80-
path,
81-
),
156+
has_imagefilepointer = kernel.get_type("_EPROCESS").has_member(
157+
"ImageFilePointer"
158+
)
159+
if not has_imagefilepointer:
160+
vollog.warning(
161+
"ImageFilePointer checks are only supported on Windows 10+ builds when the ImageFilePointer member of _EPROCESS is present"
162+
)
163+
164+
for proc in procs:
165+
process_name = utility.array_to_string(proc.ImageFileName)
166+
pid = proc.UniqueProcessId
167+
168+
# base address -> (file path, VAD instance)
169+
mapped_files: Dict[int, Tuple[str, interfaces.objects.ObjectInterface]] = {}
170+
for vad in vadinfo.VadInfo.list_vads(proc):
171+
path = vad.get_file_name()
172+
if isinstance(path, str):
173+
mapped_files[vad.get_start()] = (path, vad)
174+
175+
for (
176+
file_object_address,
177+
delete_pending,
178+
delete_on_close,
179+
base_address,
180+
) in self.check_for_ghosting(proc, mapped_files):
181+
vad_info = mapped_files.get(base_address)
182+
if vad_info:
183+
path = vad_info[0]
184+
else:
185+
path = renderers.NotAvailableValue()
186+
187+
yield 0, (
188+
pid,
189+
process_name,
190+
format_hints.Hex(base_address),
191+
format_hints.Hex(file_object_address),
192+
delete_pending or renderers.NotApplicableValue(),
193+
delete_on_close or renderers.NotApplicableValue(),
194+
path,
82195
)
83196

84197
def run(self):
@@ -88,8 +201,10 @@ def run(self):
88201
[
89202
("PID", int),
90203
("Process", str),
204+
("Base", format_hints.Hex),
91205
("FILE_OBJECT", format_hints.Hex),
92-
("DeletePending", str),
206+
("DeletePending", int),
207+
("DeleteOnClose", int),
93208
("Path", str),
94209
],
95210
self._generator(

0 commit comments

Comments
 (0)