ntmemoryapi 2.2.4__tar.gz → 2.3.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.3.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.3.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 procewss 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,7 +360,27 @@ 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) -> structs.MODULEENTRY32:
418
384
  """Get process module information."""
419
385
 
420
386
  # Process modules enumeration
@@ -426,15 +392,15 @@ class Process:
426
392
 
427
393
  raise ValueError("Module with `%s` name not found" % name)
428
394
 
429
- def get_memory_region(self, address: int) -> MEMORY_BASIC_INFORMATION:
395
+ def get_memory_region(self, address: int) -> structs.MEMORY_BASIC_INFORMATION:
430
396
  """Get memory region information located at given address."""
431
397
 
432
398
  # Prepare arguments
433
- memory_basic_information = MEMORY_BASIC_INFORMATION()
399
+ memory_basic_information = structs.MEMORY_BASIC_INFORMATION()
434
400
 
435
401
  # Try to get memory region information using it's address
436
402
  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())
403
+ raise errors.NtError("NtVirtualQueryMemory failed with status: 0x%s" % hex(result)[2:].upper())
438
404
 
439
405
  return memory_basic_information
440
406
 
@@ -445,8 +411,8 @@ class Process:
445
411
  wow64_info = ctypes.c_void_p()
446
412
 
447
413
  # 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())
414
+ if (result := _nt_query_information_process(self.handle, 0x1A, ctypes.byref(wow64_info), ctypes.sizeof(wow64_info), ctypes.byref(ctypes.c_ulong()))):
415
+ raise errors.NtError("NtQueryInformationProcess failed with status: 0x%s" % hex(result)[2:].upper())
450
416
 
451
417
  return wow64_info.value is None
452
418
 
@@ -459,7 +425,7 @@ class Process:
459
425
 
460
426
  # Try to allocate process memory
461
427
  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())
428
+ raise errors.NtError("NtAllocateVirtualMemory failed with status: 0x%s" % hex(result)[2:].upper())
463
429
 
464
430
  return allocation_address.value or 0, allocation_size.value or 0
465
431
 
@@ -472,7 +438,7 @@ class Process:
472
438
 
473
439
  # Try to deallocate process memory
474
440
  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())
441
+ raise errors.NtError("NtFreeVirtualMemory failed with status: 0x%s" % hex(result)[2:].upper())
476
442
 
477
443
  return allocation_size.value or 0
478
444
 
@@ -485,11 +451,11 @@ class Process:
485
451
 
486
452
  # Try to change memory protection
487
453
  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())
454
+ raise errors.NtError("NtProtectVirtualMemory failed with status: 0x%s" % hex(result)[2:].upper())
489
455
 
490
456
  return memory_old_protect.value or 0
