ntmemoryapi 2.2.5__tar.gz → 2.4.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.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.3
2
+ Name: ntmemoryapi
3
+ Version: 2.4.0
4
+ Summary: Add your description here
5
+ Requires-Dist: psutil>=7.2.2
6
+ Requires-Python: >=3.13
7
+ Description-Content-Type: text/markdown
8
+
@@ -0,0 +1,13 @@
1
+ [project]
2
+ name = "ntmemoryapi"
3
+ version = "2.4.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "psutil>=7.2.2",
9
+ ]
10
+
11
+ [build-system]
12
+ requires = ["uv_build>=0.10.4,<0.11.0"]
13
+ build-backend = "uv_build"
@@ -6,18 +6,20 @@
6
6
  # +-------------------------------------+
7
7
 
8
8
  import os
9
+ import typing
9
10
  import ctypes
10
11
  import psutil
11
12
 
12
13
  # Local imports
13
- from .misc import *
14
- from .embed import *
15
- from .errors import *
14
+ from . import misc
15
+ from . import embed
16
+ from . import errors
17
+ from . import structs
16
18
 
17
19
  # ==-------------------------------------------------------------------== #
18
20
  # Static and global variables, constans #
19
21
  # ==-------------------------------------------------------------------== #
20
- syscall_wrapper = DirectSyscallWrapper()
22
+ syscall_wrapper = misc.DirectSyscallWrapper()
21
23
 
22
24
  PROCESS_ID = 1
23
25
  PROCESS_NAME = 2
@@ -72,126 +74,6 @@ TH32CS_SNAPMODULE = 0x00000008
72
74
  # ==-------------------------------------------------------------------== #
73
75
  # C-structs #
74
76
  # ==-------------------------------------------------------------------== #
75
- class CLIENT_ID(ctypes.Structure):
76
- """Client-ID structure that required to open process."""
77
-
78
- _fields_ = [
79
- ("unique_process", ctypes.c_void_p),
80
- ("unique_thread", ctypes.c_void_p),
81
- ]
82
-
83
-
84
- class OBJECT_ATTRIBUTES(ctypes.Structure):
85
- """Object attributes structure that can be applied to objects or object handles."""
86
-
87
- _fields_ = [
88
- ("length", ctypes.c_ulong),
89
- ("root_directory", ctypes.c_void_p),
90
- ("object_name", ctypes.c_void_p),
91
- ("attributes", ctypes.c_ulong),
92
- ("security_descriptor", ctypes.c_void_p),
93
- ("security_quality_of_service", ctypes.c_void_p),
94
- ]
95
-
96
-
97
- class MODULEENTRY32(ctypes.Structure):
98
- """Module entry structure, describes an entry from a list of the modules belonging to the specified process."""
99
-
100
- _fields_ = [
101
- ("dw_size", ctypes.c_ulong),
102
- ("module_id", ctypes.c_ulong),
103
- ("process_id", ctypes.c_ulong),
104
- ("glbl_cnt_usage", ctypes.c_ulong),
105
- ("proc_cnt_usage", ctypes.c_ulong),
106
- ("mod_base_addr", ctypes.c_void_p),
107
- ("mod_base_size", ctypes.c_ulong),
108
- ("h_module", ctypes.c_void_p),
109
- ("sz_module", ctypes.c_char * 256),
110
- ("sz_exe_path", ctypes.c_char * 260),
111
- ]
112
-
113
- @property
114
- def name(self) -> str:
115
- """Process module name."""
116
-
117
- return self.sz_module.decode()
118
-
119
- @property
120
- def base(self) -> int:
121
- """Process module base address."""
122
-
123
- return self.mod_base_addr
124
-
125
- @property
126
- def size(self) -> int:
127
- """Process module base address."""
128
-
129
- return self.mod_base_size
130
-
131
-
132
- class MEMORY_BASIC_INFORMATION(ctypes.Structure):
133
- """Memory basic information structure that containg information about a range of pages in the virtual address space of a proces"""
134
-
135
- _fields_ = [
136
- ("m_base_address", ctypes.c_void_p),
137
- ("m_allocation_base", ctypes.c_void_p),
138
- ("m_allocation_protect", ctypes.c_ulong),
139
- ("m_partition_id", ctypes.c_ushort),
140
- ("m_region_size", ctypes.c_size_t),
141
- ("m_state", ctypes.c_ulong),
142
- ("m_protect", ctypes.c_ulong),
143
- ("m_type", ctypes.c_ulong),
144
- ]
145
-
146
- @property
147
- def base_address(self) -> int:
148
- """Memory region base address."""
149
-
150
- return self.m_base_address
151
-
152
- @property
153
- def allocation_base(self) -> int:
154
- """Memory region allocation base."""
155
-
156
- return self.m_allocation_base
157
-
158
- @property
159
- def allocation_protect(self) -> int:
160
- """Memory region allocation protect."""
161
-
162
- return self.m_allocation_protect
163
-
164
- @property
165
- def partition_id(self) -> int:
166
- """Memory region partition ID."""
167
-
168
- return self.m_partition_id
169
-
170
- @property
171
- def size(self) -> int:
172
- """Memory region size."""
173
-
174
- return self.m_region_size
175
-
176
- @property
177
- def state(self) -> int:
178
- """Memory region state."""
179
-
180
- return self.m_state
181
-
182
- @property
183
- def protect(self) -> int:
184
- """Memory region protect."""
185
-
186
- return self.m_protect
187
-
188
- @property
189
- def type(self) -> int:
190
- """Memory region type."""
191
-
192
- return self.m_type
193
-
194
-
195
77
  class PatternScanBuffer(ctypes.Structure):
196
78
  """Structure that containing pointer to array received from SIMD KMP after call of `scanAOB` function."""
197
79
 
@@ -219,10 +101,10 @@ class PatternScanBuffer(ctypes.Structure):
219
101
  # ==-------------------------------------------------------------------== #
220
102
  # Syscalls #
221
103
  # ==-------------------------------------------------------------------== #
