PyMemoryEditor 1.5.8__tar.gz → 1.5.9__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 (52) hide show
  1. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/.github/workflows/python-package.yml +1 -1
  2. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PKG-INFO +13 -5
  3. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/__init__.py +1 -1
  4. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/linux/functions.py +68 -4
  5. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/linux/process.py +25 -4
  6. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/process/__init__.py +16 -1
  7. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/sample/main_application_window.py +40 -47
  8. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/util/__init__.py +16 -1
  9. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/win32/functions.py +70 -6
  10. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/win32/process.py +28 -4
  11. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/README.md +12 -4
  12. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/tests/test_editor.py +84 -19
  13. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/.gitignore +0 -0
  14. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/LICENSE +0 -0
  15. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/enums.py +0 -0
  16. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/errors.py +0 -0
  17. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/linux/ptrace/__init__.py +0 -0
  18. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/linux/ptrace/enums.py +0 -0
  19. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/linux/types.py +0 -0
  20. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/process/errors.py +0 -0
  21. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/process/info.py +0 -0
  22. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/process/util.py +0 -0
  23. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/sample/application.py +0 -0
  24. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/sample/open_process_window.py +0 -0
  25. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/util/scan.py +0 -0
  26. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/util/search/abstract.py +0 -0
  27. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/util/search/kmp.py +0 -0
  28. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/win32/enums/__init__.py +0 -0
  29. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/win32/enums/memory_allocation_states.py +0 -0
  30. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/win32/enums/memory_protections.py +0 -0
  31. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/win32/enums/memory_types.py +0 -0
  32. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/win32/enums/process_operations.py +0 -0
  33. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/win32/enums/standard_access_rights.py +0 -0
  34. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/PyMemoryEditor/win32/types.py +0 -0
  35. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/__init__.py +0 -0
  36. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/open_process.py +0 -0
  37. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/process/__init__.py +0 -0
  38. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/process/errors.py +0 -0
  39. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/process/util.py +0 -0
  40. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/win32/enums/__init__.py +0 -0
  41. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/win32/enums/memory_allocation_states.py +0 -0
  42. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/win32/enums/memory_protections.py +0 -0
  43. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/win32/enums/memory_types.py +0 -0
  44. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/win32/enums/process_operations.py +0 -0
  45. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/win32/enums/scan_types.py +0 -0
  46. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/win32/enums/standard_access_rights.py +0 -0
  47. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/win32/functions.py +0 -0
  48. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/win32/types.py +0 -0
  49. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/build/lib/PyMemoryEditor/win32/util.py +0 -0
  50. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/pyproject.toml +0 -0
  51. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/requirements.txt +0 -0
  52. {pymemoryeditor-1.5.8 → pymemoryeditor-1.5.9}/tests/package.py +0 -0
@@ -34,7 +34,7 @@ jobs:
34
34
  pip install -r requirements.txt
35
35
  - name: Test with pytest
36
36
  run: |
37
- pytest tests -v -s
37
+ pytest tests -v -s -x
38
38
  - name: Install package
39
39
  run: |
40
40
  pip install PyMemoryEditor
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyMemoryEditor
3
- Version: 1.5.8
3
+ Version: 1.5.9
4
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>
@@ -47,6 +47,9 @@ reading and writing values in the process memory.
47
47
  pip3 install PyMemoryEditor
48
48
  ```
49
49
 
50
+ ### Tkinter application sample:
51
+ You can run a simple tkinter application to scan the memory of a process by typing `pymemoryeditor` at the CLI.
52
+
50
53
  # Basic Usage:
51
54
  Import `PyMemoryEditor` and open a process using the `OpenProcess` class, passing a window title, process name <br>
52
55
  or PID as an argument. You can use the context manager to do this.
@@ -103,13 +106,21 @@ All methods described above work even for strings, including the method `search_
103
106
  ## Progress information on searching:
104
107
  These methods has the `progress_information` parameter that returns a dictionary containing the search progress information.
105
108
  ```py
106
- for address, info process.search_by_value(..., progress_information = True):
109
+ for address, info in process.search_by_value(..., progress_information = True):
107
110
  template = "Address: 0x{:<10X} | Progress: {:.1f}%"
108
111
  progress = info["progress"] * 100
109
112
 
110
113
  print(template.format(address, progress))
111
114
  ```
112
115
 
116
+ # Reading multiple addresses efficiently:
117
+ If you have a large number of addresses where their values need to be read from memory, using the `search_by_addresses` method is much more efficient than reading the value of each address one by one.
118
+ ```py
119
+ for address, value in process.search_by_addresses(int, 4, addresses_list):
120
+ print(f"Address", address, "holds the value", value)
121
+ ```
122
+ The key advantage of this method is that it reads a memory page just once, obtaining the values of the addresses within the page. This approach reduces the frequency of system calls.
123
+
113
124
  ## Getting memory regions:
