ntmemoryapi 2.3.0__tar.gz → 2.5.0__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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ntmemoryapi
3
- Version: 2.3.0
3
+ Version: 2.5.0
4
4
  Summary: Add your description here
5
5
  Requires-Dist: psutil>=7.2.2
6
6
  Requires-Python: >=3.13
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ntmemoryapi"
3
- version = "2.3.0"
3
+ version = "2.5.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -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,333 @@ 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
+ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
421
+ """Init instance passing `*args` and `**kwargs` into parent initializer."""
422
+
423
+ # Sturcture metadata
424
+ self._address_: int = 0
425
+ self._process_: Process | None = None
426
+
427
+ # Pass `*args` and `**kwargs` to origianl structure initializer
428
+ super().__init__(*args, **kwargs)
429
+
430
+ def __invert__[T](self: T) -> T:
431
+ """Remove process link from the object to allow get raw field values without auto-RPM magic."""
432
+
433
+ # If processs link is already empty
434
+ if not isinstance(self._process_, Process):
435
+ return self
436
+
437
+ # Copy current object copy
438
+ instance_copy = self.__class__.from_buffer_copy(self)
439
+
440
+ # Erase instance proess reference
441
+ instance_copy._process_ = None
442
+
443
+ return instance_copy
444
+
445
+ def __getattribute__(self, name: str) -> typing.Any:
446
+ """Access objectr field if it exists."""
447
+
448
+ # Get field from parrent object
449
+ field = super().__getattribute__(name)
450
+
451
+ # If process reference is not defined
452
+ if (process := object.__getattribute__(self, "__dict__").get("_process_")) is None:
453
+ return field
454
+
455
+ # If field is complex type value
456
+ match field:
457
+
458
+ # If field is array
459
+ case ctypes.Array():
460
+
461
+ # Save process reference into field
462
+ field._process_ = process
463
+
464
+ # Save address into field
465
+ field._address_ = self._address_ + getattr(self.__class__, name).offset
466
+
467
+ # Override array `__getitem__` to make it boudless.
468
+ field.__class__.__getitem__ = _unbounded_array_getitem
469
+
470
+ return field
471
+
472
+ # If field is structure or union
473
+ case ctypes.Structure() | ctypes.Union():
474
+
475
+ # Save proess reference into field
476
+ field._process_ = process
477
+
478
+ # Save address into field
479
+ field._address_ = self._address_ + getattr(self.__class__, name).offset
480
+
481
+ return field
482
+
483
+ # If field is container pointer
484
+ case ctypes._Pointer():
485
+
486
+ # Read value into buffer and return it
487
+ return process.read_into_buffer(ctypes.cast(field, ctypes.c_void_p).value, field._type_())
488
+
489
+ # If field is simple c-type data
490
+ case ctypes._SimpleCData():
491
+ return field.value
492
+
493
+ # If type is not c-type data
494
+ case _:
495
+ return field
496
+
497
+
167
498
  # ==-------------------------------------------------------------------== #
168
499
  # Classes #
169
500
  # ==-------------------------------------------------------------------== #
@@ -215,7 +546,7 @@ class Process:
215
546
  if not self.is_64bit():
216
547
  raise RuntimeError("Unable to list modules for not 64 bit process using `%s` method, use another one method instead")
217
548
 
218
- # If procewss PEB retrieve failed
549
+ # If process PEB retrieve failed
219
550
  if not (process_peb := self.get_information().peb):
220
551
  raise RuntimeError("Process `%s` PEB address is null-value" % self.name)
221
552
 
@@ -380,11 +711,15 @@ class Process:
380
711
 
381
712
  return process_basic_information
382
713
 
383
- def get_module(self, name: str) -> structs.MODULEENTRY32:
714
+ def get_module(self, name: str, method: typing.Literal["pebwalk", "snapshot"] = "pebwalk") -> structs.MODULEENTRY32:
384
715
  """Get process module information."""
385
716
 
717
+ # If method is not allowed
718
+ if method.lower() not in [item.lower() for item in allowed_methods] if (allowed_methods := typing.get_args(self.list_modules.__annotations__["method"])) else True:
719
+ raise ValueError("Method literal is invalid, expected one of: `%s`" % ", ".join(allowed_methods))
720
+
386
721
  # Process modules enumeration
387
- for module in self.list_modules():
722
+ for module in self.list_modules(method):
388
723
 
389
724
  # If module have a required name
390
725
  if module.name.lower() == name.strip().lower():
@@ -520,7 +855,7 @@ class Process:
520
855
  try:
521
856
 
522
857
  # Read bytes into pre-allocated buffer
523
- self.read_into_buffer(region.base_address, read_memory_buffer, region.size)
858
+ self.read_into_buffer(region.base_address, read_memory_buffer, region.size, discard_metadata=True)
524
859
 
525
860
  except Exception:
526
861
  continue
@@ -643,7 +978,7 @@ class Process:
643
978
 
644
979
  return bytes(buffer)
645
980
 
646
- def read_into_buffer(self, address: int, buffer: typing.Any, read_bytes_size: int | None = None) -> typing.Any:
981
+ def read_into_buffer[T](self, address: int, buffer: T, read_bytes_size: int | None = None, discard_metadata: bool = False) -> T:
647
982
  """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."""
648
983
 
649
984
  # If read size is invalid
@@ -654,6 +989,12 @@ class Process:
654
989
  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)):
655
990
  raise errors.MemoryReadError(result, address)
656
991
 
992
+ # Save process reference and read address
993
+ if not discard_metadata:
994
+
995
+ buffer._process_ = self
996
+ buffer._address_ = address
997
+
657
998
  return buffer
658
999
 
659
1000
  def write_int8(self, address: int, value: int) -> None:
File without changes