222
- syscall_wrapper = DirectSyscallWrapper()
104
+ syscall_wrapper = misc.DirectSyscallWrapper()
223
105
 
224
106
  _nt_close = syscall_wrapper.wrap("NtClose", result_type=ctypes.c_ulong, arguments_types=[ctypes.c_void_p])
225
- _nt_open_process = syscall_wrapper.wrap("NtOpenProcess", result_type=ctypes.c_ulong, arguments_types=[ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(OBJECT_ATTRIBUTES), ctypes.POINTER(CLIENT_ID)])
107
+ _nt_open_process = syscall_wrapper.wrap("NtOpenProcess", result_type=ctypes.c_ulong, arguments_types=[ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(structs.OBJECT_ATTRIBUTES), ctypes.POINTER(structs.CLIENT_ID)])
226
108
 
227
109
  _nt_read_virtual_memory = syscall_wrapper.wrap("NtReadVirtualMemory", result_type=ctypes.c_ulong, arguments_types=[ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(ctypes.c_ulong)])
228
110
  _nt_write_virtual_memory = syscall_wrapper.wrap("NtWriteVirtualMemory", result_type=ctypes.c_ulong, arguments_types=[ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(ctypes.c_ulong)])
@@ -291,20 +173,20 @@ class Process:
291
173
  # ==--------------------------------== #
292
174
  # Public methods #
293
175
  # ==--------------------------------== #
294
- def __init__(self, name_or_pid: str | int, access: int = PROCESS_ALL_ACCESS) -> None:
176
+ def __init__(self, name_or_pid: str | int, access_flags: int = PROCESS_ALL_ACCESS) -> None:
295
177
  """Initialize instance to manipulate process."""
296
178
 
297
179
  # Open process by it's ID or it's name
298
180
  match name_or_pid:
299
181
 
300
182
  case str():
301
- self.handle, self.pid, self.name = self.__init_with_name(name_or_pid, access)
183
+ self.handle, self.pid, self.name, self.access_flags = *self.__init_with_name(name_or_pid, access_flags), access_flags
302
184
 
303
185
  case int():
304
- self.handle, self.pid, self.name = self.__init_with_pid(name_or_pid, access)
186
+ self.handle, self.pid, self.name, self.access_flags = *self.__init_with_pid(name_or_pid, access_flags), access_flags
305
187
 
306
188
  case _:
307
- raise OpenProcessError("Invalid `name_or_pid` argument value, have to be `int` or `str` type")
189
+ raise errors.OpenProcessError("Invalid `name_or_pid` argument value, have to be `int` or `str` type")
308
190
 
309
191
  # Try create file at temp directory to load SIMD KMP .dll (Module to blazingly fast pattern scaning)
310
192
  try:
@@ -319,54 +201,118 @@ class Process:
319
201
  # Load library
320
202
  self.__kmp = ctypes.WinDLL(appdata + "\\simdkmp.dll")
321
203
 
322
- def list_modules(self) -> list[MODULEENTRY32]:
204
+ def list_modules(self, method: typing.Literal["pebwalk", "snapshot"] = "pebwalk") -> list[structs.MODULEENTRY32]:
323
205
  """List all of the modules loaded to process."""
324
206
 
325
- # Modules snapshot functions
326
- close_handle = ctypes.windll.kernel32.CloseHandle
327
- module32_next = ctypes.windll.kernel32.Module32Next
328
- module32_first = ctypes.windll.kernel32.Module32First
329
- create_tool_help32_snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot
207
+ def list_modules_peb() -> list[structs.MODULEENTRY32]:
208
+ """Innter function to list process modules using syscall wraps and manual PEB struct walking."""
330
209
 
331
- # Create snapshot to iterate process modules
332
- if (snapshot := create_tool_help32_snapshot(TH32CS_SNAPMODULE, self.pid)) == -1:
333
- raise ctypes.WinError(descr="Unable to create snapshot for `%s` PID" % self.pid)
210
+ # If privileges are insufficient to list process modules
211
+ if not self.access_flags & PROCESS_VM_READ:
212
+ raise ctypes.WinError(descr="Unable to list process modules, insufficient privilages")
334
213
 
335
- # Result list of process modules
336
- process_modules = []
214
+ # If process is not 64-bit
215
+ if not self.is_64bit():
216
+ raise RuntimeError("Unable to list modules for not 64 bit process using `%s` method, use another one method instead")
337
217
 
338
- # Process modules enumeration
339
- try:
218
+ # If process PEB retrieve failed
219
+ if not (process_peb := self.get_information().peb):
220
+ raise RuntimeError("Process `%s` PEB address is null-value" % self.name)
340
221
 
341
- # Modules entry to iterate process modules
342
- modules_entry = MODULEENTRY32()
343
- modules_entry.dw_size = ctypes.sizeof(MODULEENTRY32)
222
+ # Result list of process modules
223
+ process_modules = []
344
224
 
345
- # Retrieve first process module snapshot
346
- if not module32_first(snapshot, ctypes.byref(modules_entry)):
347
- raise ctypes.WinError(descr="Unable to get first proces module to save to snapshot for `%s` PID" % self.pid)
225
+ # Get PEB_LDR_DATA by fixed offset
226
+ process_peb_ldr = self.read_uint64(process_peb + 0x18)
348
227
 
349
- # Iterate all of the process modules using snapshot
350
- while True:
228
+ # Get pointer to linked list head to collect all loaded modules information
229
+ process_peb_modules_ll = self.read_uint64(list_head := process_peb_ldr + 0x20)
351
230
 
352
- # Create copy of module entry
353
- ctypes.memmove(ctypes.byref(module_entry_copy := MODULEENTRY32()), ctypes.byref(modules_entry), ctypes.sizeof(MODULEENTRY32))
231
+ # Get node from modules linked list
232
+ modules_ll_node = self.read_uint64(process_peb_modules_ll)
354
233
 