491
457
 
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]:
458
+ 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
459
  """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
460
 
495
461
  # Validate given pattern
@@ -554,10 +520,10 @@ class Process:
554
520
  try:
555
521
 
556
522
  # 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)))
523
+ self.read_into_buffer(region.base_address, read_memory_buffer, region.size)
558
524
 
559
525
  except Exception:
560
- pass
526
+ continue
561
527
 
562
528
  # Pattern scan region using SIMD KMP algorithm
563
529
  self.__kmp.scanAOB(ctypes.c_void_p(ctypes.addressof(read_memory_buffer)), region.size, pattern.strip().encode(), ctypes.c_uint64(region.base_address), ctypes.c_uint64(return_first if return_first is not None else 0), ctypes.byref(scan_result := PatternScanBuffer()))
@@ -583,7 +549,7 @@ class Process:
583
549
 
584
550
  # If result failed
585
551
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer := ctypes.c_int8()), ctypes.sizeof(buffer), None)):
586
- raise MemoryReadError(result, address)
552
+ raise errors.MemoryReadError(result, address)
587
553
 
588
554
  return buffer.value
589
555
 
@@ -592,7 +558,7 @@ class Process:
592
558
 
593
559
  # If result failed
594
560
  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)
561
+ raise errors.MemoryReadError(result, address)
596
562
 
597
563
  return buffer.value
598
564
 
@@ -601,7 +567,7 @@ class Process:
601
567
 
602
568
  # If result failed
603
569
  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)
570
+ raise errors.MemoryReadError(result, address)
605
571
 
606
572
  return buffer.value
607
573
 
@@ -610,7 +576,7 @@ class Process:
610
576
 
611
577
  # If result failed
612
578
  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)
579
+ raise errors.MemoryReadError(result, address)
614
580
 
615
581
  return buffer.value
616
582
 
@@ -619,7 +585,7 @@ class Process:
619
585
 
620
586
  # If result failed
621
587
  if (result := _nt_read_virtual_memory(self.handle, address, ctypes.byref(buffer := ctypes.c_uint8()), ctypes.sizeof(buffer), None)):
622
- raise MemoryReadError(result, address)
588
+ raise errors.MemoryReadError(result, address)
623
589
 
624
590
  return buffer.value
625
591
 
@@ -628,7 +594,7 @@ class Process:
628
594
 
629
595
  # If result failed
630
596
  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)
597
+ raise errors.MemoryReadError(result, address)
632
598
 
633
599
  return buffer.value
634
600
 
@@ -637,7 +603,7 @@ class Process:
637
603
 
638
604
  # If result failed
639
605
  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)
606
+ raise errors.MemoryReadError(result, address)
641
607
 
642
608
  return buffer.value
643
609
 
@@ -646,7 +612,7 @@ class Process:
646
612
 
647
613
  # If result failed
648
614
  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)
615
+ raise errors.MemoryReadError(result, address)
650
616
 
651
617
  return buffer.value
652
618
 
@@ -655,7 +621,7 @@ class Process:
655
621
 
656
622
  # If result failed
657
623
  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)
624
+ raise errors.MemoryReadError(result, address)
659
625
 
660
626
  return buffer.value
661
627
 
@@ -664,7 +630,7 @@ class Process:
664
630
 
665
631
  # If result failed
666
632
  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)
633
+ raise errors.MemoryReadError(result, address)
668
634
 
669
635
  return buffer.value
670
636
 
@@ -673,109 +639,106 @@ class Process:
673
639
 
674
640
  # If result failed
675
641
  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)
642
+ raise errors.MemoryReadError(result, address)
677
643
 
678
644
  return bytes(buffer)
679
645
 
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`."""
646
+ def read_into_buffer(self, address: int, buffer: typing.Any, read_bytes_size: int | None = None) -> typing.Any:
647
+ """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
+
649
+ # If read size is invalid
650
+ if read_bytes_size is not None and read_bytes_size <= 0:
651
+ raise ValueError("Unable to read value into buffer due the invalid size: `%s`" % read_bytes_size)
682
652
 
683
653
  # 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)
654
+ 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
+ raise errors.MemoryReadError(result, address)
686
656
 
687
657
  return buffer
688
658
 
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
659
  def write_int8(self, address: int, value: int) -> None:
697
660
  """Write 1 byte signed integer value at given address."""
698
661
 
699
662
  # If result failed
700
663
  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)
664
+ raise errors.MemoryWriteError(result, address)
702
665
 
703
666
  def write_int16(self, address: int, value: int, big_endian: bool = False) -> None:
704
667
  """Write 2 byte signed integer value at given address."""
705
668
 
706
669
  # If result failed
707
670
  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)
671
+ raise errors.MemoryWriteError(result, address)
709
672
 
710
673
  def write_int32(self, address: int, value: int, big_endian: bool = False) -> None:
711
674
  """Write 4 byte signed integer value at given address."""
712
675
 
713
676
  # If result failed
714
677
  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)
678
+ raise errors.MemoryWriteError(result, address)
716
679
 
717
680
  def write_int64(self, address: int, value: int, big_endian: bool = False) -> None:
718
681
  """Write 8 byte signed integer value at given address."""
719
682
 
720
683
  # If result failed
721
684
  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)
685
+ raise errors.MemoryWriteError(result, address)
723
686
 
724
687
  def write_uint8(self, address: int, value: int) -> None:
725
688
  """Write 1 byte unsigned integer value at given address."""
726
689
 
727
690
  # If result failed
728
691
  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)
692
+ raise errors.MemoryWriteError(result, address)
730
693
 
731
694
  def write_uint16(self, address: int, value: int, big_endian: bool = False) -> None:
732
695
  """Write 2 byte unsigned integer value at given address."""
733
696
 
734
697
  # If result failed
735
698
  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)
699
+ raise errors.MemoryWriteError(result, address)
737
700
 
738
701
  def write_uint32(self, address: int, value: int, big_endian: bool = False) -> None:
739
702
  """Write 4 byte unsigned integer value at given address."""
740
703
 
741
704
  # If result failed
742
705
  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())
706
+ raise errors.MemoryWriteError("NtWriteVirtualMemory failed with status: 0x%s" % hex(result)[2:].upper())
744
707
 
745
708
  def write_uint64(self, address: int, value: int, big_endian: bool = False) -> None:
746
709
  """Write 8 byte unsigned integer value at given address."""
747
710
 
748
711
  # If result failed
749
712
  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)
713
+ raise errors.MemoryWriteError(result, address)
751
714
 
752
715
  def write_float32(self, address: int, value: float | int, big_endian: bool = False) -> float:
753
716
  """Write 4 byte floating-point digit value at given address."""
754
717
 
755
718
  # If result failed
756
719
  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)
720
+ raise errors.MemoryWriteError(result, address)
758
721
 
759
722
  def write_float64(self, address: int, value: float | int, big_endian: bool = False) -> float:
760
723
  """Write 8 byte floating-point digit value at given address."""
761
724
 
762
725
  # If result failed
763
726
  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)
727
+ raise errors.MemoryWriteError(result, address)
765
728
 
766
729
  def write_bytes(self, address: int, value: bytes, big_endian: bool = False) -> None:
767
730
  """Write bytes array of variadic size at given address."""
768
731
 
769
732
  # If result failed
770
733
  if (result := _nt_write_virtual_memory(self.handle, address, value[::-1] if big_endian else value, len(value), None)):
771
- raise MemoryWriteError(result, address)
734
+ raise errors.MemoryWriteError(result, address)
772
735
 
773
736
  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`."""
