ntmemoryapi 2.4.0__tar.gz → 2.5.1__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.
@@ -1,7 +1,8 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ntmemoryapi
3
- Version: 2.4.0
4
- Summary: Add your description here
3
+ Version: 2.5.1
4
+ Summary: Simple library for Windows to manipulate process virtual memory with stelthy syscall wraps
5
+ Author: Xenely
5
6
  Requires-Dist: psutil>=7.2.2
6
7
  Requires-Python: >=3.13
7
8
  Description-Content-Type: text/markdown
@@ -1,9 +1,12 @@
1
1
  [project]
2
2
  name = "ntmemoryapi"
3
- version = "2.4.0"
4
- description = "Add your description here"
3
+ version = "2.5.1"
4
+ description = "Simple library for Windows to manipulate process virtual memory with stelthy syscall wraps"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
7
+ authors = [
8
+ {name = "Xenely"}
9
+ ]
7
10
  dependencies = [
8
11
  "psutil>=7.2.2",
9
12
  ]
@@ -9,6 +9,7 @@ import os
9
9
  import typing
10
10
  import ctypes
11
11
  import psutil
12
+ import functools
12
13
 
13
14
  # Local imports
14
15
  from . import misc
@@ -24,7 +25,6 @@ syscall_wrapper = misc.DirectSyscallWrapper()
24
25
  PROCESS_ID = 1
25
26
  PROCESS_NAME = 2
26
27
 
27
-
28
28
  # ==-------------------------------------------------------------------== #
29
29
  # C-consts #
30
30
  # ==-------------------------------------------------------------------== #
@@ -150,6 +150,10 @@ def list_processes(include_process_information: int = PROCESS_ID | PROCESS_NAME)
150
150
  return processes
151
151
 
152
152
 
153
+ # ==-------------------------------------------------------------------== #
154
+ # Private functions #
155
+ # ==-------------------------------------------------------------------== #
156
+ @functools.lru_cache()
153
157
  def _get_be_buffer(soure_c_type: typing.Any) -> ctypes.BigEndianStructure:
154
158
  """Create buffer to hold data in big-endian format."""
155
159
 
@@ -164,6 +168,336 @@ def _get_be_buffer(soure_c_type: typing.Any) -> ctypes.BigEndianStructure:
164
168
  return BigEndianValue
165
169
 
166
170
 
