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.
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/METADATA +16 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/RECORD +40 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/WHEEL +5 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/top_level.txt +1 -0
- server/cheatengine/__init__.py +19 -0
- server/cheatengine/ce_bridge.py +1670 -0
- server/cheatengine/lua_interface.py +460 -0
- server/cheatengine/table_parser.py +1221 -0
- server/config/__init__.py +20 -0
- server/config/settings.py +347 -0
- server/config/whitelist.py +378 -0
- server/gui_automation/__init__.py +43 -0
- server/gui_automation/core/__init__.py +8 -0
- server/gui_automation/core/integration.py +951 -0
- server/gui_automation/demos/__init__.py +8 -0
- server/gui_automation/demos/basic_demo.py +754 -0
- server/gui_automation/demos/notepad_demo.py +460 -0
- server/gui_automation/demos/simple_demo.py +319 -0
- server/gui_automation/tools/__init__.py +8 -0
- server/gui_automation/tools/mcp_tools.py +974 -0
- server/main.py +519 -0
- server/memory/__init__.py +0 -0
- server/memory/analyzer.py +0 -0
- server/memory/reader.py +0 -0
- server/memory/scanner.py +0 -0
- server/memory/symbols.py +0 -0
- server/process/__init__.py +16 -0
- server/process/launcher.py +608 -0
- server/process/manager.py +185 -0
- server/process/monitors.py +202 -0
- server/process/permissions.py +131 -0
- server/process_whitelist.json +119 -0
- server/pyautogui/__init__.py +0 -0
- server/utils/__init__.py +37 -0
- server/utils/data_types.py +368 -0
- server/utils/formatters.py +430 -0
- server/utils/validators.py +340 -0
- 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
|