From 303620b0a2e9ad4aebc8512ed6ae51e47b608072 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 18 Aug 2022 23:41:19 -0700 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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 05/16] 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 06/16] 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 07/16] 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 08/16] 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 09/16] 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 10/16] 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 11/16] 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 12/16] 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 13/16] 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 14/16] 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 15/16] 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 16/16] 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