Skip to content

Commit a07c5d3

Browse files
authored
Merge pull request #1354 from Abyss-W4tcher/fbdev_plugin
New Linux plugin: fbdev graphics API
2 parents 93b09fd + ea2757c commit a07c5d3

File tree

4 files changed

+356
-2
lines changed

4 files changed

+356
-2
lines changed

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ full = [
2020
"capstone>=5.0.3,<6",
2121
"pycryptodome>=3.21.0,<4",
2222
"leechcorepyc>=2.19.2,<3; sys_platform != 'darwin'",
23+
# https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
24+
# 10.0.0 dropped support for Python3.7
25+
# 11.0.0 dropped support for Python3.8, which is still supported by Volatility3
26+
"pillow>=10.0.0,<11.0.0",
2327
]
2428

2529
cloud = [

volatility3/framework/constants/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# We use the SemVer 2.0.0 versioning scheme
22
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
3-
VERSION_MINOR = 13 # Number of changes that only add to the interface
3+
VERSION_MINOR = 14 # Number of changes that only add to the interface
44
VERSION_PATCH = 0 # Number of changes that do not change the interface
55
VERSION_SUFFIX = ""
66

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
2+
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
3+
#
4+
import logging
5+
import io
6+
7+
from dataclasses import dataclass
8+
from typing import Type, List, Dict, Tuple
9+
from volatility3.framework import constants, exceptions, interfaces
10+
from volatility3.framework.configuration import requirements
11+
from volatility3.framework.renderers import (
12+
format_hints,
13+
TreeGrid,
14+
NotAvailableValue,
15+
UnreadableValue,
16+
)
17+
from volatility3.framework.objects import utility
18+
from volatility3.framework.constants import architectures
19+
from volatility3.framework.symbols import linux
20+
21+
# Image manipulation functions are kept in the plugin,
22+
# to prevent a general exit on missing PIL (pillow) dependency.
23+
try:
24+
from PIL import Image
25+
26+
has_pil = True
27+
except ImportError:
28+
has_pil = False
29+
30+
vollog = logging.getLogger(__name__)
31+
32+
33+
@dataclass
34+
class Framebuffer:
35+
"""Framebuffer object internal representation. This is useful to unify a framebuffer with precalculated
36+
properties and pass it through functions conveniently."""
37+
38+
id: str
39+
xres_virtual: int
40+
yres_virtual: int
41+
line_length: int
42+
bpp: int
43+
"""Bits Per Pixel"""
44+
size: int
45+
color_fields: Dict[str, Tuple[int, int, int]]
46+
fb_info: interfaces.objects.ObjectInterface
47+
48+
49+
class Fbdev(interfaces.plugins.PluginInterface):
50+
"""Extract framebuffers from the fbdev graphics subsystem"""
51+
52+
_version = (1, 0, 0)
53+
_required_framework_version = (2, 11, 0)
54+
55+
@classmethod
56+
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
57+
return [
58+
requirements.ModuleRequirement(
59+
name="kernel",
60+
description="Linux kernel",
61+
architectures=architectures.LINUX_ARCHS,
62+
),
63+
requirements.VersionRequirement(
64+
name="linuxutils", component=linux.LinuxUtilities, version=(2, 2, 0)
65+
),
66+
requirements.BooleanRequirement(
67+
name="dump",
68+
description="Dump framebuffers",
69+
default=False,
70+
optional=True,
71+
),
72+
]
73+
74+
@classmethod
75+
def parse_fb_pixel_bitfields(
76+
cls, fb_var_screeninfo: interfaces.objects.ObjectInterface
77+
) -> Dict[str, Tuple[int, int, int]]:
78+
"""Organize a framebuffer pixel format into a dictionary.
79+
This is needed to know the position and bitlength of a color inside
80+
a pixel.
81+
82+
Args:
83+
fb_var_screeninfo: a fb_var_screeninfo kernel object instance
84+
85+
Returns:
86+
The color fields mappings
87+
88+
Documentation:
89+
include/uapi/linux/fb.h:
90+
struct fb_bitfield {
91+
__u32 offset; /* beginning of bitfield */
92+
__u32 length; /* length of bitfield */
93+
__u32 msb_right; /* != 0 : Most significant bit is right */
94+
};
95+
"""
96+
# Naturally order by RGBA
97+
color_mappings = [
98+
("R", fb_var_screeninfo.red),
99+
("G", fb_var_screeninfo.green),
100+
("B", fb_var_screeninfo.blue),
101+
("A", fb_var_screeninfo.transp),
102+
]
103+
color_fields = {}
104+
for color_code, fb_bitfield in color_mappings:
105+
color_fields[color_code] = (
106+
int(fb_bitfield.offset),
107+
int(fb_bitfield.length),
108+
int(fb_bitfield.msb_right),
109+
)
110+
return color_fields
111+
112+
@classmethod
113+
def convert_fb_raw_buffer_to_image(
114+
cls,
115+
context: interfaces.context.ContextInterface,
116+
kernel_name: str,
117+
fb: Framebuffer,
118+
):
119+
"""Convert raw framebuffer pixels to an image.
120+
121+
Args:
122+
fb: the relevant Framebuffer object
123+
124+
Returns:
125+
A PIL Image object
126+
127+
Documentation:
128+
include/uapi/linux/fb.h:
129+
/* Interpretation of offset for color fields: All offsets are from the right,
130+
* inside a "pixel" value, which is exactly 'bits_per_pixel' wide (means: you
131+
* can use the offset as right argument to <<). A pixel afterwards is a bit
132+
* stream and is written to video memory as that unmodified.
133+
"""
134+
kernel = context.modules[kernel_name]
135+
kernel_layer = context.layers[kernel.layer_name]
136+
137+
raw_pixels = io.BytesIO(kernel_layer.read(fb.fb_info.screen_base, fb.size))
138+
bytes_per_pixel = fb.bpp // 8
139+
image = Image.new("RGBA", (fb.xres_virtual, fb.yres_virtual))
140+
141+
# This is not designed to be extremely fast (numpy isn't available),
142+
# but convenient and dynamic for any color field layout.
143+
for y in range(fb.yres_virtual):
144+
for x in range(fb.xres_virtual):
145+
raw_pixel = int.from_bytes(raw_pixels.read(bytes_per_pixel), "little")
146+
pixel = [0, 0, 0, 255]
147+
# The framebuffer is expected to have been correctly constructed,
148+
# especially by parse_fb_pixel_bitfields, to get the needed RGBA mappings.
149+
for i, color_code in enumerate(["R", "G", "B", "A"]):
150+
offset, length, msb_right = fb.color_fields[color_code]
151+
if length == 0:
152+
continue
153+
color_value = (raw_pixel >> offset) & (2**length - 1)
154+
if msb_right:
155+
# Reverse bit order
156+
color_value = int(
157+
"{:0{length}b}".format(color_value, length=length)[::-1], 2
158+
)
159+
pixel[i] = color_value
160+
image.putpixel((x, y), tuple(pixel))
161+
162+
return image
163+
164+
@classmethod
165+
def dump_fb(
166+
cls,
167+
context: interfaces.context.ContextInterface,
168+
kernel_name: str,
169+
open_method: Type[interfaces.plugins.FileHandlerInterface],
170+
fb: Framebuffer,
171+
convert_to_png_image: bool,
172+
) -> str:
173+
"""Dump a Framebuffer buffer to disk.
174+
175+
Args:
176+
fb: the relevant Framebuffer object
177+
convert_to_image: a boolean specifying if the buffer should be converted to an image
178+
179+
Returns:
180+
The filename of the dumped buffer.
181+
"""
182+
kernel = context.modules[kernel_name]
183+
kernel_layer = context.layers[kernel.layer_name]
184+
id = "N-A" if isinstance(fb.id, NotAvailableValue) else fb.id
185+
base_filename = f"{id}_{fb.xres_virtual}x{fb.yres_virtual}_{fb.bpp}bpp"
186+
if convert_to_png_image:
187+
image_object = cls.convert_fb_raw_buffer_to_image(context, kernel_name, fb)
188+
raw_io_output = io.BytesIO()
189+
image_object.save(raw_io_output, "PNG")
190+
final_fb_buffer = raw_io_output.getvalue()
191+
filename = f"{base_filename}.png"
192+
else:
193+
final_fb_buffer = kernel_layer.read(fb.fb_info.screen_base, fb.size)
194+
filename = f"{base_filename}.raw"
195+
196+
with open_method(filename) as f:
197+
f.write(final_fb_buffer)
198+
return f.preferred_filename
199+
200+
@classmethod
201+
def parse_fb_info(
202+
cls,
203+
fb_info: interfaces.objects.ObjectInterface,
204+
) -> Framebuffer:
205+
"""Parse an fb_info struct
206+
Args:
207+
fb_info: an fb_info kernel object live instance
208+
209+
Returns:
210+
A Framebuffer object
211+
212+
Documentation:
213+
https://docs.kernel.org/fb/api.html:
214+
- struct fb_fix_screeninfo stores device independent unchangeable information about the frame buffer device and the current format.
215+
Those information can't be directly modified by applications, but can be changed by the driver when an application modifies the format.
216+
- struct fb_var_screeninfo stores device independent changeable information about a frame buffer device, its current format and video mode,
217+
as well as other miscellaneous parameters.
218+
"""
219+
id = utility.array_to_string(fb_info.fix.id) or NotAvailableValue()
220+
color_fields = None
221+
222+
# 0 = color, 1 = grayscale, >1 = FOURCC
223+
if fb_info.var.grayscale in [0, 1]:
224+
color_fields = cls.parse_fb_pixel_bitfields(fb_info.var)
225+
226+
# There a lot of tricky pixel formats used by drivers and vendors in include/uapi/linux/videodev2.h.
227+
# As Volatility3 is not a video format converter, it is best to play it safe and let the user parse
228+
# the raw data manually (with ffmpeg for example).
229+
elif fb_info.var.grayscale > 1:
230+
fourcc = linux.LinuxUtilities.convert_fourcc_code(fb_info.var.grayscale)
231+
warn_msg = f"""Framebuffer "{id}" uses a FOURCC pixel format "{fourcc}" that isn't natively supported.
232+
You can try using ffmpeg to decode the raw buffer. Example usage:
233+
"ffmpeg -pix_fmts" to list supported formats, then
234+
"ffmpeg -f rawvideo -video_size {fb_info.var.xres_virtual}x{fb_info.var.yres_virtual} -i <FILENAME>.raw -pix_fmt <FORMAT> output.png"."""
235+
vollog.warning(warn_msg)
236+
237+
# Prefer using the virtual resolution, instead of the visible one.
238+
# This prevents missing non-visible data stored in the framebuffer.
239+
fb = Framebuffer(
240+
id,
241+
xres_virtual=fb_info.var.xres_virtual,
242+
yres_virtual=fb_info.var.yres_virtual,
243+
line_length=fb_info.fix.line_length,
244+
bpp=fb_info.var.bits_per_pixel,
245+
size=fb_info.var.yres_virtual * fb_info.fix.line_length,
246+
color_fields=color_fields,
247+
fb_info=fb_info,
248+
)
249+
250+
return fb
251+
252+
def _generator(self):
253+
254+
if not has_pil:
255+
vollog.error(
256+
"PIL (pillow) module is required to use this plugin. Please install it manually or through pyproject.toml."
257+
)
258+
return None
259+
260+
kernel_name = self.config["kernel"]
261+
kernel = self.context.modules[kernel_name]
262+
263+
if not kernel.has_symbol("num_registered_fb"):
264+
raise exceptions.SymbolError(
265+
"num_registered_fb",
266+
kernel.symbol_table_name,
267+
"The provided symbol does not exist in the symbol table. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt.",
268+
)
269+
270+
num_registered_fb = kernel.object_from_symbol("num_registered_fb")
271+
if num_registered_fb < 1:
272+
vollog.info("No registered framebuffer in the fbdev API.")
273+
return None
274+
275+
registered_fb = kernel.object_from_symbol("registered_fb")
276+
fb_info_list = utility.array_of_pointers(
277+
registered_fb,
278+
num_registered_fb,
279+
kernel.symbol_table_name + constants.BANG + "fb_info",
280+
self.context,
281+
)
282+
283+
for fb_info in fb_info_list:
284+
fb = self.parse_fb_info(fb_info)
285+
file_output = "Disabled"
286+
if self.config["dump"]:
287+
try:
288+
file_output = self.dump_fb(
289+
self.context, kernel_name, self.open, fb, bool(fb.color_fields)
290+
)
291+
file_output = str(file_output)
292+
except exceptions.InvalidAddressException as excp:
293+
vollog.error(
294+
f'Layer {excp.layer_name} failed to read address {hex(excp.invalid_address)} when dumping framebuffer "{fb.id}".'
295+
)
296+
file_output = UnreadableValue()
297+
298+
try:
299+
fb_device_name = utility.pointer_to_string(
300+
fb.fb_info.dev.kobj.name, 256
301+
)
302+
except exceptions.InvalidAddressException:
303+
fb_device_name = NotAvailableValue()
304+
305+
yield (
306+
0,
307+
(
308+
format_hints.Hex(fb.fb_info.screen_base),
309+
fb_device_name,
310+
fb.id,
311+
fb.size,
312+
f"{fb.xres_virtual}x{fb.yres_virtual}",
313+
fb.bpp,
314+
"RUNNING" if fb.fb_info.state == 0 else "SUSPENDED",
315+
file_output,
316+
),
317+
)
318+
319+
def run(self):
320+
columns = [
321+
("Address", format_hints.Hex),
322+
("Device", str),
323+
("ID", str),
324+
("Size", int),
325+
("Virtual resolution", str),
326+
("BPP", int),
327+
("State", str),
328+
("Filename", str),
329+
]
330+
331+
return TreeGrid(
332+
columns,
333+
self._generator(),
334+
)

volatility3/framework/symbols/linux/__init__.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def __init__(self, *args, **kwargs) -> None:
7676
class LinuxUtilities(interfaces.configuration.VersionableInterface):
7777
"""Class with multiple useful linux functions."""
7878

79-
_version = (2, 1, 1)
79+
_version = (2, 2, 0)
8080
_required_framework_version = (2, 0, 0)
8181

8282
framework.require_interface_version(*_required_framework_version)
@@ -483,6 +483,22 @@ def get_module_from_volobj_type(
483483

484484
return kernel
485485

486+
@classmethod
487+
def convert_fourcc_code(cls, code: int) -> str:
488+
"""Convert a fourcc integer back to its fourcc string representation.
489+
490+
Args:
491+
code: the numerical representation of the fourcc
492+
493+
Returns:
494+
The fourcc code string.
495+
"""
496+
497+
code_bytes_length = (code.bit_length() + 7) // 8
498+
return "".join(
499+
[chr((code >> (i * 8)) & 0xFF) for i in range(code_bytes_length)]
500+
)
501+
486502

487503
class IDStorage(ABC):
488504
"""Abstraction to support both XArray and RadixTree"""

0 commit comments

Comments
 (0)