355
- # Save process module to list
356
- process_modules.append(module_entry_copy)
234
+ # Iterator through all of the modules
235
+ while modules_ll_node and modules_ll_node != list_head:
357
236
 
358
- # snapshot is over
359
- if not module32_next(snapshot, ctypes.byref(modules_entry)):
360
- break
237
+ # Read module information
238
+ module = self.read_into_buffer(modules_ll_node, structs.LDR_DATA_TABLE_ENTRY())
239
+
240
+ # Get module on disk path
241
+ module_name = module.read_name(self)
242
+
243
+ # Save process module information to list
244
+ process_modules.append(structs.MODULEENTRY32(
245
+ mod_base_addr=module.dll_base,
246
+ sz_exe_path=module_name.encode(),
247
+ sz_module=module_name.split("\\")[-1].encode(),
248
+ ))
249
+
250
+ # Update node pointer
251
+ modules_ll_node = module.flink
252
+
253
+ return process_modules
254
+
255
+ def list_modules_snapshot() -> list[structs.MODULEENTRY32]:
256
+ """Inner function to list process modules using standart WinAPI methods."""
257
+
258
+ # Result list of process modules
259
+ process_modules = []
260
+
261
+ # Modules snapshot functions
262
+ close_handle = ctypes.windll.kernel32.CloseHandle
263
+ module32_next = ctypes.windll.kernel32.Module32Next
264
+ module32_first = ctypes.windll.kernel32.Module32First
265
+ create_tool_help32_snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot
266
+
267
+ # Create snapshot to iterate process modules
268
+ if (snapshot := create_tool_help32_snapshot(TH32CS_SNAPMODULE, self.pid)) == -1:
269
+ raise ctypes.WinError(descr="Unable to create snapshot for `%s` PID" % self.pid)
270
+
271
+ # Process modules enumeration
272
+ try:
273
+
274
+ # Modules entry to iterate process modules
275
+ modules_entry = structs.MODULEENTRY32()
276
+ modules_entry.dw_size = ctypes.sizeof(structs.MODULEENTRY32)
277
+
278
+ # Retrieve first process module snapshot
279
+ if not module32_first(snapshot, ctypes.byref(modules_entry)):
280
+ raise ctypes.WinError(descr="Unable to get first proces module to save to snapshot for `%s` PID" % self.pid)
281
+
282
+ # Iterate all of the process modules using snapshot
283
+ while True:
361
284
 
362
- finally:
285
+ # Create copy of module entry
286
+ ctypes.memmove(ctypes.byref(module_entry_copy := structs.MODULEENTRY32()), ctypes.byref(modules_entry), ctypes.sizeof(structs.MODULEENTRY32))
363
287
 
364
- # Close snapshot handle
365
- close_handle(snapshot)
288
+ # Save process module information to list
289
+ process_modules.append(module_entry_copy)
366
290
 
367
- return process_modules
291
+ # snapshot is over
292
+ if not module32_next(snapshot, ctypes.byref(modules_entry)):
293
+ break
368
294
 
369
- def list_memory_regions(self, allowed_states: list[int] = [], allowed_protects: list[int] = [], allowed_types: list[int] = [], memory_regions_filter: typing.Callable[[MEMORY_BASIC_INFORMATION], bool] | None = None) -> list[MEMORY_BASIC_INFORMATION]:
295
+ finally:
296
+
297
+ # Close snapshot handle
298
+ close_handle(snapshot)
299
+
300
+ return process_modules
301
+
302
+ # If method is not allowed
303
+ 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:
304
+ raise ValueError("Method literal is invalid, expected one of: `%s`" % ", ".join(allowed_methods))
305
+
306
+ # List modules with selected find method
307
+ match method:
308
+
309
+ case "pebwalk":
310
+ return list_modules_peb()
311
+
312
+ case "snapshot":
313
+ return list_modules_snapshot()
314
+
315
+ def list_memory_regions(self, allowed_states: list[int] = [], allowed_protects: list[int] = [], allowed_types: list[int] = [], memory_regions_filter: typing.Callable[[structs.MEMORY_BASIC_INFORMATION], bool] | None = None) -> list[structs.MEMORY_BASIC_INFORMATION]:
370
316
  """List all of the aviable process memory regions."""
371
317
 
372
318
  # Result list of memory regions
@@ -377,7 +323,7 @@ class Process:
377
323
  while True:
378
324
 
379
325
  # Prepare arguments
380
- memory_basic_information = MEMORY_BASIC_INFORMATION()
326
+ memory_basic_information = structs.MEMORY_BASIC_INFORMATION()
381
327
 
382
328
  # Try to get memory region information using it's address
383
329
  if (result := _nt_virtual_query_memory(self.handle, current_address, 0, ctypes.byref(memory_basic_information), ctypes.sizeof(memory_basic_information), None)):
@@ -387,7 +333,7 @@ class Process:
387
333
  break
388
334
 
389
335
  else:
390
- raise NtError("NtVirtualQueryMemory failed with status: 0x%s" % hex(result)[2:].upper())
336
+ raise errors.NtError("NtVirtualQueryMemory failed with status: 0x%s" % hex(result)[2:].upper())
391
337
 
392
338
  # Move to next memory region
393
339
  if (next_address := current_address + memory_basic_information.size) <= current_address:
@@ -414,11 +360,35 @@ class Process:
414
360
 
415
361
  return memory_regions
416
362
 
417
- def get_module(self, name: str) -> MODULEENTRY32:
363
+ def get_information(self) -> structs.PROCESS_BASIC_INFORMATION_64:
364
+ """Gets process information."""
365
+
366
+ # If privileges are insufficient to query proess information
367
+ if not self.access_flags & PROCESS_QUERY_INFORMATION:
368
+ raise ctypes.WinError(descr="Unable to query process information, insufficient privilages")
369
+
370
+ # If process is not 64-bit
371
+ if not self.is_64bit():
372
+ raise RuntimeError("Unable to get process information for not 64 bit process using, method is not yet implemented")
373
+
374
+ # Prepare arguments
375
+ process_basic_information = structs.PROCESS_BASIC_INFORMATION_64()
376
+
377
+ # Try to get process information
378
+ if (result := _nt_query_information_process(self.handle, 0, ctypes.byref(process_basic_information), ctypes.sizeof(process_basic_information), None)):
379
+ raise errors.NtError("NtVirtualQueryMemory failed with status: 0x%s" % hex(result)[2:].upper())
380
+
381
+ return process_basic_information
382
+
383
+ def get_module(self, name: str, method: typing.Literal["pebwalk", "snapshot"] = "pebwalk") -> structs.MODULEENTRY32:
418
384
  """Get process module information."""
