PyMemoryEditor 1.5.6__tar.gz → 1.5.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PKG-INFO +8 -5
  2. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/__init__.py +7 -3
  3. pymemoryeditor-1.5.8/PyMemoryEditor/errors.py +6 -0
  4. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/linux/ptrace/__init__.py +2 -2
  5. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/linux/ptrace/enums.py +1 -1
  6. pymemoryeditor-1.5.8/PyMemoryEditor/process/__init__.py +145 -0
  7. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/process/util.py +2 -4
  8. pymemoryeditor-1.5.8/PyMemoryEditor/sample/application.py +15 -0
  9. pymemoryeditor-1.5.8/PyMemoryEditor/sample/main_application_window.py +443 -0
  10. pymemoryeditor-1.5.8/PyMemoryEditor/sample/open_process_window.py +56 -0
  11. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/util/scan.py +3 -3
  12. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/win32/functions.py +1 -1
  13. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/win32/process.py +4 -2
  14. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/README.md +5 -2
  15. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/pyproject.toml +7 -3
  16. pymemoryeditor-1.5.8/requirements.txt +2 -0
  17. pymemoryeditor-1.5.6/PyMemoryEditor/errors.py +0 -3
  18. pymemoryeditor-1.5.6/PyMemoryEditor/process/__init__.py +0 -87
  19. pymemoryeditor-1.5.6/requirements.txt +0 -3
  20. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/.github/workflows/python-package.yml +0 -0
  21. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/.gitignore +0 -0
  22. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/LICENSE +0 -0
  23. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/enums.py +0 -0
  24. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/linux/functions.py +0 -0
  25. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/linux/process.py +0 -0
  26. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/linux/types.py +0 -0
  27. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/process/errors.py +0 -0
  28. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/process/info.py +0 -0
  29. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/util/__init__.py +0 -0
  30. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/util/search/abstract.py +0 -0
  31. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/util/search/kmp.py +0 -0
  32. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/win32/enums/__init__.py +0 -0
  33. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/win32/enums/memory_allocation_states.py +0 -0
  34. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/win32/enums/memory_protections.py +0 -0
  35. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/win32/enums/memory_types.py +0 -0
  36. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/win32/enums/process_operations.py +0 -0
  37. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/win32/enums/standard_access_rights.py +0 -0
  38. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/PyMemoryEditor/win32/types.py +0 -0
  39. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/__init__.py +0 -0
  40. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/open_process.py +0 -0
  41. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/process/__init__.py +0 -0
  42. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/process/errors.py +0 -0
  43. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/process/util.py +0 -0
  44. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/win32/enums/__init__.py +0 -0
  45. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/win32/enums/memory_allocation_states.py +0 -0
  46. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/win32/enums/memory_protections.py +0 -0
  47. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/win32/enums/memory_types.py +0 -0
  48. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/win32/enums/process_operations.py +0 -0
  49. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/win32/enums/scan_types.py +0 -0
  50. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/win32/enums/standard_access_rights.py +0 -0
  51. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/win32/functions.py +0 -0
  52. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/win32/types.py +0 -0
  53. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/build/lib/PyMemoryEditor/win32/util.py +0 -0
  54. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/tests/package.py +0 -0
  55. {pymemoryeditor-1.5.6 → pymemoryeditor-1.5.8}/tests/test_editor.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyMemoryEditor
3
- Version: 1.5.6
4
- Summary: A Python library to edit and track memory of Windows and Linux processes (32 bits and 64 bits).
3
+ Version: 1.5.8
4
+ Summary: Multi-platform library developed with ctypes for reading, writing and searching process memory, in a simple and friendly way with Python 3.
5
5
  Project-URL: Homepage, https://github.com/JeanExtreme002/PyMemoryEditor
6
6
  Author-email: Jean Loui Bernard Silva de Jesus <jeanextreme002@gmail.com>
7
7
  License-Expression: MIT
@@ -20,12 +20,12 @@ Classifier: Programming Language :: Python :: 3.8
20
20
  Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Programming Language :: Python :: 3.10
22
22
  Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
23
24
  Classifier: Topic :: Scientific/Engineering
24
25
  Classifier: Topic :: Security
25
26
  Classifier: Topic :: System :: Monitoring
26
27
  Requires-Python: >=3.6
27
28
  Requires-Dist: psutil
28
- Requires-Dist: pywin32; platform_system == 'Windows'
29
29
  Provides-Extra: tests
30
30
  Requires-Dist: pytest; extra == 'tests'
31
31
  Description-Content-Type: text/markdown
