iflow-mcp_bethington-cheat-engine-server-python 0.1.0__py3-none-any.whl

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.
Files changed (40) hide show
  1. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/METADATA +16 -0
  2. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/RECORD +40 -0
  3. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/WHEEL +5 -0
  4. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/top_level.txt +1 -0
  7. server/cheatengine/__init__.py +19 -0
  8. server/cheatengine/ce_bridge.py +1670 -0
  9. server/cheatengine/lua_interface.py +460 -0
  10. server/cheatengine/table_parser.py +1221 -0
  11. server/config/__init__.py +20 -0
  12. server/config/settings.py +347 -0
  13. server/config/whitelist.py +378 -0
  14. server/gui_automation/__init__.py +43 -0
  15. server/gui_automation/core/__init__.py +8 -0
  16. server/gui_automation/core/integration.py +951 -0
  17. server/gui_automation/demos/__init__.py +8 -0
  18. server/gui_automation/demos/basic_demo.py +754 -0
  19. server/gui_automation/demos/notepad_demo.py +460 -0
  20. server/gui_automation/demos/simple_demo.py +319 -0
  21. server/gui_automation/tools/__init__.py +8 -0
  22. server/gui_automation/tools/mcp_tools.py +974 -0
  23. server/main.py +519 -0
  24. server/memory/__init__.py +0 -0
  25. server/memory/analyzer.py +0 -0
  26. server/memory/reader.py +0 -0
  27. server/memory/scanner.py +0 -0
  28. server/memory/symbols.py +0 -0
  29. server/process/__init__.py +16 -0
  30. server/process/launcher.py +608 -0
  31. server/process/manager.py +185 -0
  32. server/process/monitors.py +202 -0
  33. server/process/permissions.py +131 -0
  34. server/process_whitelist.json +119 -0
  35. server/pyautogui/__init__.py +0 -0
  36. server/utils/__init__.py +37 -0
  37. server/utils/data_types.py +368 -0
  38. server/utils/formatters.py +430 -0
  39. server/utils/validators.py +340 -0
  40. server/window_automation/__init__.py +59 -0