114
125
  Use the method `get_memory_regions()` to get the base address, size and more information of all memory regions used by the process.
115
126
 
@@ -119,6 +130,3 @@ for memory_region in process.get_memory_regions():
119
130
  size = memory_region["size"]
120
131
  information = memory_region["struct"]
121
132
  ```
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.
@@ -8,7 +8,7 @@ The package supports Windows and Linux (32-bit and 64-bit).
8
8
  """
9
9
 
10
10
  __author__ = "Jean Loui Bernard Silva de Jesus"
11
- __version__ = "1.5.8"
11
+ __version__ = "1.5.9"
12
12
 
13
13
 
14
14
  from .enums import ScanTypesEnum
@@ -7,10 +7,10 @@
7
7
  # https://man7.org/linux/man-pages/man5/proc.5.html
8
8
 
9
9
  from ctypes import addressof, sizeof
10
- from typing import Generator, Tuple, Type, TypeVar, Union
10
+ from typing import Dict, Generator, Optional, Sequence, Tuple, Type, TypeVar, Union
11
11
 
12
12
  from ..enums import ScanTypesEnum
13
- from ..util import get_c_type_of, scan_memory, scan_memory_for_exact_value
13
+ from ..util import convert_from_bytearray, get_c_type_of, scan_memory, scan_memory_for_exact_value
14
14
  from .ptrace import libc
15
15
  from .types import MEMORY_BASIC_INFORMATION, iovec
16
16
 
@@ -67,10 +67,10 @@ def read_process_memory(
67
67
  pid, (iovec * 1)(iovec(addressof(data), sizeof(data))),
68
68
  1, (iovec * 1)(iovec(address, sizeof(data))), 1, 0
69
69
  )
70
- return str(data.value) if pytype is str else data.value
70
+ return data.value.decode() if pytype is str else data.value
71
71
 
72
72
 
73
- def search_all_memory(
73
+ def search_addresses_by_value(
74
74
  pid: int,
75
75
  pytype: Type[T],
76
76
  bufflength: int,
@@ -147,6 +147,70 @@ def search_all_memory(
147
147
  checked_memory_size += size
148
148
 
149
149
 
150
+ def search_values_by_addresses(
151
+ pid: int,
152
+ pytype: Type[T],
153
+ bufflength: int,
154
+ addresses: Sequence[int],
155
+ *,
156
+ memory_regions: Optional[Sequence[Dict]] = None,
157
+ raise_error: bool = False,
158
+ ) -> Generator[Tuple[int, Optional[T]], None, None]:
159
+ """
160
+ Search the whole memory space, accessible to the process,
161
+ for the provided list of addresses, returning their values.
162
+ """
163
+ if pytype not in [bool, int, float, str, bytes]:
164
+ raise ValueError("The type must be bool, int, float, str or bytes.")
165
+
166
+ memory_regions = list(memory_regions) if memory_regions else list()
167
+ addresses = sorted(addresses)
168
+
169
+ # If no memory page has been given, get all readable memory pages.
170
+ if not memory_regions:
171
+ for region in get_memory_regions(pid):
172
+ if not b"r" in region["struct"].Privileges: continue
173
+ memory_regions.append(region)
174
+
175
+ memory_regions.sort(key=lambda region: region["address"])
176
+ address_index = 0
177
+
178
+ # Walk by each memory region.
179
+ for region in memory_regions:
180
+ if address_index >= len(addresses): continue
181
+
182
+ target_address = addresses[address_index]
183
+
184
+ # Check if the memory region contains the target address.
185
+ base_address, size = region["address"], region["size"]
186
+ if not (base_address <= target_address < base_address + size): continue
187
+
188
+ region_data = (ctypes.c_byte * size)()
189
+
190
+ # Get data from the region.
191
+ libc.process_vm_readv(
192
+ pid, (iovec * 1)(iovec(addressof(region_data), sizeof(region_data))),
193
+ 1, (iovec * 1)(iovec(base_address, sizeof(region_data))), 1, 0
194
+ )
195
+
196
+ # Get the value of each address.
197
+ while base_address <= target_address < base_address + size:
198
+ offset = target_address - base_address
199
+ address_index += 1
200
+
201
+ try:
202
+ data = region_data[offset: offset + bufflength]
203
+ data = (ctypes.c_byte * bufflength)(*data)
204
+ yield target_address, convert_from_bytearray(data, pytype, bufflength)
205
+
206
+ except Exception as error:
207
+ if raise_error: raise error
208
+ yield target_address, None
209
+
210
+ if address_index >= len(addresses): break
211
+ target_address = addresses[address_index]
212
+
213
+
150
214
  def write_process_memory(
151
215
  pid: int,
152
216
  address: int,
@@ -3,8 +3,14 @@
3
3
  from ..enums import ScanTypesEnum
4
4
  from ..errors import ClosedProcess
5
5
  from ..process import AbstractProcess
6
- from .functions import get_memory_regions, read_process_memory, search_all_memory, write_process_memory
7
- from typing import Generator, Optional, Tuple, Type, TypeVar, Union
6
+ from .functions import (
7
+ get_memory_regions,
8
+ read_process_memory,
9
+ search_addresses_by_value,
10
+ search_values_by_addresses,
11
+ write_process_memory
12
+ )
13
+ from typing import Generator, Optional, Sequence, Tuple, Type, TypeVar, Union
8
14
 
9
15
 
10
16
  T = TypeVar("T")
@@ -68,6 +74,21 @@ class LinuxProcess(AbstractProcess):
68
74
 
69
75
  return read_process_memory(self.pid, address, pytype, bufflength)
70
76
 
77
+ def search_by_addresses(
78
+ self,
79
+ pytype: Type[T],
80
+ bufflength: int,
81
+ addresses: Sequence[int],
82
+ *,
83
+ raise_error: bool = False,
84
+ ) -> Generator[Tuple[int, Optional[T]], None, None]:
85
+ """
86
+ Search the whole memory space, accessible to the process,
87
+ for the provided list of addresses, returning their values.
88
+ """
89
+ if self.__closed: raise ClosedProcess()
90
+ return search_values_by_addresses(self.pid, pytype, bufflength, addresses, raise_error=raise_error)
91
+
71
92
  def search_by_value(
72
93
  self,
73
94
  pytype: Type[T],
@@ -94,7 +115,7 @@ class LinuxProcess(AbstractProcess):
94
115
  if scan_type in [ScanTypesEnum.VALUE_BETWEEN, ScanTypesEnum.NOT_VALUE_BETWEEN]:
95
116
  raise ValueError("Use the method search_by_value_between(...) to search within a range of values.")
96
117
 
97
- return search_all_memory(self.pid, pytype, bufflength, value, scan_type, progress_information, writeable_only)
118
+ return search_addresses_by_value(self.pid, pytype, bufflength, value, scan_type, progress_information, writeable_only)
98
119
 
99
120
  def search_by_value_between(
100
121
  self,
@@ -122,7 +143,7 @@ class LinuxProcess(AbstractProcess):
122
143
  if self.__closed: raise ClosedProcess()
123
144
 
124
145
  scan_type = ScanTypesEnum.NOT_VALUE_BETWEEN if not_between else ScanTypesEnum.VALUE_BETWEEN
125
- return search_all_memory(self.pid, pytype, bufflength, (start, end), scan_type, progress_information, writeable_only)
146
+ return search_addresses_by_value(self.pid, pytype, bufflength, (start, end), scan_type, progress_information, writeable_only)
126
147
 
127
148
  def write_process_memory(
128
149
  self,
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from abc import ABC, abstractmethod
3
- from typing import Generator, Optional, Tuple, Type, TypeVar, Union
3
+ from typing import Generator, Optional, Sequence, Tuple, Type, TypeVar, Union
4
4
 
5
5
  from ..enums import ScanTypesEnum
6
6
  from ..process.info import ProcessInfo
@@ -61,6 +61,21 @@ class AbstractProcess(ABC):
61
61
  """
62
62
  raise NotImplementedError()
63
63
 
64
+ @abstractmethod
65
+ def search_by_addresses(
66
+ self,
67
+ pytype: Type[T],
68
+ bufflength: int,
69
+ addresses: Sequence[int],
70
+ *,
71
+ raise_error: bool = False,
72
+ ) -> Generator[Tuple[int, Optional[T]], None, None]:
73
+ """
74
+ Search the whole memory space, accessible to the process,
75
+ for the provided list of addresses, returning their values.
76
+ """
77
+ raise NotImplementedError()
78
+
64
79
  @abstractmethod
65
80
  def search_by_value(
66
81
  self,
@@ -21,7 +21,6 @@ class ApplicationWindow(Tk):
21
21
  self.__process = process
22
22
 
23
23
  self.__scan_type = ScanTypesEnum.EXACT_VALUE
24
- self.__value = 0
25
24
  self.__value_type = int
26
25
  self.__value_length = 4
27
26
  self.__addresses = []
@@ -211,9 +210,7 @@ class ApplicationWindow(Tk):
211
210
 
212
211
  # Start the scan.
213
212
  value = pytype(value)
214
-
215
213
  self.__value_length = length
216
- self.__value = value
217
214
 
218
215
  self.after(100, lambda: self.__start_scan(pytype, length, value, scan_type))
219
216
 
@@ -288,7 +285,7 @@ class ApplicationWindow(Tk):
288
285
  if self.__close: break
289
286
 
290
287
  self.__address_list.insert("end", f"Addr: {hex(address)[2:].upper()}")
291
- self.__value_list.insert("end", f"Value: {value}")
288
+ self.__value_list.insert("end", f"Value: loading...")
292
289
 
293
290
  self.__progress_var.set(info["progress"] * 100)
294
291
  self.__addresses.append(address)
@@ -296,6 +293,8 @@ class ApplicationWindow(Tk):
296
293
 
297
294
  self.__count_label.config(text=f"Found {len(self.__addresses)} addresses.")
298
295
 
296
+ self.__update_values()
297
+
299
298
  self.__new_scan_button.config(text="New Scan")
300
299
  self.__next_scan_button.config(text="Next Scan")
301
300
  self.__progress_var.set(100)
@@ -344,73 +343,67 @@ class ApplicationWindow(Tk):
344
343
  if char not in "0123456789ABCDEF": return False
345
344
  return True
346
345
 
347
- def __update_values(self, index = 0, *, remove = False, first_call = True):
346
+ def __update_values(self, *, remove: bool = False):
348
347
  """
349
348
  Update the values of the found addresses. If "remove" is True, it will
350
349
  compare the current value in memory and remove the address from the
351
350
  results if the comparison is False.
352
351
  """
353
- if self.__updating and first_call: return # Allow call the method once.
352
+ if self.__updating: return
354
353
 
355
- # Return if user asked for closing the application.
356
- if self.__close:
357
- self.__updating = False
358
- return
354
+ if not self.__addresses: return self.__progress_var.set(100)
359
355
 
360
356
  # Get the value to compare.
361
- if first_call:
362
- value = self.__value_entry.get().strip()
357
+ value = self.__value_entry.get().strip()
358
+ total = len(self.__addresses)
363
359
 
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")
360
+ try:
361
+ if str(self.__value_type(value)) != value:
362
+ raise ValueError()
363
+ except:
364
+ self.__value_entry.delete(0, "end")
365
+ return self.__value_entry.insert(0, "Invalid value")
370
366
 
371
- self.__value = self.__value_type(value)
372
- self.__progress_var.set(0)
367
+ self.__value = self.__value_type(value)
368
+ self.__progress_var.set(0)
373
369
 
374
370
  # Indicate the application is updating the values.
375
371
  self.__updating = True
376
372
 
377
- if index % 10 == 0: self.update()
373
+ new_scan_button_text = self.__new_scan_button["text"]
374
+ self.__new_scan_button.config(text="Updating")
378
375
 
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)
376
+ # Get the address and its current value in memory.
377
+ total, count = len(self.__addresses), 0
387
378
 
388
- # Get the current value of the address.
389
- corrupted = False
390
- value = None
379
+ for address, value in self.__process.search_by_addresses(self.__value_type, self.__value_length, self.__addresses):
380
+ self.__progress_var.set((count / total) * 100)
381
+ self.update()
391
382
 
392
- try: value = self.__process.read_process_memory(address, self.__value_type, self.__value_length)
393
- except: corrupted = True
383
+ index = self.__addresses.index(address)
384
+ count += 1
394
385
 
395
- # If "remove" is True, compare the value.
396
- if remove or corrupted:
397
- compare = self.__comp_methods[self.__scan_type]
386
+ # Return if user asked for closing the application.
387
+ if self.__close:
388
+ self.__updating = False
389
+ return
398
390
 
399
- # Remove the address from the results.
400
- if corrupted or not compare(value, self.__value):
391
+ # If value is corrupted or "remove" is True and comparison is False, remove the value from the results.
392
+ if value is None or (remove and not self.__comp_methods[self.__scan_type](value, self.__value)):
401
393
  self.__address_list.delete(index)
402
394
  self.__value_list.delete(index)
403
395
  self.__addresses.remove(address)
404
- index -= 1
405
396
 
406
- # Update the value at the listbox.
407
- else:
408
- self.__value_list.delete(index)
409
- self.__value_list.insert(index, f"Value: {value}")
397
+ # Update the value at the listbox.
398
+ else:
399
+ self.__value_list.delete(index)
400
+ self.__value_list.insert(index, f"Value: {value}")
401
+
402
+ self.__new_scan_button.config(text=new_scan_button_text)
403
+ self.__updating = False
410
404
 
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))
405
+ self.__count_label.config(text=f"Found {len(self.__addresses)} addresses.")
406
+ self.__progress_var.set(100)
414
407
 
415
408
  def __write_value(self):
416
409
  """
@@ -1,10 +1,25 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  from .scan import scan_memory, scan_memory_for_exact_value
4
- from typing import Type
4
+ from typing import Type, TypeVar
5
5
  import ctypes
6
6
 
7
7
 
8
+ T = TypeVar("T")
9
+
10
+
11
+ def convert_from_bytearray(bytearray: ctypes.Array, pytype: Type[T], length: int = 1) -> T:
12
+ """
13
+ Convert a byte array to a Python type.
14
+ """
15
+ if pytype is bytes: return bytes(bytearray)
16
+ if pytype is str: return bytes(bytearray).decode()
17
+
18
+ c_value = get_c_type_of(pytype, length)
19
+
20
+ return c_value.__class__.from_buffer(bytearray).value
21
+
22
+
8
23
  def get_c_type_of(pytype: Type, length: int = 1) -> ctypes._SimpleCData:
9
24
  """
10
25
  Return a C type of a primitive type of the Python language.
@@ -7,12 +7,12 @@
7
7
  # ...
8
8
 
9
9
  from ..enums import ScanTypesEnum
10
- from ..util import get_c_type_of, scan_memory, scan_memory_for_exact_value
10
+ from ..util import convert_from_bytearray, get_c_type_of, scan_memory, scan_memory_for_exact_value
11
11
 
12
12
  from .enums import MemoryAllocationStatesEnum, MemoryProtectionsEnum, MemoryTypesEnum
13
13
  from .types import MEMORY_BASIC_INFORMATION, SYSTEM_INFO, WNDENUMPROC
14
14
 
15
- from typing import Generator, Tuple, Type, TypeVar, Union
15
+ from typing import Dict, Generator, Optional, Sequence, Tuple, Type, TypeVar, Union
16
16
 
17
17
  import ctypes
18
18
  import ctypes.wintypes
@@ -130,7 +130,7 @@ def ReadProcessMemory(
130
130
  return data.value.decode() if pytype is str else data.value
131
131
 
132
132
 
133
- def SearchAllMemory(
133
+ def SearchAddressesByValue(
134
134
  process_handle: int,
135
135
  pytype: Type[T],
136
136
  bufflength: int,
@@ -163,7 +163,7 @@ def SearchAllMemory(
163
163
  # Get the memory regions, computing the total amount of memory to be scanned.
164
164
  checked_memory_size = 0
165
165
  memory_total = 0
166
- regions = list()
166
+ memory_regions = list()
167
167
 
168
168
  for region in GetMemoryRegions(process_handle):
169
169
 
@@ -176,10 +176,10 @@ def SearchAllMemory(
176
176
  if writeable_only and region["struct"].Protect & MemoryProtectionsEnum.PAGE_READWRITEABLE.value == 0: continue
177
177
 
178
178
  memory_total += region["size"]
179
- regions.append(region)
179
+ memory_regions.append(region)
180
180
 
181
181
  # Check each memory region used by the process.
182
- for region in regions:
182
+ for region in memory_regions:
183
183
  address, size = region["address"], region["size"]
184
184
  region_data = (ctypes.c_byte * size)()
185
185
 
@@ -206,6 +206,70 @@ def SearchAllMemory(
206
206
  checked_memory_size += size
207
207
 
208
208
 
209
+ def SearchValuesByAddresses(
210
+ process_handle: int,
211
+ pytype: Type[T],
212
+ bufflength: int,
213
+ addresses: Sequence[int],
214
+ *,
215
+ memory_regions: Optional[Sequence[Dict]] = None,
216
+ raise_error: bool = False,
217
+ ) -> Generator[Tuple[int, Optional[T]], None, None]:
218
+ """
219
+ Search the whole memory space, accessible to the process,
220
+ for the provided list of addresses, returning their values.
221
+ """
222
+ if pytype not in [bool, int, float, str, bytes]:
223
+ raise ValueError("The type must be bool, int, float, str or bytes.")
224
+
225
+ memory_regions = list(memory_regions) if memory_regions else list()
226
+ addresses = sorted(addresses)
227
+
228
+ # If no memory page has been given, get all committed, non-shared and readable memory pages.
229
+ if not memory_regions:
230
+ for region in GetMemoryRegions(process_handle):
231
+ if region["struct"].State != MemoryAllocationStatesEnum.MEM_COMMIT.value: continue
232
+ if region["struct"].Type != MemoryTypesEnum.MEM_PRIVATE.value: continue
233
+ if region["struct"].Protect & MemoryProtectionsEnum.PAGE_READABLE.value == 0: continue
234
+
235
+ memory_regions.append(region)
236
+
237
+ memory_regions.sort(key=lambda region: region["address"])
238
+ address_index = 0
239
+
240
+ # Walk by each memory region.
241
+ for region in memory_regions:
242
+ if address_index >= len(addresses): continue
243
+
244
+ target_address = addresses[address_index]
245
+
246
+ # Check if the memory region contains the target address.
247
+ base_address, size = region["address"], region["size"]
248
+ if not (base_address <= target_address < base_address + size): continue
249
+
250
+ region_data = (ctypes.c_byte * size)()
251
+
252
+ # Get data from the region.
253
+ kernel32.ReadProcessMemory(process_handle, ctypes.c_void_p(base_address), ctypes.byref(region_data), size, None)
254
+
255
+ # Get the value of each address.
256
+ while base_address <= target_address < base_address + size:
257
+ offset = target_address - base_address
258
+ address_index += 1
259
+
260
+ try:
261
+ data = region_data[offset: offset + bufflength]
262
+ data = (ctypes.c_byte * bufflength)(*data)
263
+ yield target_address, convert_from_bytearray(data, pytype, bufflength)
264
+
265
+ except Exception as error:
266
+ if raise_error: raise error
267
+ yield target_address, None
268
+
269
+ if address_index >= len(addresses): break
270
+ target_address = addresses[address_index]
271
+
272
+
209
273
  def WriteProcessMemory(
210
274
  process_handle: int,
211
275
  address: int,
@@ -10,11 +10,12 @@ from .functions import (
10
10
  GetMemoryRegions,
11
11
  GetProcessHandle,
12
12
  ReadProcessMemory,
13
- SearchAllMemory,
13
+ SearchAddressesByValue,
14
+ SearchValuesByAddresses,
14
15
  WriteProcessMemory
15
16
  )
16
17
 
17
- from typing import Generator, Optional, Tuple, Type, TypeVar, Union
18
+ from typing import Generator, Optional, Sequence, Tuple, Type, TypeVar, Union
18
19
 
19
20
 
20
21
  T = TypeVar("T")
@@ -70,6 +71,29 @@ class WindowsProcess(AbstractProcess):
70
71
 
71
72
  return GetMemoryRegions(self.__process_handle)
72
73
 
74
+ def search_by_addresses(
75
+ self,
76
+ pytype: Type[T],
77
+ bufflength: int,
78
+ addresses: Sequence[int],
79
+ *,
80
+ raise_error: bool = False,
81
+ ) -> Generator[Tuple[int, Optional[T]], None, None]:
82
+ """
83
+ Search the whole memory space, accessible to the process,
84
+ for the provided list of addresses, returning their values.
85
+ """
86
+ if self.__closed: raise ClosedProcess()
87
+
88
+ valid_permissions = [
89
+ ProcessOperationsEnum.PROCESS_ALL_ACCESS.value,
90
+ ProcessOperationsEnum.PROCESS_VM_READ.value
91
+ ]
92
+ if self.__permission.value not in valid_permissions:
93
+ raise PermissionError("The handle does not have permission to read the process memory.")
94
+
95
+ return SearchValuesByAddresses(self.__process_handle, pytype, bufflength, addresses, raise_error=raise_error)
96
+
73
97
  def search_by_value(
74
98
  self,
75
99
  pytype: Type[T],
@@ -103,7 +127,7 @@ class WindowsProcess(AbstractProcess):
103
127
  if scan_type in [ScanTypesEnum.VALUE_BETWEEN, ScanTypesEnum.NOT_VALUE_BETWEEN]:
104
128
  raise ValueError("Use the method search_by_value_between(...) to search within a range of values.")
105
129
 
106
- return SearchAllMemory(self.__process_handle, pytype, bufflength, value, scan_type, progress_information, writeable_only)
130
+ return SearchAddressesByValue(self.__process_handle, pytype, bufflength, value, scan_type, progress_information, writeable_only)
107
131
 
108
132
  def search_by_value_between(
109
133
  self,
@@ -138,7 +162,7 @@ class WindowsProcess(AbstractProcess):
138
162
  raise PermissionError("The handle does not have permission to read the process memory.")
139
163
 
140
164
  scan_type = ScanTypesEnum.NOT_VALUE_BETWEEN if not_between else ScanTypesEnum.VALUE_BETWEEN
141
- return SearchAllMemory(self.__process_handle, pytype, bufflength, (start, end), scan_type, progress_information, writeable_only)
165
+ return SearchAddressesByValue(self.__process_handle, pytype, bufflength, (start, end), scan_type, progress_information, writeable_only)
142
166
 
143
167
  def read_process_memory(
144
168
  self,
@@ -15,6 +15,9 @@ reading and writing values in the process memory.
15
15
  pip3 install PyMemoryEditor
16
16
  ```