419
385
 
386
+ # If method is not allowed
387
+ 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:
388
+ raise ValueError("Method literal is invalid, expected one of: `%s`" % ", ".join(allowed_methods))
389
+
420
390
  # Process modules enumeration
421
- for module in self.list_modules():
391
+ for module in self.list_modules(method):
422
392
 
423
393
  # If module have a required name
424
394
  if module.name.lower() == name.strip().lower():
@@ -426,15 +396,15 @@ class Process:
426
396
 
427
397
  raise ValueError("Module with `%s` name not found" % name)
428
398
 
429
- def get_memory_region(self, address: int) -> MEMORY_BASIC_INFORMATION:
399
+ def get_memory_region(self, address: int) -> structs.MEMORY_BASIC_INFORMATION:
430
400
  """Get memory region information located at given address."""
431
401
 
432
402
  # Prepare arguments
433
- memory_basic_information = MEMORY_BASIC_INFORMATION()
403
+ memory_basic_information = structs.MEMORY_BASIC_INFORMATION()
434
404
 
435
405
  # Try to get memory region information using it's address
436
406
  if (result := _nt_virtual_query_memory(self.handle, address, 0, ctypes.byref(memory_basic_information), ctypes.sizeof(memory_basic_information), None)):
437
- raise NtError("NtVirtualQueryMemory failed with status: 0x%s" % hex(result)[2:].upper())
407
+ raise errors.NtError("NtVirtualQueryMemory failed with status: 0x%s" % hex(result)[2:].upper())
438
408
 
439
409
  return memory_basic_information
440
410
 
@@ -445,8 +415,8 @@ class Process:
445
415
  wow64_info = ctypes.c_void_p()
446
416
 
447
417
  # Try to query process information
448
- if (result := _nt_query_information_process(self.handle, 26, ctypes.byref(wow64_info), ctypes.sizeof(wow64_info), ctypes.byref(ctypes.c_ulong()))):
449
- raise NtError("NtQueryInformationProcess failed with status: 0x%s" % hex(result)[2:].upper())
418
+ if (result := _nt_query_information_process(self.handle, 0x1A, ctypes.byref(wow64_info), ctypes.sizeof(wow64_info), ctypes.byref(ctypes.c_ulong()))):
419
+ raise errors.NtError("NtQueryInformationProcess failed with status: 0x%s" % hex(result)[2:].upper())
450
420
 
451
421
  return wow64_info.value is None
452
422
 
@@ -459,7 +429,7 @@ class Process:
459
429
 
460
430
  # Try to allocate process memory
461
431
  if (result := _nt_allocate_virtual_memory(self.handle, ctypes.byref(allocation_address), 0, ctypes.byref(allocation_size), memory_type, memory_protect)):
462
- raise NtError("NtAllocateVirtualMemory failed with status: 0x%s" % hex(result)[2:].upper())
432
+ raise errors.NtError("NtAllocateVirtualMemory failed with status: 0x%s" % hex(result)[2:].upper())
463
433
 
464
434
  return allocation_address.value or 0, allocation_size.value or 0
465
435
 
@@ -472,7 +442,7 @@ class Process:
472
442
 
473
443
  # Try to deallocate process memory
474
444
  if (result := _nt_free_virtual_memory(self.handle, ctypes.byref(allocation_address), ctypes.byref(allocation_size), memory_type)):
475
- raise NtError("NtFreeVirtualMemory failed with status: 0x%s" % hex(result)[2:].upper())
445
+ raise errors.NtError("NtFreeVirtualMemory failed with status: 0x%s" % hex(result)[2:].upper())
476
446
 
477
447
  return allocation_size.value or 0
478
448
 
@@ -485,11 +455,11 @@ class Process:
485
455
 
486
456
  # Try to change memory protection
487
457
  if (result := _nt_protect_virtual_memory(self.handle, ctypes.byref(protect_address), ctypes.byref(protect_size), memory_protect, ctypes.byref(memory_old_protect := ctypes.c_ulong()))):
488
- raise NtError("NtProtectVirtualMemory failed with status: 0x%s" % hex(result)[2:].upper())
458
+ raise errors.NtError("NtProtectVirtualMemory failed with status: 0x%s" % hex(result)[2:].upper())
489
459
 
490
460
  return memory_old_protect.value or 0
491
461
 
492
- def pattern_scan(self, pattern: str, allowed_states: list[int] = [MEM_COMMIT], allowed_protects: list[int] = [PAGE_READWRITE], allowed_types: list[int] = [], start_address: int | None = None, end_address: int | None = None, return_first: int | None = None, memory_regions_filter: typing.Callable[[MEMORY_BASIC_INFORMATION], bool] | None = None, finalize_regions_callback: typing.Callable[[list[MEMORY_BASIC_INFORMATION]], None] | None = None) -> list[int]:
462
+ def pattern_scan(self, pattern: str, allowed_states: list[int] = [MEM_COMMIT], allowed_protects: list[int] = [PAGE_READWRITE], allowed_types: list[int] = [], start_address: int | None = None, end_address: int | None = None, return_first: int | None = None, memory_regions_filter: typing.Callable[[structs.MEMORY_BASIC_INFORMATION], bool] | None = None, finalize_regions_callback: typing.Callable[[list[structs.MEMORY_BASIC_INFORMATION]], None] | None = None, buffered: bool = True) -> list[int]:
493
463
  """Scan process and return address that validates given pattern hex byte mask, use `??` to wildcard byte, for example - "14 00 00 00 DB FF ?? ?? FF FF 00 00"."""
