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.
- {ntmemoryapi-2.3.0 → ntmemoryapi-2.5.0}/PKG-INFO +1 -1
- {ntmemoryapi-2.3.0 → ntmemoryapi-2.5.0}/pyproject.toml +1 -1
- {ntmemoryapi-2.3.0 → ntmemoryapi-2.5.0}/src/ntmemoryapi/__init__.py +347 -6
- {ntmemoryapi-2.3.0 → ntmemoryapi-2.5.0}/README.md +0 -0
- {ntmemoryapi-2.3.0 → ntmemoryapi-2.5.0}/src/ntmemoryapi/embed.py +0 -0
- {ntmemoryapi-2.3.0 → ntmemoryapi-2.5.0}/src/ntmemoryapi/errors.py +0 -0
- {ntmemoryapi-2.3.0 → ntmemoryapi-2.5.0}/src/ntmemoryapi/misc.py +0 -0
- {ntmemoryapi-2.3.0 → ntmemoryapi-2.5.0}/src/ntmemoryapi/structs.py +0 -0
|
@@ -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
|
|
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:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|