17
17
 
18
+ ### Tkinter application sample:
19
+ You can run a simple tkinter application to scan the memory of a process by typing `pymemoryeditor` at the CLI.
20
+
18
21
  # Basic Usage:
19
22
  Import `PyMemoryEditor` and open a process using the `OpenProcess` class, passing a window title, process name <br>
20
23
  or PID as an argument. You can use the context manager to do this.
@@ -71,13 +74,21 @@ All methods described above work even for strings, including the method `search_
71
74
  ## Progress information on searching:
72
75
  These methods has the `progress_information` parameter that returns a dictionary containing the search progress information.
73
76
  ```py
74
- for address, info process.search_by_value(..., progress_information = True):
77
+ for address, info in process.search_by_value(..., progress_information = True):
75
78
  template = "Address: 0x{:<10X} | Progress: {:.1f}%"
76
79
  progress = info["progress"] * 100
77
80
 
78
81
  print(template.format(address, progress))
79
82
  ```
80
83
 
84
+ # Reading multiple addresses efficiently:
85
+ If you have a large number of addresses where their values need to be read from memory, using the `search_by_addresses` method is much more efficient than reading the value of each address one by one.
86
+ ```py
87
+ for address, value in process.search_by_addresses(int, 4, addresses_list):
88
+ print(f"Address", address, "holds the value", value)
89
+ ```
90
+ The key advantage of this method is that it reads a memory page just once, obtaining the values of the addresses within the page. This approach reduces the frequency of system calls.
91
+
81
92
  ## Getting memory regions:
82
93
  Use the method `get_memory_regions()` to get the base address, size and more information of all memory regions used by the process.
83
94
 
@@ -87,6 +98,3 @@ for memory_region in process.get_memory_regions():
87
98
  size = memory_region["size"]
88
99
  information = memory_region["struct"]
89
100
  ```
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.
@@ -68,13 +68,15 @@ def test_read_int():
68
68
 