494
464
 
495
465
  # Validate given pattern
@@ -554,7 +524,7 @@ class Process:
554
524
  try:
555
525
 
556
526
  # Read bytes into pre-allocated buffer
557
- self.read_into_buffer(region.base_address, region.size, ctypes.c_void_p(ctypes.addressof(read_memory_buffer)))
527
+ self.read_into_buffer(region.base_address, read_memory_buffer, region.size)
558
528
 
559
529
  except Exception:
560
530
  continue
@@ -583,7 +553,7 @@ class Process:
583
553
 
584
554
  # If result failed
585
555
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer := ctypes.c_int8()), ctypes.sizeof(buffer), None)):
586
- raise MemoryReadError(result, address)
556
+ raise errors.MemoryReadError(result, address)
587
557
 
588
558
  return buffer.value
589
559
 
@@ -592,7 +562,7 @@ class Process:
592
562
 
593
563
  # If result failed
594
564
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_int16)() if big_endian else ctypes.c_int16()), ctypes.sizeof(buffer), None)):
595
- raise MemoryReadError(result, address)
565
+ raise errors.MemoryReadError(result, address)
596
566
 
597
567
  return buffer.value
598
568
 
@@ -601,7 +571,7 @@ class Process:
601
571
 
602
572
  # If result failed
603
573
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_int32)() if big_endian else ctypes.c_int32()), ctypes.sizeof(buffer), None)):
604
- raise MemoryReadError(result, address)
574
+ raise errors.MemoryReadError(result, address)
605
575
 
606
576
  return buffer.value
607
577
 
@@ -610,7 +580,7 @@ class Process:
610
580
 
611
581
  # If result failed
612
582
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_int64)() if big_endian else ctypes.c_int64()), ctypes.sizeof(buffer), None)):
613
- raise MemoryReadError(result, address)
583
+ raise errors.MemoryReadError(result, address)
614
584
 
615
585
  return buffer.value
616
586
 
@@ -619,7 +589,7 @@ class Process:
619
589
 
620
590
  # If result failed
621
591
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer := ctypes.c_uint8()), ctypes.sizeof(buffer), None)):
622
- raise MemoryReadError(result, address)
592
+ raise errors.MemoryReadError(result, address)
623
593
 
624
594
  return buffer.value
625
595
 
@@ -628,7 +598,7 @@ class Process:
628
598
 
629
599
  # If result failed
630
600
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_uint16)() if big_endian else ctypes.c_uint16()), ctypes.sizeof(buffer), None)):
631
- raise MemoryReadError(result, address)
601
+ raise errors.MemoryReadError(result, address)
632
602
 
633
603
  return buffer.value
634
604
 
@@ -637,7 +607,7 @@ class Process:
637
607
 
638
608
  # If result failed
639
609
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_uint32)() if big_endian else ctypes.c_uint32()), ctypes.sizeof(buffer), None)):
640
- raise MemoryReadError(result, address)
610
+ raise errors.MemoryReadError(result, address)
641
611
 
642
612
  return buffer.value
643
613
 
@@ -646,7 +616,7 @@ class Process:
646
616
 
647
617
  # If result failed
648
618
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_uint64)() if big_endian else ctypes.c_uint64()), ctypes.sizeof(buffer), None)):
649
- raise MemoryReadError(result, address)
619
+ raise errors.MemoryReadError(result, address)
650
620
 
651
621
  return buffer.value
652
622
 
@@ -655,7 +625,7 @@ class Process:
655
625
 
656
626
  # If result failed
657
627
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_float)() if big_endian else ctypes.c_float()), ctypes.sizeof(buffer), None)):
658
- raise MemoryReadError(result, address)
628
+ raise errors.MemoryReadError(result, address)
659
629
 
660
630
  return buffer.value
661
631
 
@@ -664,7 +634,7 @@ class Process:
664
634
 
665
635
  # If result failed
666
636
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_double)() if big_endian else ctypes.c_double()), ctypes.sizeof(buffer), None)):
667
- raise MemoryReadError(result, address)
637
+ raise errors.MemoryReadError(result, address)
668
638
 
669
639
  return buffer.value
670
640
 
@@ -673,109 +643,106 @@ class Process:
673
643
 
674
644
  # If result failed
675
645
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_int8 * size)() if big_endian else (ctypes.c_int8 * size)()), ctypes.sizeof(buffer), None)):
676
- raise MemoryReadError(result, address)
646
+ raise errors.MemoryReadError(result, address)
677
647
 
678
648
  return bytes(buffer)
679
649
 