171
+ def _unbounded_array_getitem(array: ctypes.Array, index: int | slice) -> None:
172
+ """Array `__getitem__` function override to make array able to get out of bound values."""
173
+
174
+ # Get array process reference
175
+ array_process = array.__dict__.get("_process_")
176
+
177
+ # Get array base address
178
+ array_address = array.__dict__.get("_address_")
179
+
180
+ # If array process is not defined
181
+ if array_process is None:
182
+ return super(type(array), array).__getitem__(index)
183
+
184
+ # Get array value type
185
+ array_value_type = array._type_
186
+
187
+ # Get array value type size
188
+ array_value_type_size = ctypes.sizeof(array_value_type)
189
+
190
+ # If index is integer
191
+ if isinstance(index, int):
192
+
193
+ # If valus is not out of bound
194
+ if 0 <= index < len(array):
195
+ array_value = super(type(array), array).__getitem__(index)
196
+
197
+ # if valus is out of bound
198
+ else:
199
+ array_value = array._process_.read_into_buffer(array_address + array_value_type_size * index, array_value_type())
200
+
201
+ # Resolve final value
202
+ match array_value:
203
+
204
+ # If value is array
205
+ case ctypes.Array():
206
+
207
+ # Save process reference into value
208
+ array_value._process_ = array_process
209
+
210
+ # Save address into value
211
+ array_value._address_ = array_address + array_value_type_size * index
212
+
213
+ # Override array `__getitem__` to make it boudless.
214
+ array_value.__class__.__getitem__ = _unbounded_array_getitem
215
+
216
+ return array_value
217
+
218
+ # If value is structure or union
219
+ case ctypes.Structure() | ctypes.Union():
220
+
221
+ # Save proess reference into value
222
+ array_value._process_ = array_process
223
+
224
+ # Save address into value
225
+ array_value._address_ = array_address + array_value_type_size * index
226
+
227
+ return array_value
228
+
229
+ # If value is container pointer
230
+ case ctypes._Pointer():
231
+
232
+ # Read value into buffer and return it
233
+ return array_process.read_into_buffer(ctypes.cast(array_value, ctypes.c_void_p).value, array_value._type_())
234
+
235
+ # If value is simple c-type data
236
+ case ctypes._SimpleCData():
237
+ return array_value.value
238
+
239
+ # If type is not c-type data
240
+ case _:
241
+ return array_value
242
+
243
+ # If index is slice
244
+ if isinstance(index, slice):
245
+
246
+ # Array length
247
+ array_length = len(array)
248
+
249
+ # Slice start and stop values
250
+ start = index.start or 0
251
+ stop = index.stop or array_length
252
+
253
+ # List of read array values
254
+ array_values = []
255
+
256
+ # If slice start is smaller than slice stop
257
+ if start > stop:
258
+ raise RuntimeError("Array slice start `%s` can't be grater that slice stop `%s`" % (start, stop))
259
+
260
+ # Iterate slice range and collect array value
261
+ while start != stop:
262
+
263
+ # If start is out of array bound at left side
264
+ if start < 0:
265
+
266
+ # Find read batch size
267
+ read_batch_size = abs(start - min(stop, 0))
268
+
269
+ # Read batch until array start or slice stop
270
+ read_batch = array._process_.read_into_buffer(array_address + array_value_type_size * start, (array_value_type * read_batch_size)())
271
+
272
+ # Extend array values with read batch
273
+ array_values.extend(read_batch[:])
274
+
275
+ # Move start pointer
276
+ start += read_batch_size
277
+
278
+ # If start is out of array bound at right side
279
+ elif start >= array_length:
280
+
281
+ # Find read batch size
282
+ read_batch_size = abs(start - stop)
283
+
284
+ # Read batch until array start or slice stop
285
+ read_batch = array._process_.read_into_buffer(array_address + array_value_type_size * start, (array_value_type * read_batch_size)())
286
+
287
+ # Extend array values with read batch
288
+ array_values.extend(read_batch[:])
289
+
290
+ # Move start pointer
291
+ start += read_batch_size
292
+
293
+ # If array value is already read
294
+ else:
295
+
296
+ # Append value to array values
297
+ array_values.append(array[start])
298
+
299
+ # Move start pointer
300
+ start += 1
301
+
302
+ # Finalize array values
303
+ for offset, index in enumerate(range(len(array_values)), index.start or 0):
304
+
305
+ match array_values[index]:
306
+
307
+ # If value is array
308
+ case ctypes.Array():
309
+
310
+ # Save process reference into value
311
+ array_values[index]._process_ = array_process
312
+
313
+ # Save address into value
314
+ array_values[index]._address_ = array_address + array_value_type_size * offset
315
+
316
+ # Override array `__getitem__` to make it boudless.
317
+ array_values[index].__class__.__getitem__ = _unbounded_array_getitem
318
+
319
+ # If value is structure or union
320
+ case ctypes.Structure() | ctypes.Union():
321
+
322
+ # Save proess reference into value
323
+ array_values[index]._process_ = array_process
324
+
325
+ # Save address into value
326
+ array_values[index]._address_ = array_address + array_value_type_size * offset
327
+
328
+ # If value is container pointer
329
+ case ctypes._Pointer():
330
+
331
+ # Read value into buffer and return it
332
+ array_values[index] = array_process.read_into_buffer(ctypes.cast(array_values[index], ctypes.c_void_p).value, array_values[index]._type_())
333
+
334
+ # If value is simple c-type data
335
+ case ctypes._SimpleCData():
336
+ array_values[index] = array_values[index].value
337
+
338
+ return array_values
339
+
340
+
341
+ # ==-------------------------------------------------------------------== #
342
+ # Meta classes #
343
+ # ==-------------------------------------------------------------------== #
344
+ class _AutoStructureMeta(type(ctypes.Structure)):
345
+
346
+ def __new__[T](cls: T, name: str, bases: tuple[type, ...], attributes: dict[str, typing.Any]) -> T:
347
+ """Overload that invokes of new class creation to hide generic fields is they are defined."""
348
+
349
+ # Extra attributes
350
+ extra_attributes = {}
351
+
352
+ # Check if struct if generic-typed
353
+ if any(isinstance(item[1], int) for item in attributes.get("_fields_", [])):
354
+ extra_attributes["_generized_"] = attributes.pop("_fields_")
355
+
356
+ return super().__new__(cls, name, bases, attributes | extra_attributes)
357
+
358
+ def __getitem__[T](cls: T, generics: tuple[typing.Any, ...]) -> T:
359
+ """Overload that invokes on tries to unwrap generic class."""
360
+
361
+ # If structure is not generized
362
+ if not hasattr(cls, "_generized_"):
363
+ raise RuntimeError("Unable to init `%s` object due the no generic fields, use regular brackets syntax instead" % cls.__name__)
364
+
365
+ # If only one generic time passed
366
+ if not isinstance(generics, tuple):
367
+ generics = (generics,)
368
+
369
+ # Unwarp generic types and replace fields with template types
370
+ fields = []
371
+ for field in cls._generized_:
372
+
373
+ # If field is simple generic value
374
+ if len(field) == 2 and isinstance(field[1], int):
375
+ fields.append((field[0], generics[field[1]]))
376
+
377
+ # If field is complex generic value
378
+ elif len(field) == 3 and isinstance(field[1], int):
379
+
380
+ # Get generic wrapper
381
+ generic_wrapper = field[-1]
382
+
383
+ # If generic wrapper is array size
384
+ if isinstance(generic_wrapper, int):
385
+ fields.append((field[0], (generics[field[1]] * generic_wrapper)))
386
+
387
+ # If generic wrapper is container
388
+ else:
389
+ fields.append((field[0], generic_wrapper(generics[field[1]])))
390
+
391
+ # If field is not generic value
392
+ else:
393
+ fields.append(field)
394
+
395
+ return type(
396
+ "%s_%s" % (cls.__name__, "_".join(getattr(item, "__name__", str(item)) for item in generics)),
397
+ cls.__bases__,
398
+ {
399
+ key: value for key, value in cls.__dict__.items()
400
+ if key not in {"_generized_", "_fields_", "__dict__", "__weakref__"}
401
+ } | {"_fields_": fields}
402
+ )
403
+
404
+ def __call__[T](cls: T, *args: typing.Any, **kwargs: typing.Any):
405
+ """Overload that invokes on tries to create of class instance."""
406
+
407
+ # If try tp init without type templates
408
+ if "_generized_" in cls.__dict__:
409
+ raise RuntimeError("Unable to init `%s` object due the structure fields `%s` are generic and have to be typed using square brackets syntax" % (
410
+ cls.__name__,
411
+ ", ".join(item[0] for item in cls._generized_ if isinstance(item[1], int))
412
+ ))
413
+
414
+ return super().__call__(*args, **kwargs)
415
+
416
+
417
+ class AutoStructure(ctypes.Structure, metaclass=_AutoStructureMeta):
418
+ """Sturcture to automatic dereference pointers located at virtual process memory."""
419
+
420
+ # ==--------------------------------== #
421
+ # Public methods #
422
+ # ==--------------------------------== #
423
+ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
424
+ """Init instance passing `*args` and `**kwargs` into parent initializer."""
425
+
426
+ # Sturcture metadata
427
+ self._address_: int = 0
428
+ self._process_: Process | None = None
429
+
430
+ # Pass `*args` and `**kwargs` to origianl structure initializer
431
+ super().__init__(*args, **kwargs)
432
+
433
+ def __invert__[T](self: T) -> T:
434
+ """Remove process link from the object to allow get raw field values without auto-RPM magic."""
435
+
436
+ # If processs link is already empty
437
+ if not isinstance(self._process_, Process):
438
+ return self
439
+
440
+ # Copy current object copy
441
+ instance_copy = self.__class__.from_buffer_copy(self)
442
+
443
+ # Erase instance proess reference
444
+ instance_copy._process_ = None
445
+
446
+ return instance_copy
447
+
448
+ def __getattribute__(self, name: str) -> typing.Any:
449
+ """Access objectr field if it exists."""
450
+
451
+ # Get field from parrent object
452
+ field = super().__getattribute__(name)
453
+
454
+ # If process reference is not defined
455
+ if (process := object.__getattribute__(self, "__dict__").get("_process_")) is None:
456
+ return field
457
+
458
+ # If field is complex type value
459
+ match field:
460
+
461
+ # If field is array
462
+ case ctypes.Array():
463
+
464
+ # Save process reference into field
465
+ field._process_ = process
466
+
467
+ # Save address into field
468
+ field._address_ = self._address_ + getattr(self.__class__, name).offset
469
+
470
+ # Override array `__getitem__` to make it boudless.
471
+ field.__class__.__getitem__ = _unbounded_array_getitem
472
+
473
+ return field
474
+
475
+ # If field is structure or union
476
+ case ctypes.Structure() | ctypes.Union():
477
+
478
+ # Save proess reference into field
479
+ field._process_ = process
480
+
481
+ # Save address into field
482
+ field._address_ = self._address_ + getattr(self.__class__, name).offset
483
+
484
+ return field
485
+
486
+ # If field is container pointer
487
+ case ctypes._Pointer():
488
+
489
+ # Read value into buffer and return it
490
+ return process.read_into_buffer(ctypes.cast(field, ctypes.c_void_p).value, field._type_())
491
+
492
+ # If field is simple c-type data
493
+ case ctypes._SimpleCData():
494
+ return field.value
495
+
496
+ # If type is not c-type data
497
+ case _:
498
+ return field
499
+
500
+
167
501
  # ==-------------------------------------------------------------------== #