69
69
  def test_read_string():
70
70
  # Get a random text to compare the result.
71
- target_value = ctypes.create_string_buffer(generate_text(20).encode())
71
+ target_value = ctypes.create_string_buffer(20)
72
+ target_value.value = generate_text(20).encode()
73
+
72
74
  address = ctypes.addressof(target_value)
73
75
  data_length = ctypes.sizeof(target_value)
74
76
 
75
77
  # Read the process memory and compare the result.
76
78
  result = process.read_process_memory(address, str, data_length)
77
- assert type(result) is str and result == str(target_value.value)
79
+ assert type(result) is str and result == target_value.value.decode()
78
80
 
79
81
 
80
82
  def test_write_bool():
@@ -134,20 +136,69 @@ def test_write_string():
134
136
  original_value = generate_text(20).encode()
135
137
  new_value = generate_text(20).encode()
136
138
 
137
- target_value = ctypes.create_string_buffer(original_value)
139
+ target_value = ctypes.create_string_buffer(20)
140
+ target_value.value = original_value
141
+
138
142
  address = ctypes.addressof(target_value)
139
143
  data_length = ctypes.sizeof(target_value)
140
144
 
141
145
  # Write to the process memory and compare the result.
142
- process.write_process_memory(address, str, data_length, new_value)
146
+ process.write_process_memory(address, str, data_length, new_value.decode())
143
147
  assert target_value.value != original_value and target_value.value == new_value
