Skip to content

Commit 1718dc7

Browse files
authored
Merge pull request #1853 from tvanegro/dump_dirty_pages
Add dumping dirty pages to Linux Malfind
2 parents 0dd8d3a + f72b8ee commit 1718dc7

File tree

2 files changed

+78
-6
lines changed

2 files changed

+78
-6
lines changed

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

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Malfind(interfaces.plugins.PluginInterface):
1818
"""Lists process memory ranges that potentially contain injected code."""
1919

2020
_required_framework_version = (2, 0, 0)
21-
_version = (1, 0, 3)
21+
_version = (1, 0, 4)
2222

2323
@classmethod
2424
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -37,6 +37,18 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]
3737
element_type=int,
3838
optional=True,
3939
),
40+
requirements.IntRequirement(
41+
name="dump-size",
42+
description="Amount of bytes to dump for each dirty region/page found - Default 64 bytes",
43+
optional=True,
44+
default=64,
45+
),
46+
requirements.BooleanRequirement(
47+
name="dump-page",
48+
description="Dump each dirty page and content - Default off",
49+
optional=True,
50+
default=False,
51+
),
4052
]
4153

4254
def _list_injections(
@@ -51,14 +63,36 @@ def _list_injections(
5163

5264
proc_layer = self.context.layers[proc_layer_name]
5365

66+
dump_size = self.config["dump-size"]
67+
68+
# Dumping page defaults to off, as in case a whole r-xp region is dirty
69+
# this would likely dump 1000's of pages which might not always be wise nor necessary
70+
71+
dump_page = self.config["dump-page"]
72+
5473
for vma in task.mm.get_vma_iter():
5574
vma_name = vma.get_name(self.context, task)
5675
vollog.debug(
5776
f"Injections : processing PID {task.pid} : VMA {vma_name} : {hex(vma.vm_start)}-{hex(vma.vm_end)}"
5877
)
78+
79+
# If is_suspicious returns true, this means at least one page
80+
# in the region is dirty. If dump_page is true, then we dump
81+
# all dirty pages
82+
5983
if vma.is_suspicious(proc_layer) and vma_name != "[vdso]":
60-
data = proc_layer.read(vma.vm_start, 64, pad=True)
61-
yield vma, vma_name, data
84+
malicious_pages = vma.get_malicious_pages(proc_layer)
85+
offset = 0
86+
if dump_page:
87+
# Dumping each dirty page
88+
for page_addr in malicious_pages:
89+
offset = page_addr - vma.vm_start
90+
data = proc_layer.read(page_addr, dump_size, pad=True)
91+
yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {offset:#x}", data, offset
92+
else:
93+
# Original behaviour - Dump the start of the region (not necessarily matching the dirty page)
94+
data = proc_layer.read(vma.vm_start, dump_size, pad=True)
95+
yield vma, vma_name, data, offset
6296

6397
def _generator(self, tasks):
6498
# determine if we're on a 32 or 64 bit kernel
@@ -70,13 +104,15 @@ def _generator(self, tasks):
70104
for task in tasks:
71105
process_name = utility.array_to_string(task.comm)
72106

73-
for vma, vma_name, data in self._list_injections(task):
107+
for vma, vma_name, data, offset in self._list_injections(task):
74108
if is_32bit_arch:
75109
architecture = "intel"
76110
else:
77111
architecture = "intel64"
78112

79-
disasm = renderers.Disassembly(data, vma.vm_start, architecture)
113+
disasm = renderers.Disassembly(
114+
data, vma.vm_start + offset, architecture
115+
)
80116

81117
yield (
82118
0,

volatility3/framework/symbols/linux/extensions/__init__.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,42 @@ def get_name(self, context, task) -> Optional[str]:
12731273
except exceptions.InvalidAddressException:
12741274
return None
12751275

1276+
def get_malicious_pages(self, proclayer) -> List[int]:
1277+
"""Identifies and returns a list of potentially malicious memory pages.
1278+
1279+
A page is considered malicious if it is:
1280+
- Executable (protection flags match 'r-x')
1281+
- Dirty (modified since process start, according to proclayer.is_dirty())
1282+
1283+
Args:
1284+
proclayer: The process's memory layer
1285+
1286+
Returns:
1287+
List[int]: A list of virtual addresses for pages flagged as potentially malicious.
1288+
"""
1289+
1290+
malicious_pages = []
1291+
flags_str = self.get_protection()
1292+
1293+
if (
1294+
proclayer
1295+
and "r-x" in flags_str
1296+
and self.vm_file.dereference().vol.offset != 0
1297+
):
1298+
for i in range(self.vm_start, self.vm_end, proclayer.page_size):
1299+
try:
1300+
if proclayer.is_dirty(i):
1301+
vollog.debug(f"Found malicious (dirty+exec) page at {hex(i)} !")
1302+
malicious_pages.append(i)
1303+
except (
1304+
exceptions.PagedInvalidAddressException,
1305+
exceptions.InvalidAddressException,
1306+
) as excp:
1307+
vollog.debug(f"Unable to translate address {hex(i)} : {excp}")
1308+
# Abort as it is likely that other addresses in the same range will also fail
1309+
break
1310+
return malicious_pages
1311+
12761312
# used by malfind
12771313
def is_suspicious(self, proclayer=None):
12781314
ret = False
@@ -1288,7 +1324,7 @@ def is_suspicious(self, proclayer=None):
12881324
try:
12891325
if proclayer.is_dirty(i):
12901326
vollog.warning(
1291-
f"Found malicious (dirty+exec) page at {hex(i)} !"
1327+
f"Found malicious page(s) inside (dirty+exec) region {hex(self.vm_start)} !"
12921328
)
12931329
# We do not attempt to find other dirty+exec pages once we have found one
12941330
ret = True

0 commit comments

Comments
 (0)