From bd6c22ca1d70ec579891b3f45e3ce40ce7886d43 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 15 Apr 2020 01:32:33 -0700 Subject: [PATCH 01/27] windows: Don't fail search on unopenable process --- mem_edit/windows.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mem_edit/windows.py b/mem_edit/windows.py index 038c58a..0954403 100644 --- a/mem_edit/windows.py +++ b/mem_edit/windows.py @@ -205,6 +205,8 @@ class Process(AbstractProcess): return pid except ValueError: pass + except MemEditError as err: + logger.info(repr(err)) logger.info('Found no process with name {}'.format(target_name)) return None From 5c75da31d5a7ec1e43f9ab542c1f8b4eea01f44a Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 15 Apr 2020 01:33:36 -0700 Subject: [PATCH 02/27] support 64-bit python --- mem_edit/windows.py | 44 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/mem_edit/windows.py b/mem_edit/windows.py index 0954403..cdaf657 100644 --- a/mem_edit/windows.py +++ b/mem_edit/windows.py @@ -67,19 +67,51 @@ mem_types = { 'MEM_PRIVATE': 0x20000, } - # C struct for VirtualQueryEx -class MEMORY_BASIC_INFORMATION(ctypes.Structure): +class MEMORY_BASIC_INFORMATION32(ctypes.Structure): _fields_ = [ - ('BaseAddress', ctypes.c_void_p), - ('AllocationBase', ctypes.c_void_p), + ('BaseAddress', ctypes.wintypes.DWORD), + ('AllocationBase', ctypes.wintypes.DWORD), ('AllocationProtect', ctypes.wintypes.DWORD), - ('RegionSize', ctypes.wintypes.UINT), + ('RegionSize', ctypes.wintypes.DWORD), ('State', ctypes.wintypes.DWORD), ('Protect', ctypes.wintypes.DWORD), ('Type', ctypes.wintypes.DWORD), ] +class MEMORY_BASIC_INFORMATION64(ctypes.Structure): + _fields_ = [ + ('BaseAddress', ctypes.c_ulonglong), + ('AllocationBase', ctypes.c_ulonglong), + ('AllocationProtect', ctypes.wintypes.DWORD), + ('__alignment1', ctypes.wintypes.DWORD), + ('RegionSize', ctypes.c_ulonglong), + ('State', ctypes.wintypes.DWORD), + ('Protect', ctypes.wintypes.DWORD), + ('Type', ctypes.wintypes.DWORD), + ('__alignment2', ctypes.wintypes.DWORD), + ] + +PTR_SIZE = ctypes.sizeof(ctypes.c_void_p) +if PTR_SIZE == 8: # 64-bit python + MEMORY_BASIC_INFORMATION = MEMORY_BASIC_INFORMATION64 +elif PTR_SIZE == 4: # 32-bit python + MEMORY_BASIC_INFORMATION = MEMORY_BASIC_INFORMATION32 + +ctypes.windll.kernel32.VirtualQueryEx.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.LPCVOID, + ctypes.c_void_p, + ctypes.c_size_t] +ctypes.windll.kernel32.ReadProcessMemory.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.LPCVOID, + ctypes.c_void_p, + ctypes.c_size_t, + ctypes.c_void_p] +ctypes.windll.kernel32.WriteProcessMemory.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.LPCVOID, + ctypes.c_void_p, + ctypes.c_size_t, + ctypes.c_void_p] # C struct for GetSystemInfo class SYSTEM_INFO(ctypes.Structure): @@ -89,7 +121,7 @@ class SYSTEM_INFO(ctypes.Structure): ('dwPageSize', ctypes.wintypes.DWORD), ('lpMinimumApplicationAddress', ctypes.c_void_p), ('lpMaximumApplicationAddress', ctypes.c_void_p), - ('dwActiveProcessorMask', ctypes.wintypes.DWORD), + ('dwActiveProcessorMask', ctypes.c_void_p), ('dwNumberOfProcessors', ctypes.wintypes.DWORD), ('dwProcessorType', ctypes.wintypes.DWORD), ('dwAllocationGranularity', ctypes.wintypes.DWORD), From 0632b205abd708879443af9765ca57d26bd72238 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 Apr 2020 02:03:13 -0700 Subject: [PATCH 03/27] bump version number to v0.4: fixed on 64-bit python on windows --- mem_edit/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mem_edit/VERSION b/mem_edit/VERSION index be58634..bd73f47 100644 --- a/mem_edit/VERSION +++ b/mem_edit/VERSION @@ -1 +1 @@ -0.3 +0.4 From 9759645f927f42f72794b7df2f57fa721fbda39c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 1 Nov 2020 20:16:06 -0800 Subject: [PATCH 04/27] move version info into VERSION.py This avoid needing custom spec files for pyinstaller, which doesn't handle package_data by default --- mem_edit/VERSION | 1 - mem_edit/VERSION.py | 4 ++++ mem_edit/__init__.py | 6 ++---- setup.py | 6 +++--- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 mem_edit/VERSION create mode 100644 mem_edit/VERSION.py diff --git a/mem_edit/VERSION b/mem_edit/VERSION deleted file mode 100644 index bd73f47..0000000 --- a/mem_edit/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.4 diff --git a/mem_edit/VERSION.py b/mem_edit/VERSION.py new file mode 100644 index 0000000..a3aacc8 --- /dev/null +++ b/mem_edit/VERSION.py @@ -0,0 +1,4 @@ +""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """ +__version__ = ''' +0.4 +''' diff --git a/mem_edit/__init__.py b/mem_edit/__init__.py index 8947638..a4a0178 100644 --- a/mem_edit/__init__.py +++ b/mem_edit/__init__.py @@ -12,16 +12,14 @@ To get started, try: """ import platform -import pathlib from .utils import MemEditError __author__ = 'Jan Petykiewicz' -with open(pathlib.Path(__file__).parent / 'VERSION', 'r') as f: - __version__ = f.read().strip() -version = __version__ +from .VERSION import __version__ +version = __version__ # legacy compatibility system = platform.system() diff --git a/setup.py b/setup.py index 5b53ab7..3a55145 100644 --- a/setup.py +++ b/setup.py @@ -5,8 +5,8 @@ from setuptools import setup, find_packages with open('README.md', 'r') as f: long_description = f.read() -with open('mem_edit/VERSION', 'r') as f: - version = f.read().strip() +with open('mem_edit/VERSION.py', 'rt') as f: + version = f.readlines()[2].strip() setup(name='mem_edit', version=version, @@ -50,7 +50,7 @@ setup(name='mem_edit', ], packages=find_packages(), package_data={ - 'mem_edit': ['VERSION'] + 'mem_edit': [] }, install_requires=[ 'typing', From 6913f73db4e822cb75959df863b8424455cda991 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 1 Nov 2020 20:16:46 -0800 Subject: [PATCH 05/27] cosmetic and typing-related changes --- mem_edit/abstract.py | 184 ++++++++++++++++++++++--------------- mem_edit/linux.py | 18 ++-- mem_edit/utils.py | 38 +++++--- mem_edit/windows.py | 212 ++++++++++++++++++++++--------------------- setup.py | 3 +- 5 files changed, 256 insertions(+), 199 deletions(-) diff --git a/mem_edit/abstract.py b/mem_edit/abstract.py index fb463f7..f2e6cd1 100644 --- a/mem_edit/abstract.py +++ b/mem_edit/abstract.py @@ -2,7 +2,7 @@ Abstract class for cross-platform memory editing. """ -from typing import List, Tuple +from typing import List, Tuple, Optional, Union, Generator from abc import ABCMeta, abstractmethod from contextlib import contextmanager import copy @@ -23,8 +23,8 @@ class Process(metaclass=ABCMeta): (i.e., by reading from or writing to the memory used by a given process). The static methods - Process.list_available_pids() - Process.get_pid_by_name(executable_filename) + `Process.list_available_pids()` + `Process.get_pid_by_name(executable_filename)` can be used to help find the process id (pid) of the target process. They are provided for convenience only; it is probably better to use the tools built in to your operating system to discover the pid of the specific process you @@ -32,18 +32,19 @@ class Process(metaclass=ABCMeta): Once you have found the pid, you are ready to construct an instance of Process and use it to read and write to memory. Once you are done with the process, - use .close() to free up the process for access by other debuggers etc. - + use `.close()` to free up the process for access by other debuggers etc. + ``` p = Process(1239) p.close() + ``` To read/write to memory, first create a buffer using ctypes: - + ``` buffer0 = (ctypes.c_byte * 5)(39, 50, 03, 40, 30) buffer1 = ctypes.c_ulong() - + ``` and then use - + ``` p.write_memory(0x2fe, buffer0) val0 = p.read_memory(0x220, buffer0)[:] @@ -51,52 +52,52 @@ class Process(metaclass=ABCMeta): val1a = p.read_memory(0x149, buffer1).value val2b = buffer1.value assert(val1a == val2b) + ``` Searching for a value can be done in a number of ways: Search a list of addresses: - found_addresses = p.search_addresses([0x1020, 0x1030], buffer0) + `found_addresses = p.search_addresses([0x1020, 0x1030], buffer0)` Search the entire memory space: - found_addresses = p.search_all_memory(buffer0, writeable_only=False) + `found_addresses = p.search_all_memory(buffer0, writeable_only=False)` You can also get a list of which regions in memory are mapped (readable): - regions = p.list_mapped_regions(writeable_only=False) - - which can be used along with search_buffer(...) to re-create .search_all_memory(...): - + `regions = p.list_mapped_regions(writeable_only=False)` + which can be used along with search_buffer(...) to re-create .search_all_memory(...): + ``` found = [] for region_start, region_stop in regions: region_buffer = (ctypes.c_byte * (region_stop - region_start))() p.read_memory(region_start, region_buffer) found += utils.search_buffer(ctypes.c_ulong(123456790), region_buffer) - + ``` Other useful methods include the context manager, implemented as a static method: - + ``` with Process.open_process(pid) as p: # use p here, no need to call p.close() - + ``` .get_path(), which reports the path of the executable file which was used to start the process: - + ``` executable_path = p.get_path() - + ``` and deref_struct_pointer, which takes a pointer to a struct and reads out the struct members: - + ``` # struct is a list of (offset, buffer) pairs struct_defintion = [(0x0, ctypes.c_ulong()), (0x20, ctypes.c_byte())] values = p.deref_struct_pointer(0x0feab4, struct_defintion) - + ``` which is shorthand for - + ``` struct_addr = p.read_memory(0x0feab4, ctypes.c_void_p()) values = [p.read_memory(struct_addr + 0x0, ctypes.c_ulong()), p.read_memory(struct_addr + 0x20, ctypes.c_byte())] - + ``` ================= Putting all this together, a simple program which alters a magic number in the only running instance of 'magic.exe' might look like this: - + ``` import ctypes from mem_edit import Process @@ -107,9 +108,9 @@ class Process(metaclass=ABCMeta): addrs = p.search_all_memory(magic_number) assert(len(addrs) == 1) p.write_memory(addrs[0], ctypes.c_ulong(42)) - + ``` Searching for a value which changes: - + ``` pid = Process.get_pid_by_name('monitor_me.exe') with Process.open_process(pid) as p: addrs = p.search_all_memory(ctypes.c_int(40)) @@ -118,18 +119,19 @@ class Process(metaclass=ABCMeta): print('Found addresses:') for addr in filtered_addrs: print(hex(addr)) - + ``` """ @abstractmethod def __init__(self, process_id: int): """ Constructing a Process object prepares the process with specified process_id for - memory editing. Finding the process_id for the process you want to edit is often + memory editing. Finding the `process_id` for the process you want to edit is often easiest using os-specific tools (or by launching the process yourself, e.g. with - subprocess.Popen(...)). + `subprocess.Popen(...)`). - :param process_id: Process id (pid) of the target process + Args: + process_id: Process id (pid) of the target process """ pass @@ -140,7 +142,7 @@ class Process(metaclass=ABCMeta): letting other debuggers attach to it instead. This function should be called after you are done working with the process - and will no longer need it. See the Process.open_process(...) context + and will no longer need it. See the `Process.open_process(...)` context manager to avoid having to call this function yourself. """ pass @@ -148,38 +150,45 @@ class Process(metaclass=ABCMeta): @abstractmethod def write_memory(self, base_address: int, write_buffer: ctypes_buffer_t): """ - Write the given buffer to the process's address space, starting at base_address. + Write the given buffer to the process's address space, starting at `base_address`. - :param base_address: The address to write at, in the process's address space. - :param write_buffer: A ctypes object, for example, ctypes.c_ulong(48), - (ctypes.c_byte * 3)(43, 21, 0xff), or a subclass of ctypes.Structure, - which will be written into memory starting at base_address. + Args: + base_address: The address to write at, in the process's address space. + write_buffer: A ctypes object, for example, `ctypes.c_ulong(48)`, + `(ctypes.c_byte * 3)(43, 21, 0xff)`, or a subclass of `ctypes.Structure`, + which will be written into memory starting at `base_address`. """ pass @abstractmethod def read_memory(self, base_address: int, read_buffer: ctypes_buffer_t) -> ctypes_buffer_t: """ - Read into the given buffer from the process's address space, starting at base_address. + Read into the given buffer from the process's address space, starting at `base_address`. - :param base_address: The address to read from, in the process's address space. - :param read_buffer: A ctypes object, for example. ctypes.c_ulong(), - (ctypes.c_byte * 3)(), or a subclass of ctypes.Structure, which will be - overwritten with the contents of the process's memory starting at base_address. - :returns: read_buffer is returned as well as being overwritten. + Args: + base_address: The address to read from, in the process's address space. + read_buffer: A `ctypes` object, for example. `ctypes.c_ulong()`, + `(ctypes.c_byte * 3)()`, or a subclass of `ctypes.Structure`, which will be + overwritten with the contents of the process's memory starting at `base_address`. + + Returns: + `read_buffer` is returned as well as being overwritten. """ pass @abstractmethod def list_mapped_regions(self, writeable_only=True) -> List[Tuple[int, int]]: """ - Return a list of (start_address, stop_address) for the regions of the address space + Return a list of `(start_address, stop_address)` for the regions of the address space accessible to (readable and possibly writable by) the process. By default, this function does not return non-writeable regions. - :param writeable_only: If True, only return regions which are also writeable. - Default true. - :return: List of (start_address, stop_address) for each accessible memory region. + Args: + writeable_only: If `True`, only return regions which are also writeable. + Default `True`. + + Returns: + List of `(start_address, stop_address)` for each accessible memory region. """ pass @@ -188,7 +197,8 @@ class Process(metaclass=ABCMeta): """ Return the path to the executable file which was run to start this process. - :return: A string containing the path. + Returns: + A string containing the path. """ pass @@ -198,13 +208,14 @@ class Process(metaclass=ABCMeta): """ Return a list of all process ids (pids) accessible on this system. - :return: List of running process ids. + Returns: + List of running process ids. """ pass @staticmethod @abstractmethod - def get_pid_by_name(target_name: str) -> int or None: + def get_pid_by_name(target_name: str) -> Optional[int]: """ Attempt to return the process id (pid) of a process which was run with an executable file with the provided name. If no process is found, return None. @@ -215,7 +226,12 @@ class Process(metaclass=ABCMeta): Don't rely on this method if you can possibly avoid it, since it makes no attempt to confirm that it found a unique process and breaks trivially (e.g. if the executable file is renamed). - :return: Process id (pid) of a process with the provided name, or None. + + Args: + target_name: Name of the process to find the PID for + + Returns: + Process id (pid) of a process with the provided name, or `None`. """ pass @@ -225,34 +241,48 @@ class Process(metaclass=ABCMeta): ) -> List[ctypes_buffer_t]: """ Take a pointer to a struct and read out the struct members: + ``` struct_defintion = [(0x0, ctypes.c_ulong()), (0x20, ctypes.c_byte())] values = p.deref_struct_pointer(0x0feab4, struct_defintion) + ``` which is shorthand for + ``` struct_addr = p.read_memory(0x0feab4, ctypes.c_void_p()) values = [p.read_memory(struct_addr + 0x0, ctypes.c_ulong()), p.read_memory(struct_addr + 0x20, ctypes.c_byte())] + ``` - :param base_address: Address at which the struct pointer is located. - :param targets: List of (offset, read_buffer) pairs which will be read from the struct. - :return: List of read values corresponding to the provided targets. + Args: + base_address: Address at which the struct pointer is located. + targets: List of `(offset, read_buffer)` pairs which will be read from the struct. + + Return: + List of read values corresponding to the provided targets. """ base = self.read_memory(base_address, ctypes.c_void_p()).value values = [self.read_memory(base + offset, buffer) for offset, buffer in targets] return values - def search_addresses(self, addresses: List[int], needle_buffer: ctypes_buffer_t, verbatim: bool=True) -> List[int]: + def search_addresses(self, + addresses: List[int], + needle_buffer: ctypes_buffer_t, + verbatim: bool = True, + ) -> List[int]: """ Search for the provided value at each of the provided addresses, and return the addresses where it is found. - :param addresses: List of addresses which should be probed. - :param needle_buffer: The value to search for. This should be a ctypes object of the same - sorts as used by .read_memory(...), which will be compared to the contents of + Args: + addresses: List of addresses which should be probed. + needle_buffer: The value to search for. This should be a `ctypes` object of the same + sorts as used by `.read_memory(...)`, which will be compared to the contents of memory at each of the given addresses. - :param verbatim: If True, perform bitwise comparison when searching for needle_buffer. - If False, perform utils.ctypes_equal-based comparison. Default True. - :return: List of addresses where the needle_buffer was found. + verbatim: If `True`, perform bitwise comparison when searching for `needle_buffer`. + If `False`, perform `utils.ctypes_equal`-based comparison. Default `True`. + + Returns: + List of addresses where the `needle_buffer` was found. """ found = [] read_buffer = copy.copy(needle_buffer) @@ -269,18 +299,25 @@ class Process(metaclass=ABCMeta): found.append(address) return found - def search_all_memory(self, needle_buffer: ctypes_buffer_t, writeable_only: bool=True, verbatim: bool=True) -> List[int]: + def search_all_memory(self, + needle_buffer: ctypes_buffer_t, + writeable_only: bool = True, + verbatim: bool = True, + ) -> List[int]: """ Search the entire memory space accessible to the process for the provided value. - :param needle_buffer: The value to search for. This should be a ctypes object of the same - sorts as used by .read_memory(...), which will be compared to the contents of + Args: + needle_buffer: The value to search for. This should be a ctypes object of the same + sorts as used by `.read_memory(...)`, which will be compared to the contents of memory at each accessible address. - :param writeable_only: If True, only search regions where the process has write access. - Default True. - :param verbatim: If True, perform bitwise comparison when searching for needle_buffer. - If False, perform utils.ctypes_equal-based comparison. Default True. - :return: List of addresses where the needle_buffer was found. + writeable_only: If `True`, only search regions where the process has write access. + Default `True`. + verbatim: If `True`, perform bitwise comparison when searching for `needle_buffer`. + If `False`, perform `utils.ctypes_equal-based` comparison. Default `True`. + + Returns: + List of addresses where the `needle_buffer` was found. """ found = [] if verbatim: @@ -299,15 +336,20 @@ class Process(metaclass=ABCMeta): @classmethod @contextmanager - def open_process(cls, process_id: int) -> 'Process': + def open_process(cls, process_id: int) -> Generator['Process', None, None]: """ Context manager which automatically closes the constructed Process: + ``` with Process.open_process(2394) as p: # use p here # no need to run p.close() + ``` - :param process_id: Process id (pid), passed to the Process constructor. - :return: Constructed Process object. + Args: + process_id: Process id (pid), passed to the Process constructor. + + Returns: + Constructed Process object. """ process = cls(process_id) yield process diff --git a/mem_edit/linux.py b/mem_edit/linux.py index ae8404d..58460b8 100644 --- a/mem_edit/linux.py +++ b/mem_edit/linux.py @@ -2,7 +2,7 @@ Implementation of Process class for Linux """ -from typing import List, Tuple +from typing import List, Tuple, Optional from os import strerror import os import os.path @@ -20,13 +20,13 @@ logger = logging.getLogger(__name__) ptrace_commands = { - 'PTRACE_GETREGS': 12, - 'PTRACE_SETREGS': 13, - 'PTRACE_ATTACH': 16, - 'PTRACE_DETACH': 17, - 'PTRACE_SYSCALL': 24, - 'PTRACE_SEIZE': 16902, - } + 'PTRACE_GETREGS': 12, + 'PTRACE_SETREGS': 13, + 'PTRACE_ATTACH': 16, + 'PTRACE_DETACH': 17, + 'PTRACE_SYSCALL': 24, + 'PTRACE_SEIZE': 16902, + } # import ptrace() from libc @@ -91,7 +91,7 @@ class Process(AbstractProcess): return pids @staticmethod - def get_pid_by_name(target_name: str) -> int or None: + def get_pid_by_name(target_name: str) -> Optional[int]: for pid in Process.list_available_pids(): try: logger.info('Checking name for pid {}'.format(pid)) diff --git a/mem_edit/utils.py b/mem_edit/utils.py index fc15970..2c9c022 100644 --- a/mem_edit/utils.py +++ b/mem_edit/utils.py @@ -12,24 +12,29 @@ Utility functions and types: ctypes_equal(a, b) """ -from typing import List +from typing import List, Union import ctypes -ctypes_buffer_t = ctypes._SimpleCData or ctypes.Array or ctypes.Structure or ctypes.Union +ctypes_buffer_t = Union[ctypes._SimpleCData, ctypes.Array, ctypes.Structure, ctypes.Union] class MemEditError(Exception): pass -def search_buffer_verbatim(needle_buffer: ctypes_buffer_t, haystack_buffer: ctypes_buffer_t) -> List[int]: +def search_buffer_verbatim(needle_buffer: ctypes_buffer_t, + haystack_buffer: ctypes_buffer_t, + ) -> List[int]: """ Search for a buffer inside another buffer, using a direct (bitwise) comparison - :param needle_buffer: Buffer to search for. - :param haystack_buffer: Buffer to search in. - :return: List of offsets where the needle_buffer was found. + Args: + needle_buffer: Buffer to search for. + haystack_buffer: Buffer to search in. + + Returns: + List of offsets where the `needle_buffer` was found. """ found = [] @@ -45,14 +50,19 @@ def search_buffer_verbatim(needle_buffer: ctypes_buffer_t, haystack_buffer: ctyp return found -def search_buffer(needle_buffer: ctypes_buffer_t, haystack_buffer: ctypes_buffer_t) -> List[int]: +def search_buffer(needle_buffer: ctypes_buffer_t, + haystack_buffer: ctypes_buffer_t, + ) -> List[int]: """ - Search for a buffer inside another buffer, using ctypes_equal for comparison. - Much slower than search_buffer_verbatim. + Search for a buffer inside another buffer, using `ctypes_equal` for comparison. + Much slower than `search_buffer_verbatim`. - :param needle_buffer: Buffer to search for. - :param haystack_buffer: Buffer to search in. - :return: List of offsets where the needle_buffer was found. + Args: + needle_buffer: Buffer to search for. + haystack_buffer: Buffer to search in. + + Returns: + List of offsets where the needle_buffer was found. """ found = [] read_type = type(needle_buffer) @@ -63,7 +73,9 @@ def search_buffer(needle_buffer: ctypes_buffer_t, haystack_buffer: ctypes_buffer return found -def ctypes_equal(a: ctypes_buffer_t, b: ctypes_buffer_t) -> bool: +def ctypes_equal(a: ctypes_buffer_t, + b: ctypes_buffer_t, + ) -> bool: """ Check if the values stored inside two ctypes buffers are equal. """ diff --git a/mem_edit/windows.py b/mem_edit/windows.py index cdaf657..e3bb013 100644 --- a/mem_edit/windows.py +++ b/mem_edit/windows.py @@ -2,7 +2,7 @@ Implementation of Process class for Windows """ -from typing import List, Tuple +from typing import List, Tuple, Optional from math import floor from os import strerror import os.path @@ -20,77 +20,77 @@ logger = logging.getLogger(__name__) # Process handle privileges privileges = { - 'PROCESS_QUERY_INFORMATION': 0x0400, - 'PROCESS_VM_OPERATION': 0x0008, - 'PROCESS_VM_READ': 0x0010, - 'PROCESS_VM_WRITE': 0x0020, - } + 'PROCESS_QUERY_INFORMATION': 0x0400, + 'PROCESS_VM_OPERATION': 0x0008, + 'PROCESS_VM_READ': 0x0010, + 'PROCESS_VM_WRITE': 0x0020, + } privileges['PROCESS_RW'] = ( - privileges['PROCESS_QUERY_INFORMATION'] | - privileges['PROCESS_VM_OPERATION'] | - privileges['PROCESS_VM_READ'] | - privileges['PROCESS_VM_WRITE'] - ) + privileges['PROCESS_QUERY_INFORMATION'] | + privileges['PROCESS_VM_OPERATION'] | + privileges['PROCESS_VM_READ'] | + privileges['PROCESS_VM_WRITE'] + ) # Memory region states mem_states = { - 'MEM_COMMIT': 0x1000, - 'MEM_FREE': 0x10000, - 'MEM_RESERVE': 0x2000, - } + 'MEM_COMMIT': 0x1000, + 'MEM_FREE': 0x10000, + 'MEM_RESERVE': 0x2000, + } # Memory region permissions page_protections = { - 'PAGE_EXECUTE': 0x10, - 'PAGE_EXECUTE_READ': 0x20, - 'PAGE_EXECUTE_READWRITE': 0x40, - 'PAGE_EXECUTE_WRITECOPY': 0x80, - 'PAGE_NOACCESS': 0x01, - 'PAGE_READWRITE': 0x04, - 'PAGE_WRITECOPY': 0x08, - } + 'PAGE_EXECUTE': 0x10, + 'PAGE_EXECUTE_READ': 0x20, + 'PAGE_EXECUTE_READWRITE': 0x40, + 'PAGE_EXECUTE_WRITECOPY': 0x80, + 'PAGE_NOACCESS': 0x01, + 'PAGE_READWRITE': 0x04, + 'PAGE_WRITECOPY': 0x08, + } # Custom (combined) permissions page_protections['PAGE_READABLE'] = ( - page_protections['PAGE_EXECUTE_READ'] | - page_protections['PAGE_EXECUTE_READWRITE'] | - page_protections['PAGE_READWRITE'] - ) + page_protections['PAGE_EXECUTE_READ'] | + page_protections['PAGE_EXECUTE_READWRITE'] | + page_protections['PAGE_READWRITE'] + ) page_protections['PAGE_READWRITEABLE'] = ( - page_protections['PAGE_EXECUTE_READWRITE'] | - page_protections['PAGE_READWRITE'] - ) + page_protections['PAGE_EXECUTE_READWRITE'] | + page_protections['PAGE_READWRITE'] + ) # Memory types mem_types = { - 'MEM_IMAGE': 0x1000000, - 'MEM_MAPPED': 0x40000, - 'MEM_PRIVATE': 0x20000, - } + 'MEM_IMAGE': 0x1000000, + 'MEM_MAPPED': 0x40000, + 'MEM_PRIVATE': 0x20000, + } # C struct for VirtualQueryEx class MEMORY_BASIC_INFORMATION32(ctypes.Structure): _fields_ = [ - ('BaseAddress', ctypes.wintypes.DWORD), - ('AllocationBase', ctypes.wintypes.DWORD), - ('AllocationProtect', ctypes.wintypes.DWORD), - ('RegionSize', ctypes.wintypes.DWORD), - ('State', ctypes.wintypes.DWORD), - ('Protect', ctypes.wintypes.DWORD), - ('Type', ctypes.wintypes.DWORD), - ] + ('BaseAddress', ctypes.wintypes.DWORD), + ('AllocationBase', ctypes.wintypes.DWORD), + ('AllocationProtect', ctypes.wintypes.DWORD), + ('RegionSize', ctypes.wintypes.DWORD), + ('State', ctypes.wintypes.DWORD), + ('Protect', ctypes.wintypes.DWORD), + ('Type', ctypes.wintypes.DWORD), + ] class MEMORY_BASIC_INFORMATION64(ctypes.Structure): _fields_ = [ - ('BaseAddress', ctypes.c_ulonglong), - ('AllocationBase', ctypes.c_ulonglong), - ('AllocationProtect', ctypes.wintypes.DWORD), - ('__alignment1', ctypes.wintypes.DWORD), - ('RegionSize', ctypes.c_ulonglong), - ('State', ctypes.wintypes.DWORD), - ('Protect', ctypes.wintypes.DWORD), - ('Type', ctypes.wintypes.DWORD), - ('__alignment2', ctypes.wintypes.DWORD), - ] + ('BaseAddress', ctypes.c_ulonglong), + ('AllocationBase', ctypes.c_ulonglong), + ('AllocationProtect', ctypes.wintypes.DWORD), + ('__alignment1', ctypes.wintypes.DWORD), + ('RegionSize', ctypes.c_ulonglong), + ('State', ctypes.wintypes.DWORD), + ('Protect', ctypes.wintypes.DWORD), + ('Type', ctypes.wintypes.DWORD), + ('__alignment2', ctypes.wintypes.DWORD), + ] PTR_SIZE = ctypes.sizeof(ctypes.c_void_p) if PTR_SIZE == 8: # 64-bit python @@ -98,36 +98,39 @@ if PTR_SIZE == 8: # 64-bit python elif PTR_SIZE == 4: # 32-bit python MEMORY_BASIC_INFORMATION = MEMORY_BASIC_INFORMATION32 -ctypes.windll.kernel32.VirtualQueryEx.argtypes = [ctypes.wintypes.HANDLE, - ctypes.wintypes.LPCVOID, - ctypes.c_void_p, - ctypes.c_size_t] -ctypes.windll.kernel32.ReadProcessMemory.argtypes = [ctypes.wintypes.HANDLE, - ctypes.wintypes.LPCVOID, - ctypes.c_void_p, - ctypes.c_size_t, - ctypes.c_void_p] -ctypes.windll.kernel32.WriteProcessMemory.argtypes = [ctypes.wintypes.HANDLE, - ctypes.wintypes.LPCVOID, - ctypes.c_void_p, - ctypes.c_size_t, - ctypes.c_void_p] +ctypes.windll.kernel32.VirtualQueryEx.argtypes = [ + ctypes.wintypes.HANDLE, + ctypes.wintypes.LPCVOID, + ctypes.c_void_p, + ctypes.c_size_t] +ctypes.windll.kernel32.ReadProcessMemory.argtypes = [ + ctypes.wintypes.HANDLE, + ctypes.wintypes.LPCVOID, + ctypes.c_void_p, + ctypes.c_size_t, + ctypes.c_void_p] +ctypes.windll.kernel32.WriteProcessMemory.argtypes = [ + ctypes.wintypes.HANDLE, + ctypes.wintypes.LPCVOID, + ctypes.c_void_p, + ctypes.c_size_t, + ctypes.c_void_p] # C struct for GetSystemInfo class SYSTEM_INFO(ctypes.Structure): _fields_ = [ - ('wProcessorArchitecture', ctypes.wintypes.WORD), - ('wReserved', ctypes.wintypes.WORD), - ('dwPageSize', ctypes.wintypes.DWORD), - ('lpMinimumApplicationAddress', ctypes.c_void_p), - ('lpMaximumApplicationAddress', ctypes.c_void_p), - ('dwActiveProcessorMask', ctypes.c_void_p), - ('dwNumberOfProcessors', ctypes.wintypes.DWORD), - ('dwProcessorType', ctypes.wintypes.DWORD), - ('dwAllocationGranularity', ctypes.wintypes.DWORD), - ('wProcessorLevel', ctypes.wintypes.WORD), - ('wProcessorRevision', ctypes.wintypes.WORD), - ] + ('wProcessorArchitecture', ctypes.wintypes.WORD), + ('wReserved', ctypes.wintypes.WORD), + ('dwPageSize', ctypes.wintypes.DWORD), + ('lpMinimumApplicationAddress', ctypes.c_void_p), + ('lpMaximumApplicationAddress', ctypes.c_void_p), + ('dwActiveProcessorMask', ctypes.c_void_p), + ('dwNumberOfProcessors', ctypes.wintypes.DWORD), + ('dwProcessorType', ctypes.wintypes.DWORD), + ('dwAllocationGranularity', ctypes.wintypes.DWORD), + ('wProcessorLevel', ctypes.wintypes.WORD), + ('wProcessorRevision', ctypes.wintypes.WORD), + ] class Process(AbstractProcess): @@ -152,24 +155,24 @@ class Process(AbstractProcess): def write_memory(self, base_address: int, write_buffer: ctypes_buffer_t): try: ctypes.windll.kernel32.WriteProcessMemory( - self.process_handle, - base_address, - ctypes.byref(write_buffer), - ctypes.sizeof(write_buffer), - None - ) + self.process_handle, + base_address, + ctypes.byref(write_buffer), + ctypes.sizeof(write_buffer), + None + ) except (BufferError, ValueError, TypeError): raise MemEditError('Error with handle {}: {}'.format(self.process_handle, self._get_last_error())) def read_memory(self, base_address: int, read_buffer: ctypes_buffer_t) -> ctypes_buffer_t: try: ctypes.windll.kernel32.ReadProcessMemory( - self.process_handle, - base_address, - ctypes.byref(read_buffer), - ctypes.sizeof(read_buffer), - None - ) + self.process_handle, + base_address, + ctypes.byref(read_buffer), + ctypes.sizeof(read_buffer), + None + ) except (BufferError, ValueError, TypeError): raise MemEditError('Error with handle {}: {}'.format(self.process_handle, self._get_last_error())) @@ -184,10 +187,9 @@ class Process(AbstractProcess): max_path_len = 260 name_buffer = (ctypes.c_char * max_path_len)() rval = ctypes.windll.psapi.GetProcessImageFileNameA( - self.process_handle, - name_buffer, - max_path_len - ) + self.process_handle, + name_buffer, + max_path_len) if rval > 0: return name_buffer.value.decode() @@ -199,7 +201,7 @@ class Process(AbstractProcess): # According to EnumProcesses docs, you can't find out how many processes there are before # fetching the list. As a result, we grab 100 on the first try, and if we get a full list # of 100, repeatedly double the number until we get fewer than we asked for. - + n = 100 returned_size = ctypes.wintypes.DWORD() returned_size_ptr = ctypes.byref(returned_size) @@ -224,7 +226,7 @@ class Process(AbstractProcess): return pids[:num_returned] @staticmethod - def get_pid_by_name(target_name: str) -> int or None: + def get_pid_by_name(target_name: str) -> Optional[int]: for pid in Process.list_available_pids(): try: logger.info('Checking name for pid {}'.format(pid)) @@ -260,11 +262,10 @@ class Process(AbstractProcess): mbi_size = ctypes.sizeof(mbi) success = ctypes.windll.kernel32.VirtualQueryEx( - self.process_handle, - address, - mbi_ptr, - mbi_size, - ) + self.process_handle, + address, + mbi_ptr, + mbi_size) if success != mbi_size: if success == 0: @@ -279,10 +280,11 @@ class Process(AbstractProcess): page_ptr = start while page_ptr < stop: page_info = get_mem_info(page_ptr) - if page_info.Type == mem_types['MEM_PRIVATE'] and \ - page_info.State == mem_states['MEM_COMMIT'] and \ - page_info.Protect & page_protections['PAGE_READABLE'] != 0 and \ - (page_info.Protect & page_protections['PAGE_READWRITEABLE'] != 0 or not writeable_only): + if (page_info.Type == mem_types['MEM_PRIVATE'] + and page_info.State == mem_states['MEM_COMMIT'] + and page_info.Protect & page_protections['PAGE_READABLE'] != 0 + and (page_info.Protect & page_protections['PAGE_READWRITEABLE'] != 0 + or not writeable_only)): regions.append((page_ptr, page_ptr + page_info.RegionSize)) page_ptr += page_info.RegionSize diff --git a/setup.py b/setup.py index 3a55145..130ebf2 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,8 @@ from setuptools import setup, find_packages -with open('README.md', 'r') as f: + +with open('README.md', 'rt') as f: long_description = f.read() with open('mem_edit/VERSION.py', 'rt') as f: From 6ab295fc265d52ca0e584b9385f8227b2300bffc Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 1 Nov 2020 20:21:35 -0800 Subject: [PATCH 06/27] bump version to v0.5 --- mem_edit/VERSION.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mem_edit/VERSION.py b/mem_edit/VERSION.py index a3aacc8..d15f477 100644 --- a/mem_edit/VERSION.py +++ b/mem_edit/VERSION.py @@ -1,4 +1,4 @@ """ VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """ __version__ = ''' -0.4 +0.5 ''' From 5a032da984b753744536af2b18506d65a02018c8 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 8 Apr 2021 19:49:19 -0700 Subject: [PATCH 07/27] try to reduce log spam --- mem_edit/abstract.py | 1 - mem_edit/linux.py | 5 ++--- mem_edit/windows.py | 7 +++---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/mem_edit/abstract.py b/mem_edit/abstract.py index f2e6cd1..e086d4e 100644 --- a/mem_edit/abstract.py +++ b/mem_edit/abstract.py @@ -13,7 +13,6 @@ from . import utils from .utils import ctypes_buffer_t -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/mem_edit/linux.py b/mem_edit/linux.py index 58460b8..18f3b8f 100644 --- a/mem_edit/linux.py +++ b/mem_edit/linux.py @@ -15,7 +15,6 @@ from .abstract import Process as AbstractProcess from .utils import ctypes_buffer_t, MemEditError -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -94,14 +93,14 @@ class Process(AbstractProcess): def get_pid_by_name(target_name: str) -> Optional[int]: for pid in Process.list_available_pids(): try: - logger.info('Checking name for pid {}'.format(pid)) + logger.debug('Checking name for pid {}'.format(pid)) with open('/proc/{}/cmdline'.format(pid), 'rb') as cmdline: path = cmdline.read().decode().split('\x00')[0] except FileNotFoundError: continue name = os.path.basename(path) - logger.info('Name was "{}"'.format(name)) + logger.debug('Name was "{}"'.format(name)) if path is not None and name == target_name: return pid diff --git a/mem_edit/windows.py b/mem_edit/windows.py index e3bb013..b945058 100644 --- a/mem_edit/windows.py +++ b/mem_edit/windows.py @@ -14,7 +14,6 @@ from .abstract import Process as AbstractProcess from .utils import ctypes_buffer_t, MemEditError -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -229,18 +228,18 @@ class Process(AbstractProcess): def get_pid_by_name(target_name: str) -> Optional[int]: for pid in Process.list_available_pids(): try: - logger.info('Checking name for pid {}'.format(pid)) + logger.debug('Checking name for pid {}'.format(pid)) with Process.open_process(pid) as process: path = process.get_path() name = os.path.basename(path) - logger.info('Name was "{}"'.format(name)) + logger.debug('Name was "{}"'.format(name)) if path is not None and name == target_name: return pid except ValueError: pass except MemEditError as err: - logger.info(repr(err)) + logger.debug(repr(err)) logger.info('Found no process with name {}'.format(target_name)) return None From c29be9f4293a70a51ff9dfafce7f5cff117bd4df Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 8 Apr 2021 19:49:55 -0700 Subject: [PATCH 08/27] strip newlines from version string --- mem_edit/VERSION.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mem_edit/VERSION.py b/mem_edit/VERSION.py index d15f477..aa70ad0 100644 --- a/mem_edit/VERSION.py +++ b/mem_edit/VERSION.py @@ -1,4 +1,4 @@ """ VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """ __version__ = ''' 0.5 -''' +'''.strip() From ef1a39152ccbe75e63f232f40a69448718a48abc Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 8 Apr 2021 19:50:22 -0700 Subject: [PATCH 09/27] bump version to v0.6 --- mem_edit/VERSION.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mem_edit/VERSION.py b/mem_edit/VERSION.py index aa70ad0..e4f476e 100644 --- a/mem_edit/VERSION.py +++ b/mem_edit/VERSION.py @@ -1,4 +1,4 @@ """ VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """ __version__ = ''' -0.5 +0.6 '''.strip() From f3154e443dc906b8bfe23c56a12dcf9b679f8f42 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 11 Jul 2021 17:25:00 -0700 Subject: [PATCH 10/27] update email --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 130ebf2..b5a234e 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup(name='mem_edit', long_description=long_description, long_description_content_type='text/markdown', author='Jan Petykiewicz', - author_email='anewusername@gmail.com', + author_email='jan@mpxd.net', url='https://mpxd.net/code/jan/mem_edit', keywords=[ 'memory', From 46e9456fd4ef46ee440102780067f26e58af61d8 Mon Sep 17 00:00:00 2001 From: xerool Date: Sat, 30 Apr 2022 22:37:17 -0500 Subject: [PATCH 11/27] linux: wait for process before detach, and send SIGCONT I had issues with the ptrace call failing because the process had not yet stopped from SIGSTOP. From this stackoverflow answer, it seems that you can use waitpid to wait until the process is actually stopped. In python, this is exposed as os.waitpid. https://stackoverflow.com/questions/20510300/ptrace-detach-fails-after-ptrace-cont-with-errno-esrch#20525326 Additionally, the process was left frozen. I send a SIGCONT to continue the process after the detach, so that it isn't left stopped. --- mem_edit/linux.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mem_edit/linux.py b/mem_edit/linux.py index 18f3b8f..aa25a49 100644 --- a/mem_edit/linux.py +++ b/mem_edit/linux.py @@ -58,7 +58,9 @@ class Process(AbstractProcess): def close(self): os.kill(self.pid, signal.SIGSTOP) + os.waitpid(self.pid, 0) ptrace(ptrace_commands['PTRACE_DETACH'], self.pid, 0, 0) + os.kill(self.pid, signal.SIGCONT) self.pid = None def write_memory(self, base_address: int, write_buffer: ctypes_buffer_t): From 303620b0a2e9ad4aebc8512ed6ae51e47b608072 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 18 Aug 2022 23:41:19 -0700 Subject: [PATCH 12/27] Move to hatch-based build --- .gitignore | 7 ++++- MANIFEST.in | 3 --- mem_edit/LICENSE.md | 1 + mem_edit/README.md | 1 + mem_edit/VERSION.py | 4 --- mem_edit/__init__.py | 3 +-- pyproject.toml | 54 ++++++++++++++++++++++++++++++++++++++ setup.py | 62 -------------------------------------------- 8 files changed, 63 insertions(+), 72 deletions(-) delete mode 100644 MANIFEST.in create mode 120000 mem_edit/LICENSE.md create mode 120000 mem_edit/README.md delete mode 100644 mem_edit/VERSION.py create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 6ad846b..ad16919 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,13 @@ .idea/ -__pycache__ +__pycache__/ *.pyc *.egg-info/ build/ dist/ + +.pytest_cache/ +.mypy_cache/ + +*.pickle diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 2b8b271..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include README.md -include LICENSE.md -include mem_edit/VERSION diff --git a/mem_edit/LICENSE.md b/mem_edit/LICENSE.md new file mode 120000 index 0000000..7eabdb1 --- /dev/null +++ b/mem_edit/LICENSE.md @@ -0,0 +1 @@ +../LICENSE.md \ No newline at end of file diff --git a/mem_edit/README.md b/mem_edit/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/mem_edit/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/mem_edit/VERSION.py b/mem_edit/VERSION.py deleted file mode 100644 index e4f476e..0000000 --- a/mem_edit/VERSION.py +++ /dev/null @@ -1,4 +0,0 @@ -""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """ -__version__ = ''' -0.6 -'''.strip() diff --git a/mem_edit/__init__.py b/mem_edit/__init__.py index a4a0178..1052caf 100644 --- a/mem_edit/__init__.py +++ b/mem_edit/__init__.py @@ -17,8 +17,7 @@ from .utils import MemEditError __author__ = 'Jan Petykiewicz' - -from .VERSION import __version__ +__version__ = '0.6' version = __version__ # legacy compatibility diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5c10cab --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,54 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "mem_edit" +description = "Multi-platform library for memory editing" +readme = "README.md" +license = { file = "LICENSE.md" } +authors = [ + { name="Jan Petykiewicz", email="jan@mpxd.net" }, + ] +homepage = "https://mpxd.net/code/jan/mem_edit" +repository = "https://mpxd.net/code/jan/mem_edit" +keywords = [ + "memory", + "edit", + "editing", + "ReadProcessMemory", + "WriteProcessMemory", + "proc", + "mem", + "ptrace", + "multiplatform", + "scan", + "scanner", + "search", + "debug", + "cheat", + "trainer", + ] +classifiers = [ + "Programming Language :: Python :: 3", + "Development Status :: 4 - Beta", + "Environment :: Other Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development", + "Topic :: Software Development :: Debuggers", + "Topic :: Software Development :: Testing", + "Topic :: System", + "Topic :: Games/Entertainment", + "Topic :: Utilities", + ] +requires-python = ">=3.7" +dynamic = ["version"] +dependencies = [ + ] + +[tool.hatch.version] +path = "mem_edit/__init__.py" + diff --git a/setup.py b/setup.py deleted file mode 100644 index b5a234e..0000000 --- a/setup.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 - -from setuptools import setup, find_packages - - -with open('README.md', 'rt') as f: - long_description = f.read() - -with open('mem_edit/VERSION.py', 'rt') as f: - version = f.readlines()[2].strip() - -setup(name='mem_edit', - version=version, - description='Multi-platform library for memory editing', - long_description=long_description, - long_description_content_type='text/markdown', - author='Jan Petykiewicz', - author_email='jan@mpxd.net', - url='https://mpxd.net/code/jan/mem_edit', - keywords=[ - 'memory', - 'edit', - 'editing', - 'ReadProcessMemory', - 'WriteProcessMemory', - 'proc', - 'mem', - 'ptrace', - 'multiplatform', - 'scan', - 'scanner', - 'search', - 'debug', - 'cheat', - 'trainer', - ], - classifiers=[ - 'Programming Language :: Python :: 3', - 'Development Status :: 4 - Beta', - 'Environment :: Other Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU Affero General Public License v3', - 'Operating System :: POSIX :: Linux', - 'Operating System :: Microsoft :: Windows', - 'Topic :: Software Development', - 'Topic :: Software Development :: Debuggers', - 'Topic :: Software Development :: Testing', - 'Topic :: System', - 'Topic :: Games/Entertainment', - 'Topic :: Utilities', - ], - packages=find_packages(), - package_data={ - 'mem_edit': [] - }, - install_requires=[ - 'typing', - ], - extras_require={ - }, - ) - From f03ea6acaddb727a18ac19328a0f149956098980 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 18 Aug 2022 23:42:12 -0700 Subject: [PATCH 13/27] bump version to v0.7 --- mem_edit/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mem_edit/__init__.py b/mem_edit/__init__.py index 1052caf..96e5812 100644 --- a/mem_edit/__init__.py +++ b/mem_edit/__init__.py @@ -17,7 +17,7 @@ from .utils import MemEditError __author__ = 'Jan Petykiewicz' -__version__ = '0.6' +__version__ = '0.7' version = __version__ # legacy compatibility From e56ec887610954fd64e507f9f7ab4157ae0eee39 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 30 Mar 2024 17:40:18 -0700 Subject: [PATCH 14/27] modernize some code style indentation, type annotations, f-strings --- mem_edit/abstract.py | 59 +++++++++++++++++++++--------------- mem_edit/linux.py | 44 +++++++++++++++------------ mem_edit/utils.py | 33 ++++++++++++-------- mem_edit/windows.py | 72 +++++++++++++++++++++++--------------------- 4 files changed, 116 insertions(+), 92 deletions(-) diff --git a/mem_edit/abstract.py b/mem_edit/abstract.py index e086d4e..5305116 100644 --- a/mem_edit/abstract.py +++ b/mem_edit/abstract.py @@ -2,7 +2,7 @@ Abstract class for cross-platform memory editing. """ -from typing import List, Tuple, Optional, Union, Generator +from typing import Generator from abc import ABCMeta, abstractmethod from contextlib import contextmanager import copy @@ -122,7 +122,7 @@ class Process(metaclass=ABCMeta): """ @abstractmethod - def __init__(self, process_id: int): + def __init__(self, process_id: int) -> None: """ Constructing a Process object prepares the process with specified process_id for memory editing. Finding the `process_id` for the process you want to edit is often @@ -135,7 +135,7 @@ class Process(metaclass=ABCMeta): pass @abstractmethod - def close(self): + def close(self) -> None: """ Detach from the process, removing our ability to edit it and letting other debuggers attach to it instead. @@ -147,7 +147,11 @@ class Process(metaclass=ABCMeta): pass @abstractmethod - def write_memory(self, base_address: int, write_buffer: ctypes_buffer_t): + def write_memory( + self, + base_address: int, + write_buffer: ctypes_buffer_t, + ) -> None: """ Write the given buffer to the process's address space, starting at `base_address`. @@ -160,7 +164,11 @@ class Process(metaclass=ABCMeta): pass @abstractmethod - def read_memory(self, base_address: int, read_buffer: ctypes_buffer_t) -> ctypes_buffer_t: + def read_memory( + self, + base_address: int, + read_buffer: ctypes_buffer_t, + ) -> ctypes_buffer_t: """ Read into the given buffer from the process's address space, starting at `base_address`. @@ -176,7 +184,7 @@ class Process(metaclass=ABCMeta): pass @abstractmethod - def list_mapped_regions(self, writeable_only=True) -> List[Tuple[int, int]]: + def list_mapped_regions(self, writeable_only: bool = True) -> list[tuple[int, int]]: """ Return a list of `(start_address, stop_address)` for the regions of the address space accessible to (readable and possibly writable by) the process. @@ -192,18 +200,18 @@ class Process(metaclass=ABCMeta): pass @abstractmethod - def get_path(self) -> str: + def get_path(self) -> str | None: """ Return the path to the executable file which was run to start this process. Returns: - A string containing the path. + A string containing the path, or None if no path was found. """ pass @staticmethod @abstractmethod - def list_available_pids() -> List[int]: + def list_available_pids() -> list[int]: """ Return a list of all process ids (pids) accessible on this system. @@ -214,7 +222,7 @@ class Process(metaclass=ABCMeta): @staticmethod @abstractmethod - def get_pid_by_name(target_name: str) -> Optional[int]: + def get_pid_by_name(target_name: str) -> int | None: """ Attempt to return the process id (pid) of a process which was run with an executable file with the provided name. If no process is found, return None. @@ -234,10 +242,11 @@ class Process(metaclass=ABCMeta): """ pass - def deref_struct_pointer(self, - base_address: int, - targets: List[Tuple[int, ctypes_buffer_t]], - ) -> List[ctypes_buffer_t]: + def deref_struct_pointer( + self, + base_address: int, + targets: list[tuple[int, ctypes_buffer_t]], + ) -> list[ctypes_buffer_t]: """ Take a pointer to a struct and read out the struct members: ``` @@ -263,11 +272,12 @@ class Process(metaclass=ABCMeta): values = [self.read_memory(base + offset, buffer) for offset, buffer in targets] return values - def search_addresses(self, - addresses: List[int], - needle_buffer: ctypes_buffer_t, - verbatim: bool = True, - ) -> List[int]: + def search_addresses( + self, + addresses: list[int], + needle_buffer: ctypes_buffer_t, + verbatim: bool = True, + ) -> list[int]: """ Search for the provided value at each of the provided addresses, and return the addresses where it is found. @@ -298,11 +308,12 @@ class Process(metaclass=ABCMeta): found.append(address) return found - def search_all_memory(self, - needle_buffer: ctypes_buffer_t, - writeable_only: bool = True, - verbatim: bool = True, - ) -> List[int]: + def search_all_memory( + self, + needle_buffer: ctypes_buffer_t, + writeable_only: bool = True, + verbatim: bool = True, + ) -> list[int]: """ Search the entire memory space accessible to the process for the provided value. diff --git a/mem_edit/linux.py b/mem_edit/linux.py index aa25a49..e6e92ad 100644 --- a/mem_edit/linux.py +++ b/mem_edit/linux.py @@ -2,7 +2,6 @@ Implementation of Process class for Linux """ -from typing import List, Tuple, Optional from os import strerror import os import os.path @@ -35,41 +34,46 @@ _ptrace.argtypes = (ctypes.c_ulong,) * 4 _ptrace.restype = ctypes.c_long -def ptrace(command: int, pid: int = 0, arg1: int = 0, arg2: int = 0) -> int: +def ptrace( + command: int, + pid: int = 0, + arg1: int = 0, + arg2: int = 0, + ) -> int: """ - Call ptrace() with the provided pid and arguments. See the ```man ptrace```. + Call ptrace() with the provided pid and arguments. See `man ptrace`. """ - logger.debug('ptrace({}, {}, {}, {})'.format(command, pid, arg1, arg2)) + logger.debug(f'ptrace({command}, {pid}, {arg1}, {arg2})') result = _ptrace(command, pid, arg1, arg2) if result == -1: err_no = ctypes.get_errno() if err_no: - raise MemEditError('ptrace({}, {}, {}, {})'.format(command, pid, arg1, arg2) + - ' failed with error {}: {}'.format(err_no, strerror(err_no))) + raise MemEditError(f'ptrace({command}, {pid}, {arg1}, {arg2})' + + f' failed with error {err_no}: {strerror(err_no)}') return result class Process(AbstractProcess): - pid = None + pid: int | None - def __init__(self, process_id: int): + def __init__(self, process_id: int) -> None: ptrace(ptrace_commands['PTRACE_SEIZE'], process_id) self.pid = process_id - def close(self): + def close(self) -> None: os.kill(self.pid, signal.SIGSTOP) os.waitpid(self.pid, 0) ptrace(ptrace_commands['PTRACE_DETACH'], self.pid, 0, 0) os.kill(self.pid, signal.SIGCONT) self.pid = None - def write_memory(self, base_address: int, write_buffer: ctypes_buffer_t): - with open('/proc/{}/mem'.format(self.pid), 'rb+') as mem: + def write_memory(self, base_address: int, write_buffer: ctypes_buffer_t) -> None: + with open(f'/proc/{self.pid}/mem', 'rb+') as mem: mem.seek(base_address) mem.write(write_buffer) def read_memory(self, base_address: int, read_buffer: ctypes_buffer_t) -> ctypes_buffer_t: - with open('/proc/{}/mem'.format(self.pid), 'rb+') as mem: + with open(f'/proc/{self.pid}/mem', 'rb+') as mem: mem.seek(base_address) mem.readinto(read_buffer) return read_buffer @@ -82,7 +86,7 @@ class Process(AbstractProcess): return '' @staticmethod - def list_available_pids() -> List[int]: + def list_available_pids() -> list[int]: pids = [] for pid_str in os.listdir('/proc'): try: @@ -92,26 +96,26 @@ class Process(AbstractProcess): return pids @staticmethod - def get_pid_by_name(target_name: str) -> Optional[int]: + def get_pid_by_name(target_name: str) -> int | None: for pid in Process.list_available_pids(): try: - logger.debug('Checking name for pid {}'.format(pid)) - with open('/proc/{}/cmdline'.format(pid), 'rb') as cmdline: + logger.debug(f'Checking name for pid {pid}') + with open(f'/proc/{pid}/cmdline', 'rb') as cmdline: path = cmdline.read().decode().split('\x00')[0] except FileNotFoundError: continue name = os.path.basename(path) - logger.debug('Name was "{}"'.format(name)) + logger.debug(f'Name was "{name}"') if path is not None and name == target_name: return pid - logger.info('Found no process with name {}'.format(target_name)) + logger.info(f'Found no process with name {target_name}') return None - def list_mapped_regions(self, writeable_only: bool = True) -> List[Tuple[int, int]]: + def list_mapped_regions(self, writeable_only: bool = True) -> list[tuple[int, int]]: regions = [] - with open('/proc/{}/maps'.format(self.pid), 'r') as maps: + with open(f'/proc/{self.pid}/maps', 'r') as maps: for line in maps: bounds, privileges = line.split()[0:2] diff --git a/mem_edit/utils.py b/mem_edit/utils.py index 2c9c022..a1f0bc3 100644 --- a/mem_edit/utils.py +++ b/mem_edit/utils.py @@ -11,21 +11,26 @@ Utility functions and types: Check if two buffers (ctypes objects) store equal values: ctypes_equal(a, b) """ - -from typing import List, Union +from typing import Union import ctypes -ctypes_buffer_t = Union[ctypes._SimpleCData, ctypes.Array, ctypes.Structure, ctypes.Union] +ctypes_buffer_t = Union[ + ctypes._SimpleCData, + ctypes.Array, + ctypes.Structure, + ctypes.Union, + ] class MemEditError(Exception): pass -def search_buffer_verbatim(needle_buffer: ctypes_buffer_t, - haystack_buffer: ctypes_buffer_t, - ) -> List[int]: +def search_buffer_verbatim( + needle_buffer: ctypes_buffer_t, + haystack_buffer: ctypes_buffer_t, + ) -> list[int]: """ Search for a buffer inside another buffer, using a direct (bitwise) comparison @@ -50,9 +55,10 @@ def search_buffer_verbatim(needle_buffer: ctypes_buffer_t, return found -def search_buffer(needle_buffer: ctypes_buffer_t, - haystack_buffer: ctypes_buffer_t, - ) -> List[int]: +def search_buffer( + needle_buffer: ctypes_buffer_t, + haystack_buffer: ctypes_buffer_t, + ) -> list[int]: """ Search for a buffer inside another buffer, using `ctypes_equal` for comparison. Much slower than `search_buffer_verbatim`. @@ -73,9 +79,10 @@ def search_buffer(needle_buffer: ctypes_buffer_t, return found -def ctypes_equal(a: ctypes_buffer_t, - b: ctypes_buffer_t, - ) -> bool: +def ctypes_equal( + a: ctypes_buffer_t, + b: ctypes_buffer_t, + ) -> bool: """ Check if the values stored inside two ctypes buffers are equal. """ @@ -87,7 +94,7 @@ def ctypes_equal(a: ctypes_buffer_t, elif isinstance(a, ctypes.Structure) or isinstance(a, ctypes.Union): for attr_name, attr_type in a._fields_: a_attr, b_attr = (getattr(x, attr_name) for x in (a, b)) - if isinstance(a, ctypes_buffer_t): + if isinstance(a, (ctypes.Array, ctypes.Structure, ctypes.Union, ctypes._SimpleCData)): if not ctypes_equal(a_attr, b_attr): return False elif not a_attr == b_attr: diff --git a/mem_edit/windows.py b/mem_edit/windows.py index b945058..dc03521 100644 --- a/mem_edit/windows.py +++ b/mem_edit/windows.py @@ -2,7 +2,6 @@ Implementation of Process class for Windows """ -from typing import List, Tuple, Optional from math import floor from os import strerror import os.path @@ -25,10 +24,10 @@ privileges = { 'PROCESS_VM_WRITE': 0x0020, } privileges['PROCESS_RW'] = ( - privileges['PROCESS_QUERY_INFORMATION'] | - privileges['PROCESS_VM_OPERATION'] | - privileges['PROCESS_VM_READ'] | - privileges['PROCESS_VM_WRITE'] + privileges['PROCESS_QUERY_INFORMATION'] + | privileges['PROCESS_VM_OPERATION'] + | privileges['PROCESS_VM_READ'] + | privileges['PROCESS_VM_WRITE'] ) # Memory region states @@ -50,13 +49,13 @@ page_protections = { } # Custom (combined) permissions page_protections['PAGE_READABLE'] = ( - page_protections['PAGE_EXECUTE_READ'] | - page_protections['PAGE_EXECUTE_READWRITE'] | - page_protections['PAGE_READWRITE'] + page_protections['PAGE_EXECUTE_READ'] + | page_protections['PAGE_EXECUTE_READWRITE'] + | page_protections['PAGE_READWRITE'] ) page_protections['PAGE_READWRITEABLE'] = ( - page_protections['PAGE_EXECUTE_READWRITE'] | - page_protections['PAGE_READWRITE'] + page_protections['PAGE_EXECUTE_READWRITE'] + | page_protections['PAGE_READWRITE'] ) # Memory types @@ -91,7 +90,9 @@ class MEMORY_BASIC_INFORMATION64(ctypes.Structure): ('__alignment2', ctypes.wintypes.DWORD), ] + PTR_SIZE = ctypes.sizeof(ctypes.c_void_p) +MEMORY_BASIC_INFORMATION: ctypes.Structure if PTR_SIZE == 8: # 64-bit python MEMORY_BASIC_INFORMATION = MEMORY_BASIC_INFORMATION64 elif PTR_SIZE == 4: # 32-bit python @@ -133,9 +134,9 @@ class SYSTEM_INFO(ctypes.Structure): class Process(AbstractProcess): - process_handle = None + process_handle: int | None - def __init__(self, process_id: int): + def __init__(self, process_id: int) -> None: process_handle = ctypes.windll.kernel32.OpenProcess( privileges['PROCESS_RW'], False, @@ -143,15 +144,15 @@ class Process(AbstractProcess): ) if not process_handle: - raise MemEditError('Couldn\'t open process {}'.format(process_id)) + raise MemEditError(f'Couldn\'t open process {process_id}') self.process_handle = process_handle - def close(self): + def close(self) -> None: ctypes.windll.kernel32.CloseHandle(self.process_handle) self.process_handle = None - def write_memory(self, base_address: int, write_buffer: ctypes_buffer_t): + def write_memory(self, base_address: int, write_buffer: ctypes_buffer_t) -> None: try: ctypes.windll.kernel32.WriteProcessMemory( self.process_handle, @@ -161,7 +162,7 @@ class Process(AbstractProcess): None ) except (BufferError, ValueError, TypeError): - raise MemEditError('Error with handle {}: {}'.format(self.process_handle, self._get_last_error())) + raise MemEditError(f'Error with handle {self.process_handle}: {self._get_last_error()}') def read_memory(self, base_address: int, read_buffer: ctypes_buffer_t) -> ctypes_buffer_t: try: @@ -173,22 +174,23 @@ class Process(AbstractProcess): None ) except (BufferError, ValueError, TypeError): - raise MemEditError('Error with handle {}: {}'.format(self.process_handle, self._get_last_error())) + raise MemEditError(f'Error with handle {self.process_handle}: {self._get_last_error()}') return read_buffer @staticmethod - def _get_last_error() -> Tuple[int, str]: + def _get_last_error() -> tuple[int, str]: err = ctypes.windll.kernel32.GetLastError() return err, strerror(err) - def get_path(self) -> str: + def get_path(self) -> str | None: max_path_len = 260 name_buffer = (ctypes.c_char * max_path_len)() rval = ctypes.windll.psapi.GetProcessImageFileNameA( - self.process_handle, - name_buffer, - max_path_len) + self.process_handle, + name_buffer, + max_path_len, + ) if rval > 0: return name_buffer.value.decode() @@ -196,28 +198,28 @@ class Process(AbstractProcess): return None @staticmethod - def list_available_pids() -> List[int]: + def list_available_pids() -> list[int]: # According to EnumProcesses docs, you can't find out how many processes there are before # fetching the list. As a result, we grab 100 on the first try, and if we get a full list # of 100, repeatedly double the number until we get fewer than we asked for. - n = 100 + nn = 100 returned_size = ctypes.wintypes.DWORD() returned_size_ptr = ctypes.byref(returned_size) while True: - pids = (ctypes.wintypes.DWORD * n)() + pids = (ctypes.wintypes.DWORD * nn)() size = ctypes.sizeof(pids) pids_ptr = ctypes.byref(pids) success = ctypes.windll.Psapi.EnumProcesses(pids_ptr, size, returned_size_ptr) if not success: - raise MemEditError('Failed to enumerate processes: n={}'.format(n)) + raise MemEditError(f'Failed to enumerate processes: nn={nn}') num_returned = floor(returned_size.value / ctypes.sizeof(ctypes.wintypes.DWORD)) - if n == num_returned: - n *= 2 + if nn == num_returned: + nn *= 2 continue else: break @@ -225,15 +227,15 @@ class Process(AbstractProcess): return pids[:num_returned] @staticmethod - def get_pid_by_name(target_name: str) -> Optional[int]: + def get_pid_by_name(target_name: str) -> int | None: for pid in Process.list_available_pids(): try: - logger.debug('Checking name for pid {}'.format(pid)) + logger.debug(f'Checking name for pid {pid}') with Process.open_process(pid) as process: path = process.get_path() name = os.path.basename(path) - logger.debug('Name was "{}"'.format(name)) + logger.debug(f'Name was "{name}"') if path is not None and name == target_name: return pid except ValueError: @@ -241,10 +243,10 @@ class Process(AbstractProcess): except MemEditError as err: logger.debug(repr(err)) - logger.info('Found no process with name {}'.format(target_name)) + logger.info(f'Found no process with name {target_name}') return None - def list_mapped_regions(self, writeable_only: bool = True) -> List[Tuple[int, int]]: + def list_mapped_regions(self, writeable_only: bool = True) -> list[tuple[int, int]]: sys_info = SYSTEM_INFO() sys_info_ptr = ctypes.byref(sys_info) ctypes.windll.kernel32.GetSystemInfo(sys_info_ptr) @@ -268,8 +270,8 @@ class Process(AbstractProcess): if success != mbi_size: if success == 0: - raise MemEditError('Failed VirtualQueryEx with handle ' + - '{}: {}'.format(self.process_handle, self._get_last_error())) + raise MemEditError('Failed VirtualQueryEx with handle ' + + f'{self.process_handle}: {self._get_last_error()}') else: raise MemEditError('VirtualQueryEx output too short!') From f7c7496cfd700ab00a1bb5c2f96be137ea343e1c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 30 Mar 2024 17:41:23 -0700 Subject: [PATCH 15/27] get_path should return None on failure --- mem_edit/linux.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mem_edit/linux.py b/mem_edit/linux.py index e6e92ad..80d6e0c 100644 --- a/mem_edit/linux.py +++ b/mem_edit/linux.py @@ -78,12 +78,12 @@ class Process(AbstractProcess): mem.readinto(read_buffer) return read_buffer - def get_path(self) -> str: + def get_path(self) -> str | None: try: - with open('/proc/{}/cmdline', 'rb') as f: - return f.read().decode().split('\x00')[0] + with open(f'/proc/{self.pid}/cmdline', 'rb') as ff: + return ff.read().decode().split('\x00')[0] except FileNotFoundError: - return '' + return None @staticmethod def list_available_pids() -> list[int]: From bdf0fb323e29699b3102db6a9c8548ce85cd78a3 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 30 Mar 2024 17:41:44 -0700 Subject: [PATCH 16/27] early bailout conditions caught by type check --- mem_edit/linux.py | 2 ++ mem_edit/windows.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/mem_edit/linux.py b/mem_edit/linux.py index 80d6e0c..c5b1950 100644 --- a/mem_edit/linux.py +++ b/mem_edit/linux.py @@ -61,6 +61,8 @@ class Process(AbstractProcess): self.pid = process_id def close(self) -> None: + if self.pid is None: + return os.kill(self.pid, signal.SIGSTOP) os.waitpid(self.pid, 0) ptrace(ptrace_commands['PTRACE_DETACH'], self.pid, 0, 0) diff --git a/mem_edit/windows.py b/mem_edit/windows.py index dc03521..49f92eb 100644 --- a/mem_edit/windows.py +++ b/mem_edit/windows.py @@ -233,6 +233,8 @@ class Process(AbstractProcess): logger.debug(f'Checking name for pid {pid}') with Process.open_process(pid) as process: path = process.get_path() + if path is None: + continue name = os.path.basename(path) logger.debug(f'Name was "{name}"') From f10674e2b5c41e94c229ecf144094197b528f5a8 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 30 Mar 2024 17:43:25 -0700 Subject: [PATCH 17/27] flake8 preferences --- .flake8 | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..0042015 --- /dev/null +++ b/.flake8 @@ -0,0 +1,29 @@ +[flake8] +ignore = + # E501 line too long + E501, + # W391 newlines at EOF + W391, + # E241 multiple spaces after comma + E241, + # E302 expected 2 newlines + E302, + # W503 line break before binary operator (to be deprecated) + W503, + # E265 block comment should start with '# ' + E265, + # E123 closing bracket does not match indentation of opening bracket's line + E123, + # E124 closing bracket does not match visual indentation + E124, + # E221 multiple spaces before operator + E221, + # E201 whitespace after '[' + E201, + # E741 ambiguous variable name 'I' + E741, + + +per-file-ignores = + # F401 import without use + */__init__.py: F401, From b889ad813366237ce1a4e38cfc74536a93d8fb1e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 30 Mar 2024 17:44:19 -0700 Subject: [PATCH 18/27] update readme and note github mirror --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 53789b7..443daff 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ -# mem_edit +# mem_edit **mem_edit** is a multi-platform memory editing library written in Python. **Homepage:** https://mpxd.net/code/jan/mem_edit +* PyPI: https://pypi.org/project/mem-edit/ +* Github mirror: https://github.com/anewusername/mem_edit **Capabilities:** * Scan all readable memory used by a process. @@ -18,7 +20,7 @@ ## Installation **Dependencies:** -* python 3 (written and tested with 3.7) +* python >=3.11 * ctypes * typing (for type annotations) From e316322fbf38cf2d9d165543bd2616194447d53a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 30 Mar 2024 17:45:41 -0700 Subject: [PATCH 19/27] Update reqs --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5c10cab..f9c1de2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ keywords = [ ] classifiers = [ "Programming Language :: Python :: 3", - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Environment :: Other Environment", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Affero General Public License v3", @@ -44,7 +44,7 @@ classifiers = [ "Topic :: Games/Entertainment", "Topic :: Utilities", ] -requires-python = ">=3.7" +requires-python = ">=3.11" dynamic = ["version"] dependencies = [ ] From e26381a5782a1aef60178f325dcbbd28834ef4e9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 30 Mar 2024 17:46:16 -0700 Subject: [PATCH 20/27] bump version to v0.8 --- mem_edit/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mem_edit/__init__.py b/mem_edit/__init__.py index 96e5812..8be6c1e 100644 --- a/mem_edit/__init__.py +++ b/mem_edit/__init__.py @@ -17,7 +17,7 @@ from .utils import MemEditError __author__ = 'Jan Petykiewicz' -__version__ = '0.7' +__version__ = '0.8' version = __version__ # legacy compatibility From 1ecab0865130d300ef6830899fd0a4c2e5228cf9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 31 Jul 2024 22:53:55 -0700 Subject: [PATCH 21/27] add ruff config --- pyproject.toml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index f9c1de2..132f221 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,3 +52,36 @@ dependencies = [ [tool.hatch.version] path = "mem_edit/__init__.py" + +[tool.ruff] +exclude = [ + ".git", + "dist", + ] +line-length = 145 +indent-width = 4 +lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +lint.select = [ + "NPY", "E", "F", "W", "B", "ANN", "UP", "SLOT", "SIM", "LOG", + "C4", "ISC", "PIE", "PT", "RET", "TCH", "PTH", "INT", + "ARG", "PL", "R", "TRY", + "G010", "G101", "G201", "G202", + "Q002", "Q003", "Q004", + ] +lint.ignore = [ + #"ANN001", # No annotation + "ANN002", # *args + "ANN003", # **kwargs + "ANN401", # Any + "ANN101", # self: Self + "SIM108", # single-line if / else assignment + "RET504", # x=y+z; return x + "PIE790", # unnecessary pass + "ISC003", # non-implicit string concatenation + "C408", # dict(x=y) instead of {'x': y} + "PLR09", # Too many xxx + "PLR2004", # magic number + "PLC0414", # import x as x + "TRY003", # Long exception message + ] + From db1dcc5e839357e650e8a00be1deda2024e8477a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 31 Jul 2024 22:54:10 -0700 Subject: [PATCH 22/27] re-export using "import x as x" --- mem_edit/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mem_edit/__init__.py b/mem_edit/__init__.py index 8be6c1e..fcedd6f 100644 --- a/mem_edit/__init__.py +++ b/mem_edit/__init__.py @@ -23,8 +23,8 @@ version = __version__ # legacy compatibility system = platform.system() if system == 'Windows': - from .windows import Process + from .windows import Process as Process elif system == 'Linux': - from .linux import Process + from .linux import Process as Process else: raise MemEditError('Only Linux and Windows are currently supported.') From 42c69867b8fbe283dbe9614c86de5fcb125cb335 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 31 Jul 2024 22:54:27 -0700 Subject: [PATCH 23/27] using == on purpose here --- mem_edit/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mem_edit/utils.py b/mem_edit/utils.py index a1f0bc3..5099845 100644 --- a/mem_edit/utils.py +++ b/mem_edit/utils.py @@ -86,7 +86,7 @@ def ctypes_equal( """ Check if the values stored inside two ctypes buffers are equal. """ - if not type(a) == type(b): + if not type(a) == type(b): # noqa: E721 return False if isinstance(a, ctypes.Array): From fec96b92a7305130133a0a383b378145b5cbac05 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 1 Aug 2024 00:20:53 -0700 Subject: [PATCH 24/27] improve error handling --- mem_edit/abstract.py | 2 +- mem_edit/windows.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/mem_edit/abstract.py b/mem_edit/abstract.py index 5305116..8e1e2f3 100644 --- a/mem_edit/abstract.py +++ b/mem_edit/abstract.py @@ -341,7 +341,7 @@ class Process(metaclass=ABCMeta): self.read_memory(start, region_buffer) found += [offset + start for offset in search(needle_buffer, region_buffer)] except OSError: - logger.error('Failed to read in range 0x{} - 0x{}'.format(start, stop)) + logger.exception(f'Failed to read in range 0x{start} - 0x{stop}') return found @classmethod diff --git a/mem_edit/windows.py b/mem_edit/windows.py index 49f92eb..9de6b06 100644 --- a/mem_edit/windows.py +++ b/mem_edit/windows.py @@ -161,8 +161,8 @@ class Process(AbstractProcess): ctypes.sizeof(write_buffer), None ) - except (BufferError, ValueError, TypeError): - raise MemEditError(f'Error with handle {self.process_handle}: {self._get_last_error()}') + except (BufferError, ValueError, TypeError) as err: + raise MemEditError(f'Error with handle {self.process_handle}: {self._get_last_error()}') from err def read_memory(self, base_address: int, read_buffer: ctypes_buffer_t) -> ctypes_buffer_t: try: @@ -173,8 +173,8 @@ class Process(AbstractProcess): ctypes.sizeof(read_buffer), None ) - except (BufferError, ValueError, TypeError): - raise MemEditError(f'Error with handle {self.process_handle}: {self._get_last_error()}') + except (BufferError, ValueError, TypeError) as err: + raise MemEditError(f'Error with handle {self.process_handle}: {self._get_last_error()}') from err return read_buffer @@ -274,8 +274,7 @@ class Process(AbstractProcess): if success == 0: raise MemEditError('Failed VirtualQueryEx with handle ' + f'{self.process_handle}: {self._get_last_error()}') - else: - raise MemEditError('VirtualQueryEx output too short!') + raise MemEditError('VirtualQueryEx output too short!') return mbi From 5f2b1a432e7725cab0310a7d7af93ec6c11db35d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 1 Aug 2024 00:21:55 -0700 Subject: [PATCH 25/27] improve type annotations --- mem_edit/abstract.py | 6 +++--- mem_edit/utils.py | 13 ++++++------- mem_edit/windows.py | 4 ++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/mem_edit/abstract.py b/mem_edit/abstract.py index 8e1e2f3..f21a528 100644 --- a/mem_edit/abstract.py +++ b/mem_edit/abstract.py @@ -1,8 +1,8 @@ """ Abstract class for cross-platform memory editing. """ - -from typing import Generator +from typing import Self +from collections.abc import Generator from abc import ABCMeta, abstractmethod from contextlib import contextmanager import copy @@ -346,7 +346,7 @@ class Process(metaclass=ABCMeta): @classmethod @contextmanager - def open_process(cls, process_id: int) -> Generator['Process', None, None]: + def open_process(cls: type[Self], process_id: int) -> Generator[Self, None, None]: """ Context manager which automatically closes the constructed Process: ``` diff --git a/mem_edit/utils.py b/mem_edit/utils.py index 5099845..fac9f3c 100644 --- a/mem_edit/utils.py +++ b/mem_edit/utils.py @@ -11,16 +11,15 @@ Utility functions and types: Check if two buffers (ctypes objects) store equal values: ctypes_equal(a, b) """ -from typing import Union import ctypes -ctypes_buffer_t = Union[ - ctypes._SimpleCData, - ctypes.Array, - ctypes.Structure, - ctypes.Union, - ] +ctypes_buffer_t = ( + ctypes._SimpleCData + | ctypes.Array + | ctypes.Structure + | ctypes.Union + ) class MemEditError(Exception): diff --git a/mem_edit/windows.py b/mem_edit/windows.py index 9de6b06..c165f99 100644 --- a/mem_edit/windows.py +++ b/mem_edit/windows.py @@ -92,7 +92,7 @@ class MEMORY_BASIC_INFORMATION64(ctypes.Structure): PTR_SIZE = ctypes.sizeof(ctypes.c_void_p) -MEMORY_BASIC_INFORMATION: ctypes.Structure +MEMORY_BASIC_INFORMATION: type[ctypes.Structure] if PTR_SIZE == 8: # 64-bit python MEMORY_BASIC_INFORMATION = MEMORY_BASIC_INFORMATION64 elif PTR_SIZE == 4: # 32-bit python @@ -256,7 +256,7 @@ class Process(AbstractProcess): start = sys_info.lpMinimumApplicationAddress stop = sys_info.lpMaximumApplicationAddress - def get_mem_info(address): + def get_mem_info(address: int) -> MEMORY_BASIC_INFORMATION: """ Query the memory region starting at or before 'address' to get its size/type/state/permissions. """ From 5a82f04f9edda312fe792ac55279f8a9737b9c40 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 1 Aug 2024 00:22:59 -0700 Subject: [PATCH 26/27] flatten and simplify some code --- mem_edit/utils.py | 2 +- mem_edit/windows.py | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/mem_edit/utils.py b/mem_edit/utils.py index fac9f3c..96d0180 100644 --- a/mem_edit/utils.py +++ b/mem_edit/utils.py @@ -96,7 +96,7 @@ def ctypes_equal( if isinstance(a, (ctypes.Array, ctypes.Structure, ctypes.Union, ctypes._SimpleCData)): if not ctypes_equal(a_attr, b_attr): return False - elif not a_attr == b_attr: + elif a_attr != b_attr: return False return True diff --git a/mem_edit/windows.py b/mem_edit/windows.py index c165f99..50246ec 100644 --- a/mem_edit/windows.py +++ b/mem_edit/windows.py @@ -192,10 +192,9 @@ class Process(AbstractProcess): max_path_len, ) - if rval > 0: - return name_buffer.value.decode() - else: + if rval <= 0: return None + return name_buffer.value.decode() @staticmethod def list_available_pids() -> list[int]: @@ -218,11 +217,9 @@ class Process(AbstractProcess): num_returned = floor(returned_size.value / ctypes.sizeof(ctypes.wintypes.DWORD)) - if nn == num_returned: - nn *= 2 - continue - else: + if nn != num_returned: break + nn *= 2 return pids[:num_returned] From 8f7955543c10962548a4f23395f451de6da9a6f0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 1 Aug 2024 00:23:25 -0700 Subject: [PATCH 27/27] use pathlib --- mem_edit/linux.py | 13 +++++++------ mem_edit/windows.py | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/mem_edit/linux.py b/mem_edit/linux.py index c5b1950..94509bd 100644 --- a/mem_edit/linux.py +++ b/mem_edit/linux.py @@ -9,6 +9,7 @@ import signal import ctypes import ctypes.util import logging +from pathlib import Path from .abstract import Process as AbstractProcess from .utils import ctypes_buffer_t, MemEditError @@ -70,19 +71,19 @@ class Process(AbstractProcess): self.pid = None def write_memory(self, base_address: int, write_buffer: ctypes_buffer_t) -> None: - with open(f'/proc/{self.pid}/mem', 'rb+') as mem: + with Path(f'/proc/{self.pid}/mem').open('rb+') as mem: mem.seek(base_address) mem.write(write_buffer) def read_memory(self, base_address: int, read_buffer: ctypes_buffer_t) -> ctypes_buffer_t: - with open(f'/proc/{self.pid}/mem', 'rb+') as mem: + with Path(f'/proc/{self.pid}/mem').open('rb+') as mem: mem.seek(base_address) mem.readinto(read_buffer) return read_buffer def get_path(self) -> str | None: try: - with open(f'/proc/{self.pid}/cmdline', 'rb') as ff: + with Path(f'/proc/{self.pid}/cmdline').open('rb') as ff: return ff.read().decode().split('\x00')[0] except FileNotFoundError: return None @@ -102,12 +103,12 @@ class Process(AbstractProcess): for pid in Process.list_available_pids(): try: logger.debug(f'Checking name for pid {pid}') - with open(f'/proc/{pid}/cmdline', 'rb') as cmdline: + with Path(f'/proc/{pid}/cmdline').open('rb') as cmdline: path = cmdline.read().decode().split('\x00')[0] except FileNotFoundError: continue - name = os.path.basename(path) + name = Path(path).name logger.debug(f'Name was "{name}"') if path is not None and name == target_name: return pid @@ -117,7 +118,7 @@ class Process(AbstractProcess): def list_mapped_regions(self, writeable_only: bool = True) -> list[tuple[int, int]]: regions = [] - with open(f'/proc/{self.pid}/maps', 'r') as maps: + with Path(f'/proc/{self.pid}/maps').open('r') as maps: for line in maps: bounds, privileges = line.split()[0:2] diff --git a/mem_edit/windows.py b/mem_edit/windows.py index 50246ec..8dfd9b8 100644 --- a/mem_edit/windows.py +++ b/mem_edit/windows.py @@ -4,7 +4,7 @@ Implementation of Process class for Windows from math import floor from os import strerror -import os.path +from pathlib import Path import ctypes import ctypes.wintypes import logging @@ -233,7 +233,7 @@ class Process(AbstractProcess): if path is None: continue - name = os.path.basename(path) + name = Path(path).name logger.debug(f'Name was "{name}"') if path is not None and name == target_name: return pid