144
148
 
145
149
 
150
+ def test_search_by_int_addresses():
151
+ # Get random values to compare the result.
152
+ test_length = 10
153
+
154
+ target_values = [ctypes.c_int(random.randint(0, 10000)) for i in range(test_length)]
155
+ data_length = ctypes.sizeof(target_values[0])
156
+
157
+ target_values = {ctypes.addressof(v): v for v in target_values}
158
+ addresses = list(target_values.keys())
159
+
160
+ for address, value in process.search_by_addresses(int, data_length, addresses):
161
+ assert target_values[address].value == value and type(value) is int
162
+
163
+ def test_search_by_float_addresses():
164
+ # Get random values to compare the result.
165
+ test_length = 10
166
+
167
+ target_values = [ctypes.c_double(random.randint(0, 10000) / random.randint(0, 10000)) for i in range(test_length)]
168
+ data_length = ctypes.sizeof(target_values[0])
169
+
170
+ target_values = {ctypes.addressof(v): v for v in target_values}
171
+ addresses = list(target_values.keys())
172
+
173
+ for address, value in process.search_by_addresses(float, data_length, addresses):
174
+ assert target_values[address].value == value and type(value) is float
175
+
176
+
177
+ def test_search_by_string_addresses():
178
+ # Get random values to compare the result.
179
+ string_length, test_length = 20, 10
180
+
181
+ target_values = list()
182
+
183
+ for i in range(test_length):
184
+ value = ctypes.create_string_buffer(string_length)
185
+ value.value = generate_text(string_length).encode()
186
+ target_values.append(value)
187
+
188
+ data_length = ctypes.sizeof(target_values[0])
189
+
190
+ target_values = {ctypes.addressof(v): v for v in target_values}
191
+ addresses = list(target_values.keys())
192
+
193
+ for address, value in process.search_by_addresses(str, data_length, addresses):
194
+ assert target_values[address].value.decode() == value and type(value) is str
195
+
196
+
146
197
  def test_search_by_int():
