ntmemoryapi 2.4.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.4.0 → ntmemoryapi-2.5.0}/PKG-INFO +1 -1
- {ntmemoryapi-2.4.0 → ntmemoryapi-2.5.0}/pyproject.toml +1 -1
- {ntmemoryapi-2.4.0 → ntmemoryapi-2.5.0}/src/ntmemoryapi/__init__.py +340 -3
- {ntmemoryapi-2.4.0 → ntmemoryapi-2.5.0}/README.md +0 -0
- {ntmemoryapi-2.4.0 → ntmemoryapi-2.5.0}/src/ntmemoryapi/embed.py +0 -0
- {ntmemoryapi-2.4.0 → ntmemoryapi-2.5.0}/src/ntmemoryapi/errors.py +0 -0
- {ntmemoryapi-2.4.0 → ntmemoryapi-2.5.0}/src/ntmemoryapi/misc.py +0 -0
- {ntmemoryapi-2.4.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
|
# ==-------------------------------------------------------------------== #
|
|
@@ -524,7 +855,7 @@ class Process:
|
|
|
524
855
|
try:
|
|
525
856
|
|
|
526
857
|
# Read bytes into pre-allocated buffer
|
|
527
|
-
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)
|
|
528
859
|
|
|
529
860
|
except Exception:
|
|
530
861
|
continue
|
|
@@ -647,7 +978,7 @@ class Process:
|
|
|
647
978
|
|
|
648
979
|
return bytes(buffer)
|
|
649
980
|
|
|
650
|
-
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:
|
|
651
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."""
|
|
652
983
|
|
|
653
984
|
# If read size is invalid
|
|
@@ -658,6 +989,12 @@ class Process:
|
|
|
658
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)):
|
|
659
990
|
raise errors.MemoryReadError(result, address)
|
|
660
991
|
|
|
992
|
+
# Save process reference and read address
|
|
993
|
+
if not discard_metadata:
|
|
994
|
+
|
|
995
|
+
buffer._process_ = self
|
|
996
|
+
buffer._address_ = address
|
|
997
|
+
|
|
661
998
|
return buffer
|
|
662
999
|
|
|
663
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
|