@@ -0,0 +1,1670 @@
1
+ """
2
+ Cheat Engine Bridge Module
3
+ Provides direct interface to Cheat Engine API and processes
4
+ """
5
+
6
+ import logging
7
+ import ctypes
8
+ import ctypes.wintypes
9
+ import struct
10
+ import winreg
11
+ import re
12
+ import time
13
+ import string
14
+ from typing import Dict, List, Optional, Any, Tuple, Union
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ # Conditional imports for advanced features
19
+ try:
20
+ import capstone
21
+ CAPSTONE_AVAILABLE = True
22
+ except ImportError:
23
+ CAPSTONE_AVAILABLE = False
24
+ capstone = None
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ @dataclass
29
+ class CEInstallation:
30
+ """Information about Cheat Engine installation"""
31
+ path: str
32
+ version: str
33
+ executable: str
34
+ available: bool
35
+
36
+ @dataclass
37
+ class CEProcess:
38
+ """Cheat Engine process information"""
39
+ pid: int
40
+ name: str
41
+ handle: int
42
+ base_address: int
43
+ modules: List[Dict[str, Any]]
44
+
45
+ @dataclass
46
+ class MemoryScanResult:
47
+ """Result from memory scanning operation"""
48
+ address: int
49
+ value: Union[int, float, str, bytes]
50
+ data_type: str
51
+ size: int
52
+ region_info: Optional[Dict[str, Any]] = None
53
+
54
+ @dataclass
55
+ class DisassemblyResult:
56
+ """Result from code disassembly"""
57
+ address: int
58
+ bytes_data: bytes
59
+ mnemonic: str
60
+ op_str: str
61
+ size: int
62
+ groups: List[str] = None
63
+
64
+ class CheatEngineBridge:
65
+ """Bridge interface to Cheat Engine functionality"""
66
+
67
+ def __init__(self):
68
+ self.ce_installation = self._detect_cheat_engine()
69
+ self.kernel32 = ctypes.windll.kernel32
70
+ self.user32 = ctypes.windll.user32
71
+ self.psapi = ctypes.windll.psapi
72
+ self._setup_windows_api()
73
+
74
+ def _detect_cheat_engine(self) -> CEInstallation:
75
+ """Detect Cheat Engine installation"""
76
+
77
+ # Common installation paths
78
+ common_paths = [
79
+ r"C:\dbengine",
80
+ r"C:\Program Files\Cheat Engine",
81
+ r"C:\Program Files (x86)\Cheat Engine",
82
+ r"C:\Cheat Engine"
83
+ ]
84
+
85
+ # Check registry for installation path
86
+ registry_path = None
87
+ try:
88
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
89
+ r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cheat Engine") as key:
90
+ registry_path, _ = winreg.QueryValueEx(key, "InstallLocation")
91
+ except FileNotFoundError:
92
+ try:
93
+ # Try 32-bit registry on 64-bit system
94
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
95
+ r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Cheat Engine") as key:
96
+ registry_path, _ = winreg.QueryValueEx(key, "InstallLocation")
97
+ except FileNotFoundError:
98
+ pass
99
+
100
+ if registry_path:
101
+ common_paths.insert(0, registry_path)
102
+
103
+ # Check each path
104
+ for path in common_paths:
105
+ ce_path = Path(path)
106
+ if ce_path.exists():
107
+ # Try different executable names in order of preference
108
+ exe_candidates = [
109
+ "dbengine-x86_64.exe",
110
+ "cheatengine-x86_64.exe",
111
+ "cheatengine.exe"
112
+ ]
113
+
114
+ exe_path = None
115
+ for exe_name in exe_candidates:
116
+ candidate_path = ce_path / exe_name
117
+ if candidate_path.exists():
118
+ exe_path = candidate_path
119
+ break
120
+
121
+ if exe_path:
122
+ version = self._get_ce_version(exe_path)
123
+ return CEInstallation(
124
+ path=str(ce_path),
125
+ version=version,
126
+ executable=str(exe_path),
127
+ available=True
128
+ )
129
+
130
+ return CEInstallation(
131
+ path="",
132
+ version="",
133
+ executable="",
134
+ available=False
135
+ )
136
+
137
+ def _get_ce_version(self, exe_path: Path) -> str:
138
+ """Get Cheat Engine version from executable using multiple methods"""
139
+ version_info = {
140
+ 'file_version': 'Unknown',
141
+ 'product_version': 'Unknown',
142
+ 'version_string': 'Unknown'
143
+ }
144
+
145
+ try:
146
+ # Method 1: Try win32api for detailed version info
147
+ try:
148
+ import win32api
149
+ info = win32api.GetFileVersionInfo(str(exe_path), "\\")
150
+ ms = info['FileVersionMS']
151
+ ls = info['FileVersionLS']
152
+ file_version = f"{win32api.HIWORD(ms)}.{win32api.LOWORD(ms)}.{win32api.HIWORD(ls)}.{win32api.LOWORD(ls)}"
153
+ version_info['file_version'] = file_version
154
+
155
+ # Try to get product version
156
+ try:
157
+ product_info = win32api.GetFileVersionInfo(str(exe_path), "\\VarFileInfo\\Translation")
158
+ if product_info:
159
+ lang, codepage = product_info[0]
160
+ product_version = win32api.GetFileVersionInfo(str(exe_path), f"\\StringFileInfo\\{lang:04x}{codepage:04x}\\ProductVersion")
161
+ if product_version:
162
+ version_info['product_version'] = product_version
163
+ except:
164
+ pass
165
+
166
+ return file_version
167
+
168
+ except ImportError:
169
+ # Method 2: Try alternative version detection using ctypes
170
+ try:
171
+ import ctypes
172
+ from ctypes import wintypes
173
+
174
+ # Get file version info size
175
+ size = ctypes.windll.version.GetFileVersionInfoSizeW(str(exe_path), None)
176
+ if size:
177
+ res = ctypes.create_string_buffer(size)
178
+ ctypes.windll.version.GetFileVersionInfoW(str(exe_path), None, size, res)
179
+
180
+ # Extract version info
181
+ r = ctypes.c_uint()
182
+ l = ctypes.c_uint()
183
+ ctypes.windll.version.VerQueryValueW(res, "\\", ctypes.byref(r), ctypes.byref(l))
184
+
185
+ version_struct = ctypes.cast(r, ctypes.POINTER(ctypes.c_uint * 4)).contents
186
+ version = f"{version_struct[0] >> 16}.{version_struct[0] & 0xFFFF}.{version_struct[1] >> 16}.{version_struct[1] & 0xFFFF}"
187
+ version_info['file_version'] = version
188
+ return version
189
+ except:
190
+ pass
191
+
192
+ # Method 3: Parse version from filename if possible
193
+ filename = exe_path.name.lower()
194
+ import re
195
+ version_pattern = r'(\d+\.\d+(?:\.\d+)?(?:\.\d+)?)'
196
+ match = re.search(version_pattern, filename)
197
+ if match:
198
+ version_info['version_string'] = match.group(1)
199
+ return match.group(1)
200
+
201
+ except Exception as e:
202
+ logger.warning(f"Could not determine CE version from {exe_path}: {e}")
203
+
204
+ return "Unknown"
205
+
206
+ def get_cheat_engine_version_info(self) -> Dict[str, Any]:
207
+ """Get comprehensive Cheat Engine version and installation information"""
208
+ info = {
209
+ 'detected': self.ce_installation.available,
210
+ 'installation_path': self.ce_installation.path,
211
+ 'executable_path': self.ce_installation.executable,
212
+ 'version': self.ce_installation.version,
213
+ 'detection_methods': [],
214
+ 'running_processes': [],
215
+ 'registry_info': {},
216
+ 'alternative_installations': []
217
+ }
218
+
219
+ # Method 1: File system detection (already done in __init__)
220
+ if self.ce_installation.available:
221
+ info['detection_methods'].append({
222
+ 'method': 'file_system',
223
+ 'status': 'success',
224
+ 'path': self.ce_installation.path,
225
+ 'version': self.ce_installation.version,
226
+ 'executable': self.ce_installation.executable
227
+ })
228
+
229
+ # Method 2: Check for running Cheat Engine processes
230
+ try:
231
+ import psutil
232
+ ce_processes = []
233
+ for proc in psutil.process_iter(['pid', 'name', 'exe', 'cmdline']):
234
+ try:
235
+ proc_name = proc.info['name'].lower()
236
+ if any(ce_name in proc_name for ce_name in ['cheatengine', 'dbengine']):
237
+ proc_info = {
238
+ 'pid': proc.info['pid'],
239
+ 'name': proc.info['name'],
240
+ 'exe_path': proc.info['exe'],
241
+ 'cmdline': proc.info['cmdline']
242
+ }
243
+
244
+ # Try to get version from running process
245
+ if proc.info['exe']:
246
+ try:
247
+ version = self._get_ce_version(Path(proc.info['exe']))
248
+ proc_info['version'] = version
249
+ except:
250
+ proc_info['version'] = 'Unknown'
251
+
252
+ ce_processes.append(proc_info)
253
+
254
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
255
+ # Skip processes that are inaccessible
256
+ continue
257
+
258
+ info['running_processes'] = ce_processes
259
+ if ce_processes:
260
+ info['detection_methods'].append({
261
+ 'method': 'running_process',
262
+ 'status': 'success',
263
+ 'count': len(ce_processes),
264
+ 'processes': ce_processes
265
+ })
266
+
267
+ except Exception as e:
268
+ logger.warning(f"Failed to check for running CE processes: {e}")
269
+ info['detection_methods'].append({
270
+ 'method': 'running_process',
271
+ 'status': 'error',
272
+ 'error': str(e)
273
+ })
274
+
275
+ # Method 3: Enhanced registry detection
276
+ try:
277
+ import winreg
278
+ registry_keys = [
279
+ (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cheat Engine"),
280
+ (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Cheat Engine"),
281
+ (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cheat Engine"),
282
+ (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\CheatEngine"),
283
+ (winreg.HKEY_CURRENT_USER, r"SOFTWARE\CheatEngine")
284
+ ]
285
+
286
+ for hkey, subkey in registry_keys:
287
+ try:
288
+ with winreg.OpenKey(hkey, subkey) as key:
289
+ reg_info = {}
290
+ try:
291
+ reg_info['install_location'], _ = winreg.QueryValueEx(key, "InstallLocation")
292
+ except FileNotFoundError:
293
+ pass
294
+ try:
295
+ reg_info['display_version'], _ = winreg.QueryValueEx(key, "DisplayVersion")
296
+ except FileNotFoundError:
297
+ pass
298
+ try:
299
+ reg_info['display_name'], _ = winreg.QueryValueEx(key, "DisplayName")
300
+ except FileNotFoundError:
301
+ pass
302
+ try:
303
+ reg_info['publisher'], _ = winreg.QueryValueEx(key, "Publisher")
304
+ except FileNotFoundError:
305
+ pass
306
+
307
+ if reg_info:
308
+ info['registry_info'][f"{hkey}\\{subkey}"] = reg_info
309
+
310
+ except FileNotFoundError:
311
+ continue
312
+
313
+ if info['registry_info']:
314
+ info['detection_methods'].append({
315
+ 'method': 'registry',
316
+ 'status': 'success',
317
+ 'registry_entries': len(info['registry_info'])
318
+ })
319
+
320
+ except Exception as e:
321
+ logger.warning(f"Failed to check registry for CE info: {e}")
322
+ info['detection_methods'].append({
323
+ 'method': 'registry',
324
+ 'status': 'error',
325
+ 'error': str(e)
326
+ })
327
+
328
+ # Method 4: Search for alternative installations
329
+ try:
330
+ alternative_paths = [
331
+ r"C:\dbengine",
332
+ r"C:\Program Files\Cheat Engine",
333
+ r"C:\Program Files (x86)\Cheat Engine",
334
+ r"C:\Cheat Engine",
335
+ r"D:\Cheat Engine",
336
+ r"E:\Cheat Engine",
337
+ str(Path.home() / "Desktop" / "Cheat Engine"),
338
+ str(Path.home() / "Downloads" / "Cheat Engine")
339
+ ]
340
+
341
+ alternatives = []
342
+ for alt_path in alternative_paths:
343
+ alt_path_obj = Path(alt_path)
344
+ if alt_path_obj.exists() and str(alt_path_obj) != self.ce_installation.path:
345
+ # Try different executable names
346
+ exe_candidates = [
347
+ "dbengine-x86_64.exe",
348
+ "cheatengine-x86_64.exe",
349
+ "cheatengine.exe"
350
+ ]
351
+
352
+ for exe_name in exe_candidates:
353
+ exe_path = alt_path_obj / exe_name
354
+ if exe_path.exists():
355
+ version = self._get_ce_version(exe_path)
356
+ alternatives.append({
357
+ 'path': str(alt_path_obj),
358
+ 'executable': str(exe_path),
359
+ 'version': version
360
+ })
361
+ break
362
+
363
+ info['alternative_installations'] = alternatives
364
+ if alternatives:
365
+ info['detection_methods'].append({
366
+ 'method': 'alternative_search',
367
+ 'status': 'success',
368
+ 'found_count': len(alternatives)
369
+ })
370
+
371
+ except Exception as e:
372
+ logger.warning(f"Failed to search for alternative CE installations: {e}")
373
+ info['detection_methods'].append({
374
+ 'method': 'alternative_search',
375
+ 'status': 'error',
376
+ 'error': str(e)
377
+ })
378
+
379
+ return info
380
+
381
+ def _setup_windows_api(self):
382
+ """Setup Windows API function signatures"""
383
+
384
+ # OpenProcess
385
+ self.kernel32.OpenProcess.argtypes = [ctypes.wintypes.DWORD, ctypes.wintypes.BOOL, ctypes.wintypes.DWORD]
386
+ self.kernel32.OpenProcess.restype = ctypes.wintypes.HANDLE
387
+
388
+ # ReadProcessMemory
389
+ self.kernel32.ReadProcessMemory.argtypes = [
390
+ ctypes.wintypes.HANDLE, # hProcess
391
+ ctypes.wintypes.LPCVOID, # lpBaseAddress
392
+ ctypes.wintypes.LPVOID, # lpBuffer
393
+ ctypes.c_size_t, # nSize
394
+ ctypes.POINTER(ctypes.c_size_t) # lpNumberOfBytesRead
395
+ ]
396
+ self.kernel32.ReadProcessMemory.restype = ctypes.wintypes.BOOL
397
+
398
+ # WriteProcessMemory
399
+ self.kernel32.WriteProcessMemory.argtypes = [
400
+ ctypes.wintypes.HANDLE, # hProcess
401
+ ctypes.wintypes.LPVOID, # lpBaseAddress
402
+ ctypes.wintypes.LPCVOID, # lpBuffer
403
+ ctypes.c_size_t, # nSize
404
+ ctypes.POINTER(ctypes.c_size_t) # lpNumberOfBytesWritten
405
+ ]
406
+ self.kernel32.WriteProcessMemory.restype = ctypes.wintypes.BOOL
407
+
408
+ # VirtualQueryEx
409
+ self.kernel32.VirtualQueryEx.argtypes = [
410
+ ctypes.wintypes.HANDLE,
411
+ ctypes.wintypes.LPCVOID,
412
+ ctypes.wintypes.LPVOID,
413
+ ctypes.c_size_t
414
+ ]
415
+ self.kernel32.VirtualQueryEx.restype = ctypes.c_size_t
416
+
417
+ # CloseHandle
418
+ self.kernel32.CloseHandle.argtypes = [ctypes.wintypes.HANDLE]
419
+ self.kernel32.CloseHandle.restype = ctypes.wintypes.BOOL
420
+
421
+ # EnumProcessModules
422
+ self.psapi.EnumProcessModules.argtypes = [
423
+ ctypes.wintypes.HANDLE,
424
+ ctypes.POINTER(ctypes.wintypes.HMODULE),
425
+ ctypes.wintypes.DWORD,
426
+ ctypes.POINTER(ctypes.wintypes.DWORD)
427
+ ]
428
+ self.psapi.EnumProcessModules.restype = ctypes.wintypes.BOOL
429
+
430
+ # GetModuleInformation
431
+ self.psapi.GetModuleInformation.argtypes = [
432
+ ctypes.wintypes.HANDLE,
433
+ ctypes.wintypes.HMODULE,
434
+ ctypes.wintypes.LPVOID,
435
+ ctypes.wintypes.DWORD
436
+ ]
437
+ self.psapi.GetModuleInformation.restype = ctypes.wintypes.BOOL
438
+
439
+ def get_ce_installation_info(self) -> Dict[str, Any]:
440
+ """Get Cheat Engine installation information"""
441
+ return {
442
+ 'available': self.ce_installation.available,
443
+ 'path': self.ce_installation.path,
444
+ 'version': self.ce_installation.version,
445
+ 'executable': self.ce_installation.executable
446
+ }
447
+
448
+ def open_process_handle(self, pid: int, access_rights: int = 0x1F0FFF) -> Optional[int]:
449
+ """Open a handle to a process
450
+
451
+ Args:
452
+ pid: Process ID
453
+ access_rights: Access rights (default: PROCESS_ALL_ACCESS)
454
+
455
+ Returns:
456
+ Process handle or None if failed
457
+ """
458
+ try:
459
+ handle = self.kernel32.OpenProcess(access_rights, False, pid)
460
+ if handle:
461
+ return handle
462
+ else:
463
+ error = ctypes.get_last_error()
464
+ logger.error(f"Failed to open process {pid}: Error {error}")
465
+ return None
466
+ except Exception as e:
467
+ logger.error(f"Exception opening process {pid}: {e}")
468
+ return None
469
+
470
+ def close_process_handle(self, handle: int) -> bool:
471
+ """Close a process handle"""
472
+ try:
473
+ return bool(self.kernel32.CloseHandle(handle))
474
+ except Exception as e:
475
+ logger.error(f"Error closing handle {handle}: {e}")
476
+ return False
477
+
478
+ def read_process_memory(self, handle: int, address: int, size: int) -> Optional[bytes]:
479
+ """Read memory from process
480
+
481
+ Args:
482
+ handle: Process handle
483
+ address: Memory address
484
+ size: Number of bytes to read
485
+
486
+ Returns:
487
+ Bytes read or None if failed
488
+ """
489
+ try:
490
+ buffer = ctypes.create_string_buffer(size)
491
+ bytes_read = ctypes.c_size_t()
492
+
493
+ success = self.kernel32.ReadProcessMemory(
494
+ handle,
495
+ ctypes.c_void_p(address),
496
+ buffer,
497
+ size,
498
+ ctypes.byref(bytes_read)
499
+ )
500
+
501
+ if success:
502
+ return buffer.raw[:bytes_read.value]
503
+ else:
504
+ error = ctypes.get_last_error()
505
+ logger.error(f"ReadProcessMemory failed: Error {error}")
506
+ return None
507
+
508
+ except Exception as e:
509
+ logger.error(f"Exception reading memory at 0x{address:X}: {e}")
510
+ return None
511
+
512
+ def write_process_memory(self, handle: int, address: int, data: bytes) -> bool:
513
+ """Write memory to process (READ-ONLY MODE - NOT IMPLEMENTED)
514
+
515
+ This method is intentionally not implemented for safety.
516
+ The server operates in read-only mode.
517
+ """
518
+ logger.warning("Write operations are disabled in read-only mode")
519
+ return False
520
+
521
+ def query_memory_info(self, handle: int, address: int) -> Optional[Dict[str, Any]]:
522
+ """Query memory information at address"""
523
+
524
+ class MEMORY_BASIC_INFORMATION(ctypes.Structure):
525
+ _fields_ = [
526
+ ("BaseAddress", ctypes.c_void_p),
527
+ ("AllocationBase", ctypes.c_void_p),
528
+ ("AllocationProtect", ctypes.wintypes.DWORD),
529
+ ("RegionSize", ctypes.c_size_t),
530
+ ("State", ctypes.wintypes.DWORD),
531
+ ("Protect", ctypes.wintypes.DWORD),
532
+ ("Type", ctypes.wintypes.DWORD),
533
+ ]
534
+
535
+ try:
536
+ mbi = MEMORY_BASIC_INFORMATION()
537
+ size = self.kernel32.VirtualQueryEx(
538
+ handle,
539
+ ctypes.c_void_p(address),
540
+ ctypes.byref(mbi),
541
+ ctypes.sizeof(mbi)
542
+ )
543
+
544
+ if size:
545
+ return {
546
+ 'base_address': mbi.BaseAddress,
547
+ 'allocation_base': mbi.AllocationBase,
548
+ 'allocation_protect': mbi.AllocationProtect,
549
+ 'region_size': mbi.RegionSize,
550
+ 'state': mbi.State,
551
+ 'protect': mbi.Protect,
552
+ 'type': mbi.Type
553
+ }
554
+ else:
555
+ return None
556
+
557
+ except Exception as e:
558
+ logger.error(f"Error querying memory at 0x{address:X}: {e}")
559
+ return None
560
+
561
+ def enum_process_modules(self, handle: int) -> List[Dict[str, Any]]:
562
+ """Enumerate modules in a process"""
563
+ modules = []
564
+
565
+ try:
566
+ # Get module count
567
+ module_handles = (ctypes.wintypes.HMODULE * 1024)()
568
+ bytes_needed = ctypes.wintypes.DWORD()
569
+
570
+ success = self.psapi.EnumProcessModules(
571
+ handle,
572
+ module_handles,
573
+ ctypes.sizeof(module_handles),
574
+ ctypes.byref(bytes_needed)
575
+ )
576
+
577
+ if not success:
578
+ return modules
579
+
580
+ module_count = bytes_needed.value // ctypes.sizeof(ctypes.wintypes.HMODULE)
581
+
582
+ # Get module information
583
+ class MODULEINFO(ctypes.Structure):
584
+ _fields_ = [
585
+ ("lpBaseOfDll", ctypes.c_void_p),
586
+ ("SizeOfImage", ctypes.wintypes.DWORD),
587
+ ("EntryPoint", ctypes.c_void_p),
588
+ ]
589
+
590
+ for i in range(min(module_count, 1024)):
591
+ if module_handles[i]:
592
+ module_info = MODULEINFO()
593
+ success = self.psapi.GetModuleInformation(
594
+ handle,
595
+ module_handles[i],
596
+ ctypes.byref(module_info),
597
+ ctypes.sizeof(module_info)
598
+ )
599
+
600
+ if success:
601
+ # Get module name
602
+ module_name = self._get_module_name(handle, module_handles[i])
603
+
604
+ modules.append({
605
+ 'handle': module_handles[i],
606
+ 'name': module_name,
607
+ 'base_address': module_info.lpBaseOfDll,
608
+ 'size': module_info.SizeOfImage,
609
+ 'entry_point': module_info.EntryPoint
610
+ })
611
+
612
+ return modules
613
+
614
+ except Exception as e:
615
+ logger.error(f"Error enumerating modules: {e}")
616
+ return modules
617
+
618
+ def _get_module_name(self, handle: int, module_handle: int) -> str:
619
+ """Get module name from handle"""
620
+ try:
621
+ # Try GetModuleFileNameEx
622
+ buffer = ctypes.create_unicode_buffer(260) # MAX_PATH
623
+ length = self.psapi.GetModuleFileNameExW(
624
+ handle,
625
+ module_handle,
626
+ buffer,
627
+ 260
628
+ )
629
+
630
+ if length:
631
+ full_path = buffer.value
632
+ return Path(full_path).name
633
+ else:
634
+ return f"Module_{module_handle:X}"
635
+
636
+ except Exception:
637
+ return f"Module_{module_handle:X}"
638
+
639
+ def find_pattern_in_memory(self, handle: int, pattern: bytes, start_address: int = 0,
640
+ end_address: int = 0x7FFFFFFF, alignment: int = 1) -> List[int]:
641
+ """Find byte pattern in process memory
642
+
643
+ Args:
644
+ handle: Process handle
645
+ pattern: Byte pattern to search for
646
+ start_address: Start search address
647
+ end_address: End search address
648
+ alignment: Memory alignment for search
649
+
650
+ Returns:
651
+ List of addresses where pattern was found
652
+ """
653
+ found_addresses = []
654
+ current_address = start_address
655
+ chunk_size = 1024 * 1024 # 1MB chunks
656
+
657
+ try:
658
+ while current_address < end_address:
659
+ # Query memory region
660
+ memory_info = self.query_memory_info(handle, current_address)
661
+ if not memory_info:
662
+ current_address += 0x1000 # Skip 4KB
663
+ continue
664
+
665
+ # Skip if region is not committed or readable
666
+ if (memory_info['state'] != 0x1000 or # MEM_COMMIT
667
+ memory_info['protect'] & 0x01 or # PAGE_NOACCESS
668
+ memory_info['protect'] & 0x100): # PAGE_GUARD
669
+ current_address = memory_info['base_address'] + memory_info['region_size']
670
+ continue
671
+
672
+ # Read memory in chunks
673
+ region_start = memory_info['base_address']
674
+ region_size = memory_info['region_size']
675
+
676
+ for offset in range(0, region_size, chunk_size):
677
+ chunk_address = region_start + offset
678
+ read_size = min(chunk_size, region_size - offset)
679
+
680
+ data = self.read_process_memory(handle, chunk_address, read_size)
681
+ if data:
682
+ # Search for pattern in chunk
683
+ pattern_addresses = self._search_pattern_in_data(
684
+ data, pattern, chunk_address, alignment
685
+ )
686
+ found_addresses.extend(pattern_addresses)
687
+
688
+ current_address = region_start + region_size
689
+
690
+ # Limit results to prevent memory issues
691
+ if len(found_addresses) > 10000:
692
+ logger.warning("Pattern search found too many results, truncating")
693
+ break
694
+
695
+ return found_addresses
696
+
697
+ except Exception as e:
698
+ logger.error(f"Error in pattern search: {e}")
699
+ return found_addresses
700
+
701
+ def _search_pattern_in_data(self, data: bytes, pattern: bytes, base_address: int,
702
+ alignment: int) -> List[int]:
703
+ """Search for pattern in data chunk"""
704
+ addresses = []
705
+
706
+ for i in range(0, len(data) - len(pattern) + 1, alignment):
707
+ if data[i:i+len(pattern)] == pattern:
708
+ addresses.append(base_address + i)
709
+
710
+ return addresses
711
+
712
+ def scan_memory_for_value(self, handle: int, value: Union[int, float, str],
713
+ data_type: str, start_address: int = 0,
714
+ end_address: int = 0x7FFFFFFF) -> List[MemoryScanResult]:
715
+ """Scan memory for specific values
716
+
717
+ Args:
718
+ handle: Process handle
719
+ value: Value to search for
720
+ data_type: Data type ('int8', 'int16', 'int32', 'int64', 'float', 'double', 'string')
721
+ start_address: Start search address
722
+ end_address: End search address
723
+
724
+ Returns:
725
+ List of MemoryScanResult objects
726
+ """
727
+ results = []
728
+ search_pattern = self._value_to_bytes(value, data_type)
729
+ if not search_pattern:
730
+ return results
731
+
732
+ try:
733
+ current_address = start_address
734
+ chunk_size = 1024 * 1024 # 1MB chunks
735
+
736
+ while current_address < end_address:
737
+ memory_info = self.query_memory_info(handle, current_address)
738
+ if not memory_info or not self._is_memory_readable(memory_info['protect']):
739
+ current_address += 0x1000
740
+ continue
741
+
742
+ region_start = memory_info['base_address']
743
+ region_size = memory_info['region_size']
744
+
745
+ for offset in range(0, region_size, chunk_size):
746
+ chunk_address = region_start + offset
747
+ read_size = min(chunk_size, region_size - offset)
748
+
749
+ data = self.read_process_memory(handle, chunk_address, read_size)
750
+ if data:
751
+ for pattern_bytes in search_pattern:
752
+ addresses = self._search_pattern_in_data(
753
+ data, pattern_bytes, chunk_address, 1
754
+ )
755
+
756
+ for addr in addresses:
757
+ # Check limit before adding more results
758
+ if len(results) >= 10000:
759
+ logger.warning("Memory scan found too many results, truncating")
760
+ return results
761
+
762
+ # Read the actual value at this address
763
+ actual_value = self._read_typed_value(handle, addr, data_type)
764
+ if actual_value is not None:
765
+ results.append(MemoryScanResult(
766
+ address=addr,
767
+ value=actual_value,
768
+ data_type=data_type,
769
+ size=len(pattern_bytes),
770
+ region_info=memory_info
771
+ ))
772
+
773
+ current_address = region_start + region_size
774
+
775
+ # Limit results to prevent memory issues
776
+ if len(results) > 10000:
777
+ logger.warning("Memory scan found too many results, truncating")
778
+ break
779
+
780
+ return results
781
+
782
+ except Exception as e:
783
+ logger.error(f"Error in memory value scan: {e}")
784
+ return results
785
+
786
+ def scan_memory_range(self, handle: int, min_value: Union[int, float],
787
+ max_value: Union[int, float], data_type: str,
788
+ start_address: int = 0, end_address: int = 0x7FFFFFFF) -> List[MemoryScanResult]:
789
+ """Scan memory for values within a range
790
+
791
+ Args:
792
+ handle: Process handle
793
+ min_value: Minimum value
794
+ max_value: Maximum value
795
+ data_type: Data type
796
+ start_address: Start search address
797
+ end_address: End search address
798
+
799
+ Returns:
800
+ List of MemoryScanResult objects
801
+ """
802
+ results = []
803
+ type_size = self._get_type_size(data_type)
804
+
805
+ try:
806
+ current_address = start_address
807
+ chunk_size = 1024 * 1024
808
+
809
+ while current_address < end_address:
810
+ memory_info = self.query_memory_info(handle, current_address)
811
+ if not memory_info or not self._is_memory_readable(memory_info['protect']):
812
+ current_address += 0x1000
813
+ continue
814
+
815
+ region_start = memory_info['base_address']
816
+ region_size = memory_info['region_size']
817
+
818
+ for offset in range(0, region_size, chunk_size):
819
+ chunk_address = region_start + offset
820
+ read_size = min(chunk_size, region_size - offset)
821
+
822
+ data = self.read_process_memory(handle, chunk_address, read_size)
823
+ if data:
824
+ # Scan through data at type-sized intervals
825
+ for i in range(0, len(data) - type_size + 1, type_size):
826
+ addr = chunk_address + i
827
+ value = self._bytes_to_value(data[i:i+type_size], data_type)
828
+
829
+ if value is not None and min_value <= value <= max_value:
830
+ results.append(MemoryScanResult(
831
+ address=addr,
832
+ value=value,
833
+ data_type=data_type,
834
+ size=type_size,
835
+ region_info=memory_info
836
+ ))
837
+
838
+ current_address = region_start + region_size
839
+
840
+ if len(results) > 10000:
841
+ logger.warning("Range scan found too many results, truncating")
842
+ break
843
+
844
+ return results
845
+
846
+ except Exception as e:
847
+ logger.error(f"Error in memory range scan: {e}")
848
+ return results
849
+
850
+ def find_pattern_with_wildcards(self, handle: int, pattern_string: str,
851
+ start_address: int = 0, end_address: int = 0x7FFFFFFF) -> List[int]:
852
+ """Find byte pattern with wildcards (? or ??)
853
+
854
+ Args:
855
+ handle: Process handle
856
+ pattern_string: Pattern string like "48 8B ? 48 ?? 05"
857
+ start_address: Start search address
858
+ end_address: End search address
859
+
860
+ Returns:
861
+ List of addresses where pattern was found
862
+ """
863
+ found_addresses = []
864
+
865
+ try:
866
+ # Parse pattern string
867
+ pattern_parts = pattern_string.upper().split()
868
+ pattern_bytes = []
869
+ mask = []
870
+
871
+ for part in pattern_parts:
872
+ if part == '?' or part == '??':
873
+ pattern_bytes.append(0x00)
874
+ mask.append(False)
875
+ else:
876
+ try:
877
+ byte_val = int(part, 16)
878
+ pattern_bytes.append(byte_val)
879
+ mask.append(True)
880
+ except ValueError:
881
+ logger.error(f"Invalid hex byte in pattern: {part}")
882
+ return found_addresses
883
+
884
+ if not pattern_bytes:
885
+ return found_addresses
886
+
887
+ pattern = bytes(pattern_bytes)
888
+ current_address = start_address
889
+ chunk_size = 1024 * 1024
890
+
891
+ while current_address < end_address:
892
+ memory_info = self.query_memory_info(handle, current_address)
893
+ if not memory_info or not self._is_memory_readable(memory_info['protect']):
894
+ current_address += 0x1000
895
+ continue
896
+
897
+ region_start = memory_info['base_address']
898
+ region_size = memory_info['region_size']
899
+
900
+ for offset in range(0, region_size, chunk_size):
901
+ chunk_address = region_start + offset
902
+ read_size = min(chunk_size, region_size - offset)
903
+
904
+ data = self.read_process_memory(handle, chunk_address, read_size)
905
+ if data:
906
+ addresses = self._search_wildcard_pattern(
907
+ data, pattern, mask, chunk_address
908
+ )
909
+ found_addresses.extend(addresses)
910
+
911
+ current_address = region_start + region_size
912
+
913
+ if len(found_addresses) > 10000:
914
+ logger.warning("Wildcard pattern search found too many results, truncating")
915
+ break
916
+
917
+ return found_addresses
918
+
919
+ except Exception as e:
920
+ logger.error(f"Error in wildcard pattern search: {e}")
921
+ return found_addresses
922
+
923
+ def _search_wildcard_pattern(self, data: bytes, pattern: bytes,
924
+ mask: List[bool], base_address: int) -> List[int]:
925
+ """Search for wildcard pattern in data"""
926
+ addresses = []
927
+
928
+ for i in range(len(data) - len(pattern) + 1):
929
+ match = True
930
+ for j, check_byte in enumerate(mask):
931
+ if check_byte and data[i + j] != pattern[j]:
932
+ match = False
933
+ break
934
+
935
+ if match:
936
+ addresses.append(base_address + i)
937
+
938
+ return addresses
939
+
940
+ def resolve_pointer_chain(self, handle: int, base_address: int,
941
+ offsets: List[int]) -> Optional[int]:
942
+ """Resolve a pointer chain
943
+
944
+ Args:
945
+ handle: Process handle
946
+ base_address: Base address to start from
947
+ offsets: List of offsets to follow
948
+
949
+ Returns:
950
+ Final address or None if failed
951
+ """
952
+ try:
953
+ current_address = base_address
954
+
955
+ for i, offset in enumerate(offsets):
956
+ if i < len(offsets) - 1:
957
+ # Read pointer value (assuming 64-bit)
958
+ pointer_data = self.read_process_memory(handle, current_address + offset, 8)
959
+ if not pointer_data:
960
+ return None
961
+
962
+ # Unpack as 64-bit pointer
963
+ try:
964
+ current_address = struct.unpack('<Q', pointer_data)[0]
965
+ except struct.error:
966
+ return None
967
+ else:
968
+ # Final offset
969
+ current_address += offset
970
+
971
+ return current_address
972
+
973
+ except Exception as e:
974
+ logger.error(f"Error resolving pointer chain: {e}")
975
+ return None
976
+
977
+ def find_pointer_chains_to_address(self, handle: int, target_address: int,
978
+ max_depth: int = 4, max_results: int = 100) -> List[Dict[str, Any]]:
979
+ """Find pointer chains that lead to a target address
980
+
981
+ Args:
982
+ handle: Process handle
983
+ target_address: Target address to find chains to
984
+ max_depth: Maximum chain depth
985
+ max_results: Maximum number of results
986
+
987
+ Returns:
988
+ List of pointer chain information
989
+ """
990
+ chains = []
991
+
992
+ try:
993
+ # Get all memory regions that could contain pointers
994
+ memory_map = self.get_detailed_memory_map(handle)
995
+ readable_regions = [r for r in memory_map if r['readable'] and not r['executable']]
996
+
997
+ # Find all pointers that point to the target address
998
+ ptr_size = ctypes.sizeof(ctypes.c_void_p)
999
+ target_bytes = struct.pack('<Q' if ptr_size == 8 else '<I', target_address)
1000
+
1001
+ level_pointers = {0: [target_address]} # Level 0 is the target itself
1002
+
1003
+ for depth in range(1, max_depth + 1):
1004
+ current_level = []
1005
+
1006
+ for region in readable_regions:
1007
+ region_data = self.read_process_memory(
1008
+ handle, region['base_address'],
1009
+ min(region['region_size'], 1024 * 1024) # Limit to 1MB per region
1010
+ )
1011
+
1012
+ if not region_data:
1013
+ continue
1014
+
1015
+ # Look for pointers to any address in the previous level
1016
+ for prev_addr in level_pointers.get(depth - 1, []):
1017
+ if ptr_size == 8:
1018
+ search_bytes = struct.pack('<Q', prev_addr)
1019
+ else:
1020
+ search_bytes = struct.pack('<I', prev_addr)
1021
+
1022
+ # Find all occurrences of this pointer
1023
+ offset = 0
1024
+ while True:
1025
+ pos = region_data.find(search_bytes, offset)
1026
+ if pos == -1:
1027
+ break
1028
+
1029
+ pointer_addr = region['base_address'] + pos
1030
+ current_level.append(pointer_addr)
1031
+
1032
+ # Record the chain
1033
+ if len(chains) < max_results:
1034
+ chains.append({
1035
+ 'depth': depth,
1036
+ 'pointer_address': pointer_addr,
1037
+ 'points_to': prev_addr,
1038
+ 'target_address': target_address,
1039
+ 'region_info': region
1040
+ })
1041
+
1042
+ offset = pos + ptr_size
1043
+
1044
+ level_pointers[depth] = current_level[:1000] # Limit per level
1045
+
1046
+ if len(chains) >= max_results:
1047
+ break
1048
+
1049
+ return chains
1050
+
1051
+ except Exception as e:
1052
+ logger.error(f"Error finding pointer chains to 0x{target_address:X}: {e}")
1053
+ return chains
1054
+
1055
+ def compare_memory_snapshots(self, handle: int, address: int, size: int,
1056
+ previous_data: bytes) -> Dict[str, Any]:
1057
+ """Compare current memory with previous snapshot
1058
+
1059
+ Args:
1060
+ handle: Process handle
1061
+ address: Memory address
1062
+ size: Size to compare
1063
+ previous_data: Previous memory snapshot
1064
+
1065
+ Returns:
1066
+ Comparison results
1067
+ """
1068
+ comparison = {
1069
+ 'address': address,
1070
+ 'size': size,
1071
+ 'changes': [],
1072
+ 'summary': {}
1073
+ }
1074
+
1075
+ try:
1076
+ current_data = self.read_process_memory(handle, address, size)
1077
+ if not current_data:
1078
+ comparison['summary']['error'] = 'Could not read current memory'
1079
+ return comparison
1080
+
1081
+ if len(current_data) != len(previous_data):
1082
+ comparison['summary']['size_mismatch'] = True
1083
+ min_size = min(len(current_data), len(previous_data))
1084
+ current_data = current_data[:min_size]
1085
+ previous_data = previous_data[:min_size]
1086
+
1087
+ # Find all changed bytes
1088
+ changes = []
1089
+ changed_regions = []
1090
+ current_region_start = None
1091
+
1092
+ for i, (old_byte, new_byte) in enumerate(zip(previous_data, current_data)):
1093
+ if old_byte != new_byte:
1094
+ if current_region_start is None:
1095
+ current_region_start = i
1096
+
1097
+ changes.append({
1098
+ 'offset': i,
1099
+ 'address': address + i,
1100
+ 'old_value': old_byte,
1101
+ 'new_value': new_byte
1102
+ })
1103
+ else:
1104
+ if current_region_start is not None:
1105
+ changed_regions.append({
1106
+ 'start_offset': current_region_start,
1107
+ 'end_offset': i - 1,
1108
+ 'start_address': address + current_region_start,
1109
+ 'end_address': address + i - 1,
1110
+ 'size': i - current_region_start
1111
+ })
1112
+ current_region_start = None
1113
+
1114
+ # Close the last region if needed
1115
+ if current_region_start is not None:
1116
+ changed_regions.append({
1117
+ 'start_offset': current_region_start,
1118
+ 'end_offset': len(current_data) - 1,
1119
+ 'start_address': address + current_region_start,
1120
+ 'end_address': address + len(current_data) - 1,
1121
+ 'size': len(current_data) - current_region_start
1122
+ })
1123
+
1124
+ comparison['changes'] = changes[:1000] # Limit results
1125
+ comparison['changed_regions'] = changed_regions
1126
+ comparison['summary'] = {
1127
+ 'total_changes': len(changes),
1128
+ 'changed_bytes_percentage': (len(changes) / len(current_data)) * 100,
1129
+ 'largest_changed_region': max(changed_regions, key=lambda x: x['size'])['size'] if changed_regions else 0,
1130
+ 'number_of_regions': len(changed_regions)
1131
+ }
1132
+
1133
+ return comparison
1134
+
1135
+ except Exception as e:
1136
+ logger.error(f"Error comparing memory snapshots: {e}")
1137
+ comparison['summary']['error'] = str(e)
1138
+ return comparison
1139
+
1140
+ def create_memory_snapshot(self, handle: int, address: int, size: int) -> Optional[bytes]:
1141
+ """Create a memory snapshot for later comparison
1142
+
1143
+ Args:
1144
+ handle: Process handle
1145
+ address: Memory address
1146
+ size: Size to snapshot
1147
+
1148
+ Returns:
1149
+ Memory snapshot data
1150
+ """
1151
+ try:
1152
+ return self.read_process_memory(handle, address, size)
1153
+ except Exception as e:
1154
+ logger.error(f"Error creating memory snapshot: {e}")
1155
+ return None
1156
+
1157
+ def search_for_changed_values(self, handle: int, old_value: Union[int, float],
1158
+ new_value: Union[int, float], data_type: str,
1159
+ previous_results: List[MemoryScanResult] = None) -> List[MemoryScanResult]:
1160
+ """Search for values that changed from old_value to new_value
1161
+
1162
+ Args:
1163
+ handle: Process handle
1164
+ old_value: Previous value
1165
+ new_value: Current value
1166
+ data_type: Data type
1167
+ previous_results: Previous scan results to filter
1168
+
1169
+ Returns:
1170
+ List of addresses where value changed
1171
+ """
1172
+ results = []
1173
+
1174
+ try:
1175
+ if previous_results:
1176
+ # Filter previous results
1177
+ for result in previous_results:
1178
+ current_value = self._read_typed_value(handle, result.address, data_type)
1179
+ if current_value == new_value:
1180
+ # Create snapshot to compare later
1181
+ results.append(MemoryScanResult(
1182
+ address=result.address,
1183
+ value=current_value,
1184
+ data_type=data_type,
1185
+ size=result.size,
1186
+ region_info=result.region_info
1187
+ ))
1188
+ else:
1189
+ # Scan all memory for new_value
1190
+ results = self.scan_memory_for_value(handle, new_value, data_type)
1191
+
1192
+ return results
1193
+
1194
+ except Exception as e:
1195
+ logger.error(f"Error searching for changed values: {e}")
1196
+ return results
1197
+
1198
+ def create_ce_process_info(self, pid: int) -> Optional[CEProcess]:
1199
+ """Create CEProcess object with full process information"""
1200
+ try:
1201
+ handle = self.open_process_handle(pid)
1202
+ if not handle:
1203
+ return None
1204
+
1205
+ # Get process name
1206
+ import psutil
1207
+ try:
1208
+ process = psutil.Process(pid)
1209
+ process_name = process.name()
1210
+ except psutil.NoSuchProcess:
1211
+ process_name = f"Process_{pid}"
1212
+
1213
+ # Get modules
1214
+ modules = self.enum_process_modules(handle)
1215
+
1216
+ # Get base address (first module's base address)
1217
+ base_address = modules[0]['base_address'] if modules else 0
1218
+
1219
+ return CEProcess(
1220
+ pid=pid,
1221
+ name=process_name,
1222
+ handle=handle,
1223
+ base_address=base_address,
1224
+ modules=modules
1225
+ )
1226
+
1227
+ except Exception as e:
1228
+ logger.error(f"Error creating CE process info for PID {pid}: {e}")
1229
+ return None
1230
+
1231
+ def get_detailed_memory_map(self, handle: int) -> List[Dict[str, Any]]:
1232
+ """Get detailed memory map of process"""
1233
+ memory_regions = []
1234
+ current_address = 0
1235
+
1236
+ try:
1237
+ while current_address < 0x7FFFFFFF: # 2GB limit for 32-bit compat
1238
+ memory_info = self.query_memory_info(handle, current_address)
1239
+ if not memory_info:
1240
+ current_address += 0x1000
1241
+ continue
1242
+
1243
+ # Add region info
1244
+ region = {
1245
+ 'base_address': memory_info['base_address'],
1246
+ 'region_size': memory_info['region_size'],
1247
+ 'state': self._memory_state_to_string(memory_info['state']),
1248
+ 'protect': self._memory_protect_to_string(memory_info['protect']),
1249
+ 'type': self._memory_type_to_string(memory_info['type']),
1250
+ 'readable': self._is_memory_readable(memory_info['protect']),
1251
+ 'writable': self._is_memory_writable(memory_info['protect']),
1252
+ 'executable': self._is_memory_executable(memory_info['protect'])
1253
+ }
1254
+
1255
+ memory_regions.append(region)
1256
+ current_address = memory_info['base_address'] + memory_info['region_size']
1257
+
1258
+ # Prevent infinite loops
1259
+ if len(memory_regions) > 10000:
1260
+ break
1261
+
1262
+ return memory_regions
1263
+
1264
+ except Exception as e:
1265
+ logger.error(f"Error getting memory map: {e}")
1266
+ return memory_regions
1267
+
1268
+ def _memory_state_to_string(self, state: int) -> str:
1269
+ """Convert memory state constant to string"""
1270
+ states = {
1271
+ 0x1000: "MEM_COMMIT",
1272
+ 0x10000: "MEM_FREE",
1273
+ 0x2000: "MEM_RESERVE"
1274
+ }
1275
+ return states.get(state, f"Unknown_{state:X}")
1276
+
1277
+ def _memory_protect_to_string(self, protect: int) -> str:
1278
+ """Convert memory protection constant to string"""
1279
+ protections = {
1280
+ 0x01: "PAGE_NOACCESS",
1281
+ 0x02: "PAGE_READONLY",
1282
+ 0x04: "PAGE_READWRITE",
1283
+ 0x08: "PAGE_WRITECOPY",
1284
+ 0x10: "PAGE_EXECUTE",
1285
+ 0x20: "PAGE_EXECUTE_READ",
1286
+ 0x40: "PAGE_EXECUTE_READWRITE",
1287
+ 0x80: "PAGE_EXECUTE_WRITECOPY"
1288
+ }
1289
+
1290
+ base_protect = protect & 0xFF
1291
+ result = protections.get(base_protect, f"Unknown_{base_protect:X}")
1292
+
1293
+ # Add modifiers
1294
+ if protect & 0x100:
1295
+ result += " | PAGE_GUARD"
1296
+ if protect & 0x200:
1297
+ result += " | PAGE_NOCACHE"
1298
+ if protect & 0x400:
1299
+ result += " | PAGE_WRITECOMBINE"
1300
+
1301
+ return result
1302
+
1303
+ def _memory_type_to_string(self, mem_type: int) -> str:
1304
+ """Convert memory type constant to string"""
1305
+ types = {
1306
+ 0x1000000: "MEM_IMAGE",
1307
+ 0x40000: "MEM_MAPPED",
1308
+ 0x20000: "MEM_PRIVATE"
1309
+ }
1310
+ return types.get(mem_type, f"Unknown_{mem_type:X}")
1311
+
1312
+ def _is_memory_readable(self, protect: int) -> bool:
1313
+ """Check if memory is readable"""
1314
+ readable_protects = {0x02, 0x04, 0x08, 0x20, 0x40, 0x80}
1315
+ return (protect & 0xFF) in readable_protects
1316
+
1317
+ def _is_memory_writable(self, protect: int) -> bool:
1318
+ """Check if memory is writable"""
1319
+ writable_protects = {0x04, 0x08, 0x40, 0x80}
1320
+ return (protect & 0xFF) in writable_protects
1321
+
1322
+ def _is_memory_executable(self, protect: int) -> bool:
1323
+ """Check if memory is executable"""
1324
+ executable_protects = {0x10, 0x20, 0x40, 0x80}
1325
+ return (protect & 0xFF) in executable_protects
1326
+
1327
+ def _value_to_bytes(self, value: Union[int, float, str], data_type: str) -> List[bytes]:
1328
+ """Convert value to byte patterns for searching"""
1329
+ patterns = []
1330
+
1331
+ try:
1332
+ if data_type == 'int8':
1333
+ patterns.append(struct.pack('<b', int(value)))
1334
+ elif data_type == 'int16':
1335
+ patterns.append(struct.pack('<h', int(value)))
1336
+ elif data_type == 'int32':
1337
+ patterns.append(struct.pack('<i', int(value)))
1338
+ elif data_type == 'int64':
1339
+ patterns.append(struct.pack('<q', int(value)))
1340
+ elif data_type == 'uint8':
1341
+ patterns.append(struct.pack('<B', int(value)))
1342
+ elif data_type == 'uint16':
1343
+ patterns.append(struct.pack('<H', int(value)))
1344
+ elif data_type == 'uint32':
1345
+ patterns.append(struct.pack('<I', int(value)))
1346
+ elif data_type == 'uint64':
1347
+ patterns.append(struct.pack('<Q', int(value)))
1348
+ elif data_type == 'float':
1349
+ patterns.append(struct.pack('<f', float(value)))
1350
+ elif data_type == 'double':
1351
+ patterns.append(struct.pack('<d', float(value)))
1352
+ elif data_type == 'string':
1353
+ # Support multiple encodings
1354
+ str_value = str(value)
1355
+ patterns.append(str_value.encode('utf-8'))
1356
+ patterns.append(str_value.encode('utf-16le'))
1357
+ patterns.append(str_value.encode('ascii', errors='ignore'))
1358
+
1359
+ return patterns
1360
+
1361
+ except (struct.error, ValueError, UnicodeError) as e:
1362
+ logger.error(f"Error converting value {value} to {data_type}: {e}")
1363
+ return []
1364
+
1365
+ def _bytes_to_value(self, data: bytes, data_type: str) -> Optional[Union[int, float]]:
1366
+ """Convert bytes to typed value"""
1367
+ try:
1368
+ if len(data) < self._get_type_size(data_type):
1369
+ return None
1370
+
1371
+ if data_type == 'int8':
1372
+ return struct.unpack('<b', data[:1])[0]
1373
+ elif data_type == 'int16':
1374
+ return struct.unpack('<h', data[:2])[0]
1375
+ elif data_type == 'int32':
1376
+ return struct.unpack('<i', data[:4])[0]
1377
+ elif data_type == 'int64':
1378
+ return struct.unpack('<q', data[:8])[0]
1379
+ elif data_type == 'uint8':
1380
+ return struct.unpack('<B', data[:1])[0]
1381
+ elif data_type == 'uint16':
1382
+ return struct.unpack('<H', data[:2])[0]
1383
+ elif data_type == 'uint32':
1384
+ return struct.unpack('<I', data[:4])[0]
1385
+ elif data_type == 'uint64':
1386
+ return struct.unpack('<Q', data[:8])[0]
1387
+ elif data_type == 'float':
1388
+ return struct.unpack('<f', data[:4])[0]
1389
+ elif data_type == 'double':
1390
+ return struct.unpack('<d', data[:8])[0]
1391
+
1392
+ return None
1393
+
1394
+ except struct.error:
1395
+ return None
1396
+
1397
+ def _get_type_size(self, data_type: str) -> int:
1398
+ """Get size in bytes for data type"""
1399
+ sizes = {
1400
+ 'int8': 1, 'uint8': 1,
1401
+ 'int16': 2, 'uint16': 2,
1402
+ 'int32': 4, 'uint32': 4,
1403
+ 'int64': 8, 'uint64': 8,
1404
+ 'float': 4, 'double': 8
1405
+ }
1406
+ return sizes.get(data_type, 1)
1407
+
1408
+ def _read_typed_value(self, handle: int, address: int, data_type: str) -> Optional[Union[int, float, str]]:
1409
+ """Read a typed value from memory"""
1410
+ size = self._get_type_size(data_type)
1411
+
1412
+ if data_type == 'string':
1413
+ # Try to read a reasonable string length
1414
+ data = self.read_process_memory(handle, address, 256)
1415
+ if data:
1416
+ # Try different encodings
1417
+ for encoding in ['utf-8', 'utf-16le', 'ascii']:
1418
+ try:
1419
+ # Find null terminator
1420
+ if encoding == 'utf-16le':
1421
+ null_pos = data.find(b'\x00\x00')
1422
+ if null_pos >= 0 and null_pos % 2 == 0:
1423
+ string_data = data[:null_pos+2]
1424
+ else:
1425
+ string_data = data[:32] # Limit length
1426
+ else:
1427
+ null_pos = data.find(b'\x00')
1428
+ string_data = data[:null_pos] if null_pos >= 0 else data[:64]
1429
+
1430
+ return string_data.decode(encoding).rstrip('\x00')
1431
+ except UnicodeDecodeError:
1432
+ continue
1433
+ return None
1434
+ else:
1435
+ data = self.read_process_memory(handle, address, size)
1436
+ if data:
1437
+ return self._bytes_to_value(data, data_type)
1438
+ return None
1439
+
1440
+ def disassemble_code(self, handle: int, address: int, size: int = 64,
1441
+ architecture: str = 'x64') -> List[DisassemblyResult]:
1442
+ """Disassemble code at address
1443
+
1444
+ Args:
1445
+ handle: Process handle
1446
+ address: Code address
1447
+ size: Number of bytes to disassemble
1448
+ architecture: Target architecture ('x64' or 'x86')
1449
+
1450
+ Returns:
1451
+ List of DisassemblyResult objects
1452
+ """
1453
+ results = []
1454
+
1455
+ if not CAPSTONE_AVAILABLE:
1456
+ logger.warning("Capstone engine not available for disassembly")
1457
+ return results
1458
+
1459
+ try:
1460
+ # Read code bytes
1461
+ code_data = self.read_process_memory(handle, address, size)
1462
+ if not code_data:
1463
+ return results
1464
+
1465
+ # Initialize disassembler
1466
+ if architecture == 'x64':
1467
+ md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64)
1468
+ else:
1469
+ md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32)
1470
+
1471
+ md.detail = True
1472
+
1473
+ # Disassemble
1474
+ for insn in md.disasm(code_data, address):
1475
+ groups = []
1476
+ if hasattr(insn, 'groups'):
1477
+ for group_id in insn.groups:
1478
+ group_name = insn.group_name(group_id)
1479
+ if group_name:
1480
+ groups.append(group_name)
1481
+
1482
+ results.append(DisassemblyResult(
1483
+ address=insn.address,
1484
+ bytes_data=insn.bytes,
1485
+ mnemonic=insn.mnemonic,
1486
+ op_str=insn.op_str,
1487
+ size=insn.size,
1488
+ groups=groups
1489
+ ))
1490
+
1491
+ return results
1492
+
1493
+ except Exception as e:
1494
+ logger.error(f"Error disassembling code at 0x{address:X}: {e}")
1495
+ return results
1496
+
1497
+ def find_string_references(self, handle: int, target_string: str,
1498
+ start_address: int = 0, end_address: int = 0x7FFFFFFF) -> List[Dict[str, Any]]:
1499
+ """Find references to a string in memory
1500
+
1501
+ Args:
1502
+ handle: Process handle
1503
+ target_string: String to find references to
1504
+ start_address: Start search address
1505
+ end_address: End search address
1506
+
1507
+ Returns:
1508
+ List of reference information
1509
+ """
1510
+ references = []
1511
+
1512
+ try:
1513
+ # First, find all instances of the string
1514
+ string_addresses = []
1515
+ for encoding in ['utf-8', 'utf-16le', 'ascii']:
1516
+ try:
1517
+ pattern = target_string.encode(encoding)
1518
+ addresses = self.find_pattern_in_memory(handle, pattern, start_address, end_address)
1519
+ for addr in addresses:
1520
+ string_addresses.append({
1521
+ 'address': addr,
1522
+ 'encoding': encoding,
1523
+ 'size': len(pattern)
1524
+ })
1525
+ except UnicodeEncodeError:
1526
+ continue
1527
+
1528
+ # Find code that references these strings
1529
+ for string_info in string_addresses:
1530
+ string_addr = string_info['address']
1531
+
1532
+ # Look for pointer references to this string
1533
+ pointer_patterns = []
1534
+ if ctypes.sizeof(ctypes.c_void_p) == 8: # 64-bit
1535
+ pointer_patterns.append(struct.pack('<Q', string_addr))
1536
+ else: # 32-bit
1537
+ pointer_patterns.append(struct.pack('<I', string_addr))
1538
+
1539
+ for pattern in pointer_patterns:
1540
+ ref_addresses = self.find_pattern_in_memory(handle, pattern, start_address, end_address)
1541
+ for ref_addr in ref_addresses:
1542
+ # Check if this is in an executable region
1543
+ memory_info = self.query_memory_info(handle, ref_addr)
1544
+ if memory_info and self._is_memory_executable(memory_info['protect']):
1545
+ references.append({
1546
+ 'reference_address': ref_addr,
1547
+ 'string_address': string_addr,
1548
+ 'string_encoding': string_info['encoding'],
1549
+ 'string_value': target_string,
1550
+ 'region_info': memory_info
1551
+ })
1552
+
1553
+ return references
1554
+
1555
+ except Exception as e:
1556
+ logger.error(f"Error finding string references: {e}")
1557
+ return references
1558
+
1559
+ def analyze_data_structures(self, handle: int, address: int, size: int = 256) -> Dict[str, Any]:
1560
+ """Analyze potential data structures at address
1561
+
1562
+ Args:
1563
+ handle: Process handle
1564
+ address: Address to analyze
1565
+ size: Size of data to analyze
1566
+
1567
+ Returns:
1568
+ Analysis results
1569
+ """
1570
+ analysis = {
1571
+ 'address': address,
1572
+ 'size': size,
1573
+ 'potential_types': [],
1574
+ 'patterns': [],
1575
+ 'pointers': [],
1576
+ 'strings': []
1577
+ }
1578
+
1579
+ try:
1580
+ data = self.read_process_memory(handle, address, size)
1581
+ if not data:
1582
+ return analysis
1583
+
1584
+ # Look for potential pointers
1585
+ ptr_size = ctypes.sizeof(ctypes.c_void_p)
1586
+ for i in range(0, len(data) - ptr_size + 1, ptr_size):
1587
+ if ptr_size == 8:
1588
+ ptr_value = struct.unpack('<Q', data[i:i+8])[0]
1589
+ else:
1590
+ ptr_value = struct.unpack('<I', data[i:i+4])[0]
1591
+
1592
+ # Check if this looks like a valid pointer
1593
+ # Use appropriate ranges for 32-bit vs 64-bit
1594
+ if ptr_size == 8:
1595
+ # 64-bit: typical user space ranges
1596
+ if 0x10000 <= ptr_value <= 0x7FFFFFFFFFFF:
1597
+ # Try to read what it points to
1598
+ pointed_data = self.read_process_memory(handle, ptr_value, 64)
1599
+ if pointed_data:
1600
+ analysis['pointers'].append({
1601
+ 'offset': i,
1602
+ 'address': ptr_value,
1603
+ 'preview': pointed_data[:16].hex()
1604
+ })
1605
+ else:
1606
+ # 32-bit: traditional range
1607
+ if 0x10000 <= ptr_value <= 0x7FFFFFFF:
1608
+ # Try to read what it points to
1609
+ pointed_data = self.read_process_memory(handle, ptr_value, 64)
1610
+ if pointed_data:
1611
+ analysis['pointers'].append({
1612
+ 'offset': i,
1613
+ 'address': ptr_value,
1614
+ 'preview': pointed_data[:16].hex()
1615
+ })
1616
+
1617
+ # Look for potential strings
1618
+ for encoding in ['utf-8', 'ascii']:
1619
+ try:
1620
+ decoded = data.decode(encoding, errors='ignore')
1621
+ # Find printable strings of reasonable length
1622
+ current_string = ""
1623
+ for char in decoded:
1624
+ if char in string.printable and char not in '\r\n\t':
1625
+ current_string += char
1626
+ else:
1627
+ if len(current_string) >= 4:
1628
+ analysis['strings'].append({
1629
+ 'value': current_string,
1630
+ 'encoding': encoding,
1631
+ 'length': len(current_string)
1632
+ })
1633
+ current_string = ""
1634
+
1635
+ if len(current_string) >= 4:
1636
+ analysis['strings'].append({
1637
+ 'value': current_string,
1638
+ 'encoding': encoding,
1639
+ 'length': len(current_string)
1640
+ })
1641
+ except UnicodeDecodeError:
1642
+ pass
1643
+
1644
+ # Look for patterns
1645
+ # Common patterns like repetitive data, arrays, etc.
1646
+ if len(data) >= 16:
1647
+ # Check for repeating patterns
1648
+ for pattern_size in [4, 8, 16]:
1649
+ if len(data) >= pattern_size * 2:
1650
+ pattern = data[:pattern_size]
1651
+ if data[pattern_size:pattern_size*2] == pattern:
1652
+ analysis['patterns'].append({
1653
+ 'type': 'repeating',
1654
+ 'pattern_size': pattern_size,
1655
+ 'pattern': pattern.hex()
1656
+ })
1657
+
1658
+ # Suggest potential data types
1659
+ if len(analysis['pointers']) > 2:
1660
+ analysis['potential_types'].append('vtable or function pointer array')
1661
+ if len(analysis['strings']) > 0:
1662
+ analysis['potential_types'].append('object with string members')
1663
+ if len(analysis['patterns']) > 0:
1664
+ analysis['potential_types'].append('array or structured data')
1665
+
1666
+ return analysis
1667
+
1668
+ except Exception as e:
1669
+ logger.error(f"Error analyzing data structures at 0x{address:X}: {e}")
1670
+ return analysis