147
198
  # Get random values to compare the result.
148
- length = 10
199
+ test_length = 10
149
200
 
150
- target_values = [ctypes.c_int(random.randint(0, 10000)) for i in range(length)]
201
+ target_values = [ctypes.c_int(random.randint(0, 10000)) for i in range(test_length)]
151
202
  addresses = [ctypes.addressof(v) for v in target_values]
152
203
  data_length = ctypes.sizeof(target_values[0])
153
204
 
@@ -172,15 +223,15 @@ def test_search_by_int():
172
223
  value = process.read_process_memory(found_address, int, data_length)
173
224
  if min_value <= value <= max_value: correct += 1
174
225
 
175
- assert found / length >= 0.7
226
+ assert found / test_length >= 0.7
176
227
  assert correct / total >= 0.7 # Some of the addresses are beyond our control and may have their values changed.
177
228
 
178
229
 
179
230
  def test_search_by_float():
180
231
  # Get random values to compare the result.
181
- length = 10
232
+ test_length = 10
182
233
 
183
- target_values = [ctypes.c_double(random.randint(0, 10000)) for i in range(length)]
234
+ target_values = [ctypes.c_double(random.randint(0, 10000)) for i in range(test_length)]
184
235
  addresses = [ctypes.addressof(v) for v in target_values]
