mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-09-28 04:24:53 +02:00
315 lines
12 KiB
Python
315 lines
12 KiB
Python
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
|
|
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
|
#
|
|
import logging
|
|
import io
|
|
|
|
# Image manipulation functions are kept in the plugin,
|
|
# to prevent a general exit on missing PIL (pillow) dependency.
|
|
from PIL import Image
|
|
from dataclasses import dataclass
|
|
from typing import Type, List, Dict, Tuple
|
|
from volatility3.framework import constants, exceptions, interfaces
|
|
from volatility3.framework.configuration import requirements
|
|
from volatility3.framework.renderers import format_hints, TreeGrid, NotAvailableValue
|
|
from volatility3.framework.objects import utility
|
|
from volatility3.framework.constants import architectures
|
|
from volatility3.framework.symbols import linux
|
|
|
|
vollog = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class Framebuffer:
|
|
"""Framebuffer object internal representation. This is useful to unify an framebuffer with precalculated
|
|
properties and pass it through functions conveniently."""
|
|
|
|
id: str
|
|
xres_virtual: int
|
|
yres_virtual: int
|
|
line_length: int
|
|
bpp: int
|
|
"""Bits Per Pixel"""
|
|
size: int
|
|
color_fields: Dict[str, Tuple[int, int, int]]
|
|
fb_info: interfaces.objects.ObjectInterface
|
|
|
|
|
|
class Fbdev(interfaces.plugins.PluginInterface):
|
|
"""Extract framebuffers from the fbdev graphics subsystem"""
|
|
|
|
_version = (1, 0, 0)
|
|
_required_framework_version = (2, 11, 0)
|
|
|
|
@classmethod
|
|
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
|
return [
|
|
requirements.ModuleRequirement(
|
|
name="kernel",
|
|
description="Linux kernel",
|
|
architectures=architectures.LINUX_ARCHS,
|
|
),
|
|
requirements.BooleanRequirement(
|
|
name="dump",
|
|
description="Dump framebuffers",
|
|
default=False,
|
|
optional=True,
|
|
),
|
|
]
|
|
|
|
@classmethod
|
|
def parse_fb_pixel_bitfields(
|
|
cls, fb_var_screeninfo: interfaces.objects.ObjectInterface
|
|
) -> Dict[str, Tuple[int, int, int]]:
|
|
"""Organize a framebuffer pixel format into a dictionary.
|
|
This is needed to know the position and bitlength of a color inside
|
|
a pixel.
|
|
|
|
Args:
|
|
fb_var_screeninfo: a fb_var_screeninfo kernel object instance
|
|
|
|
Returns:
|
|
The color fields mappings
|
|
|
|
Documentation:
|
|
include/uapi/linux/fb.h:
|
|
struct fb_bitfield {
|
|
__u32 offset; /* beginning of bitfield */
|
|
__u32 length; /* length of bitfield */
|
|
__u32 msb_right; /* != 0 : Most significant bit is right */
|
|
};
|
|
"""
|
|
# Naturally order by RGBA
|
|
color_mappings = [
|
|
("R", fb_var_screeninfo.red),
|
|
("G", fb_var_screeninfo.green),
|
|
("B", fb_var_screeninfo.blue),
|
|
("A", fb_var_screeninfo.transp),
|
|
]
|
|
color_fields = {}
|
|
for color_code, fb_bitfield in color_mappings:
|
|
color_fields[color_code] = (
|
|
int(fb_bitfield.offset),
|
|
int(fb_bitfield.length),
|
|
int(fb_bitfield.msb_right),
|
|
)
|
|
return color_fields
|
|
|
|
@classmethod
|
|
def convert_fb_raw_buffer_to_image(
|
|
cls,
|
|
context: interfaces.context.ContextInterface,
|
|
kernel_name: str,
|
|
fb: Framebuffer,
|
|
) -> Image.Image:
|
|
"""Convert raw framebuffer pixels to an image.
|
|
|
|
Args:
|
|
fb: the relevant Framebuffer object
|
|
|
|
Returns:
|
|
A PIL Image object
|
|
|
|
Documentation:
|
|
include/uapi/linux/fb.h:
|
|
/* Interpretation of offset for color fields: All offsets are from the right,
|
|
* inside a "pixel" value, which is exactly 'bits_per_pixel' wide (means: you
|
|
* can use the offset as right argument to <<). A pixel afterwards is a bit
|
|
* stream and is written to video memory as that unmodified.
|
|
"""
|
|
kernel = context.modules[kernel_name]
|
|
kernel_layer = context.layers[kernel.layer_name]
|
|
|
|
raw_pixels = io.BytesIO(kernel_layer.read(fb.fb_info.screen_base, fb.size))
|
|
bytes_per_pixel = fb.bpp // 8
|
|
image = Image.new("RGBA", (fb.xres_virtual, fb.yres_virtual))
|
|
|
|
# This is not designed to be extremely fast (numpy isn't available),
|
|
# but convenient and dynamic for any color field layout.
|
|
for y in range(fb.yres_virtual):
|
|
for x in range(fb.xres_virtual):
|
|
raw_pixel = int.from_bytes(raw_pixels.read(bytes_per_pixel), "little")
|
|
pixel = [0, 0, 0, 255]
|
|
# The framebuffer is expected to have been correctly constructed,
|
|
# especially by parse_fb_pixel_bitfields, to get the needed RGBA mappings.
|
|
for i, color_code in enumerate(["R", "G", "B", "A"]):
|
|
offset, length, msb_right = fb.color_fields[color_code]
|
|
if length == 0:
|
|
continue
|
|
color_value = (raw_pixel >> offset) & (2**length - 1)
|
|
if msb_right:
|
|
# Reverse bit order
|
|
color_value = int(
|
|
"{:0{length}b}".format(color_value, length=length)[::-1], 2
|
|
)
|
|
pixel[i] = color_value
|
|
image.putpixel((x, y), tuple(pixel))
|
|
|
|
return image
|
|
|
|
@classmethod
|
|
def dump_fb(
|
|
cls,
|
|
context: interfaces.context.ContextInterface,
|
|
kernel_name: str,
|
|
open_method: Type[interfaces.plugins.FileHandlerInterface],
|
|
fb: Framebuffer,
|
|
convert_to_image: bool,
|
|
image_format: str = "PNG",
|
|
) -> str:
|
|
"""Dump a Framebuffer raw buffer to disk.
|
|
|
|
Args:
|
|
fb: the relevant Framebuffer object
|
|
convert_to_image: a boolean specifying if the buffer should be converted to an image
|
|
image_format: the target PIL image format (defaults to PNG)
|
|
|
|
Returns:
|
|
The filename of the dumped buffer.
|
|
"""
|
|
kernel = context.modules[kernel_name]
|
|
kernel_layer = context.layers[kernel.layer_name]
|
|
base_filename = f"{fb.id}_{fb.xres_virtual}x{fb.yres_virtual}_{fb.bpp}bpp"
|
|
if convert_to_image:
|
|
image = cls.convert_fb_raw_buffer_to_image(context, kernel_name, fb)
|
|
output = io.BytesIO()
|
|
image.save(output, image_format)
|
|
file_handle = open_method(f"{base_filename}.{image_format.lower()}")
|
|
file_handle.write(output.getvalue())
|
|
else:
|
|
raw_pixels = kernel_layer.read(fb.fb_info.screen_base, fb.size)
|
|
file_handle = open_method(f"{base_filename}.raw")
|
|
file_handle.write(raw_pixels)
|
|
|
|
file_handle.close()
|
|
return file_handle.preferred_filename
|
|
|
|
@classmethod
|
|
def parse_fb_info(
|
|
cls,
|
|
fb_info: interfaces.objects.ObjectInterface,
|
|
) -> Framebuffer:
|
|
"""Parse an fb_info struct
|
|
Args:
|
|
fb_info: an fb_info kernel object live instance
|
|
|
|
Returns:
|
|
A Framebuffer object
|
|
|
|
Documentation:
|
|
https://docs.kernel.org/fb/api.html:
|
|
- struct fb_fix_screeninfo stores device independent unchangeable information about the frame buffer device and the current format.
|
|
Those information can't be directly modified by applications, but can be changed by the driver when an application modifies the format.
|
|
- struct fb_var_screeninfo stores device independent changeable information about a frame buffer device, its current format and video mode,
|
|
as well as other miscellaneous parameters.
|
|
"""
|
|
# NotAvailableValue() messes with the filename output on disk
|
|
id = utility.array_to_string(fb_info.fix.id) or "N-A"
|
|
color_fields = None
|
|
|
|
# 0 = color, 1 = grayscale, >1 = FOURCC
|
|
if fb_info.var.grayscale in [0, 1]:
|
|
color_fields = cls.parse_fb_pixel_bitfields(fb_info.var)
|
|
|
|
# There a lot of tricky pixel formats used by drivers and vendors in include/uapi/linux/videodev2.h.
|
|
# As Volatility3 is not a video format converter, it is best to play it safe and let the user parse
|
|
# the raw data manually (with ffmpeg for example).
|
|
elif fb_info.var.grayscale > 1:
|
|
fourcc = linux.LinuxUtilities.convert_fourcc_code(fb_info.var.grayscale)
|
|
warn_msg = f"""Framebuffer "{id}" uses a FOURCC pixel format "{fourcc}" that isn't natively supported.
|
|
You can try using ffmpeg to decode the raw buffer. Example usage:
|
|
"ffmpeg -pix_fmts" to list supported formats, then
|
|
"ffmpeg -f rawvideo -video_size {fb_info.var.xres_virtual}x{fb_info.var.yres_virtual} -i <FILENAME>.raw -pix_fmt <FORMAT> output.png"."""
|
|
vollog.warning(warn_msg)
|
|
|
|
# Prefer using the virtual resolution, instead of the visible one.
|
|
# This prevents missing non-visible data stored in the framebuffer.
|
|
fb = Framebuffer(
|
|
id,
|
|
xres_virtual=fb_info.var.xres_virtual,
|
|
yres_virtual=fb_info.var.yres_virtual,
|
|
line_length=fb_info.fix.line_length,
|
|
bpp=fb_info.var.bits_per_pixel,
|
|
size=fb_info.var.yres_virtual * fb_info.fix.line_length,
|
|
color_fields=color_fields,
|
|
fb_info=fb_info,
|
|
)
|
|
|
|
return fb
|
|
|
|
def _generator(self):
|
|
kernel_name = self.config["kernel"]
|
|
kernel = self.context.modules[kernel_name]
|
|
|
|
if not kernel.has_symbol("num_registered_fb"):
|
|
raise exceptions.SymbolError(
|
|
"num_registered_fb",
|
|
kernel.symbol_table_name,
|
|
"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.",
|
|
)
|
|
|
|
num_registered_fb = kernel.object_from_symbol("num_registered_fb")
|
|
if num_registered_fb < 1:
|
|
vollog.info("No registered framebuffer in the fbdev API.")
|
|
return None
|
|
|
|
registered_fb = kernel.object_from_symbol("registered_fb")
|
|
fb_info_list = utility.array_of_pointers(
|
|
registered_fb,
|
|
num_registered_fb,
|
|
kernel.symbol_table_name + constants.BANG + "fb_info",
|
|
self.context,
|
|
)
|
|
|
|
for fb_info in fb_info_list:
|
|
fb = self.parse_fb_info(fb_info)
|
|
file_output = "Disabled"
|
|
if self.config["dump"]:
|
|
try:
|
|
file_output = self.dump_fb(
|
|
self.context, kernel_name, self.open, fb, bool(fb.color_fields)
|
|
)
|
|
except exceptions.InvalidAddressException as excp:
|
|
vollog.error(
|
|
f'Layer {excp.layer_name} failed to read address {hex(excp.invalid_address)} when dumping framebuffer "{fb.id}".'
|
|
)
|
|
file_output = "Error"
|
|
|
|
try:
|
|
fb_device_name = utility.pointer_to_string(
|
|
fb.fb_info.dev.kobj.name, 256
|
|
)
|
|
except exceptions.InvalidAddressException:
|
|
fb_device_name = NotAvailableValue()
|
|
|
|
yield (
|
|
0,
|
|
(
|
|
format_hints.Hex(fb.fb_info.screen_base),
|
|
fb_device_name,
|
|
fb.id,
|
|
fb.size,
|
|
f"{fb.xres_virtual}x{fb.yres_virtual}",
|
|
fb.bpp,
|
|
"RUNNING" if fb.fb_info.state == 0 else "SUSPENDED",
|
|
str(file_output),
|
|
),
|
|
)
|
|
|
|
def run(self):
|
|
columns = [
|
|
("Address", format_hints.Hex),
|
|
("Device", str),
|
|
("ID", str),
|
|
("Size", int),
|
|
("Virtual resolution", str),
|
|
("BPP", int),
|
|
("State", str),
|
|
("Filename", str),
|
|
]
|
|
|
|
return TreeGrid(
|
|
columns,
|
|
self._generator(),
|
|
)
|