Source code for libdebug.data.memory_view

#
# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
# Copyright (c) 2023-2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#

from __future__ import annotations

from collections.abc import Callable, MutableSequence
from typing import TYPE_CHECKING

from libdebug.debugger.internal_debugger_instance_manager import provide_internal_debugger
from libdebug.liblog import liblog

if TYPE_CHECKING:
    from libdebug.debugger.internal_debugger import InternalDebugger


[docs] class MemoryView(MutableSequence): """A memory interface for the target process. This class must be used to read and write memory of the target process. Attributes: getter (Callable[[int], bytes]): A function that reads memory from the target process. setter (Callable[[int, bytes], None]): A function that writes memory to the target process. maps_provider (Callable[[], list[MemoryMap]]): A function that returns the memory maps of the target process. unit_size (int, optional): The data size used by the getter and setter functions. Defaults to 8. align_to (int, optional): The address alignment that must be used when reading and writing memory. Defaults to 1. """ context: InternalDebugger """The debugging context of the target process.""" def __init__( self: MemoryView, getter: Callable[[int], bytes], setter: Callable[[int, bytes], None], unit_size: int = 8, align_to: int = 1, ) -> None: """Initializes the MemoryView.""" self.getter = getter self.setter = setter self.unit_size = unit_size self.align_to = align_to self._internal_debugger = provide_internal_debugger(self) self.maps_provider = self._internal_debugger.debugging_interface.maps
[docs] def read(self: MemoryView, address: int, size: int) -> bytes: """Reads memory from the target process. Args: address (int): The address to read from. size (int): The number of bytes to read. Returns: bytes: The read bytes. """ if self.align_to == 1: data = b"" remainder = size % self.unit_size for i in range(address, address + size - remainder, self.unit_size): data += self.getter(i) if remainder: data += self.getter(address + size - remainder)[:remainder] return data else: prefix = address % self.align_to prefix_size = self.unit_size - prefix data = self.getter(address - prefix)[prefix:] remainder = (size - prefix_size) % self.unit_size for i in range( address + prefix_size, address + size - remainder, self.unit_size, ): data += self.getter(i) if remainder: data += self.getter(address + size - remainder)[:remainder] return data
[docs] def write(self: MemoryView, address: int, data: bytes) -> None: """Writes memory to the target process. Args: address (int): The address to write to. data (bytes): The data to write. """ size = len(data) if self.align_to == 1: remainder = size % self.unit_size base = address else: prefix = address % self.align_to prefix_size = self.unit_size - prefix prev_data = self.getter(address - prefix) self.setter(address - prefix, prev_data[:prefix_size] + data[:prefix]) remainder = (size - prefix_size) % self.unit_size base = address + prefix_size for i in range(base, address + size - remainder, self.unit_size): self.setter(i, data[i - address : i - address + self.unit_size]) if remainder: prev_data = self.getter(address + size - remainder) self.setter( address + size - remainder, data[size - remainder :] + prev_data[remainder:], )
def __getitem__(self: MemoryView, key: int | slice | str | tuple) -> bytes: """Read from memory, either a single byte or a byte string. Args: key (int | slice | str | tuple): The key to read from memory. """ return self._manage_memory_read_type(key) def __setitem__(self: MemoryView, key: int | slice | str | tuple, value: bytes) -> None: """Write to memory, either a single byte or a byte string. Args: key (int | slice | str | tuple): The key to write to memory. value (bytes): The value to write. """ self._manage_memory_write_type(key, value) def _manage_memory_read_type(self: MemoryView, key: int | slice | str | tuple, file: str = "default") -> bytes: """Manage the read from memory, according to the typing. Args: key (int | slice | str | tuple): The key to read from memory. file (str, optional): The user-defined backing file to resolve the address in. Defaults to "default" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file). """ if isinstance(key, int): address = self._internal_debugger.resolve_address(key, file) return self.read(address, 1) elif isinstance(key, slice): if isinstance(key.start, str): start = self._internal_debugger.resolve_symbol(key.start, file) else: start = self._internal_debugger.resolve_address(key.start, file) if isinstance(key.stop, str): stop = self._internal_debugger.resolve_symbol(key.stop, file) else: stop = self._internal_debugger.resolve_address(key.stop, file) if stop < start: raise ValueError("Invalid slice range.") return self.read(start, stop - start) elif isinstance(key, str): address = self._internal_debugger.resolve_symbol(key, file) return self.read(address, 1) elif isinstance(key, tuple): return self._manage_memory_read_tuple(key) else: raise TypeError("Invalid key type.") def _manage_memory_read_tuple(self: MemoryView, key: tuple) -> bytes: """Manage the read from memory, when the access is through a tuple. Args: key (tuple): The key to read from memory. """ if len(key) == 3: # It can only be a tuple of the type (address, size, file) address, size, file = key if not isinstance(file, str): raise TypeError("Invalid type for the backing file. Expected string.") elif len(key) == 2: left, right = key if isinstance(right, str): # The right element can only be the backing file return self._manage_memory_read_type(left, right) elif isinstance(right, int): # The right element must be the size address = left size = right file = "default" else: raise TypeError("Tuple must have 2 or 3 elements.") if not isinstance(size, int): raise TypeError("Invalid type for the size. Expected int.") if isinstance(address, str): address = self._internal_debugger.resolve_symbol(address, file) elif isinstance(address, int): address = self._internal_debugger.resolve_address(address, file) else: raise TypeError("Invalid type for the address. Expected int or string.") return self.read(address, size) def _manage_memory_write_type( self: MemoryView, key: int | slice | str | tuple, value: bytes, file: str = "default", ) -> None: """Manage the write to memory, according to the typing. Args: key (int | slice | str | tuple): The key to read from memory. value (bytes): The value to write. file (str, optional): The user-defined backing file to resolve the address in. Defaults to "default" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file). """ if isinstance(key, int): address = self._internal_debugger.resolve_address(key, file) self.write(address, value) elif isinstance(key, slice): if isinstance(key.start, str): start = self._internal_debugger.resolve_symbol(key.start, file) else: start = self._internal_debugger.resolve_address(key.start, file) if key.stop is not None: if isinstance(key.stop, str): stop = self._internal_debugger.resolve_symbol(key.stop, file) else: stop = self._internal_debugger.resolve_address(key.stop, file) if stop < start: raise ValueError("Invalid slice range") if len(value) != stop - start: liblog.warning(f"Mismatch between slice width and value size, writing {len(value)} bytes.") self.write(start, value) elif isinstance(key, str): address = self._internal_debugger.resolve_symbol(key, file) self.write(address, value) elif isinstance(key, tuple): self._manage_memory_write_tuple(key, value) else: raise TypeError("Invalid key type.") def _manage_memory_write_tuple(self: MemoryView, key: tuple, value: bytes) -> None: """Manage the write to memory, when the access is through a tuple. Args: key (tuple): The key to read from memory. value (bytes): The value to write. """ if len(key) == 3: # It can only be a tuple of the type (address, size, file) address, size, file = key if not isinstance(file, str): raise TypeError("Invalid type for the backing file. Expected string.") elif len(key) == 2: left, right = key if isinstance(right, str): # The right element can only be the backing file self._manage_memory_write_type(left, value, right) return elif isinstance(right, int): # The right element must be the size address = left size = right file = "default" else: raise TypeError("Tuple must have 2 or 3 elements.") if not isinstance(size, int): raise TypeError("Invalid type for the size. Expected int.") if isinstance(address, str): address = self._internal_debugger.resolve_symbol(address, file) elif isinstance(address, int): address = self._internal_debugger.resolve_address(address, file) else: raise TypeError("Invalid type for the address. Expected int or string.") if len(value) != size: liblog.warning(f"Mismatch between specified size and actual value size, writing {len(value)} bytes.") self.write(address, value) def __delitem__(self: MemoryView, key: int | slice | str | tuple) -> None: """MemoryView doesn't support deletion.""" raise NotImplementedError("MemoryView doesn't support deletion") def __len__(self: MemoryView) -> None: """MemoryView doesn't support length.""" raise NotImplementedError("MemoryView doesn't support length")
[docs] def insert(self: MemoryView, index: int, value: int) -> None: """MemoryView doesn't support insertion.""" raise NotImplementedError("MemoryView doesn't support insertion")