185
236
  data_length = ctypes.sizeof(target_values[0])
186
237
 
@@ -205,15 +256,21 @@ def test_search_by_float():
205
256
  value = process.read_process_memory(found_address, float, data_length)
206
257
  if min_value <= value <= max_value: correct += 1
207
258
 
208
- assert found / length >= 0.7
259
+ assert found / test_length >= 0.7
209
260
  assert correct / total >= 0.7 # Some of the addresses are beyond our control and may have their values changed.
210
261
 
211
262
 
212
263
  def test_search_by_string():
213
264
  # Get random values to compare the result.
214
- length = 10
265
+ string_length, test_length = 20, 10
266
+
267
+ target_values = list()
268
+
269
+ for i in range(test_length):
270
+ value = ctypes.create_string_buffer(string_length)
271
+ value.value = generate_text(string_length).encode()
272
+ target_values.append(value)
215
273
 
216
- target_values = [ctypes.create_string_buffer(generate_text(20).encode()) for i in range(length)]
217
274
  data_length = ctypes.sizeof(target_values[0])
218
275
 
219
276
  total = 0
@@ -231,22 +288,30 @@ def test_search_by_string():
231
288
  total += 1
232
289
 
233
290
  # Check if the address really points to a valid value.
234
- value = process.read_process_memory(found_address, str, data_length)
235
- if value == str(target_value.value): correct += 1
291
+ try:
292
+ value = process.read_process_memory(found_address, str, data_length)
293
+ if value == target_value.value.decode(): correct += 1
294
+ except: pass
236
295
 