737
+ """Write size of buffer byte value at given address. Buffer have to be able passed at `ctypes.byref` and `ctypes.sizeof`."""
775
738
 
776
739
  # If result failed
777
740
  if (result := _nt_write_virtual_memory(self.handle, address, ctypes.byref(buffer), ctypes.sizeof(buffer), None)):
778
- raise MemoryWriteError(result, address)
741
+ raise errors.MemoryWriteError(result, address)
779
742
 
780
743
  def close(self) -> None:
781
744
  """Close opened process using it's handle, have to be called once on stop interacting with process."""
@@ -786,7 +749,7 @@ class Process:
786
749
  # ==--------------------------------== #
787
750
  # Private methods #
788
751
  # ==--------------------------------== #
789
- def __init_with_pid(self, pid: int, access: int, listed_processes: list[dict[str, int | str]] | None = None) -> tuple[int, int, str]:
752
+ def __init_with_pid(self, pid: int, access_flags: int, listed_processes: list[dict[str, int | str]] | None = None) -> tuple[int, int, str]:
790
753
  """Open process handle by it's ID with desired access."""
791
754
 
792
755
  # Iterate all of the processes
@@ -799,22 +762,22 @@ class Process:
799
762
  break
800
763
 
801
764
  else:
802
- raise OpenProcessError("Process with `%s` ID not found" % pid)
765
+ raise errors.OpenProcessError("Process with `%s` ID not found" % pid)
803
766
 
804
767
  # Prepare arguments
805
- object_attributes = OBJECT_ATTRIBUTES()
806
- object_attributes.length = ctypes.sizeof(OBJECT_ATTRIBUTES)
768
+ object_attributes = structs.OBJECT_ATTRIBUTES()
769
+ object_attributes.length = ctypes.sizeof(structs.OBJECT_ATTRIBUTES)
807
770
 
808
- client_id = CLIENT_ID()
771
+ client_id = structs.CLIENT_ID()
809
772
  client_id.unique_process = pid
810
773
 
811
774
  # 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())
775
+ if (result := _nt_open_process(ctypes.byref(handle := ctypes.c_void_p()), access_flags, ctypes.byref(object_attributes), ctypes.byref(client_id))):
776
+ raise errors.NtError("NtOpenProcess failed with status: 0x%s" % hex(result)[2:].upper())
814
777
 
815
778
  return handle.value, pid, process_name
816
779
 
817
- def __init_with_name(self, name: str, access: int) -> tuple[int, int, str]:
780
+ def __init_with_name(self, name: str, access_flags: int) -> tuple[int, int, str]:
818
781
  """Open process hanle by it's name with desired access."""
819
782
 
820
783
  # Iterate all of the processes using snapshot
@@ -822,6 +785,6 @@ class Process:
822
785
 
823
786
  # If process have a reqired name
824
787
  if process["name"].lower() == name.strip().lower():
825
- return self.__init_with_pid(process["id"], access, listed_processes)
788
+ return self.__init_with_pid(process["id"], access_flags, listed_processes)
826
789
 
827
- raise OpenProcessError("Process with `%s` name not found" % name)
790
+ 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.4
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.4"
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