680
- def read_buffer(self, address: int, buffer: typing.Any) -> typing.Any:
681
- """Read size of buffer byte value located at given address to buffer. Buffer have to be able passed at `ctypes.byref` and `ctype.sizeof`."""
650
+ def read_into_buffer(self, address: int, buffer: typing.Any, read_bytes_size: int | None = None) -> typing.Any:
651
+ """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
+
653
+ # If read size is invalid
654
+ if read_bytes_size is not None and read_bytes_size <= 0:
655
+ raise ValueError("Unable to read value into buffer due the invalid size: `%s`" % read_bytes_size)
682
656
 
683
657
  # If result failed
684
- if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer), ctypes.sizeof(buffer), None)):
685
- raise MemoryReadError(result, address)
658
+ 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
+ raise errors.MemoryReadError(result, address)
686
660
 
687
661
  return buffer
688
662
 
689
- def read_into_buffer(self, address: int, size: int, buffer_pointer: int) -> None:
690
- """Read bytes array of variadic size located at given address into pre-allocated internal buffer by pointer."""
691
-
692
- # If result failed
693
- if (result := _nt_read_virtual_memory(self.handle, address, buffer_pointer, size, None)):
694
- raise MemoryReadError(result, address)
695
-
696
663
  def write_int8(self, address: int, value: int) -> None:
697
664
  """Write 1 byte signed integer value at given address."""
698
665
 
699
666
  # If result failed
700
667
  if (result := _nt_write_virtual_memory(self.handle, address, ctypes.byref(buffer := ctypes.c_int8(value)), ctypes.sizeof(buffer), None)):
701
- raise MemoryWriteError(result, address)
668
+ raise errors.MemoryWriteError(result, address)
702
669
 
703
670
  def write_int16(self, address: int, value: int, big_endian: bool = False) -> None:
704
671
  """Write 2 byte signed integer value at given address."""
705
672
 
706
673
  # If result failed
707
674
  if (result := _nt_write_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_int16(value)) if big_endian else ctypes.c_int16(value)), ctypes.sizeof(buffer), None)):
708
- raise MemoryWriteError(result, address)
675
+ raise errors.MemoryWriteError(result, address)
709
676
 
710
677
  def write_int32(self, address: int, value: int, big_endian: bool = False) -> None:
711
678
  """Write 4 byte signed integer value at given address."""
712
679
 
713
680
  # If result failed
714
681
  if (result := _nt_write_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_int32(value)) if big_endian else ctypes.c_int32(value)), ctypes.sizeof(buffer), None)):
715
- raise MemoryWriteError(result, address)
682
+ raise errors.MemoryWriteError(result, address)
716
683
 
717
684
  def write_int64(self, address: int, value: int, big_endian: bool = False) -> None:
718
685
  """Write 8 byte signed integer value at given address."""
719
686
 
720
687
  # If result failed
721
688
  if (result := _nt_write_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_int64(value)) if big_endian else ctypes.c_int64(value)), ctypes.sizeof(buffer), None)):
722
- raise MemoryWriteError(result, address)
689
+ raise errors.MemoryWriteError(result, address)
723
690
 
724
691
  def write_uint8(self, address: int, value: int) -> None:
725
692
  """Write 1 byte unsigned integer value at given address."""
726
693
 
727
694
  # If result failed
728
695
  if (result := _nt_write_virtual_memory(self.handle, address, ctypes.byref(buffer := ctypes.c_uint8(value)), ctypes.sizeof(buffer), None)):
729
- raise MemoryWriteError(result, address)
696
+ raise errors.MemoryWriteError(result, address)
730
697
 
731
698
  def write_uint16(self, address: int, value: int, big_endian: bool = False) -> None:
732
699
  """Write 2 byte unsigned integer value at given address."""
733
700
 
734
701
  # If result failed
735
702
  if (result := _nt_write_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_uint16(value)) if big_endian else ctypes.c_uint16(value)), ctypes.sizeof(buffer), None)):
736
- raise MemoryWriteError(result, address)
703
+ raise errors.MemoryWriteError(result, address)
737
704
 
738
705
  def write_uint32(self, address: int, value: int, big_endian: bool = False) -> None:
739
706
  """Write 4 byte unsigned integer value at given address."""
740
707
 
741
708
  # If result failed
742
709
  if (result := _nt_write_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_uint32(value)) if big_endian else ctypes.c_uint32(value)), ctypes.sizeof(buffer), None)):
743
- raise MemoryWriteError("NtWriteVirtualMemory failed with status: 0x%s" % hex(result)[2:].upper())
710
+ raise errors.MemoryWriteError("NtWriteVirtualMemory failed with status: 0x%s" % hex(result)[2:].upper())
744
711
 
745
712
  def write_uint64(self, address: int, value: int, big_endian: bool = False) -> None:
746
713
  """Write 8 byte unsigned integer value at given address."""
747
714
 
748
715
  # If result failed
749
716
  if (result := _nt_write_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_uint64(value)) if big_endian else ctypes.c_uint64(value)), ctypes.sizeof(buffer), None)):
750
- raise MemoryWriteError(result, address)
717
+ raise errors.MemoryWriteError(result, address)
751
718
 
752
719
  def write_float32(self, address: int, value: float | int, big_endian: bool = False) -> float:
753
720
  """Write 4 byte floating-point digit value at given address."""
754
721
 
755
722
  # If result failed
756
723
  if (result := _nt_write_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_float(value)) if big_endian else ctypes.c_float(value)), ctypes.sizeof(buffer), None)):
757
- raise MemoryWriteError(result, address)
724
+ raise errors.MemoryWriteError(result, address)
758
725
 
759
726
  def write_float64(self, address: int, value: float | int, big_endian: bool = False) -> float:
760
727
  """Write 8 byte floating-point digit value at given address."""
761
728
 
762
729
  # If result failed
763
730
  if (result := _nt_write_virtual_memory(self.handle, address, ctypes.byref(buffer := _get_be_buffer(ctypes.c_double(value)) if big_endian else ctypes.c_double(value)), ctypes.sizeof(buffer), None)):
764
- raise MemoryWriteError(result, address)
731
+ raise errors.MemoryWriteError(result, address)
765
732
 
766
733
  def write_bytes(self, address: int, value: bytes, big_endian: bool = False) -> None:
767
734
  """Write bytes array of variadic size at given address."""
768
735
 
769
736
  # If result failed
770
737
  if (result := _nt_write_virtual_memory(self.handle, address, value[::-1] if big_endian else value, len(value), None)):
771
- raise MemoryWriteError(result, address)
738
+ raise errors.MemoryWriteError(result, address)
772
739
 
773
740
  def write_buffer(self, address: int, buffer: typing.Any) -> None:
774
- """Write size of buffer byte value at given address. Buffer have to be able passed at `ctypes.byref` and `ctype.sizeof`."""
741
+ """Write size of buffer byte value at given address. Buffer have to be able passed at `ctypes.byref` and `ctypes.sizeof`."""
775
742
 
776
743
  # If result failed
777
744
  if (result := _nt_write_virtual_memory(self.handle, address, ctypes.byref(buffer), ctypes.sizeof(buffer), None)):
778
- raise MemoryWriteError(result, address)
745
+ raise errors.MemoryWriteError(result, address)
779
746
 
780
747
  def close(self) -> None:
781
748
  """Close opened process using it's handle, have to be called once on stop interacting with process."""