237
- assert found / length >= 0.7
296
+ assert found / test_length >= 0.7
238
297
  assert correct / total >= 0.7 # Some of the addresses are beyond our control and may have their values changed.
239
298
 
240
299
 
241
300
  def test_search_by_string_between():
242
301
  # Get random values to compare the result.
243
- length = 10
302
+ string_length, test_length = 20, 10
303
+
304
+ values = list()
305
+
306
+ for i in range(test_length * 2):
307
+ value = ctypes.create_string_buffer(string_length)
308
+ value.value = generate_text(string_length).encode()
309
+ values.append(value)
244
310
 
245
- values = [ctypes.create_string_buffer(generate_text(20).encode()) for i in range(length * 2)]
246
311
  values.sort(key = lambda target_value: target_value.value)
247
312
 
248
313
  # Half of the set of strings is the target and the other half contains string that should be ignored by the scanner.
249
- target_values = [target_value for target_value in values[length // 4: length - length // 4]]
314
+ target_values = [target_value for target_value in values[test_length // 4: test_length - test_length // 4]]
250
315
 
251
316
  addresses = [ctypes.addressof(v) for v in values]
252
317
  target_addresses = [ctypes.addressof(v) for v in target_values]
@@ -269,7 +334,7 @@ def test_search_by_string_between():
269
334
  elif found_address in addresses:
270
335
  raise ValueError("Scanner returned the address of a clearly invalid string.")
271
336
 
272
- assert found / length >= 0.5
337
+ assert found / test_length >= 0.5
273
338
 
274
339
 
275
340
  def test_close_process():
File without changes