Skip to content

Commit 857cd8d

Browse files
author
atcuno
committed
Updates from ikelos' feedback
1 parent 178f7c4 commit 857cd8d

File tree

1 file changed

+56
-11
lines changed

1 file changed

+56
-11
lines changed

volatility3/framework/plugins/windows/hollowprocesses.py

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
33
#
44
import logging
5-
from typing import NamedTuple
5+
from typing import NamedTuple, Dict
66

77
from volatility3.framework import interfaces, exceptions, constants
88
from volatility3.framework import renderers
@@ -12,21 +12,25 @@
1212

1313
vollog = logging.getLogger(__name__)
1414

15-
VadInfo = NamedTuple(
16-
"VadInfo",
15+
VadData = NamedTuple(
16+
"VadData",
1717
[
1818
("protection", str),
1919
("path", str),
2020
],
2121
)
2222

23-
DLLInfo = NamedTuple(
24-
"DLLInfo",
23+
DLLData = NamedTuple(
24+
"DLLData",
2525
[
2626
("path", str),
2727
],
2828
)
2929

30+
### Useful references on process hollowing
31+
# https://cysinfo.com/detecting-deceptive-hollowing-techniques/
32+
# https://github.com/m0n0ph1/Process-Hollowing
33+
3034

3135
class HollowProcesses(interfaces.plugins.PluginInterface):
3236
"""Lists hollowed processes"""
@@ -56,7 +60,16 @@ def get_requirements(cls):
5660
),
5761
]
5862

59-
def _get_vads_map(self, proc):
63+
def _get_vads_data(
64+
self, proc: interfaces.objects.ObjectInterface
65+
) -> Dict[int, VadData]:
66+
"""
67+
Returns a dictionary of:
68+
base address -> (protection string, file name)
69+
For each mapped VAD in the process. This is used
70+
for quick lookups of data and matching the DLL
71+
at the same base address as the VAD
72+
"""
6073
vads = {}
6174

6275
kernel = self.context.modules[self.config["kernel"]]
@@ -73,11 +86,23 @@ def _get_vads_map(self, proc):
7386
if not fn or not isinstance(fn, str):
7487
fn = "<Non-File Backed Region>"
7588

76-
vads[vad.get_start()] = VadInfo(protection_string, fn)
89+
vads[vad.get_start()] = VadData(protection_string, fn)
7790

7891
return vads
7992

80-
def _get_dlls_map(self, proc):
93+
def _get_dlls_map(
94+
self, proc: interfaces.objects.ObjectInterface
95+
) -> Dict[int, DLLData]:
96+
"""
97+
Returns a dictionary of:
98+
base address -> path
99+
for each DLL loaded in the process
100+
101+
This is used to cross compare with
102+
the corresponding VAD and to have a
103+
backup path source in case of smear
104+
in the VAD
105+
"""
81106
dlls = {}
82107

83108
for entry in proc.load_order_modules():
@@ -91,11 +116,14 @@ def _get_dlls_map(self, proc):
91116
except exceptions.InvalidAddressException:
92117
FullDllName = renderers.UnreadableValue()
93118

94-
dlls[base] = DLLInfo(FullDllName)
119+
dlls[base] = DLLData(FullDllName)
95120

96121
return dlls
97122

98-
def _get_image_base(self, proc):
123+
def _get_image_base(self, proc: interfaces.objects.ObjectInterface) -> int:
124+
"""
125+
Uses the PEB to get the image base of the process
126+
"""
99127
kernel = self.context.modules[self.config["kernel"]]
100128

101129
try:
@@ -110,13 +138,29 @@ def _get_image_base(self, proc):
110138
return None
111139

112140
def _check_load_address(self, proc, _, __):
141+
"""
142+
Detects when the image base in the PEB, which is writable by process malware,
143+
does not match the section base address - whose value lives in kernel memory.
144+
Many malware samples will manipulate their image base to fool AVs/EDRs and
145+
as a necessary part of certain hollowing techniques
146+
"""
113147
image_base = self._get_image_base(proc)
114148
if image_base is not None and image_base != proc.SectionBaseAddress:
115149
yield "The ImageBaseAddress reported from the PEB ({:#x}) does not match the process SectionBaseAddress ({:#x})".format(
116150
image_base, proc.SectionBaseAddress
117151
)
118152

119153
def _check_exe_protection(self, proc, vads, __):
154+
"""
155+
Legitimately mapped application executables and DLLs
156+
will have a VAD present and its initial protection will be
157+
PAGE_EXECUTE_WRITECOPY.
158+
Many process hollowing and code injection techniques will
159+
unmap the real executable and/or map in executables with
160+
incorrect permissions.
161+
This check verifies the VAD for the application exe.
162+
`_check_dlls_protection` checks for DLLs mapped in the process.
163+
"""
120164
base = proc.SectionBaseAddress
121165

122166
if base not in vads:
@@ -134,6 +178,7 @@ def _check_dlls_protection(self, _, vads, dlls):
134178
if dll_base not in vads:
135179
continue
136180

181+
# PAGE_EXECUTE_WRITECOPY is the only valid permission for mapped DLLs and .exe files
137182
if vads[dll_base].protection != "PAGE_EXECUTE_WRITECOPY":
138183
yield "Unexpected protection ({}) for DLL in the PEB's load order list ({:#x}) with path {}".format(
139184
vads[dll_base].protection, dll_base, dlls[dll_base].path
@@ -155,7 +200,7 @@ def _generator(self, procs):
155200
if len(dlls) < 3:
156201
continue
157202

158-
vads = self._get_vads_map(proc)
203+
vads = self._get_vads_data(proc)
159204
if len(vads) < 5:
160205
continue
161206

0 commit comments

Comments
 (0)