@@ -39,7 +39,7 @@ reading and writing values in the process memory.
39
39
  [![Pypi](https://img.shields.io/pypi/v/PyMemoryEditor)](https://pypi.org/project/PyMemoryEditor/)
40
40
  [![License](https://img.shields.io/pypi/l/PyMemoryEditor)](https://pypi.org/project/PyMemoryEditor/)
41
41
  [![Platforms](https://img.shields.io/badge/platforms-Windows%20%7C%20Linux-8A2BE2)](https://pypi.org/project/PyMemoryEditor/)
42
- [![Python Version](https://img.shields.io/badge/python-3.6%20%7C%20...%20%7C%203.10%20%7C%203.11-blue)](https://pypi.org/project/PyMemoryEditor/)
42
+ [![Python Version](https://img.shields.io/badge/python-3.6%20%7C%20...%20%7C%203.11%20%7C%203.12-blue)](https://pypi.org/project/PyMemoryEditor/)
43
43
  [![Downloads](https://static.pepy.tech/personalized-badge/pymemoryeditor?period=total&units=international_system&left_color=grey&right_color=orange&left_text=Downloads)](https://pypi.org/project/PyMemoryEditor/)
44
44
 
45
45
  # Installing PyMemoryEditor:
@@ -110,7 +110,7 @@ for address, info process.search_by_value(..., progress_information = True):
110
110
  print(template.format(address, progress))
111
111
  ```
112
112
 
113
- # Getting memory regions:
113
+ ## Getting memory regions:
114
114
  Use the method `get_memory_regions()` to get the base address, size and more information of all memory regions used by the process.
115
115
 
116
116
  ```py
@@ -119,3 +119,6 @@ for memory_region in process.get_memory_regions():
119
119
  size = memory_region["size"]
120
120
  information = memory_region["struct"]
121
121
  ```
122
+
123
+ # Tkinter application sample:
124
+ You can run a simple tkinter application to scan the memory of a process by typing `pymemoryeditor` at the CLI.
@@ -1,12 +1,14 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  """
4
- A Python library developed with ctypes to manipulate Windows and Linux processes (32 bits and 64 bits),
5
- reading and writing values in the process memory.
4
+ Multi-platform library developed with ctypes for reading, writing and
5
+ searching process memory, in a simple and friendly way with Python 3.
6
+
7
+ The package supports Windows and Linux (32-bit and 64-bit).
6
8
  """
7
9
 
8
10
  __author__ = "Jean Loui Bernard Silva de Jesus"
9
- __version__ = "1.5.6"
11
+ __version__ = "1.5.8"
10
12
 
11
13
 
12
14
  from .enums import ScanTypesEnum
@@ -21,4 +23,6 @@ if "win" in sys.platform:
21
23
  # For Linux.
22
24
  else:
23
25
  from .linux.process import LinuxProcess
26
+ from .linux.ptrace import ptrace
27
+ from .linux.ptrace.enums import PtraceCommandsEnum
24
28
  OpenProcess = LinuxProcess
@@ -0,0 +1,6 @@
1
+ from .process.errors import ProcessIDNotExistsError, ProcessNotFoundError
2
+
3
+
4
+ class ClosedProcess(Exception):
5
+ def __str__(self):
6
+ return "operation on closed process."
@@ -5,7 +5,7 @@
5
5
  # https://refspecs.linuxbase.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/baselib-ptrace-1.html
6
6
  # ...
7
7
 
8
- from .enums import CommandsEnum
8
+ from .enums import PtraceCommandsEnum
9
9
 
10
10
  from ctypes.util import find_library
11
11
  import ctypes
@@ -15,7 +15,7 @@ libc.ptrace.argtypes = (ctypes.c_ulong,) * 4
15
15
  libc.ptrace.restype = ctypes.c_long
16
16
 
17
17
 
18
- def ptrace(command: CommandsEnum, pid: int, *args: int) -> int:
18
+ def ptrace(command: PtraceCommandsEnum, pid: int, *args: int) -> int:
19
19
  """
20
20
  Run ptrace() system call with the provided command, pid and arguments.
21
21
  """
@@ -3,7 +3,7 @@
3
3
  from enum import Enum
4
4
 
5
5
 
6
- class CommandsEnum(Enum):
6
+ class PtraceCommandsEnum(Enum):
7
7
  """
8
8
  Enum with commands for ptrace() system call.
9
9
 
@@ -0,0 +1,145 @@
1
+ # -*- coding: utf-8 -*-
2
+ from abc import ABC, abstractmethod
3
+ from typing import Generator, Optional, Tuple, Type, TypeVar, Union
4
+
5
+ from ..enums import ScanTypesEnum
6
+ from ..process.info import ProcessInfo
7
+
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ class AbstractProcess(ABC):
13
+ """
14
+ Abstract class to represent a process.
15
+ """
16
+
17
+ @abstractmethod
18
+ def __init__(self, *, window_title: Optional[str] = None, process_name: Optional[str] = None, pid: Optional[int] = None):
19
+ """
20
+ :param window_title: window title of the target program.
21
+ :param process_name: name of the target process.
22
+ :param pid: process ID.
23
+ """
24
+ self._process_info = ProcessInfo()
25
+
26
+ # Set the attributes to the process.
27
+ if pid:
28
+ self._process_info.pid = pid
29
+
30
+ elif window_title:
31
+ self._process_info.window_title = window_title
32
+
33
+ elif process_name:
34
+ self._process_info.process_name = process_name
35
+
36
+ else:
37
+ raise TypeError("You must pass an argument to one of these parameters (window_title, process_name, pid).")
38
+
39
+ def __enter__(self):
40
+ return self
41
+
42
+ def __exit__(self, exc_type, exc_value, exc_traceback):
43
+ self.close()
44
+
45
+ @property
46
+ def pid(self) -> int:
47
+ return self._process_info.pid
48
+
49
+ @abstractmethod
50
+ def close(self) -> bool:
51
+ """
52
+ Close the process handle.
53
+ """
54
+ raise NotImplementedError()
55
+
56
+ @abstractmethod
57
+ def get_memory_regions(self) -> Generator[dict, None, None]:
58
+ """
59
+ Generates dictionaries with the address, size and other
60
+ information of each memory region used by the process.
61
+ """
62
+ raise NotImplementedError()
63
+
64
+ @abstractmethod
65
+ def search_by_value(
66
+ self,
67
+ pytype: Type[T],
68
+ bufflength: int,
69
+ value: Union[bool, int, float, str, bytes],
70
+ scan_type: ScanTypesEnum = ScanTypesEnum.EXACT_VALUE,
71
+ *,
72
+ progress_information: bool = False,
73
+ writeable_only: bool = False,
74
+ ) -> Generator[Union[int, Tuple[int, dict]], None, None]:
75
+ """
76
+ Search the whole memory space, accessible to the process,
77
+ for the provided value, returning the found addresses.
78
+
79
+ :param pytype: type of value to be queried (bool, int, float, str or bytes).
80
+ :param bufflength: value size in bytes (1, 2, 4, 8).
81
+ :param value: value to be queried (bool, int, float, str or bytes).
82
+ :param scan_type: the way to compare the values.
83
+ :param progress_information: if True, a dictionary with the progress information will be return.
84
+ :param writeable_only: if True, search only at writeable memory regions.
85
+ """
86
+ raise NotImplementedError()
87
+
88
+ def search_by_value_between(
89
+ self,
90
+ pytype: Type[T],
91
+ bufflength: int,
92
+ start: Union[bool, int, float, str, bytes],
93
+ end: Union[bool, int, float, str, bytes],
94
+ *,
95
+ not_between: bool = False,
96
+ progress_information: bool = False,
97
+ writeable_only: bool = False,
98
+ ) -> Generator[Union[int, Tuple[int, dict]], None, None]:
99
+ """
100
+ Search the whole memory space, accessible to the process,
101
+ for a value within the provided range, returning the found addresses.
102
+
103
+ :param pytype: type of value to be queried (bool, int, float, str or bytes).
104
+ :param bufflength: value size in bytes (1, 2, 4, 8).
105
+ :param start: minimum inclusive value to be queried (bool, int, float, str or bytes).
106
+ :param end: maximum inclusive value to be queried (bool, int, float, str or bytes).
107
+ :param not_between: if True, return only addresses of values that are NOT within the range.
108
+ :param progress_information: if True, a dictionary with the progress information will be return.
109
+ :param writeable_only: if True, search only at writeable memory regions.
110
+ """
111
+ raise NotImplementedError()
112
+
113
+ @abstractmethod
114
+ def read_process_memory(
115
+ self,
116
+ address: int,
117
+ pytype: Type[T],
118
+ bufflength: int
119
+ ) -> T:
120
+ """
121
+ Return a value from a memory address.
122
+
123
+ :param address: target memory address (ex: 0x006A9EC0).
124
+ :param pytype: type of the value to be received (bool, int, float, str or bytes).
125
+ :param bufflength: value size in bytes (1, 2, 4, 8).
126
+ """
127
+ raise NotImplementedError()
128
+
129
+ @abstractmethod
130
+ def write_process_memory(
131
+ self,
132
+ address: int,
133
+ pytype: Type[T],
134
+ bufflength: int,
135
+ value: Union[bool, int, float, str, bytes]
136
+ ) -> T:
137
+ """
138
+ Write a value to a memory address.
139
+
140
+ :param address: target memory address (ex: 0x006A9EC0).
141
+ :param pytype: type of value to be written into memory (bool, int, float, str or bytes).
142
+ :param bufflength: value size in bytes (1, 2, 4, 8).
143
+ :param value: value to be written (bool, int, float, str or bytes).
144
+ """
145
+ raise NotImplementedError()
@@ -4,8 +4,7 @@ import psutil
4
4
  import sys
5
5
 
6
6
  if "win" in sys.platform:
7
- from win32.win32gui import FindWindow
8
- from win32.win32process import GetWindowThreadProcessId
7
+ from ..win32.functions import GetProcessIdByWindowTitle
9
8
 
10
9
 
11
10
  def get_process_id_by_process_name(process_name: str) -> int:
@@ -24,8 +23,7 @@ def get_process_id_by_window_title(window_title: str) -> int:
24
23
  if "win" not in sys.platform:
25
24
  raise OSError("This function is compatible only with Windows OS.")
26
25
 
27
- hwnd = FindWindow(None, window_title)
28
- return GetWindowThreadProcessId(hwnd)[1] if hwnd else 0
26
+ return GetProcessIdByWindowTitle(window_title)
29
27
 
30
28
 
31
29
  def pid_exists(pid: int) -> bool:
@@ -0,0 +1,15 @@
1
+ from .main_application_window import ApplicationWindow
2
+ from .open_process_window import OpenProcessWindow
3
+
4
+
5
+ def main(*args, **kwargs):
6
+ open_process_window = OpenProcessWindow()
7
+ process = open_process_window.get_process()
8
+
9
+ if process:
10
+ ApplicationWindow(process)
11
+ process.close()
12
+
13
+
14
+ if __name__ == "__main__":
15
+ main()
@@ -0,0 +1,443 @@
1
+ from tkinter import DoubleVar, Frame, Label, Menu, Listbox, Scrollbar, Tk
2
+ from tkinter.ttk import Button, Entry, Menubutton, Progressbar
3
+
4
+ from PyMemoryEditor import ScanTypesEnum
5
+ from PyMemoryEditor.process import AbstractProcess
6
+
7
+
8
+ class ApplicationWindow(Tk):
9
+ """
10
+ Main window.
11
+ """
12
+ __comp_methods = {
13
+ ScanTypesEnum.EXACT_VALUE: lambda x, y: x == y,
14
+ ScanTypesEnum.NOT_EXACT_VALUE: lambda x, y: x != y,
15
+ ScanTypesEnum.BIGGER_THAN: lambda x, y: x > y,
16
+ ScanTypesEnum.SMALLER_THAN: lambda x, y: x < y,
17
+ }
18
+
19
+ def __init__(self, process: AbstractProcess):
20
+ super().__init__()
21
+ self.__process = process
22
+
23
+ self.__scan_type = ScanTypesEnum.EXACT_VALUE
24
+ self.__value = 0
25
+ self.__value_type = int
26
+ self.__value_length = 4
27
+ self.__addresses = []
28
+
29
+ self.__updating = False # Indicate the application is updating the values of the found addresses.
30
+ self.__scanning = False # Indicate a scan has started.
31
+
32
+ self["bg"] = "white"
33
+
34
+ self.title(f"PyMemoryEditor (Sample) - Process ID: {process.pid}")
35
+ self.geometry("900x400")
36
+
37
+ self.protocol("WM_DELETE_WINDOW", self.__on_close)
38
+ self.__close = False
39
+
40
+ self.__addresses = []
41
+
42
+ self.__build()
43
+ self.mainloop()
44
+
45
+ def __build(self):
46
+ """
47
+ Build the widgets of the window.
48
+ """
49
+ self.__entry_register_int = self.register(self.__validate_int_entry)
50
+ self.__entry_register_hex = self.register(self.__validate_hex_entry)
51
+
52
+ self.__input_frame_1 = Frame(self)
53
+ self.__input_frame_1["bg"] = "white"
54
+ self.__input_frame_1.pack(padx=5, fill="x", expand=True)
55
+
56
+ self.__scan_input_frame = Frame(self.__input_frame_1)
57
+ self.__scan_input_frame["bg"] = "white"
58
+ self.__scan_input_frame.pack(fill="x", expand=True)
59
+
60
+ # Value input.
61
+ Label(self.__scan_input_frame, text="Value: ", bg="white", font=("Arial", 12)).pack(side="left")
62
+
63
+ self.__value_entry = Entry(self.__scan_input_frame)
64
+ self.__value_entry.pack(side="left", expand=True, fill="x")
65
+
66
+ Label(self.__scan_input_frame, bg="white").pack(side="left")
67
+
68
+ # Value length.
69
+ Label(self.__scan_input_frame, text="Length (Bytes): ", bg="white", font=("Arial", 12)).pack(side="left")
70
+
71
+ self.__length_entry = Entry(self.__scan_input_frame, width=5)
72
+ self.__length_entry.insert(0, "4")
73
+ self.__length_entry.config(validate="key", validatecommand=(self.__entry_register_int, "%P"))
74
+ self.__length_entry.pack(side="left")
75
+
76
+ Label(self.__scan_input_frame, bg="white").pack(side="left", padx=5)
77
+
78
+ # Value type input.
79
+ self.__type_menu_button = Menubutton(self.__scan_input_frame, width=10)
80
+ self.__type_menu_button.pack(side="left")
81
+
82
+ self.__type_menu = Menu(tearoff=0, bg="white")
83
+ self.__type_menu.add_command(label="Boolean", command=lambda: self.__set_value_type(0))
84
+ self.__type_menu.add_command(label="Integer", command=lambda: self.__set_value_type(1))
85
+ self.__type_menu.add_command(label="Float", command=lambda: self.__set_value_type(2))
86
+ self.__type_menu.add_command(label="String", command=lambda: self.__set_value_type(3))
87
+ self.__type_menu_button.config(menu=self.__type_menu, text = "Integer")
88
+
89
+ Label(self.__scan_input_frame, bg="white").pack(side="left", padx=10)
90
+
91
+ # Scan type input.
92
+ Label(self.__scan_input_frame, text="Scan Type: ", bg="white", font=("Arial", 12)).pack(side="left")
93
+
94
+ self.__scan_menu_button = Menubutton(self.__scan_input_frame, width=15)
95
+ self.__scan_menu_button.pack(side="left")
96
+
97
+ self.__scan_menu = Menu(tearoff=0, bg="white")
98
+ self.__scan_menu.add_command(label="Exact Value", command=lambda: self.__set_scan_type(0))
99
+ self.__scan_menu.add_command(label="Not Exact Value", command=lambda: self.__set_scan_type(1))
100
+ self.__scan_menu.add_command(label="Smaller Than", command=lambda: self.__set_scan_type(2))
101
+ self.__scan_menu.add_command(label="Bigger Than", command=lambda: self.__set_scan_type(3))
102
+ self.__scan_menu_button.config(menu=self.__scan_menu, text = "Exact Value")
103
+
104
+ Label(self.__scan_input_frame, bg="white").pack(side="left", padx=5)
105
+
106
+ # Buttons for scanning.
107
+ self.__new_scan_button = Button(self.__scan_input_frame, text="First Scan", command=self.__new_scan)
108
+ self.__new_scan_button.pack(side="left")
109
+
110
+ Label(self.__scan_input_frame, bg="white").pack(side="left")
111
+
112
+ self.__next_scan_button = Button(self.__scan_input_frame, command=self.__next_scan)
113
+ self.__next_scan_button.pack(side="left")
114
+
115
+ # Scanning process bar.
116
+ self.__progress_var = DoubleVar()
117
+
118
+ self.__progress_bar = Progressbar(self.__input_frame_1, variable=self.__progress_var)
119
+ self.__progress_bar.pack(pady=5, fill="x", expand=True)
120
+
121
+ # Label for counting and button for updating values.
122
+ self.__result_frame = Frame(self)
123
+ self.__result_frame["bg"] = "white"
124
+ self.__result_frame.pack(padx=5, fill="both", expand=True)
125
+
126
+ self.__count_frame = Frame(self.__result_frame)
127
+ self.__count_frame["bg"] = "white"
128
+ self.__count_frame.pack(pady=5, fill="x", expand=True)
129
+
130
+ self.__count_label = Label(self.__count_frame, font=("Arial", 8), bg="white")
131
+ self.__count_label.config(text="Start a new scan to find memory addresses.")
132
+ self.__count_label.pack(side="left")
133
+
134
+ Button(self.__count_frame, text="Update Values", command=self.__update_values).pack(side="right")
135
+
136
+ # List with addresses and their values.
137
+ self.__list_frame = Frame(self.__result_frame)
138
+ self.__list_frame["bg"] = "white"
139
+ self.__list_frame.pack(fill="both", expand=True)
140
+
141
+ self.__scrollbar = Scrollbar(self.__list_frame, orient="vertical", command=self.__on_move_list_box)
142
+
143
+ self.__address_list = Listbox(self.__list_frame, width=20)
144
+ self.__address_list.bind("<MouseWheel>", self.__on_mouse_wheel)
145
+ self.__address_list.bind("<<ListboxSelect>>", self.__select_address)
146
+ self.__address_list.config(yscrollcommand = self.__scrollbar.set)
147
+ self.__address_list.pack(side="left", fill="y")
148
+
149
+ self.__value_list = Listbox(self.__list_frame)
150
+ self.__value_list.bind("<MouseWheel>", self.__on_mouse_wheel)
151
+ self.__value_list.bind("<<ListboxSelect>>", self.__select_value)
152
+ self.__value_list.config(yscrollcommand = self.__scrollbar.set)
153
+ self.__value_list.pack(side="left", fill="both", expand=True)
154
+
155
+ self.__scrollbar.pack(side="left", fill="y")
156
+
157
+ # Widgets for change the value of a memory address.
158
+ self.__input_frame_2 = Frame(self)
159
+ self.__input_frame_2["bg"] = "white"
160
+ self.__input_frame_2.pack(padx=5, fill="x", expand=True)
161
+
162
+ Label(self.__input_frame_2, text="Address:", bg="white").pack(side="left")
163
+
164
+ self.__address_entry = Entry(self.__input_frame_2)
165
+ self.__address_entry.config(validate="key", validatecommand=(self.__entry_register_hex, "%P"))
166
+ self.__address_entry.pack(side="left")
167
+
168
+ Label(self.__input_frame_2, bg="white").pack(side="left")
169
+
170
+ Label(self.__input_frame_2, text="New Value:", bg="white").pack(side="left")
171
+
172
+ self.__new_value_entry = Entry(self.__input_frame_2)
173
+ self.__new_value_entry.pack(side="left", fill="x", expand=True)
174
+
175
+ Button(self.__input_frame_2, text="Replace", command=self.__write_value).pack(side="left")
176
+
177
+ def __new_scan(self):
178
+ """
179
+ Start a new seach at the whole memory of the process.
180
+ """
181
+ if self.__new_scan_button["text"].lower() == "scanning" or self.__updating: return
182
+ self.__next_scan_button.config(text="")
183
+
184
+ self.__addresses = []
185
+
186
+ # If a scan is already in progress, clear all results and get everything ready for a new scan.
187
+ if self.__scanning:
188
+ self.__new_scan_button.config(text="First Scan")
189
+ self.__count_label.config(text="Start a new scan to find memory addresses.")
190
+ self.__address_list.delete(0, "end")
191
+ self.__value_list.delete(0, "end")
192
+ self.__progress_var.set(0)
193
+ self.__scanning = False
194
+ return
195
+
196
+ # Get the inputs.
197
+ value = self.__value_entry.get().strip()
198
+ length = int(self.__length_entry.get())
199
+ pytype = self.__value_type
200
+ scan_type = self.__scan_type
201
+
202
+ if not value or length == 0: return
203
+
204
+ # Check if the value is valid for the selected value type.
205
+ try:
206
+ if str(pytype(value)) != value or (pytype is str and length < len(value)):
207
+ raise ValueError()
208
+ except:
209
+ self.__value_entry.delete(0, "end")
210
+ return self.__value_entry.insert(0, "Invalid value")
211
+
212
+ # Start the scan.
213
+ value = pytype(value)
214
+
215
+ self.__value_length = length
216
+ self.__value = value
217
+
218
+ self.after(100, lambda: self.__start_scan(pytype, length, value, scan_type))
219
+
220
+ def __next_scan(self):
221
+ """
222
+ Filter the found addresses.
223
+ """
224
+ self.__update_values(remove=True)
225
+
226
+ def __on_close(self, *args):
227
+ """
228
+ Event to close the program graciously.
229
+ """
230
+ self.__close = True
231
+
232
+ if self.__updating or self.__new_scan_button["text"].lower() == "scanning":
233
+ self.update()
234
+ return self.after(10, self.__on_close)
235
+
236
+ self.destroy()
237
+
238
+ def __on_move_list_box(self, *args):
239
+ """
240
+ Event to sync the listboxes.
241
+ """
242
+ self.__address_list.yview(*args)
243
+ self.__value_list.yview(*args)
244
+
245
+ def __on_mouse_wheel(self, event):
246
+ """
247
+ Event to sync the listboxes.
248
+ """
249
+ self.__address_list.yview("scroll", event.delta, "units")
250
+ self.__value_list.yview("scroll", event.delta, "units")
251
+ return "break"
252
+
253
+ def __select_address(self, event):
254
+ """
255
+ Event to get the selected address and copy it.
256
+ """
257
+ selection = event.widget.curselection()
258
+ if not selection: return
259
+
260
+ address = self.__address_list.get(int(selection[0])).split(" ")[-1]
261
+ if not address: return
262
+
263
+ self.__address_entry.delete(0, "end")
264
+ self.__address_entry.insert(0, address)
265
+
266
+ def __select_value(self, event):
267
+ """
268
+ Event to get the selected value and copy it.
269
+ """
270
+ selection = event.widget.curselection()
271
+ if not selection: return
272
+
273
+ value = self.__value_list.get(int(selection[0]))[len("Value: "):]
274
+ self.__new_value_entry.delete(0, "end")
275
+ self.__new_value_entry.insert(0, value)
276
+
277
+ def __start_scan(self, pytype, length, value, scan_type):
278
+ """
279
+ Search for a value on the whole memory of the process.
280
+ """
281
+ self.__new_scan_button.config(text="Scanning")
282
+ self.update()
283
+
284
+ self.__scanning = True
285
+ self.__addresses = []
286
+
287
+ for address, info in self.__process.search_by_value(pytype, length, value, scan_type, progress_information=True):
288
+ if self.__close: break
289
+
290
+ self.__address_list.insert("end", f"Addr: {hex(address)[2:].upper()}")
291
+ self.__value_list.insert("end", f"Value: {value}")
292
+
293
+ self.__progress_var.set(info["progress"] * 100)
294
+ self.__addresses.append(address)
295
+ self.update()
296
+
297
+ self.__count_label.config(text=f"Found {len(self.__addresses)} addresses.")
298
+
299
+ self.__new_scan_button.config(text="New Scan")
300
+ self.__next_scan_button.config(text="Next Scan")
301
+ self.__progress_var.set(100)
302
+
303
+ def __set_scan_type(self, scan_type: int):
304
+ """
305
+ Method for the Menubutton to select a scan type.
306
+ """
307
+ # Allow select a new scan type only if program is not getting new addresses or updating their values.
308
+ if self.__new_scan_button["text"].lower() == "scanning" or self.__updating: return
309
+
310
+ self.__scan_type = [
311
+ ScanTypesEnum.EXACT_VALUE,
312
+ ScanTypesEnum.NOT_EXACT_VALUE,
313
+ ScanTypesEnum.SMALLER_THAN,
314
+ ScanTypesEnum.BIGGER_THAN,
315
+ ][scan_type]
316
+
317
+ text = " ".join(word.capitalize() for word in self.__scan_type.name.split("_"))
318
+ self.__scan_menu_button.config(text=text)
319
+
320
+ def __set_value_type(self, value_type: int):
321
+ """
322
+ Method for the Menubutton to select a value type.
323
+ """
324
+ if self.__scanning: return
325
+
326
+ self.__value_type = [bool, int, float, str][value_type]
327
+ self.__type_menu_button.config(text=["Boolean", "Integer", "Float", "String"][value_type])
328
+
329
+ def __validate_int_entry(self, string):
330
+ """
331
+ Method to validate if an input is integer.
332
+ """
333
+ if self.__scanning: return False
334
+
335
+ for char in string:
336
+ if char not in "0123456789": return False
337
+ return True
338
+
339
+ def __validate_hex_entry(self, string):
340
+ """
341
+ Method to validate if an input is hexadecimal.
342
+ """
343
+ for char in string.upper():
344
+ if char not in "0123456789ABCDEF": return False
345
+ return True
346
+
347
+ def __update_values(self, index = 0, *, remove = False, first_call = True):
348
+ """
349
+ Update the values of the found addresses. If "remove" is True, it will
350
+ compare the current value in memory and remove the address from the
351
+ results if the comparison is False.
352
+ """
353
+ if self.__updating and first_call: return # Allow call the method once.
354
+
355
+ # Return if user asked for closing the application.
356
+ if self.__close:
357
+ self.__updating = False
358
+ return
359
+
360
+ # Get the value to compare.
361
+ if first_call:
362
+ value = self.__value_entry.get().strip()
363
+
364
+ try:
365
+ if str(self.__value_type(value)) != value:
366
+ raise ValueError()
367
+ except:
368
+ self.__value_entry.delete(0, "end")
369
+ return self.__value_entry.insert(0, "Invalid value")
370
+
371
+ self.__value = self.__value_type(value)
372
+ self.__progress_var.set(0)
373
+
374
+ # Indicate the application is updating the values.
375
+ self.__updating = True
376
+
377
+ if index % 10 == 0: self.update()
378
+
379
+ # Get the address from the list.
380
+ try:
381
+ address = self.__addresses[index]
382
+ except:
383
+ self.__updating = False
384
+
385
+ self.__count_label.config(text=f"Found {len(self.__addresses)} addresses.")
386
+ return self.__progress_var.set(100 if not first_call else 0)
387
+
388
+ # Get the current value of the address.
389
+ corrupted = False
390
+ value = None
391
+
392
+ try: value = self.__process.read_process_memory(address, self.__value_type, self.__value_length)
393
+ except: corrupted = True
394
+
395
+ # If "remove" is True, compare the value.
396
+ if remove or corrupted:
397
+ compare = self.__comp_methods[self.__scan_type]
398
+
399
+ # Remove the address from the results.
400
+ if corrupted or not compare(value, self.__value):
401
+ self.__address_list.delete(index)
402
+ self.__value_list.delete(index)
403
+ self.__addresses.remove(address)
404
+ index -= 1
405
+
406
+ # Update the value at the listbox.
407
+ else:
408
+ self.__value_list.delete(index)
409
+ self.__value_list.insert(index, f"Value: {value}")
410
+
411
+ # Call the method again recursively
412
+ if index % 10 == 0: self.__progress_var.set((index / len(self.__addresses)) * 100)
413
+ self.after(5, lambda: self.__update_values(index + 1, remove=remove, first_call=False))
414
+
415
+ def __write_value(self):
416
+ """
417
+ Change the value in memory of an address of the result list.
418
+ """
419
+ try:
420
+ address = int(self.__address_entry.get().strip(), 16)
421
+ if address not in self.__addresses: raise ValueError()
422
+ except:
423
+ self.__address_entry.delete(0, "end")
424
+ return self.__address_entry.insert(0, "00000000")
425
+
426
+ # Get the inputs.
427
+ value = self.__new_value_entry.get()
428
+ pytype = self.__value_type
429
+ length = self.__value_length
430
+
431
+ if not value or length == 0: return
432
+
433
+ # Check if the value is valid for the selected value type.
434
+ try:
435
+ if str(pytype(value)) != value or (pytype is str and length < len(value)):
436
+ raise ValueError()
437
+ except:
438
+ self.__new_value_entry.delete(0, "end")
439
+ return self.__new_value_entry.insert(0, "Invalid value")
440
+
441
+ # Write the new value.
442
+ self.__process.write_process_memory(address, pytype, length, pytype(value))
443
+
@@ -0,0 +1,56 @@
1
+ from tkinter import Label, Tk
2
+ from tkinter.ttk import Button, Entry
3
+ from typing import Optional
4
+
5
+ from PyMemoryEditor import OpenProcess
6
+ from PyMemoryEditor.errors import ProcessIDNotExistsError, ProcessNotFoundError
7
+ from PyMemoryEditor.process import AbstractProcess
8
+
9
+
10
+ class OpenProcessWindow(Tk):
11
+ """
12
+ Window for opening a process.
13
+ """
14
+ def __init__(self):
15
+ super().__init__()
16
+ self.__process = None
17
+
18
+ self["bg"] = "white"
19
+
20
+ self.geometry("350x100")
21
+ self.title("PyMemoryEditor (Sample)")
22
+
23
+ Label(self, text="Insert the PID or the process name:", bg="white", font=("Arial", 10)).pack(padx=20)
24
+
25
+ self.__entry = Entry(self)
26
+ self.__entry.pack(padx=50, fill="x", expand=True)
27
+
28
+ Button(self, text="Open Process", command=self.__open_process).pack(pady=10)
29
+
30
+ self.mainloop()
31
+
32
+ def __open_process(self):
33
+ """
34
+ Open the process by the user input.
35
+ """
36
+ entry = self.__entry.get().strip()
37
+
38
+ try:
39
+ self.__process = OpenProcess(pid = int(entry))
40
+ return self.destroy()
41
+
42
+ except ValueError:
43
+ try:
44
+ self.__process = OpenProcess(process_name = entry)
45
+ return self.destroy()
46
+ except (ProcessIDNotExistsError, ProcessNotFoundError): pass
47
+ except (ProcessIDNotExistsError, ProcessNotFoundError): pass
48
+
49
+ self.__entry.delete(0, "end")
50
+ self.__entry.insert(0, "Process not found.")
51
+
52
+ def get_process(self) -> Optional[AbstractProcess]:
53
+ """
54
+ Return the opened process.
55
+ """
56
+ return self.__process
@@ -3,7 +3,7 @@
3
3
  from ..enums import ScanTypesEnum
4
4
  from .search.kmp import KMPSearch
5
5
 
6
- from typing import Sequence, Tuple, Union
6
+ from typing import Generator, Sequence, Tuple, Union
7
7
  import ctypes
8
8
  import sys
9
9
 
@@ -15,7 +15,7 @@ def scan_memory_for_exact_value(
15
15
  target_value_size: int,
16
16
  comparison: ScanTypesEnum = ScanTypesEnum.EXACT_VALUE,
17
17
  *args, **kwargs
18
- ):
18
+ ) -> Generator[int, None, None]:
19
19
  """
20
20
  Search for an exact value at the memory region.
21
21
 
@@ -50,7 +50,7 @@ def scan_memory(
50
50
  target_value_size: int,
51
51
  scan_type: ScanTypesEnum,
52
52
  is_string: bool,
53
- ):
53
+ ) -> Generator[int, None, None]:
54
54
  """
55
55
  Search for a value at the memory region.
56
56
  """
@@ -127,7 +127,7 @@ def ReadProcessMemory(
127
127
  data = get_c_type_of(pytype, bufflength)
128
128
  kernel32.ReadProcessMemory(process_handle, ctypes.c_void_p(address), ctypes.byref(data), bufflength, None)
129
129
 
130
- return str(data.value) if pytype is str else data.value
130
+ return data.value.decode() if pytype is str else data.value
131
131
 
132
132
 
133
133
  def SearchAllMemory(
@@ -56,8 +56,10 @@ class WindowsProcess(AbstractProcess):
56
56
  """
57
57
  Close the process handle.
58
58
  """
59
- self.__closed = True
60
- return CloseProcessHandle(self.__process_handle) != 0
59
+ if self.__closed: return True
60
+
61
+ self.__closed = CloseProcessHandle(self.__process_handle) != 0
62
+ return self.__closed
61
63
 
62
64
  def get_memory_regions(self) -> Generator[dict, None, None]:
63
65
  """
@@ -7,7 +7,7 @@ reading and writing values in the process memory.
7
7
  [![Pypi](https://img.shields.io/pypi/v/PyMemoryEditor)](https://pypi.org/project/PyMemoryEditor/)
8
8
  [![License](https://img.shields.io/pypi/l/PyMemoryEditor)](https://pypi.org/project/PyMemoryEditor/)
9
9
  [![Platforms](https://img.shields.io/badge/platforms-Windows%20%7C%20Linux-8A2BE2)](https://pypi.org/project/PyMemoryEditor/)
10
- [![Python Version](https://img.shields.io/badge/python-3.6%20%7C%20...%20%7C%203.10%20%7C%203.11-blue)](https://pypi.org/project/PyMemoryEditor/)
10
+ [![Python Version](https://img.shields.io/badge/python-3.6%20%7C%20...%20%7C%203.11%20%7C%203.12-blue)](https://pypi.org/project/PyMemoryEditor/)
11
11
  [![Downloads](https://static.pepy.tech/personalized-badge/pymemoryeditor?period=total&units=international_system&left_color=grey&right_color=orange&left_text=Downloads)](https://pypi.org/project/PyMemoryEditor/)
12
12
 
13
13
  # Installing PyMemoryEditor:
@@ -78,7 +78,7 @@ for address, info process.search_by_value(..., progress_information = True):
78
78
  print(template.format(address, progress))
79
79
  ```
80
80
 
81
- # Getting memory regions:
81
+ ## Getting memory regions:
82
82
  Use the method `get_memory_regions()` to get the base address, size and more information of all memory regions used by the process.
83
83
 
84
84
  ```py
@@ -87,3 +87,6 @@ for memory_region in process.get_memory_regions():
87
87
  size = memory_region["size"]
88
88
  information = memory_region["struct"]
89
89
  ```
90
+
91
+ # Tkinter application sample:
92
+ You can run a simple tkinter application to scan the memory of a process by typing `pymemoryeditor` at the CLI.
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "PyMemoryEditor"
3
3
  dynamic = ["version"]
4
- description = "A Python library to edit and track memory of Windows and Linux processes (32 bits and 64 bits)."
4
+ description = "Multi-platform library developed with ctypes for reading, writing and searching process memory, in a simple and friendly way with Python 3."
5
5
  authors = [
6
6
  { name = "Jean Loui Bernard Silva de Jesus", email = "jeanextreme002@gmail.com" },
7
7
  ]
@@ -36,13 +36,14 @@ classifiers = [
36
36
  "Programming Language :: Python :: 3.9",
37
37
  "Programming Language :: Python :: 3.10",
38
38
  "Programming Language :: Python :: 3.11",
39
+ "Programming Language :: Python :: 3.12",
39
40
  "Topic :: Scientific/Engineering",
40
41
  "Topic :: Security",
41
42
  "Topic :: System :: Monitoring"
42
43
  ]
43
44
  exclude = ["tests"]
44
45
  requires-python = ">=3.6"
45
- dependencies = ['pywin32 ; platform_system == "Windows"', "psutil"]
46
+ dependencies = ["psutil"]
46
47
 
47
48
  [project.optional-dependencies]
48
49
  tests = [
@@ -57,4 +58,7 @@ path = "PyMemoryEditor/__init__.py"
57
58
 
58
59
  [build-system]
59
60
  requires = ["hatchling"]
60
- build-backend = "hatchling.build"
61
+ build-backend = "hatchling.build"
62
+
63
+ [project.scripts]
64
+ pymemoryeditor = "PyMemoryEditor.sample.application:main"
@@ -0,0 +1,2 @@
1
+ psutil
2
+ pytest
@@ -1,3 +0,0 @@
1
- class ClosedProcess(Exception):
2
- def __str__(self):
3
- return "operation on closed process."
@@ -1,87 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from abc import ABC, abstractmethod
3
- from typing import Optional, Type, TypeVar, Union
4
-
5
- from ..process.info import ProcessInfo
6
-
7
-
8
- T = TypeVar("T")
9
-
10
-
11
- class AbstractProcess(ABC):
12
- """
13
- Abstract class to represent a process.
14
- """
15
-
16
- @abstractmethod
17
- def __init__(self, *, window_title: Optional[str] = None, process_name: Optional[str] = None, pid: Optional[int] = None):
18
- """
19
- :param window_title: window title of the target program.
20
- :param process_name: name of the target process.
21
- :param pid: process ID.
22
- """
23
- self._process_info = ProcessInfo()
24
-
25
- # Set the attributes to the process.
26
- if pid:
27
- self._process_info.pid = pid
28
-
29
- elif window_title:
30
- self._process_info.window_title = window_title
31
-
32
- elif process_name:
33
- self._process_info.process_name = process_name
34
-
35
- else:
36
- raise TypeError("You must pass an argument to one of these parameters (window_title, process_name, pid).")
37
-
38
- def __enter__(self):
39
- return self
40
-
41
- def __exit__(self, exc_type, exc_value, exc_traceback):
42
- self.close()
43
-
44
- @property
45
- def pid(self) -> int:
46
- return self._process_info.pid
47
-
48
- @abstractmethod
49
- def close(self) -> bool:
50
- """
51
- Close the process handle.
52
- """
53
- raise NotImplementedError()
54
-
55
- @abstractmethod
56
- def read_process_memory(
57
- self,
58
- address: int,
59
- pytype: Type[T],
60
- bufflength: int
61
- ) -> T:
62
- """
63
- Return a value from a memory address.
64
-
65
- :param address: target memory address (ex: 0x006A9EC0).
66
- :param pytype: type of the value to be received (bool, int, float, str or bytes).
67
- :param bufflength: value size in bytes (1, 2, 4, 8).
68
- """
69
- raise NotImplementedError()
70
-
71
- @abstractmethod
72
- def write_process_memory(
73
- self,
74
- address: int,
75
- pytype: Type[T],
76
- bufflength: int,
77
- value: Union[bool, int, float, str, bytes]
78
- ) -> T:
79
- """
80
- Write a value to a memory address.
81
-
82
- :param address: target memory address (ex: 0x006A9EC0).
83
- :param pytype: type of value to be written into memory (bool, int, float, str or bytes).
84
- :param bufflength: value size in bytes (1, 2, 4, 8).
85
- :param value: value to be written (bool, int, float, str or bytes).
86
- """
87
- raise NotImplementedError()
@@ -1,3 +0,0 @@
1
- pywin32 ; sys_platform == 'win32'
2
- psutil
3
- pytest
File without changes