@@ -786,7 +753,7 @@ class Process:
786
753
  # ==--------------------------------== #
787
754
  # Private methods #
788
755
  # ==--------------------------------== #
789
- def __init_with_pid(self, pid: int, access: int, listed_processes: list[dict[str, int | str]] | None = None) -> tuple[int, int, str]:
756
+ def __init_with_pid(self, pid: int, access_flags: int, listed_processes: list[dict[str, int | str]] | None = None) -> tuple[int, int, str]:
790
757
  """Open process handle by it's ID with desired access."""
791
758
 
792
759
  # Iterate all of the processes
@@ -799,22 +766,22 @@ class Process:
799
766
  break
800
767
 
801
768
  else:
802
- raise OpenProcessError("Process with `%s` ID not found" % pid)
769
+ raise errors.OpenProcessError("Process with `%s` ID not found" % pid)
803
770
 
804
771
  # Prepare arguments
805
- object_attributes = OBJECT_ATTRIBUTES()
806
- object_attributes.length = ctypes.sizeof(OBJECT_ATTRIBUTES)
772
+ object_attributes = structs.OBJECT_ATTRIBUTES()
773
+ object_attributes.length = ctypes.sizeof(structs.OBJECT_ATTRIBUTES)
807
774
 
808
- client_id = CLIENT_ID()
775
+ client_id = structs.CLIENT_ID()
809
776
  client_id.unique_process = pid
810
777
 
811
778
  # Try to open process using it's ID
812
- if (result := _nt_open_process(ctypes.byref(handle := ctypes.c_void_p()), access, ctypes.byref(object_attributes), ctypes.byref(client_id))):
813
- raise NtError("NtOpenProcess failed with status: 0x%s" % hex(result)[2:].upper())
779
+ if (result := _nt_open_process(ctypes.byref(handle := ctypes.c_void_p()), access_flags, ctypes.byref(object_attributes), ctypes.byref(client_id))):
780
+ raise errors.NtError("NtOpenProcess failed with status: 0x%s" % hex(result)[2:].upper())
814
781
 
815
782
  return handle.value, pid, process_name
816
783
 
817
- def __init_with_name(self, name: str, access: int) -> tuple[int, int, str]:
784
+ def __init_with_name(self, name: str, access_flags: int) -> tuple[int, int, str]:
818
785
  """Open process hanle by it's name with desired access."""
819
786
 
820
787
  # Iterate all of the processes using snapshot
@@ -822,6 +789,6 @@ class Process:
822
789
 
823
790
  # If process have a reqired name
824
791
  if process["name"].lower() == name.strip().lower():
825
- return self.__init_with_pid(process["id"], access, listed_processes)
792
+ return self.__init_with_pid(process["id"], access_flags, listed_processes)
826
793
 