168
502
  # Classes #
169
503
  # ==-------------------------------------------------------------------== #
@@ -524,7 +858,7 @@ class Process:
524
858
  try:
525
859
 
526
860
  # Read bytes into pre-allocated buffer
527
- self.read_into_buffer(region.base_address, read_memory_buffer, region.size)
861
+ self.read_into_buffer(region.base_address, read_memory_buffer, region.size, discard_metadata=True)
528
862
 
529
863
  except Exception:
530
864
  continue
@@ -647,7 +981,7 @@ class Process:
647
981
 
648
982
  return bytes(buffer)
649
983
 
650
- def read_into_buffer(self, address: int, buffer: typing.Any, read_bytes_size: int | None = None) -> typing.Any:
984
+ def read_into_buffer[T](self, address: int, buffer: T, read_bytes_size: int | None = None, discard_metadata: bool = False) -> T:
651
985
  """Read value located at given address into buffer. Buffer have to be able passed at `ctypes.byref` and `ctypes.sizeof`. Read bytes size can be changed by passing `read_bytes_size` argument."""
652
986
 
653
987
  # If read size is invalid
@@ -658,6 +992,12 @@ class Process:
658
992
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer), ctypes.sizeof(buffer) if read_bytes_size is None else read_bytes_size, None)):
659
993
  raise errors.MemoryReadError(result, address)
660
994
 
995
+ # Save process reference and read address
996
+ if not discard_metadata:
997
+
998
+ buffer._process_ = self
999
+ buffer._address_ = address
1000
+
661
1001
  return buffer
662
1002
 
663
1003
  def write_int8(self, address: int, value: int) -> None:
@@ -5,7 +5,6 @@
5
5
  # | Discord: xenely |
6
6
  # +-------------------------------------+
7
7
 
8
-
9
8
  import typing
10
9
  import ctypes
11
10
 
File without changes