827
- raise OpenProcessError("Process with `%s` name not found" % name)
794
+ raise errors.OpenProcessError("Process with `%s` name not found" % name)
@@ -0,0 +1,219 @@
1
+ # +-------------------------------------+
2
+ # | ~ Author : Xenely ~ |
3
+ # +=====================================+
4
+ # | GitHub: https://github.com/Xenely14 |
5
+ # | Discord: xenely |
6
+ # +-------------------------------------+
7
+
8
+
9
+ import typing
10
+ import ctypes
11
+
12
+
13
+ # ==-------------------------------------------------------------------== #
14
+ # C-structs #
15
+ # ==-------------------------------------------------------------------== #
16
+ class CLIENT_ID(ctypes.Structure):
17
+ """Client-ID structure that required to open process."""
18
+
19
+ _fields_ = [
20
+ ("unique_process", ctypes.c_void_p),
21
+ ("unique_thread", ctypes.c_void_p),
22
+ ]
23
+
24
+
25
+ class LIST_ENTRY(ctypes.Structure):
26
+ """Linked list WinAPI node structure."""
27
+
28
+ _fields_ = [
29
+ ("flink", ctypes.c_void_p),
30
+ ("blink", ctypes.c_void_p),
31
+ ]
32
+
33
+
34
+ class UNICODE_STRING(ctypes.Structure):
35
+ """Unicode string structure that holds data to represent UTF-16LE WinAPI strings."""
36
+
37
+ _fields_ = [
38
+ ("length", ctypes.c_short),
39
+ ("maximum_length", ctypes.c_short),
40
+ ("buffer", ctypes.c_void_p),
41
+ ]
42
+
43
+ def read(self, process: typing.Any) -> str:
44
+ """Read string from process using it's instance."""
45
+
46
+ return process.read_bytes(self.buffer, self.length).decode("utf-16le", errors="ignore")
47
+
48
+
49
+ class LDR_DATA_TABLE_ENTRY(ctypes.Structure):
50
+ """Linked list entry structure that contains imformation about loaded into the process modules."""
51
+
52
+ _fields_ = [
53
+ ("flink", ctypes.c_void_p),
54
+ ("blink", ctypes.c_void_p),
55
+ ("reserved", ctypes.c_byte * 16),
56
+ ("dll_base", ctypes.c_void_p),
57
+ ("reserved2", ctypes.c_byte * 16),
58
+ ("base_dll_name", UNICODE_STRING),
59
+ ]
60
+
61
+ def read_name(self, process: typing.Any) -> str:
62
+ """Read module name."""
63
+
64
+ return self.base_dll_name.read(process)
65
+
66
+
67
+ class OBJECT_ATTRIBUTES(ctypes.Structure):
68
+ """Object attributes structure that can be applied to objects or object handles."""
69
+
70
+ _fields_ = [
71
+ ("length", ctypes.c_ulong),
72
+ ("root_directory", ctypes.c_void_p),
73
+ ("object_name", ctypes.c_void_p),
74
+ ("attributes", ctypes.c_ulong),
75
+ ("security_descriptor", ctypes.c_void_p),
76
+ ("security_quality_of_service", ctypes.c_void_p),
77
+ ]
78
+
79
+
80
+ class MODULEENTRY32(ctypes.Structure):
81
+ """Module entry structure, describes an entry from a list of the modules belonging to the specified process."""
82
+
83
+ _fields_ = [
84
+ ("dw_size", ctypes.c_ulong),
85
+ ("module_id", ctypes.c_ulong),
86
+ ("process_id", ctypes.c_ulong),
87
+ ("glbl_cnt_usage", ctypes.c_ulong),
88
+ ("proc_cnt_usage", ctypes.c_ulong),
89
+ ("mod_base_addr", ctypes.c_void_p),
90
+ ("mod_base_size", ctypes.c_ulong),
91
+ ("h_module", ctypes.c_void_p),
92
+ ("sz_module", ctypes.c_char * 256),
93
+ ("sz_exe_path", ctypes.c_char * 260),
94
+ ]
95
+
96
+ @property
97
+ def name(self) -> str:
98
+ """Process module name."""
99
+
100
+ return self.sz_module.decode()
101
+
102
+ @property
103
+ def path(self) -> str:
104
+ """process module on disk path."""
105
+
106
+ return self.sz_exe_path.decode()
107
+
108
+ @property
109
+ def base(self) -> int:
110
+ """Process module base address."""
111
+
112
+ return self.mod_base_addr
113
+
114
+ @property
115
+ def size(self) -> int:
116
+ """Process module base address."""
117
+
118
+ return self.mod_base_size
119
+
120
+
121
+ class MEMORY_BASIC_INFORMATION(ctypes.Structure):
122
+ """Memory basic information structure that contains information about a range of pages in the virtual address space of a proces"""
123
+
124
+ _fields_ = [
125
+ ("m_base_address", ctypes.c_void_p),
126
+ ("m_allocation_base", ctypes.c_void_p),
127
+ ("m_allocation_protect", ctypes.c_ulong),
128
+ ("m_partition_id", ctypes.c_ushort),
129
+ ("m_region_size", ctypes.c_size_t),
130
+ ("m_state", ctypes.c_ulong),
131
+ ("m_protect", ctypes.c_ulong),
132
+ ("m_type", ctypes.c_ulong),
133
+ ]
134
+
135
+ @property
136
+ def base_address(self) -> int:
137
+ """Memory region base address."""
138
+
139
+ return self.m_base_address
140
+
141
+ @property
142
+ def allocation_base(self) -> int:
143
+ """Memory region allocation base."""
144
+
145
+ return self.m_allocation_base
146
+
147
+ @property
148
+ def allocation_protect(self) -> int:
149
+ """Memory region allocation protect."""
150
+
151
+ return self.m_allocation_protect
152
+
153
+ @property
154
+ def partition_id(self) -> int:
155
+ """Memory region partition ID."""
156
+
157
+ return self.m_partition_id
158
+
159
+ @property
160
+ def size(self) -> int:
161
+ """Memory region size."""
162
+
163
+ return self.m_region_size
164
+
165
+ @property
166
+ def state(self) -> int:
167
+ """Memory region state."""
168
+
169
+ return self.m_state
170
+
171
+ @property
172
+ def protect(self) -> int:
173
+ """Memory region protect."""
174
+
175
+ return self.m_protect
176
+
177
+ @property
178
+ def type(self) -> int:
179
+ """Memory region type."""
180
+
181
+ return self.m_type
182
+
183
+
184
+ class PROCESS_BASIC_INFORMATION_X32(ctypes.Structure):
185
+ """Process basic information structure that contains information about the OS-process."""
186
+
187
+ _fields_ = [
188
+ ("ExitStatus", ctypes.c_uint32),
189
+ ("PebBaseAddress", ctypes.c_uint32),
190
+ ("AffinityMask", ctypes.c_uint32),
191
+ ("BasePriority", ctypes.c_int32),
192
+ ("UniqueProcessId", ctypes.c_uint32),
193
+ ("InheritedFromUniqueProcessId", ctypes.c_uint32),
194
+ ]
195
+
196
+ @property
197
+ def peb(self) -> int:
198
+ """Process peb base address."""
199
+
200
+ return self.peb_base_address
201
+
202
+
203
+ class PROCESS_BASIC_INFORMATION_64(ctypes.Structure):
204
+ """Process basic information structure that contains information about the OS-process."""
205
+
206
+ _fields_ = [
207
+ ("exit_status", ctypes.c_void_p),
208
+ ("peb_base_address", ctypes.c_void_p),
209
+ ("affinity_mask", ctypes.c_void_p),
210
+ ("base_priority", ctypes.c_uint32),
211
+ ("unique_process_id", ctypes.c_void_p),
212
+ ("inherited_from_unique_process_id", ctypes.c_void_p),
213
+ ]
214
+
215
+ @property
216
+ def peb(self) -> int:
217
+ """Process peb base address."""
218
+
219
+ return self.peb_base_address
@@ -1,9 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: ntmemoryapi
3
- Version: 2.2.5
4
- Summary: Simple library for Windows to manipulate process virtual memory with stelthy syscall wraps.
5
- Author: Xenely
6
- Requires-Dist: psutil>=7.1.3
7
- Requires-Python: >=3.10
8
- Description-Content-Type: text/markdown
9
-
@@ -1,19 +0,0 @@
1
- [project]
2
- name = "ntmemoryapi"
3
- version = "2.2.5"
4
- description = "Simple library for Windows to manipulate process virtual memory with stelthy syscall wraps."
5
- authors = [
6
- {name = "Xenely"}
7
- ]
8
- readme = "README.md"
9
- requires-python = ">=3.10"
10
- dependencies = [
11
- "psutil>=7.1.3",
12
- ]
13
-
14
- [project.scripts]
15
- ntmemoryapi = "ntmemoryapi:main"
16
-
17
- [build-system]
18
- requires = ["uv_build>=0.9.9,<0.10.0"]
19
- build-backend = "uv_build"
File without changes