crabcode 0.1.6__py3-none-win_amd64.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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- crabcode_search/tool.py +268 -0
|
@@ -0,0 +1,1806 @@
|
|
|
1
|
+
"""Process memory inspection helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import ctypes
|
|
7
|
+
import ctypes.wintypes
|
|
8
|
+
import platform
|
|
9
|
+
import re
|
|
10
|
+
import struct
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_NUMERIC_FORMATS: dict[str, str] = {
|
|
18
|
+
"int8": "b",
|
|
19
|
+
"uint8": "B",
|
|
20
|
+
"int16": "h",
|
|
21
|
+
"uint16": "H",
|
|
22
|
+
"int32": "i",
|
|
23
|
+
"uint32": "I",
|
|
24
|
+
"int64": "q",
|
|
25
|
+
"uint64": "Q",
|
|
26
|
+
"float32": "f",
|
|
27
|
+
"float64": "d",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MemoryBackend:
|
|
32
|
+
name = "unavailable"
|
|
33
|
+
available = False
|
|
34
|
+
note = "direct memory backend unavailable"
|
|
35
|
+
|
|
36
|
+
def regions(self, pid: int) -> list["MemoryRegion"]:
|
|
37
|
+
raise RuntimeError(self.note)
|
|
38
|
+
|
|
39
|
+
def read(self, pid: int, address: int, size: int) -> bytes:
|
|
40
|
+
raise RuntimeError(self.note)
|
|
41
|
+
|
|
42
|
+
def write(self, pid: int, address: int, data: bytes) -> None:
|
|
43
|
+
raise RuntimeError(self.note)
|
|
44
|
+
|
|
45
|
+
def protect(self, pid: int, address: int, size: int, writable: bool, executable: bool) -> Any:
|
|
46
|
+
raise RuntimeError("memory protection changes are unavailable on this backend")
|
|
47
|
+
|
|
48
|
+
def restore_protection(self, pid: int, address: int, size: int, token: Any) -> None:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def flush_instruction_cache(self, pid: int, address: int, size: int) -> None:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class LinuxProcfsMemoryBackend(MemoryBackend):
|
|
56
|
+
name = "linux-procfs"
|
|
57
|
+
available = True
|
|
58
|
+
note = "Linux /proc/<pid>/mem backend"
|
|
59
|
+
|
|
60
|
+
def regions(self, pid: int) -> list["MemoryRegion"]:
|
|
61
|
+
maps = Path(f"/proc/{pid}/maps")
|
|
62
|
+
text = maps.read_text(encoding="utf-8", errors="replace")
|
|
63
|
+
regions: list[MemoryRegion] = []
|
|
64
|
+
for line in text.splitlines():
|
|
65
|
+
parts = line.split(maxsplit=5)
|
|
66
|
+
if len(parts) < 5:
|
|
67
|
+
continue
|
|
68
|
+
start_raw, end_raw = parts[0].split("-", 1)
|
|
69
|
+
path = parts[5] if len(parts) > 5 else ""
|
|
70
|
+
regions.append(
|
|
71
|
+
MemoryRegion(
|
|
72
|
+
start=int(start_raw, 16),
|
|
73
|
+
end=int(end_raw, 16),
|
|
74
|
+
permissions=parts[1],
|
|
75
|
+
offset=int(parts[2], 16),
|
|
76
|
+
device=parts[3],
|
|
77
|
+
inode=parts[4],
|
|
78
|
+
path=path,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return regions
|
|
82
|
+
|
|
83
|
+
def read(self, pid: int, address: int, size: int) -> bytes:
|
|
84
|
+
with open(f"/proc/{pid}/mem", "rb", buffering=0) as handle:
|
|
85
|
+
handle.seek(address)
|
|
86
|
+
return handle.read(size)
|
|
87
|
+
|
|
88
|
+
def write(self, pid: int, address: int, data: bytes) -> None:
|
|
89
|
+
with open(f"/proc/{pid}/mem", "r+b", buffering=0) as handle:
|
|
90
|
+
handle.seek(address)
|
|
91
|
+
handle.write(data)
|
|
92
|
+
|
|
93
|
+
def protect(self, pid: int, address: int, size: int, writable: bool, executable: bool) -> Any:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def restore_protection(self, pid: int, address: int, size: int, token: Any) -> None:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class WindowsMemoryBackend(MemoryBackend):
|
|
101
|
+
name = "windows-kernel32"
|
|
102
|
+
note = "Windows kernel32 ReadProcessMemory/WriteProcessMemory backend"
|
|
103
|
+
|
|
104
|
+
PROCESS_QUERY_INFORMATION = 0x0400
|
|
105
|
+
PROCESS_VM_OPERATION = 0x0008
|
|
106
|
+
PROCESS_VM_READ = 0x0010
|
|
107
|
+
PROCESS_VM_WRITE = 0x0020
|
|
108
|
+
MEM_COMMIT = 0x1000
|
|
109
|
+
PAGE_NOACCESS = 0x01
|
|
110
|
+
PAGE_READONLY = 0x02
|
|
111
|
+
PAGE_READWRITE = 0x04
|
|
112
|
+
PAGE_WRITECOPY = 0x08
|
|
113
|
+
PAGE_EXECUTE = 0x10
|
|
114
|
+
PAGE_EXECUTE_READ = 0x20
|
|
115
|
+
PAGE_EXECUTE_READWRITE = 0x40
|
|
116
|
+
PAGE_EXECUTE_WRITECOPY = 0x80
|
|
117
|
+
PAGE_GUARD = 0x100
|
|
118
|
+
|
|
119
|
+
def __init__(self) -> None:
|
|
120
|
+
self.available = platform.system().lower() == "windows"
|
|
121
|
+
if not self.available:
|
|
122
|
+
return
|
|
123
|
+
self.kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
124
|
+
self._configure_api()
|
|
125
|
+
|
|
126
|
+
def _configure_api(self) -> None:
|
|
127
|
+
wintypes = ctypes.wintypes
|
|
128
|
+
self.kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
|
129
|
+
self.kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
130
|
+
self.kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
131
|
+
self.kernel32.CloseHandle.restype = wintypes.BOOL
|
|
132
|
+
self.kernel32.ReadProcessMemory.argtypes = [
|
|
133
|
+
wintypes.HANDLE,
|
|
134
|
+
ctypes.c_void_p,
|
|
135
|
+
ctypes.c_void_p,
|
|
136
|
+
ctypes.c_size_t,
|
|
137
|
+
ctypes.POINTER(ctypes.c_size_t),
|
|
138
|
+
]
|
|
139
|
+
self.kernel32.ReadProcessMemory.restype = wintypes.BOOL
|
|
140
|
+
self.kernel32.WriteProcessMemory.argtypes = [
|
|
141
|
+
wintypes.HANDLE,
|
|
142
|
+
ctypes.c_void_p,
|
|
143
|
+
ctypes.c_void_p,
|
|
144
|
+
ctypes.c_size_t,
|
|
145
|
+
ctypes.POINTER(ctypes.c_size_t),
|
|
146
|
+
]
|
|
147
|
+
self.kernel32.WriteProcessMemory.restype = wintypes.BOOL
|
|
148
|
+
self.kernel32.VirtualProtectEx.argtypes = [
|
|
149
|
+
wintypes.HANDLE,
|
|
150
|
+
ctypes.c_void_p,
|
|
151
|
+
ctypes.c_size_t,
|
|
152
|
+
wintypes.DWORD,
|
|
153
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
154
|
+
]
|
|
155
|
+
self.kernel32.VirtualProtectEx.restype = wintypes.BOOL
|
|
156
|
+
self.kernel32.VirtualQueryEx.argtypes = [
|
|
157
|
+
wintypes.HANDLE,
|
|
158
|
+
ctypes.c_void_p,
|
|
159
|
+
ctypes.c_void_p,
|
|
160
|
+
ctypes.c_size_t,
|
|
161
|
+
]
|
|
162
|
+
self.kernel32.VirtualQueryEx.restype = ctypes.c_size_t
|
|
163
|
+
self.kernel32.FlushInstructionCache.argtypes = [
|
|
164
|
+
wintypes.HANDLE,
|
|
165
|
+
ctypes.c_void_p,
|
|
166
|
+
ctypes.c_size_t,
|
|
167
|
+
]
|
|
168
|
+
self.kernel32.FlushInstructionCache.restype = wintypes.BOOL
|
|
169
|
+
|
|
170
|
+
def _open(self, pid: int, access: int) -> Any:
|
|
171
|
+
handle = self.kernel32.OpenProcess(access, False, pid)
|
|
172
|
+
if not handle:
|
|
173
|
+
error_code = ctypes.get_last_error()
|
|
174
|
+
raise OSError(
|
|
175
|
+
error_code,
|
|
176
|
+
f"OpenProcess failed for pid {pid}; try running CrabCode as Administrator, "
|
|
177
|
+
"ensure the target process is not running with higher privileges, and note that "
|
|
178
|
+
"protected/system processes may still deny memory access",
|
|
179
|
+
)
|
|
180
|
+
return handle
|
|
181
|
+
|
|
182
|
+
def regions(self, pid: int) -> list["MemoryRegion"]:
|
|
183
|
+
class MEMORY_BASIC_INFORMATION(ctypes.Structure):
|
|
184
|
+
_fields_ = [
|
|
185
|
+
("BaseAddress", ctypes.c_void_p),
|
|
186
|
+
("AllocationBase", ctypes.c_void_p),
|
|
187
|
+
("AllocationProtect", ctypes.c_ulong),
|
|
188
|
+
("PartitionId", ctypes.c_ushort),
|
|
189
|
+
("RegionSize", ctypes.c_size_t),
|
|
190
|
+
("State", ctypes.c_ulong),
|
|
191
|
+
("Protect", ctypes.c_ulong),
|
|
192
|
+
("Type", ctypes.c_ulong),
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
regions: list[MemoryRegion] = []
|
|
196
|
+
handle = self._open(pid, self.PROCESS_QUERY_INFORMATION | self.PROCESS_VM_READ)
|
|
197
|
+
try:
|
|
198
|
+
address = 0
|
|
199
|
+
max_address = (1 << (ctypes.sizeof(ctypes.c_void_p) * 8)) - 1
|
|
200
|
+
mbi = MEMORY_BASIC_INFORMATION()
|
|
201
|
+
mbi_size = ctypes.sizeof(mbi)
|
|
202
|
+
while address < max_address:
|
|
203
|
+
result = self.kernel32.VirtualQueryEx(
|
|
204
|
+
handle,
|
|
205
|
+
ctypes.c_void_p(address),
|
|
206
|
+
ctypes.byref(mbi),
|
|
207
|
+
mbi_size,
|
|
208
|
+
)
|
|
209
|
+
if not result:
|
|
210
|
+
break
|
|
211
|
+
base = int(mbi.BaseAddress or 0)
|
|
212
|
+
size = int(mbi.RegionSize or 0)
|
|
213
|
+
if size <= 0:
|
|
214
|
+
address += 0x1000
|
|
215
|
+
continue
|
|
216
|
+
if int(mbi.State) == self.MEM_COMMIT:
|
|
217
|
+
regions.append(
|
|
218
|
+
MemoryRegion(
|
|
219
|
+
start=base,
|
|
220
|
+
end=base + size,
|
|
221
|
+
permissions=self._protection_to_permissions(int(mbi.Protect)),
|
|
222
|
+
offset=0,
|
|
223
|
+
device="",
|
|
224
|
+
inode="",
|
|
225
|
+
path="",
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
next_address = base + size
|
|
229
|
+
if next_address <= address:
|
|
230
|
+
break
|
|
231
|
+
address = next_address
|
|
232
|
+
finally:
|
|
233
|
+
self.kernel32.CloseHandle(handle)
|
|
234
|
+
return regions
|
|
235
|
+
|
|
236
|
+
def read(self, pid: int, address: int, size: int) -> bytes:
|
|
237
|
+
handle = self._open(pid, self.PROCESS_VM_READ | self.PROCESS_QUERY_INFORMATION)
|
|
238
|
+
try:
|
|
239
|
+
buffer = ctypes.create_string_buffer(size)
|
|
240
|
+
read = ctypes.c_size_t(0)
|
|
241
|
+
ok = self.kernel32.ReadProcessMemory(
|
|
242
|
+
handle,
|
|
243
|
+
ctypes.c_void_p(address),
|
|
244
|
+
buffer,
|
|
245
|
+
size,
|
|
246
|
+
ctypes.byref(read),
|
|
247
|
+
)
|
|
248
|
+
if not ok:
|
|
249
|
+
raise OSError(ctypes.get_last_error(), f"ReadProcessMemory failed at {hex(address)}")
|
|
250
|
+
return buffer.raw[: read.value]
|
|
251
|
+
finally:
|
|
252
|
+
self.kernel32.CloseHandle(handle)
|
|
253
|
+
|
|
254
|
+
def write(self, pid: int, address: int, data: bytes) -> None:
|
|
255
|
+
handle = self._open(
|
|
256
|
+
pid,
|
|
257
|
+
self.PROCESS_VM_WRITE | self.PROCESS_VM_OPERATION | self.PROCESS_QUERY_INFORMATION,
|
|
258
|
+
)
|
|
259
|
+
try:
|
|
260
|
+
buffer = ctypes.create_string_buffer(data)
|
|
261
|
+
written = ctypes.c_size_t(0)
|
|
262
|
+
ok = self.kernel32.WriteProcessMemory(
|
|
263
|
+
handle,
|
|
264
|
+
ctypes.c_void_p(address),
|
|
265
|
+
buffer,
|
|
266
|
+
len(data),
|
|
267
|
+
ctypes.byref(written),
|
|
268
|
+
)
|
|
269
|
+
if not ok or written.value != len(data):
|
|
270
|
+
raise OSError(ctypes.get_last_error(), f"WriteProcessMemory failed at {hex(address)}")
|
|
271
|
+
finally:
|
|
272
|
+
self.kernel32.CloseHandle(handle)
|
|
273
|
+
|
|
274
|
+
def protect(self, pid: int, address: int, size: int, writable: bool, executable: bool) -> Any:
|
|
275
|
+
handle = self._open(pid, self.PROCESS_VM_OPERATION | self.PROCESS_QUERY_INFORMATION)
|
|
276
|
+
new_protect = self.PAGE_EXECUTE_READWRITE if executable else self.PAGE_READWRITE
|
|
277
|
+
old_protect = ctypes.wintypes.DWORD(0)
|
|
278
|
+
try:
|
|
279
|
+
ok = self.kernel32.VirtualProtectEx(
|
|
280
|
+
handle,
|
|
281
|
+
ctypes.c_void_p(address),
|
|
282
|
+
size,
|
|
283
|
+
new_protect,
|
|
284
|
+
ctypes.byref(old_protect),
|
|
285
|
+
)
|
|
286
|
+
if not ok:
|
|
287
|
+
raise OSError(ctypes.get_last_error(), f"VirtualProtectEx failed at {hex(address)}")
|
|
288
|
+
return int(old_protect.value)
|
|
289
|
+
finally:
|
|
290
|
+
self.kernel32.CloseHandle(handle)
|
|
291
|
+
|
|
292
|
+
def restore_protection(self, pid: int, address: int, size: int, token: Any) -> None:
|
|
293
|
+
if token is None:
|
|
294
|
+
return
|
|
295
|
+
handle = self._open(pid, self.PROCESS_VM_OPERATION | self.PROCESS_QUERY_INFORMATION)
|
|
296
|
+
old_protect = ctypes.wintypes.DWORD(0)
|
|
297
|
+
try:
|
|
298
|
+
self.kernel32.VirtualProtectEx(
|
|
299
|
+
handle,
|
|
300
|
+
ctypes.c_void_p(address),
|
|
301
|
+
size,
|
|
302
|
+
int(token),
|
|
303
|
+
ctypes.byref(old_protect),
|
|
304
|
+
)
|
|
305
|
+
finally:
|
|
306
|
+
self.kernel32.CloseHandle(handle)
|
|
307
|
+
|
|
308
|
+
def flush_instruction_cache(self, pid: int, address: int, size: int) -> None:
|
|
309
|
+
handle = self._open(pid, self.PROCESS_VM_OPERATION | self.PROCESS_QUERY_INFORMATION)
|
|
310
|
+
try:
|
|
311
|
+
ok = self.kernel32.FlushInstructionCache(handle, ctypes.c_void_p(address), size)
|
|
312
|
+
if not ok:
|
|
313
|
+
raise OSError(ctypes.get_last_error(), f"FlushInstructionCache failed at {hex(address)}")
|
|
314
|
+
finally:
|
|
315
|
+
self.kernel32.CloseHandle(handle)
|
|
316
|
+
|
|
317
|
+
def _protection_to_permissions(self, protect: int) -> str:
|
|
318
|
+
if protect & self.PAGE_GUARD or protect == self.PAGE_NOACCESS:
|
|
319
|
+
return "---p"
|
|
320
|
+
readable = protect in {
|
|
321
|
+
self.PAGE_READONLY,
|
|
322
|
+
self.PAGE_READWRITE,
|
|
323
|
+
self.PAGE_WRITECOPY,
|
|
324
|
+
self.PAGE_EXECUTE_READ,
|
|
325
|
+
self.PAGE_EXECUTE_READWRITE,
|
|
326
|
+
self.PAGE_EXECUTE_WRITECOPY,
|
|
327
|
+
}
|
|
328
|
+
writable = protect in {
|
|
329
|
+
self.PAGE_READWRITE,
|
|
330
|
+
self.PAGE_WRITECOPY,
|
|
331
|
+
self.PAGE_EXECUTE_READWRITE,
|
|
332
|
+
self.PAGE_EXECUTE_WRITECOPY,
|
|
333
|
+
}
|
|
334
|
+
executable = protect in {
|
|
335
|
+
self.PAGE_EXECUTE,
|
|
336
|
+
self.PAGE_EXECUTE_READ,
|
|
337
|
+
self.PAGE_EXECUTE_READWRITE,
|
|
338
|
+
self.PAGE_EXECUTE_WRITECOPY,
|
|
339
|
+
}
|
|
340
|
+
return (
|
|
341
|
+
("r" if readable else "-")
|
|
342
|
+
+ ("w" if writable else "-")
|
|
343
|
+
+ ("x" if executable else "-")
|
|
344
|
+
+ "p"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class MacOSMachMemoryBackend(MemoryBackend):
|
|
349
|
+
name = "macos-mach"
|
|
350
|
+
note = "macOS Mach task memory backend"
|
|
351
|
+
|
|
352
|
+
KERN_SUCCESS = 0
|
|
353
|
+
VM_PROT_READ = 0x01
|
|
354
|
+
VM_PROT_WRITE = 0x02
|
|
355
|
+
VM_PROT_EXECUTE = 0x04
|
|
356
|
+
|
|
357
|
+
def __init__(self) -> None:
|
|
358
|
+
self.available = platform.system().lower() == "darwin"
|
|
359
|
+
if not self.available:
|
|
360
|
+
return
|
|
361
|
+
self.lib = ctypes.CDLL("/usr/lib/libSystem.B.dylib", use_errno=True)
|
|
362
|
+
self._configure_api()
|
|
363
|
+
|
|
364
|
+
def _configure_api(self) -> None:
|
|
365
|
+
self.lib.mach_task_self.restype = ctypes.c_uint32
|
|
366
|
+
self.lib.task_for_pid.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.POINTER(ctypes.c_uint32)]
|
|
367
|
+
self.lib.task_for_pid.restype = ctypes.c_int
|
|
368
|
+
self.lib.mach_port_deallocate.argtypes = [ctypes.c_uint32, ctypes.c_uint32]
|
|
369
|
+
self.lib.mach_vm_read_overwrite.argtypes = [
|
|
370
|
+
ctypes.c_uint32,
|
|
371
|
+
ctypes.c_uint64,
|
|
372
|
+
ctypes.c_uint64,
|
|
373
|
+
ctypes.c_void_p,
|
|
374
|
+
ctypes.POINTER(ctypes.c_uint64),
|
|
375
|
+
]
|
|
376
|
+
self.lib.mach_vm_read_overwrite.restype = ctypes.c_int
|
|
377
|
+
self.lib.mach_vm_write.argtypes = [
|
|
378
|
+
ctypes.c_uint32,
|
|
379
|
+
ctypes.c_uint64,
|
|
380
|
+
ctypes.c_void_p,
|
|
381
|
+
ctypes.c_uint32,
|
|
382
|
+
]
|
|
383
|
+
self.lib.mach_vm_write.restype = ctypes.c_int
|
|
384
|
+
self.lib.mach_vm_protect.argtypes = [
|
|
385
|
+
ctypes.c_uint32,
|
|
386
|
+
ctypes.c_uint64,
|
|
387
|
+
ctypes.c_uint64,
|
|
388
|
+
ctypes.c_int,
|
|
389
|
+
ctypes.c_int,
|
|
390
|
+
]
|
|
391
|
+
self.lib.mach_vm_protect.restype = ctypes.c_int
|
|
392
|
+
self.lib.mach_vm_region_recurse.argtypes = [
|
|
393
|
+
ctypes.c_uint32,
|
|
394
|
+
ctypes.POINTER(ctypes.c_uint64),
|
|
395
|
+
ctypes.POINTER(ctypes.c_uint64),
|
|
396
|
+
ctypes.POINTER(ctypes.c_uint32),
|
|
397
|
+
ctypes.c_void_p,
|
|
398
|
+
ctypes.POINTER(ctypes.c_uint32),
|
|
399
|
+
]
|
|
400
|
+
self.lib.mach_vm_region_recurse.restype = ctypes.c_int
|
|
401
|
+
|
|
402
|
+
def _task_for_pid(self, pid: int) -> int:
|
|
403
|
+
task = ctypes.c_uint32(0)
|
|
404
|
+
kr = self.lib.task_for_pid(self.lib.mach_task_self(), int(pid), ctypes.byref(task))
|
|
405
|
+
if kr != self.KERN_SUCCESS:
|
|
406
|
+
raise PermissionError(
|
|
407
|
+
f"task_for_pid failed for pid {pid} with kern_return={kr}; "
|
|
408
|
+
"grant Developer Tools/debugging permission, run with sufficient privileges, "
|
|
409
|
+
"and note that SIP/protected processes may still deny access"
|
|
410
|
+
)
|
|
411
|
+
return int(task.value)
|
|
412
|
+
|
|
413
|
+
def _deallocate(self, task: int) -> None:
|
|
414
|
+
try:
|
|
415
|
+
self.lib.mach_port_deallocate(self.lib.mach_task_self(), task)
|
|
416
|
+
except Exception:
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
def regions(self, pid: int) -> list["MemoryRegion"]:
|
|
420
|
+
class VMRegionSubmapInfo64(ctypes.Structure):
|
|
421
|
+
_fields_ = [
|
|
422
|
+
("protection", ctypes.c_uint32),
|
|
423
|
+
("max_protection", ctypes.c_uint32),
|
|
424
|
+
("inheritance", ctypes.c_uint32),
|
|
425
|
+
("offset", ctypes.c_uint64),
|
|
426
|
+
("user_tag", ctypes.c_uint32),
|
|
427
|
+
("pages_resident", ctypes.c_uint32),
|
|
428
|
+
("pages_shared_now_private", ctypes.c_uint32),
|
|
429
|
+
("pages_swapped_out", ctypes.c_uint32),
|
|
430
|
+
("pages_dirtied", ctypes.c_uint32),
|
|
431
|
+
("ref_count", ctypes.c_uint32),
|
|
432
|
+
("shadow_depth", ctypes.c_uint16),
|
|
433
|
+
("external_pager", ctypes.c_uint8),
|
|
434
|
+
("share_mode", ctypes.c_uint8),
|
|
435
|
+
("is_submap", ctypes.c_uint32),
|
|
436
|
+
("behavior", ctypes.c_uint32),
|
|
437
|
+
("object_id", ctypes.c_uint32),
|
|
438
|
+
("user_wired_count", ctypes.c_uint16),
|
|
439
|
+
("pages_reusable", ctypes.c_uint32),
|
|
440
|
+
("object_id_full", ctypes.c_uint64),
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
task = self._task_for_pid(pid)
|
|
444
|
+
regions: list[MemoryRegion] = []
|
|
445
|
+
try:
|
|
446
|
+
address = ctypes.c_uint64(0)
|
|
447
|
+
size = ctypes.c_uint64(0)
|
|
448
|
+
depth = ctypes.c_uint32(0)
|
|
449
|
+
max_regions = 20000
|
|
450
|
+
for _ in range(max_regions):
|
|
451
|
+
info = VMRegionSubmapInfo64()
|
|
452
|
+
count = ctypes.c_uint32(ctypes.sizeof(info) // ctypes.sizeof(ctypes.c_uint32))
|
|
453
|
+
kr = self.lib.mach_vm_region_recurse(
|
|
454
|
+
task,
|
|
455
|
+
ctypes.byref(address),
|
|
456
|
+
ctypes.byref(size),
|
|
457
|
+
ctypes.byref(depth),
|
|
458
|
+
ctypes.byref(info),
|
|
459
|
+
ctypes.byref(count),
|
|
460
|
+
)
|
|
461
|
+
if kr != self.KERN_SUCCESS:
|
|
462
|
+
break
|
|
463
|
+
if info.is_submap:
|
|
464
|
+
depth.value += 1
|
|
465
|
+
continue
|
|
466
|
+
start = int(address.value)
|
|
467
|
+
region_size = int(size.value)
|
|
468
|
+
regions.append(
|
|
469
|
+
MemoryRegion(
|
|
470
|
+
start=start,
|
|
471
|
+
end=start + region_size,
|
|
472
|
+
permissions=self._protection_to_permissions(int(info.protection)),
|
|
473
|
+
offset=int(info.offset),
|
|
474
|
+
device="",
|
|
475
|
+
inode="",
|
|
476
|
+
path="",
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
address.value = start + region_size
|
|
480
|
+
finally:
|
|
481
|
+
self._deallocate(task)
|
|
482
|
+
return regions
|
|
483
|
+
|
|
484
|
+
def read(self, pid: int, address: int, size: int) -> bytes:
|
|
485
|
+
task = self._task_for_pid(pid)
|
|
486
|
+
try:
|
|
487
|
+
buffer = ctypes.create_string_buffer(size)
|
|
488
|
+
out_size = ctypes.c_uint64(0)
|
|
489
|
+
kr = self.lib.mach_vm_read_overwrite(
|
|
490
|
+
task,
|
|
491
|
+
ctypes.c_uint64(address),
|
|
492
|
+
ctypes.c_uint64(size),
|
|
493
|
+
buffer,
|
|
494
|
+
ctypes.byref(out_size),
|
|
495
|
+
)
|
|
496
|
+
if kr != self.KERN_SUCCESS:
|
|
497
|
+
raise OSError(f"mach_vm_read_overwrite failed at {hex(address)} with kern_return={kr}")
|
|
498
|
+
return buffer.raw[: int(out_size.value)]
|
|
499
|
+
finally:
|
|
500
|
+
self._deallocate(task)
|
|
501
|
+
|
|
502
|
+
def write(self, pid: int, address: int, data: bytes) -> None:
|
|
503
|
+
task = self._task_for_pid(pid)
|
|
504
|
+
try:
|
|
505
|
+
buffer = ctypes.create_string_buffer(data)
|
|
506
|
+
kr = self.lib.mach_vm_write(
|
|
507
|
+
task,
|
|
508
|
+
ctypes.c_uint64(address),
|
|
509
|
+
buffer,
|
|
510
|
+
ctypes.c_uint32(len(data)),
|
|
511
|
+
)
|
|
512
|
+
if kr != self.KERN_SUCCESS:
|
|
513
|
+
raise OSError(f"mach_vm_write failed at {hex(address)} with kern_return={kr}")
|
|
514
|
+
finally:
|
|
515
|
+
self._deallocate(task)
|
|
516
|
+
|
|
517
|
+
def protect(self, pid: int, address: int, size: int, writable: bool, executable: bool) -> Any:
|
|
518
|
+
old_protection = self._protection_for_address(pid, address)
|
|
519
|
+
task = self._task_for_pid(pid)
|
|
520
|
+
protection = self.VM_PROT_READ
|
|
521
|
+
if writable:
|
|
522
|
+
protection |= self.VM_PROT_WRITE
|
|
523
|
+
if executable:
|
|
524
|
+
protection |= self.VM_PROT_EXECUTE
|
|
525
|
+
try:
|
|
526
|
+
kr = self.lib.mach_vm_protect(
|
|
527
|
+
task,
|
|
528
|
+
ctypes.c_uint64(address),
|
|
529
|
+
ctypes.c_uint64(size),
|
|
530
|
+
0,
|
|
531
|
+
protection,
|
|
532
|
+
)
|
|
533
|
+
if kr != self.KERN_SUCCESS:
|
|
534
|
+
raise OSError(f"mach_vm_protect failed at {hex(address)} with kern_return={kr}")
|
|
535
|
+
finally:
|
|
536
|
+
self._deallocate(task)
|
|
537
|
+
return old_protection
|
|
538
|
+
|
|
539
|
+
def restore_protection(self, pid: int, address: int, size: int, token: Any) -> None:
|
|
540
|
+
if token is None:
|
|
541
|
+
return
|
|
542
|
+
task = self._task_for_pid(pid)
|
|
543
|
+
try:
|
|
544
|
+
self.lib.mach_vm_protect(
|
|
545
|
+
task,
|
|
546
|
+
ctypes.c_uint64(address),
|
|
547
|
+
ctypes.c_uint64(size),
|
|
548
|
+
0,
|
|
549
|
+
int(token),
|
|
550
|
+
)
|
|
551
|
+
finally:
|
|
552
|
+
self._deallocate(task)
|
|
553
|
+
|
|
554
|
+
def _protection_for_address(self, pid: int, address: int) -> int | None:
|
|
555
|
+
for region in self.regions(pid):
|
|
556
|
+
if region.start <= address < region.end:
|
|
557
|
+
protection = 0
|
|
558
|
+
if "r" in region.permissions:
|
|
559
|
+
protection |= self.VM_PROT_READ
|
|
560
|
+
if "w" in region.permissions:
|
|
561
|
+
protection |= self.VM_PROT_WRITE
|
|
562
|
+
if "x" in region.permissions:
|
|
563
|
+
protection |= self.VM_PROT_EXECUTE
|
|
564
|
+
return protection
|
|
565
|
+
return None
|
|
566
|
+
|
|
567
|
+
def _protection_to_permissions(self, protection: int) -> str:
|
|
568
|
+
return (
|
|
569
|
+
("r" if protection & self.VM_PROT_READ else "-")
|
|
570
|
+
+ ("w" if protection & self.VM_PROT_WRITE else "-")
|
|
571
|
+
+ ("x" if protection & self.VM_PROT_EXECUTE else "-")
|
|
572
|
+
+ "p"
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _select_memory_backend() -> MemoryBackend:
|
|
577
|
+
system = platform.system().lower()
|
|
578
|
+
if system == "linux" and Path("/proc").exists():
|
|
579
|
+
return LinuxProcfsMemoryBackend()
|
|
580
|
+
if system == "windows":
|
|
581
|
+
return WindowsMemoryBackend()
|
|
582
|
+
if system == "darwin":
|
|
583
|
+
return MacOSMachMemoryBackend()
|
|
584
|
+
return MemoryBackend()
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
@dataclass
|
|
588
|
+
class MemoryRegion:
|
|
589
|
+
start: int
|
|
590
|
+
end: int
|
|
591
|
+
permissions: str
|
|
592
|
+
offset: int
|
|
593
|
+
device: str
|
|
594
|
+
inode: str
|
|
595
|
+
path: str
|
|
596
|
+
|
|
597
|
+
@property
|
|
598
|
+
def size(self) -> int:
|
|
599
|
+
return max(0, self.end - self.start)
|
|
600
|
+
|
|
601
|
+
def to_dict(self) -> dict[str, Any]:
|
|
602
|
+
return {
|
|
603
|
+
"start": hex(self.start),
|
|
604
|
+
"end": hex(self.end),
|
|
605
|
+
"size": self.size,
|
|
606
|
+
"permissions": self.permissions,
|
|
607
|
+
"readable": "r" in self.permissions,
|
|
608
|
+
"writable": "w" in self.permissions,
|
|
609
|
+
"executable": "x" in self.permissions,
|
|
610
|
+
"private": "p" in self.permissions,
|
|
611
|
+
"shared": "s" in self.permissions,
|
|
612
|
+
"offset": hex(self.offset),
|
|
613
|
+
"device": self.device,
|
|
614
|
+
"inode": self.inode,
|
|
615
|
+
"path": self.path,
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
class MemoryInspector:
|
|
620
|
+
"""Process memory search/read/write for supported platforms.
|
|
621
|
+
|
|
622
|
+
Linux uses /proc/<pid>/maps and /proc/<pid>/mem, Windows uses documented
|
|
623
|
+
kernel32 process-memory APIs, and macOS uses Mach task-memory APIs.
|
|
624
|
+
"""
|
|
625
|
+
|
|
626
|
+
def __init__(self, *, config: dict[str, Any] | None = None) -> None:
|
|
627
|
+
self.config = config or {}
|
|
628
|
+
self.backend = _select_memory_backend()
|
|
629
|
+
self.max_search_results = max(1, int(self.config.get("max_search_results", 100)))
|
|
630
|
+
self.max_search_bytes = max(1, int(self.config.get("max_search_bytes", 128 * 1024 * 1024)))
|
|
631
|
+
self.chunk_size = max(1, int(self.config.get("memory_search_chunk_bytes", 1024 * 1024)))
|
|
632
|
+
self._searches: dict[str, dict[str, Any]] = {}
|
|
633
|
+
self._freezes: dict[str, dict[str, Any]] = {}
|
|
634
|
+
self._aob_scans: dict[str, dict[str, Any]] = {}
|
|
635
|
+
self._patches: dict[str, dict[str, Any]] = {}
|
|
636
|
+
|
|
637
|
+
async def close(self) -> None:
|
|
638
|
+
await self.unfreeze(all_freezes=True)
|
|
639
|
+
|
|
640
|
+
def capabilities(self) -> dict[str, bool | str]:
|
|
641
|
+
available = bool(self.backend.available)
|
|
642
|
+
return {
|
|
643
|
+
"memory_regions": available,
|
|
644
|
+
"memory_read": available,
|
|
645
|
+
"memory_search": available,
|
|
646
|
+
"memory_refine": available,
|
|
647
|
+
"memory_write": available,
|
|
648
|
+
"memory_freeze": available,
|
|
649
|
+
"memory_unfreeze": True,
|
|
650
|
+
"memory_freezes": True,
|
|
651
|
+
"aob_scan": available,
|
|
652
|
+
"pointer_scan": available,
|
|
653
|
+
"pointer_resolve": available,
|
|
654
|
+
"code_read": available,
|
|
655
|
+
"code_patch": available,
|
|
656
|
+
"code_restore": True,
|
|
657
|
+
"code_patches": True,
|
|
658
|
+
"backend": self.backend.name,
|
|
659
|
+
"backend_note": self.backend.note,
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
def regions(
|
|
663
|
+
self,
|
|
664
|
+
pid: int,
|
|
665
|
+
*,
|
|
666
|
+
readable: bool = False,
|
|
667
|
+
writable: bool = False,
|
|
668
|
+
executable: bool | None = None,
|
|
669
|
+
limit: int = 500,
|
|
670
|
+
) -> dict[str, Any]:
|
|
671
|
+
if not self.backend.available:
|
|
672
|
+
return {"error": f"memory_regions unavailable on {platform.system()}"}
|
|
673
|
+
try:
|
|
674
|
+
parsed = self._linux_regions(pid)
|
|
675
|
+
except Exception as exc:
|
|
676
|
+
return {"pid": pid, "error": str(exc)}
|
|
677
|
+
filtered: list[MemoryRegion] = []
|
|
678
|
+
for region in parsed:
|
|
679
|
+
if readable and "r" not in region.permissions:
|
|
680
|
+
continue
|
|
681
|
+
if writable and "w" not in region.permissions:
|
|
682
|
+
continue
|
|
683
|
+
if executable is not None and ("x" in region.permissions) != executable:
|
|
684
|
+
continue
|
|
685
|
+
filtered.append(region)
|
|
686
|
+
return {
|
|
687
|
+
"pid": pid,
|
|
688
|
+
"regions": [region.to_dict() for region in filtered[: max(1, limit)]],
|
|
689
|
+
"total_regions": len(filtered),
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
def read(self, pid: int, *, address: int | str, size: int) -> dict[str, Any]:
|
|
693
|
+
parsed_address = parse_address(address)
|
|
694
|
+
read_size = max(1, min(int(size), int(self.config.get("max_memory_read_bytes", 4096))))
|
|
695
|
+
try:
|
|
696
|
+
data = self._read_memory(pid, parsed_address, read_size)
|
|
697
|
+
except Exception as exc:
|
|
698
|
+
return {"pid": pid, "address": hex(parsed_address), "error": str(exc)}
|
|
699
|
+
return {
|
|
700
|
+
"pid": pid,
|
|
701
|
+
"address": hex(parsed_address),
|
|
702
|
+
"size": len(data),
|
|
703
|
+
"hex": data.hex(),
|
|
704
|
+
"ascii": _ascii_preview(data),
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
def write(
|
|
708
|
+
self,
|
|
709
|
+
pid: int,
|
|
710
|
+
*,
|
|
711
|
+
address: int | str,
|
|
712
|
+
value_type: str,
|
|
713
|
+
value: Any = None,
|
|
714
|
+
value_hex: str | None = None,
|
|
715
|
+
endian: str = "little",
|
|
716
|
+
) -> dict[str, Any]:
|
|
717
|
+
parsed_address = parse_address(address)
|
|
718
|
+
new_bytes = encode_value(value_type, value=value, value_hex=value_hex, endian=endian)
|
|
719
|
+
if not new_bytes:
|
|
720
|
+
return {"pid": pid, "address": hex(parsed_address), "error": "write value encoded to empty bytes"}
|
|
721
|
+
max_write = int(self.config.get("max_memory_write_bytes", 64))
|
|
722
|
+
if len(new_bytes) > max_write:
|
|
723
|
+
return {
|
|
724
|
+
"pid": pid,
|
|
725
|
+
"address": hex(parsed_address),
|
|
726
|
+
"error": f"write size {len(new_bytes)} exceeds max_memory_write_bytes={max_write}",
|
|
727
|
+
}
|
|
728
|
+
try:
|
|
729
|
+
old_bytes = self._read_memory(pid, parsed_address, len(new_bytes))
|
|
730
|
+
self._write_memory(pid, parsed_address, new_bytes)
|
|
731
|
+
verify = self._read_memory(pid, parsed_address, len(new_bytes))
|
|
732
|
+
except Exception as exc:
|
|
733
|
+
return {"pid": pid, "address": hex(parsed_address), "error": str(exc)}
|
|
734
|
+
return {
|
|
735
|
+
"pid": pid,
|
|
736
|
+
"address": hex(parsed_address),
|
|
737
|
+
"value_type": value_type,
|
|
738
|
+
"old_hex": old_bytes.hex(),
|
|
739
|
+
"written_hex": new_bytes.hex(),
|
|
740
|
+
"verify_hex": verify.hex(),
|
|
741
|
+
"verified": verify == new_bytes,
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async def freeze(
|
|
745
|
+
self,
|
|
746
|
+
pid: int,
|
|
747
|
+
*,
|
|
748
|
+
address: int | str,
|
|
749
|
+
value_type: str,
|
|
750
|
+
value: Any = None,
|
|
751
|
+
value_hex: str | None = None,
|
|
752
|
+
endian: str = "little",
|
|
753
|
+
interval_seconds: float | None = None,
|
|
754
|
+
) -> dict[str, Any]:
|
|
755
|
+
max_freezes = int(self.config.get("max_memory_freezes", 32))
|
|
756
|
+
if len(self._freezes) >= max_freezes:
|
|
757
|
+
return {"error": f"max_memory_freezes reached: {max_freezes}"}
|
|
758
|
+
|
|
759
|
+
parsed_address = parse_address(address)
|
|
760
|
+
data = encode_value(value_type, value=value, value_hex=value_hex, endian=endian)
|
|
761
|
+
if not data:
|
|
762
|
+
return {"pid": pid, "address": hex(parsed_address), "error": "freeze value encoded to empty bytes"}
|
|
763
|
+
max_write = int(self.config.get("max_memory_write_bytes", 64))
|
|
764
|
+
if len(data) > max_write:
|
|
765
|
+
return {
|
|
766
|
+
"pid": pid,
|
|
767
|
+
"address": hex(parsed_address),
|
|
768
|
+
"error": f"freeze write size {len(data)} exceeds max_memory_write_bytes={max_write}",
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
try:
|
|
772
|
+
old_bytes = self._read_memory(pid, parsed_address, len(data))
|
|
773
|
+
self._write_memory(pid, parsed_address, data)
|
|
774
|
+
verify = self._read_memory(pid, parsed_address, len(data))
|
|
775
|
+
except Exception as exc:
|
|
776
|
+
return {"pid": pid, "address": hex(parsed_address), "error": str(exc)}
|
|
777
|
+
|
|
778
|
+
min_interval = float(self.config.get("min_memory_freeze_interval_seconds", 0.05))
|
|
779
|
+
default_interval = float(self.config.get("memory_freeze_interval_seconds", 0.25))
|
|
780
|
+
interval = max(min_interval, float(interval_seconds or default_interval))
|
|
781
|
+
freeze_id = f"freeze-{uuid.uuid4().hex[:10]}"
|
|
782
|
+
state: dict[str, Any] = {
|
|
783
|
+
"freeze_id": freeze_id,
|
|
784
|
+
"pid": pid,
|
|
785
|
+
"address": hex(parsed_address),
|
|
786
|
+
"value_type": value_type,
|
|
787
|
+
"value_hex": data.hex(),
|
|
788
|
+
"endian": endian,
|
|
789
|
+
"interval_seconds": interval,
|
|
790
|
+
"writes": 1,
|
|
791
|
+
"active": True,
|
|
792
|
+
"last_error": None,
|
|
793
|
+
"old_hex": old_bytes.hex(),
|
|
794
|
+
"verify_hex": verify.hex(),
|
|
795
|
+
"verified": verify == data,
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async def _loop() -> None:
|
|
799
|
+
max_errors = int(self.config.get("max_memory_freeze_errors", 3))
|
|
800
|
+
consecutive_errors = 0
|
|
801
|
+
while state.get("active"):
|
|
802
|
+
await asyncio.sleep(interval)
|
|
803
|
+
try:
|
|
804
|
+
self._write_memory(pid, parsed_address, data)
|
|
805
|
+
state["writes"] = int(state.get("writes", 0)) + 1
|
|
806
|
+
state["last_error"] = None
|
|
807
|
+
consecutive_errors = 0
|
|
808
|
+
except asyncio.CancelledError:
|
|
809
|
+
raise
|
|
810
|
+
except Exception as exc:
|
|
811
|
+
consecutive_errors += 1
|
|
812
|
+
state["last_error"] = str(exc)
|
|
813
|
+
if consecutive_errors >= max_errors:
|
|
814
|
+
state["active"] = False
|
|
815
|
+
break
|
|
816
|
+
|
|
817
|
+
task = asyncio.create_task(_loop())
|
|
818
|
+
state["task"] = task
|
|
819
|
+
self._freezes[freeze_id] = state
|
|
820
|
+
return self._freeze_snapshot(state, include_task=False)
|
|
821
|
+
|
|
822
|
+
def freezes(self) -> dict[str, Any]:
|
|
823
|
+
return {
|
|
824
|
+
"freezes": [
|
|
825
|
+
self._freeze_snapshot(state, include_task=False)
|
|
826
|
+
for state in self._freezes.values()
|
|
827
|
+
]
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async def unfreeze(
|
|
831
|
+
self,
|
|
832
|
+
freeze_id: str | None = None,
|
|
833
|
+
*,
|
|
834
|
+
all_freezes: bool = False,
|
|
835
|
+
) -> dict[str, Any]:
|
|
836
|
+
if all_freezes:
|
|
837
|
+
target_ids = list(self._freezes)
|
|
838
|
+
elif freeze_id:
|
|
839
|
+
target_ids = [freeze_id]
|
|
840
|
+
else:
|
|
841
|
+
return {"error": "freeze_id is required unless all_freezes=true"}
|
|
842
|
+
|
|
843
|
+
stopped: list[dict[str, Any]] = []
|
|
844
|
+
missing: list[str] = []
|
|
845
|
+
for target_id in target_ids:
|
|
846
|
+
state = self._freezes.pop(target_id, None)
|
|
847
|
+
if not state:
|
|
848
|
+
missing.append(target_id)
|
|
849
|
+
continue
|
|
850
|
+
state["active"] = False
|
|
851
|
+
task = state.get("task")
|
|
852
|
+
if isinstance(task, asyncio.Task) and not task.done():
|
|
853
|
+
task.cancel()
|
|
854
|
+
try:
|
|
855
|
+
await task
|
|
856
|
+
except asyncio.CancelledError:
|
|
857
|
+
pass
|
|
858
|
+
stopped.append(self._freeze_snapshot(state, include_task=False))
|
|
859
|
+
return {"stopped": stopped, "missing": missing}
|
|
860
|
+
|
|
861
|
+
def search(
|
|
862
|
+
self,
|
|
863
|
+
pid: int,
|
|
864
|
+
*,
|
|
865
|
+
value_type: str,
|
|
866
|
+
value: Any = None,
|
|
867
|
+
value_hex: str | None = None,
|
|
868
|
+
endian: str = "little",
|
|
869
|
+
writable_only: bool = True,
|
|
870
|
+
max_results: int | None = None,
|
|
871
|
+
max_scan_bytes: int | None = None,
|
|
872
|
+
) -> dict[str, Any]:
|
|
873
|
+
if not self.backend.available:
|
|
874
|
+
return {"error": f"memory_search unavailable on {platform.system()}"}
|
|
875
|
+
needle = encode_value(value_type, value=value, value_hex=value_hex, endian=endian)
|
|
876
|
+
if not needle:
|
|
877
|
+
return {"error": "search value encoded to empty bytes"}
|
|
878
|
+
limit_results = self._bounded_limit(max_results, self.max_search_results)
|
|
879
|
+
limit_bytes = self._bounded_limit(max_scan_bytes, self.max_search_bytes)
|
|
880
|
+
try:
|
|
881
|
+
regions = [
|
|
882
|
+
region
|
|
883
|
+
for region in self._linux_regions(pid)
|
|
884
|
+
if "r" in region.permissions and (not writable_only or "w" in region.permissions)
|
|
885
|
+
]
|
|
886
|
+
except Exception as exc:
|
|
887
|
+
return {"pid": pid, "error": str(exc)}
|
|
888
|
+
results, scanned, stopped_reason = self._search_regions(
|
|
889
|
+
pid,
|
|
890
|
+
regions,
|
|
891
|
+
needle,
|
|
892
|
+
limit_results=limit_results,
|
|
893
|
+
limit_bytes=limit_bytes,
|
|
894
|
+
value_type=value_type,
|
|
895
|
+
endian=endian,
|
|
896
|
+
)
|
|
897
|
+
search_id = f"mem-{uuid.uuid4().hex[:10]}"
|
|
898
|
+
self._remember(
|
|
899
|
+
self._searches,
|
|
900
|
+
search_id,
|
|
901
|
+
{
|
|
902
|
+
"pid": pid,
|
|
903
|
+
"value_type": value_type,
|
|
904
|
+
"endian": endian,
|
|
905
|
+
"bytes": needle.hex(),
|
|
906
|
+
"results": results,
|
|
907
|
+
},
|
|
908
|
+
int(self.config.get("max_memory_searches", 64)),
|
|
909
|
+
)
|
|
910
|
+
return {
|
|
911
|
+
"pid": pid,
|
|
912
|
+
"search_id": search_id,
|
|
913
|
+
"value_type": value_type,
|
|
914
|
+
"pattern_hex": needle.hex(),
|
|
915
|
+
"writable_only": writable_only,
|
|
916
|
+
"scanned_bytes": scanned,
|
|
917
|
+
"stopped_reason": stopped_reason,
|
|
918
|
+
"result_count": len(results),
|
|
919
|
+
"results": results,
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
def refine(
|
|
923
|
+
self,
|
|
924
|
+
search_id: str,
|
|
925
|
+
*,
|
|
926
|
+
comparison: str = "changed",
|
|
927
|
+
value: Any = None,
|
|
928
|
+
value_hex: str | None = None,
|
|
929
|
+
max_results: int | None = None,
|
|
930
|
+
) -> dict[str, Any]:
|
|
931
|
+
previous = self._searches.get(search_id)
|
|
932
|
+
if not previous:
|
|
933
|
+
return {"error": f"unknown memory search_id: {search_id}"}
|
|
934
|
+
pid = int(previous["pid"])
|
|
935
|
+
value_type = str(previous["value_type"])
|
|
936
|
+
endian = str(previous["endian"])
|
|
937
|
+
width = len(bytes.fromhex(str(previous["bytes"])))
|
|
938
|
+
expected = None
|
|
939
|
+
if comparison == "equals":
|
|
940
|
+
expected = encode_value(value_type, value=value, value_hex=value_hex, endian=endian)
|
|
941
|
+
limit_results = self._bounded_limit(max_results, self.max_search_results)
|
|
942
|
+
refined: list[dict[str, Any]] = []
|
|
943
|
+
for result in previous["results"]:
|
|
944
|
+
if len(refined) >= limit_results:
|
|
945
|
+
break
|
|
946
|
+
address = parse_address(result["address"])
|
|
947
|
+
try:
|
|
948
|
+
current = self._read_memory(pid, address, width)
|
|
949
|
+
except Exception:
|
|
950
|
+
continue
|
|
951
|
+
old = bytes.fromhex(str(result["bytes_hex"]))
|
|
952
|
+
if self._matches_refine(
|
|
953
|
+
comparison=comparison,
|
|
954
|
+
old=old,
|
|
955
|
+
current=current,
|
|
956
|
+
expected=expected,
|
|
957
|
+
value_type=value_type,
|
|
958
|
+
endian=endian,
|
|
959
|
+
):
|
|
960
|
+
refined.append(self._result_entry(address, current, value_type, endian))
|
|
961
|
+
new_search_id = f"mem-{uuid.uuid4().hex[:10]}"
|
|
962
|
+
self._remember(
|
|
963
|
+
self._searches,
|
|
964
|
+
new_search_id,
|
|
965
|
+
{
|
|
966
|
+
"pid": pid,
|
|
967
|
+
"value_type": value_type,
|
|
968
|
+
"endian": endian,
|
|
969
|
+
"bytes": (expected or bytes.fromhex(str(previous["bytes"]))).hex(),
|
|
970
|
+
"results": refined,
|
|
971
|
+
},
|
|
972
|
+
int(self.config.get("max_memory_searches", 64)),
|
|
973
|
+
)
|
|
974
|
+
return {
|
|
975
|
+
"pid": pid,
|
|
976
|
+
"source_search_id": search_id,
|
|
977
|
+
"search_id": new_search_id,
|
|
978
|
+
"comparison": comparison,
|
|
979
|
+
"result_count": len(refined),
|
|
980
|
+
"results": refined,
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
def aob_scan(
|
|
984
|
+
self,
|
|
985
|
+
pid: int,
|
|
986
|
+
*,
|
|
987
|
+
pattern: str,
|
|
988
|
+
executable_only: bool = True,
|
|
989
|
+
writable_only: bool = False,
|
|
990
|
+
module_filter: str | None = None,
|
|
991
|
+
max_results: int | None = None,
|
|
992
|
+
max_scan_bytes: int | None = None,
|
|
993
|
+
) -> dict[str, Any]:
|
|
994
|
+
if not self.backend.available:
|
|
995
|
+
return {"error": f"aob_scan unavailable on {platform.system()}"}
|
|
996
|
+
parsed = parse_aob_pattern(pattern)
|
|
997
|
+
if not parsed:
|
|
998
|
+
return {"error": "pattern is empty"}
|
|
999
|
+
limit_results = self._bounded_limit(max_results, self.max_search_results)
|
|
1000
|
+
limit_bytes = self._bounded_limit(max_scan_bytes, self.max_search_bytes)
|
|
1001
|
+
regions = []
|
|
1002
|
+
try:
|
|
1003
|
+
for region in self._linux_regions(pid):
|
|
1004
|
+
if "r" not in region.permissions:
|
|
1005
|
+
continue
|
|
1006
|
+
if executable_only and "x" not in region.permissions:
|
|
1007
|
+
continue
|
|
1008
|
+
if writable_only and "w" not in region.permissions:
|
|
1009
|
+
continue
|
|
1010
|
+
if module_filter and module_filter not in region.path:
|
|
1011
|
+
continue
|
|
1012
|
+
regions.append(region)
|
|
1013
|
+
except Exception as exc:
|
|
1014
|
+
return {"pid": pid, "error": str(exc)}
|
|
1015
|
+
results, scanned, stopped_reason = self._search_aob_regions(
|
|
1016
|
+
pid,
|
|
1017
|
+
regions,
|
|
1018
|
+
parsed,
|
|
1019
|
+
limit_results=limit_results,
|
|
1020
|
+
limit_bytes=limit_bytes,
|
|
1021
|
+
)
|
|
1022
|
+
aob_id = f"aob-{uuid.uuid4().hex[:10]}"
|
|
1023
|
+
self._remember(
|
|
1024
|
+
self._aob_scans,
|
|
1025
|
+
aob_id,
|
|
1026
|
+
{"pid": pid, "pattern": pattern, "results": results},
|
|
1027
|
+
int(self.config.get("max_aob_scans", 64)),
|
|
1028
|
+
)
|
|
1029
|
+
return {
|
|
1030
|
+
"pid": pid,
|
|
1031
|
+
"aob_id": aob_id,
|
|
1032
|
+
"pattern": pattern,
|
|
1033
|
+
"executable_only": executable_only,
|
|
1034
|
+
"writable_only": writable_only,
|
|
1035
|
+
"module_filter": module_filter,
|
|
1036
|
+
"scanned_bytes": scanned,
|
|
1037
|
+
"stopped_reason": stopped_reason,
|
|
1038
|
+
"result_count": len(results),
|
|
1039
|
+
"results": results,
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
def pointer_scan(
|
|
1043
|
+
self,
|
|
1044
|
+
pid: int,
|
|
1045
|
+
*,
|
|
1046
|
+
target_address: int | str,
|
|
1047
|
+
max_depth: int = 3,
|
|
1048
|
+
max_offset: int = 4096,
|
|
1049
|
+
pointer_size: int | None = None,
|
|
1050
|
+
align: int | None = None,
|
|
1051
|
+
writable_only: bool = True,
|
|
1052
|
+
max_results: int | None = None,
|
|
1053
|
+
max_scan_bytes: int | None = None,
|
|
1054
|
+
) -> dict[str, Any]:
|
|
1055
|
+
if not self.backend.available:
|
|
1056
|
+
return {"error": f"pointer_scan unavailable on {platform.system()}"}
|
|
1057
|
+
target = parse_address(target_address)
|
|
1058
|
+
ptr_size = pointer_size or struct.calcsize("P")
|
|
1059
|
+
if ptr_size not in {4, 8}:
|
|
1060
|
+
return {"error": "pointer_size must be 4 or 8"}
|
|
1061
|
+
step = max(1, int(align or ptr_size))
|
|
1062
|
+
depth_limit = max(1, min(int(max_depth), int(self.config.get("max_pointer_scan_depth", 5))))
|
|
1063
|
+
offset_limit = max(0, int(max_offset))
|
|
1064
|
+
pointer_result_cap = max(1, int(self.config.get("max_pointer_scan_results", 100)))
|
|
1065
|
+
limit_results = self._bounded_limit(max_results, pointer_result_cap)
|
|
1066
|
+
limit_bytes = self._bounded_limit(max_scan_bytes, self.max_search_bytes)
|
|
1067
|
+
endian = str(self.config.get("pointer_endian", "little"))
|
|
1068
|
+
|
|
1069
|
+
try:
|
|
1070
|
+
regions = [
|
|
1071
|
+
region
|
|
1072
|
+
for region in self._linux_regions(pid)
|
|
1073
|
+
if "r" in region.permissions and (not writable_only or "w" in region.permissions)
|
|
1074
|
+
]
|
|
1075
|
+
except Exception as exc:
|
|
1076
|
+
return {"pid": pid, "error": str(exc)}
|
|
1077
|
+
module_bases = self._module_bases(regions)
|
|
1078
|
+
frontier: list[dict[str, Any]] = [
|
|
1079
|
+
{
|
|
1080
|
+
"target": target,
|
|
1081
|
+
"offsets": [],
|
|
1082
|
+
"depth": 0,
|
|
1083
|
+
}
|
|
1084
|
+
]
|
|
1085
|
+
results: list[dict[str, Any]] = []
|
|
1086
|
+
scanned_total = 0
|
|
1087
|
+
stopped_reason = "completed"
|
|
1088
|
+
for depth in range(1, depth_limit + 1):
|
|
1089
|
+
if len(results) >= limit_results or not frontier:
|
|
1090
|
+
break
|
|
1091
|
+
target_nodes = frontier[: int(self.config.get("max_pointer_scan_frontier", 256))]
|
|
1092
|
+
next_frontier: list[dict[str, Any]] = []
|
|
1093
|
+
candidates, scanned, reason = self._scan_pointer_level(
|
|
1094
|
+
pid,
|
|
1095
|
+
regions,
|
|
1096
|
+
target_nodes,
|
|
1097
|
+
pointer_size=ptr_size,
|
|
1098
|
+
align=step,
|
|
1099
|
+
max_offset=offset_limit,
|
|
1100
|
+
max_results=limit_results - len(results),
|
|
1101
|
+
max_scan_bytes=max(1, limit_bytes - scanned_total),
|
|
1102
|
+
endian=endian,
|
|
1103
|
+
module_bases=module_bases,
|
|
1104
|
+
)
|
|
1105
|
+
scanned_total += scanned
|
|
1106
|
+
for candidate in candidates:
|
|
1107
|
+
results.append(candidate)
|
|
1108
|
+
raw_offsets = [parse_address(v) for v in candidate["offsets"]]
|
|
1109
|
+
next_frontier.append(
|
|
1110
|
+
{
|
|
1111
|
+
"target": parse_address(candidate["base_address"]),
|
|
1112
|
+
"offsets": raw_offsets,
|
|
1113
|
+
"depth": depth,
|
|
1114
|
+
}
|
|
1115
|
+
)
|
|
1116
|
+
if len(results) >= limit_results:
|
|
1117
|
+
stopped_reason = "max_results"
|
|
1118
|
+
break
|
|
1119
|
+
if scanned_total >= limit_bytes:
|
|
1120
|
+
stopped_reason = "max_scan_bytes"
|
|
1121
|
+
break
|
|
1122
|
+
if reason != "completed":
|
|
1123
|
+
stopped_reason = reason
|
|
1124
|
+
if reason == "max_results":
|
|
1125
|
+
break
|
|
1126
|
+
frontier = next_frontier
|
|
1127
|
+
return {
|
|
1128
|
+
"pid": pid,
|
|
1129
|
+
"target_address": hex(target),
|
|
1130
|
+
"pointer_size": ptr_size,
|
|
1131
|
+
"max_depth": depth_limit,
|
|
1132
|
+
"max_offset": offset_limit,
|
|
1133
|
+
"writable_only": writable_only,
|
|
1134
|
+
"scanned_bytes": scanned_total,
|
|
1135
|
+
"stopped_reason": stopped_reason,
|
|
1136
|
+
"result_count": len(results),
|
|
1137
|
+
"chains": results[:limit_results],
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
def pointer_resolve(
|
|
1141
|
+
self,
|
|
1142
|
+
pid: int,
|
|
1143
|
+
*,
|
|
1144
|
+
base_address: int | str | None = None,
|
|
1145
|
+
offsets: list[Any] | None = None,
|
|
1146
|
+
module_path: str | None = None,
|
|
1147
|
+
module_offset: int | str | None = None,
|
|
1148
|
+
pointer_size: int | None = None,
|
|
1149
|
+
endian: str = "little",
|
|
1150
|
+
) -> dict[str, Any]:
|
|
1151
|
+
if not self.backend.available:
|
|
1152
|
+
return {"error": f"pointer_resolve unavailable on {platform.system()}"}
|
|
1153
|
+
ptr_size = pointer_size or struct.calcsize("P")
|
|
1154
|
+
if ptr_size not in {4, 8}:
|
|
1155
|
+
return {"error": "pointer_size must be 4 or 8"}
|
|
1156
|
+
try:
|
|
1157
|
+
base = self._resolve_base_address(
|
|
1158
|
+
pid,
|
|
1159
|
+
base_address=base_address,
|
|
1160
|
+
module_path=module_path,
|
|
1161
|
+
module_offset=module_offset,
|
|
1162
|
+
)
|
|
1163
|
+
except Exception as exc:
|
|
1164
|
+
return {"pid": pid, "error": str(exc)}
|
|
1165
|
+
parsed_offsets = [parse_address(v) for v in offsets or []]
|
|
1166
|
+
current = base
|
|
1167
|
+
steps: list[dict[str, Any]] = []
|
|
1168
|
+
for offset in parsed_offsets:
|
|
1169
|
+
try:
|
|
1170
|
+
ptr_bytes = self._read_memory(pid, current, ptr_size)
|
|
1171
|
+
except Exception as exc:
|
|
1172
|
+
return {"pid": pid, "base_address": hex(base), "error": str(exc), "steps": steps}
|
|
1173
|
+
pointee = int.from_bytes(ptr_bytes, endian)
|
|
1174
|
+
next_address = pointee + offset
|
|
1175
|
+
steps.append(
|
|
1176
|
+
{
|
|
1177
|
+
"read_address": hex(current),
|
|
1178
|
+
"pointer_value": hex(pointee),
|
|
1179
|
+
"offset": hex(offset),
|
|
1180
|
+
"next_address": hex(next_address),
|
|
1181
|
+
}
|
|
1182
|
+
)
|
|
1183
|
+
current = next_address
|
|
1184
|
+
return {
|
|
1185
|
+
"pid": pid,
|
|
1186
|
+
"base_address": hex(base),
|
|
1187
|
+
"offsets": [hex(v) for v in parsed_offsets],
|
|
1188
|
+
"address": hex(current),
|
|
1189
|
+
"steps": steps,
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
def code_read(self, pid: int, *, address: int | str, size: int) -> dict[str, Any]:
|
|
1193
|
+
return self.read(pid, address=address, size=size)
|
|
1194
|
+
|
|
1195
|
+
def code_patch(
|
|
1196
|
+
self,
|
|
1197
|
+
pid: int,
|
|
1198
|
+
*,
|
|
1199
|
+
address: int | str,
|
|
1200
|
+
patch_hex: str,
|
|
1201
|
+
expected_hex: str | None = None,
|
|
1202
|
+
patch_id: str | None = None,
|
|
1203
|
+
) -> dict[str, Any]:
|
|
1204
|
+
parsed_address = parse_address(address)
|
|
1205
|
+
try:
|
|
1206
|
+
patch = encode_value("bytes", value_hex=patch_hex)
|
|
1207
|
+
expected = encode_value("bytes", value_hex=expected_hex) if expected_hex else None
|
|
1208
|
+
except ValueError as exc:
|
|
1209
|
+
return {"pid": pid, "address": hex(parsed_address), "error": str(exc)}
|
|
1210
|
+
if not patch:
|
|
1211
|
+
return {"pid": pid, "address": hex(parsed_address), "error": "patch_hex encoded to empty bytes"}
|
|
1212
|
+
max_patch = int(self.config.get("max_code_patch_bytes", 64))
|
|
1213
|
+
if len(patch) > max_patch:
|
|
1214
|
+
return {"pid": pid, "address": hex(parsed_address), "error": f"patch size {len(patch)} exceeds max_code_patch_bytes={max_patch}"}
|
|
1215
|
+
if expected is not None and len(expected) != len(patch):
|
|
1216
|
+
return {"pid": pid, "address": hex(parsed_address), "error": "expected_hex length must match patch_hex length"}
|
|
1217
|
+
saved_patch_id = patch_id or self._new_patch_id()
|
|
1218
|
+
if saved_patch_id in self._patches:
|
|
1219
|
+
return {"pid": pid, "address": hex(parsed_address), "error": f"patch_id is already active: {saved_patch_id}"}
|
|
1220
|
+
overlap = self._overlapping_patch(pid, parsed_address, len(patch))
|
|
1221
|
+
if overlap is not None:
|
|
1222
|
+
return {
|
|
1223
|
+
"pid": pid,
|
|
1224
|
+
"address": hex(parsed_address),
|
|
1225
|
+
"error": f"patch overlaps active patch: {overlap}",
|
|
1226
|
+
}
|
|
1227
|
+
write_attempted = False
|
|
1228
|
+
old = b""
|
|
1229
|
+
try:
|
|
1230
|
+
old = self._read_memory(pid, parsed_address, len(patch))
|
|
1231
|
+
if len(old) != len(patch):
|
|
1232
|
+
return {
|
|
1233
|
+
"pid": pid,
|
|
1234
|
+
"address": hex(parsed_address),
|
|
1235
|
+
"error": f"could not read all original bytes: expected {len(patch)}, got {len(old)}",
|
|
1236
|
+
}
|
|
1237
|
+
if expected is not None and old != expected:
|
|
1238
|
+
return {
|
|
1239
|
+
"pid": pid,
|
|
1240
|
+
"address": hex(parsed_address),
|
|
1241
|
+
"error": "expected bytes did not match current process memory",
|
|
1242
|
+
"expected_hex": expected.hex(),
|
|
1243
|
+
"actual_hex": old.hex(),
|
|
1244
|
+
}
|
|
1245
|
+
token, protect_error = self._try_protect_memory(
|
|
1246
|
+
pid,
|
|
1247
|
+
parsed_address,
|
|
1248
|
+
len(patch),
|
|
1249
|
+
writable=True,
|
|
1250
|
+
executable=True,
|
|
1251
|
+
)
|
|
1252
|
+
try:
|
|
1253
|
+
write_attempted = True
|
|
1254
|
+
self._write_memory(pid, parsed_address, patch)
|
|
1255
|
+
flush_error = self._try_flush_instruction_cache(pid, parsed_address, len(patch))
|
|
1256
|
+
verify = self._read_memory(pid, parsed_address, len(patch))
|
|
1257
|
+
except Exception as exc:
|
|
1258
|
+
if protect_error is not None:
|
|
1259
|
+
raise RuntimeError(f"{exc}; protection change also failed: {protect_error}") from exc
|
|
1260
|
+
raise
|
|
1261
|
+
finally:
|
|
1262
|
+
if token is not None:
|
|
1263
|
+
self._restore_protection(pid, parsed_address, len(patch), token)
|
|
1264
|
+
except Exception as exc:
|
|
1265
|
+
if write_attempted:
|
|
1266
|
+
record = {
|
|
1267
|
+
"patch_id": saved_patch_id,
|
|
1268
|
+
"pid": pid,
|
|
1269
|
+
"address": hex(parsed_address),
|
|
1270
|
+
"old_hex": old.hex(),
|
|
1271
|
+
"patch_hex": patch.hex(),
|
|
1272
|
+
"verified": False,
|
|
1273
|
+
"active": True,
|
|
1274
|
+
"error": f"patch write may have succeeded; restore record retained: {exc}",
|
|
1275
|
+
}
|
|
1276
|
+
self._patches[saved_patch_id] = record
|
|
1277
|
+
return dict(record)
|
|
1278
|
+
return {"pid": pid, "address": hex(parsed_address), "error": str(exc)}
|
|
1279
|
+
record = {
|
|
1280
|
+
"patch_id": saved_patch_id,
|
|
1281
|
+
"pid": pid,
|
|
1282
|
+
"address": hex(parsed_address),
|
|
1283
|
+
"old_hex": old.hex(),
|
|
1284
|
+
"patch_hex": patch.hex(),
|
|
1285
|
+
"verify_hex": verify.hex(),
|
|
1286
|
+
"verified": verify == patch,
|
|
1287
|
+
"active": verify != old,
|
|
1288
|
+
}
|
|
1289
|
+
if flush_error is not None:
|
|
1290
|
+
record["flush_warning"] = str(flush_error)
|
|
1291
|
+
if verify != patch:
|
|
1292
|
+
record["error"] = "patch verification failed; restore record retained"
|
|
1293
|
+
if verify != old:
|
|
1294
|
+
self._patches[saved_patch_id] = record
|
|
1295
|
+
else:
|
|
1296
|
+
record["recovery_not_needed"] = True
|
|
1297
|
+
return dict(record)
|
|
1298
|
+
self._patches[saved_patch_id] = record
|
|
1299
|
+
return dict(record)
|
|
1300
|
+
|
|
1301
|
+
def code_patches(self) -> dict[str, Any]:
|
|
1302
|
+
return {"patches": list(self._patches.values())}
|
|
1303
|
+
|
|
1304
|
+
def code_restore(
|
|
1305
|
+
self,
|
|
1306
|
+
*,
|
|
1307
|
+
patch_id: str | None = None,
|
|
1308
|
+
all_patches: bool = False,
|
|
1309
|
+
) -> dict[str, Any]:
|
|
1310
|
+
if all_patches:
|
|
1311
|
+
target_ids = list(self._patches)
|
|
1312
|
+
elif patch_id:
|
|
1313
|
+
target_ids = [patch_id]
|
|
1314
|
+
else:
|
|
1315
|
+
return {"error": "patch_id is required unless all_patches=true"}
|
|
1316
|
+
restored: list[dict[str, Any]] = []
|
|
1317
|
+
missing: list[str] = []
|
|
1318
|
+
for target_id in reversed(target_ids):
|
|
1319
|
+
record = self._patches.get(target_id)
|
|
1320
|
+
if not record:
|
|
1321
|
+
missing.append(target_id)
|
|
1322
|
+
continue
|
|
1323
|
+
pid = int(record["pid"])
|
|
1324
|
+
address = parse_address(record["address"])
|
|
1325
|
+
old = bytes.fromhex(str(record["old_hex"]))
|
|
1326
|
+
patch = bytes.fromhex(str(record["patch_hex"]))
|
|
1327
|
+
try:
|
|
1328
|
+
if record.get("verified", False):
|
|
1329
|
+
current = self._read_memory(pid, address, len(patch))
|
|
1330
|
+
if current != patch:
|
|
1331
|
+
restored.append(
|
|
1332
|
+
{
|
|
1333
|
+
**record,
|
|
1334
|
+
"restored": False,
|
|
1335
|
+
"error": "current bytes no longer match the recorded patch",
|
|
1336
|
+
"actual_hex": current.hex(),
|
|
1337
|
+
}
|
|
1338
|
+
)
|
|
1339
|
+
continue
|
|
1340
|
+
token, protect_error = self._try_protect_memory(
|
|
1341
|
+
pid,
|
|
1342
|
+
address,
|
|
1343
|
+
len(old),
|
|
1344
|
+
writable=True,
|
|
1345
|
+
executable=True,
|
|
1346
|
+
)
|
|
1347
|
+
try:
|
|
1348
|
+
self._write_memory(pid, address, old)
|
|
1349
|
+
flush_error = self._try_flush_instruction_cache(pid, address, len(old))
|
|
1350
|
+
verify = self._read_memory(pid, address, len(old))
|
|
1351
|
+
except Exception as exc:
|
|
1352
|
+
if protect_error is not None:
|
|
1353
|
+
raise RuntimeError(f"{exc}; protection change also failed: {protect_error}") from exc
|
|
1354
|
+
raise
|
|
1355
|
+
finally:
|
|
1356
|
+
if token is not None:
|
|
1357
|
+
self._restore_protection(pid, address, len(old), token)
|
|
1358
|
+
except Exception as exc:
|
|
1359
|
+
restored.append({**record, "restored": False, "error": str(exc)})
|
|
1360
|
+
continue
|
|
1361
|
+
restore_verified = verify == old
|
|
1362
|
+
if restore_verified:
|
|
1363
|
+
record["active"] = False
|
|
1364
|
+
restored_record = {**record, "restored": restore_verified, "verify_hex": verify.hex()}
|
|
1365
|
+
if flush_error is not None:
|
|
1366
|
+
restored_record["flush_warning"] = str(flush_error)
|
|
1367
|
+
if not restore_verified:
|
|
1368
|
+
restored_record["error"] = "restore verification failed; restore record retained"
|
|
1369
|
+
restored.append(restored_record)
|
|
1370
|
+
continue
|
|
1371
|
+
restored.append(restored_record)
|
|
1372
|
+
self._patches.pop(target_id, None)
|
|
1373
|
+
return {"restored": restored, "missing": missing}
|
|
1374
|
+
|
|
1375
|
+
def _new_patch_id(self) -> str:
|
|
1376
|
+
while True:
|
|
1377
|
+
patch_id = f"patch-{uuid.uuid4().hex[:10]}"
|
|
1378
|
+
if patch_id not in self._patches:
|
|
1379
|
+
return patch_id
|
|
1380
|
+
|
|
1381
|
+
def _overlapping_patch(self, pid: int, address: int, size: int) -> str | None:
|
|
1382
|
+
end = address + size
|
|
1383
|
+
for patch_id, record in self._patches.items():
|
|
1384
|
+
if int(record["pid"]) != pid:
|
|
1385
|
+
continue
|
|
1386
|
+
existing_start = parse_address(record["address"])
|
|
1387
|
+
existing_end = existing_start + len(bytes.fromhex(str(record["patch_hex"])))
|
|
1388
|
+
if address < existing_end and existing_start < end:
|
|
1389
|
+
return patch_id
|
|
1390
|
+
return None
|
|
1391
|
+
|
|
1392
|
+
@staticmethod
|
|
1393
|
+
def _bounded_limit(requested: int | None, maximum: int) -> int:
|
|
1394
|
+
requested_value = maximum if requested is None else int(requested)
|
|
1395
|
+
return min(max(1, requested_value), max(1, int(maximum)))
|
|
1396
|
+
|
|
1397
|
+
@staticmethod
|
|
1398
|
+
def _remember(
|
|
1399
|
+
entries: dict[str, dict[str, Any]],
|
|
1400
|
+
entry_id: str,
|
|
1401
|
+
value: dict[str, Any],
|
|
1402
|
+
maximum: int,
|
|
1403
|
+
) -> None:
|
|
1404
|
+
limit = max(1, int(maximum))
|
|
1405
|
+
while len(entries) >= limit:
|
|
1406
|
+
entries.pop(next(iter(entries)))
|
|
1407
|
+
entries[entry_id] = value
|
|
1408
|
+
|
|
1409
|
+
def _linux_regions(self, pid: int) -> list[MemoryRegion]:
|
|
1410
|
+
return self.backend.regions(pid)
|
|
1411
|
+
|
|
1412
|
+
def _read_memory(self, pid: int, address: int, size: int) -> bytes:
|
|
1413
|
+
return self.backend.read(pid, address, size)
|
|
1414
|
+
|
|
1415
|
+
def _write_memory(self, pid: int, address: int, data: bytes) -> None:
|
|
1416
|
+
self.backend.write(pid, address, data)
|
|
1417
|
+
|
|
1418
|
+
def _protect_memory(self, pid: int, address: int, size: int, *, writable: bool, executable: bool) -> Any:
|
|
1419
|
+
return self.backend.protect(pid, address, size, writable=writable, executable=executable)
|
|
1420
|
+
|
|
1421
|
+
def _try_protect_memory(self, pid: int, address: int, size: int, *, writable: bool, executable: bool) -> tuple[Any, Exception | None]:
|
|
1422
|
+
try:
|
|
1423
|
+
return self._protect_memory(pid, address, size, writable=writable, executable=executable), None
|
|
1424
|
+
except Exception as exc:
|
|
1425
|
+
return None, exc
|
|
1426
|
+
|
|
1427
|
+
def _restore_protection(self, pid: int, address: int, size: int, token: Any) -> None:
|
|
1428
|
+
self.backend.restore_protection(pid, address, size, token)
|
|
1429
|
+
|
|
1430
|
+
def _flush_instruction_cache(self, pid: int, address: int, size: int) -> None:
|
|
1431
|
+
self.backend.flush_instruction_cache(pid, address, size)
|
|
1432
|
+
|
|
1433
|
+
def _try_flush_instruction_cache(self, pid: int, address: int, size: int) -> Exception | None:
|
|
1434
|
+
try:
|
|
1435
|
+
self._flush_instruction_cache(pid, address, size)
|
|
1436
|
+
except Exception as exc:
|
|
1437
|
+
return exc
|
|
1438
|
+
return None
|
|
1439
|
+
|
|
1440
|
+
def _search_regions(
|
|
1441
|
+
self,
|
|
1442
|
+
pid: int,
|
|
1443
|
+
regions: list[MemoryRegion],
|
|
1444
|
+
needle: bytes,
|
|
1445
|
+
*,
|
|
1446
|
+
limit_results: int,
|
|
1447
|
+
limit_bytes: int,
|
|
1448
|
+
value_type: str,
|
|
1449
|
+
endian: str,
|
|
1450
|
+
) -> tuple[list[dict[str, Any]], int, str]:
|
|
1451
|
+
results: list[dict[str, Any]] = []
|
|
1452
|
+
scanned = 0
|
|
1453
|
+
stopped_reason = "completed"
|
|
1454
|
+
overlap = max(0, len(needle) - 1)
|
|
1455
|
+
for region in regions:
|
|
1456
|
+
if scanned >= limit_bytes or len(results) >= limit_results:
|
|
1457
|
+
break
|
|
1458
|
+
region_remaining = min(region.size, limit_bytes - scanned)
|
|
1459
|
+
offset = 0
|
|
1460
|
+
tail = b""
|
|
1461
|
+
while offset < region_remaining:
|
|
1462
|
+
if len(results) >= limit_results:
|
|
1463
|
+
stopped_reason = "max_results"
|
|
1464
|
+
break
|
|
1465
|
+
read_size = min(self.chunk_size, region_remaining - offset)
|
|
1466
|
+
try:
|
|
1467
|
+
chunk = self._read_memory(pid, region.start + offset, read_size)
|
|
1468
|
+
except Exception:
|
|
1469
|
+
offset += read_size
|
|
1470
|
+
scanned += read_size
|
|
1471
|
+
tail = b""
|
|
1472
|
+
continue
|
|
1473
|
+
haystack = tail + chunk
|
|
1474
|
+
base = region.start + offset - len(tail)
|
|
1475
|
+
index = haystack.find(needle)
|
|
1476
|
+
while index != -1:
|
|
1477
|
+
address = base + index
|
|
1478
|
+
if address + len(needle) > region.start + offset:
|
|
1479
|
+
results.append(self._result_entry(address, needle, value_type, endian))
|
|
1480
|
+
if len(results) >= limit_results:
|
|
1481
|
+
stopped_reason = "max_results"
|
|
1482
|
+
break
|
|
1483
|
+
index = haystack.find(needle, index + 1)
|
|
1484
|
+
if stopped_reason == "max_results":
|
|
1485
|
+
break
|
|
1486
|
+
tail = haystack[-overlap:] if overlap else b""
|
|
1487
|
+
offset += read_size
|
|
1488
|
+
scanned += read_size
|
|
1489
|
+
if scanned >= limit_bytes and stopped_reason == "completed":
|
|
1490
|
+
stopped_reason = "max_scan_bytes"
|
|
1491
|
+
return results, scanned, stopped_reason
|
|
1492
|
+
|
|
1493
|
+
def _search_aob_regions(
|
|
1494
|
+
self,
|
|
1495
|
+
pid: int,
|
|
1496
|
+
regions: list[MemoryRegion],
|
|
1497
|
+
pattern: list[int | None],
|
|
1498
|
+
*,
|
|
1499
|
+
limit_results: int,
|
|
1500
|
+
limit_bytes: int,
|
|
1501
|
+
) -> tuple[list[dict[str, Any]], int, str]:
|
|
1502
|
+
results: list[dict[str, Any]] = []
|
|
1503
|
+
scanned = 0
|
|
1504
|
+
stopped_reason = "completed"
|
|
1505
|
+
overlap = max(0, len(pattern) - 1)
|
|
1506
|
+
for region in regions:
|
|
1507
|
+
if scanned >= limit_bytes or len(results) >= limit_results:
|
|
1508
|
+
break
|
|
1509
|
+
region_remaining = min(region.size, limit_bytes - scanned)
|
|
1510
|
+
offset = 0
|
|
1511
|
+
tail = b""
|
|
1512
|
+
while offset < region_remaining:
|
|
1513
|
+
if len(results) >= limit_results:
|
|
1514
|
+
stopped_reason = "max_results"
|
|
1515
|
+
break
|
|
1516
|
+
read_size = min(self.chunk_size, region_remaining - offset)
|
|
1517
|
+
try:
|
|
1518
|
+
chunk = self._read_memory(pid, region.start + offset, read_size)
|
|
1519
|
+
except Exception:
|
|
1520
|
+
offset += read_size
|
|
1521
|
+
scanned += read_size
|
|
1522
|
+
tail = b""
|
|
1523
|
+
continue
|
|
1524
|
+
haystack = tail + chunk
|
|
1525
|
+
base = region.start + offset - len(tail)
|
|
1526
|
+
for index in find_aob_matches(haystack, pattern):
|
|
1527
|
+
address = base + index
|
|
1528
|
+
if address + len(pattern) <= region.start + offset:
|
|
1529
|
+
continue
|
|
1530
|
+
matched = haystack[index : index + len(pattern)]
|
|
1531
|
+
results.append(
|
|
1532
|
+
{
|
|
1533
|
+
"address": hex(address),
|
|
1534
|
+
"matched_hex": matched.hex(),
|
|
1535
|
+
"region": region.to_dict(),
|
|
1536
|
+
}
|
|
1537
|
+
)
|
|
1538
|
+
if len(results) >= limit_results:
|
|
1539
|
+
stopped_reason = "max_results"
|
|
1540
|
+
break
|
|
1541
|
+
if stopped_reason == "max_results":
|
|
1542
|
+
break
|
|
1543
|
+
tail = haystack[-overlap:] if overlap else b""
|
|
1544
|
+
offset += read_size
|
|
1545
|
+
scanned += read_size
|
|
1546
|
+
if scanned >= limit_bytes and stopped_reason == "completed":
|
|
1547
|
+
stopped_reason = "max_scan_bytes"
|
|
1548
|
+
return results, scanned, stopped_reason
|
|
1549
|
+
|
|
1550
|
+
def _scan_pointer_level(
|
|
1551
|
+
self,
|
|
1552
|
+
pid: int,
|
|
1553
|
+
regions: list[MemoryRegion],
|
|
1554
|
+
target_nodes: list[dict[str, Any]],
|
|
1555
|
+
*,
|
|
1556
|
+
pointer_size: int,
|
|
1557
|
+
align: int,
|
|
1558
|
+
max_offset: int,
|
|
1559
|
+
max_results: int,
|
|
1560
|
+
max_scan_bytes: int,
|
|
1561
|
+
endian: str,
|
|
1562
|
+
module_bases: dict[str, int],
|
|
1563
|
+
) -> tuple[list[dict[str, Any]], int, str]:
|
|
1564
|
+
candidates: list[dict[str, Any]] = []
|
|
1565
|
+
scanned = 0
|
|
1566
|
+
stopped_reason = "completed"
|
|
1567
|
+
for region in regions:
|
|
1568
|
+
if scanned >= max_scan_bytes or len(candidates) >= max_results:
|
|
1569
|
+
break
|
|
1570
|
+
region_remaining = min(region.size, max_scan_bytes - scanned)
|
|
1571
|
+
offset = 0
|
|
1572
|
+
while offset + pointer_size <= region_remaining:
|
|
1573
|
+
read_size = min(self.chunk_size, region_remaining - offset)
|
|
1574
|
+
try:
|
|
1575
|
+
chunk = self._read_memory(pid, region.start + offset, read_size)
|
|
1576
|
+
except Exception:
|
|
1577
|
+
offset += read_size
|
|
1578
|
+
scanned += read_size
|
|
1579
|
+
continue
|
|
1580
|
+
for chunk_offset in range(0, max(0, len(chunk) - pointer_size + 1), align):
|
|
1581
|
+
pointer_address = region.start + offset + chunk_offset
|
|
1582
|
+
pointer_value = int.from_bytes(chunk[chunk_offset : chunk_offset + pointer_size], endian)
|
|
1583
|
+
for node in target_nodes:
|
|
1584
|
+
target = int(node["target"])
|
|
1585
|
+
delta = target - pointer_value
|
|
1586
|
+
if 0 <= delta <= max_offset:
|
|
1587
|
+
offsets = [delta, *[parse_address(v) for v in node["offsets"]]]
|
|
1588
|
+
module_ref = self._module_reference(pointer_address, region, module_bases)
|
|
1589
|
+
candidates.append(
|
|
1590
|
+
{
|
|
1591
|
+
"base_address": hex(pointer_address),
|
|
1592
|
+
"base_module": module_ref,
|
|
1593
|
+
"offsets": [hex(v) for v in offsets],
|
|
1594
|
+
"depth": len(offsets),
|
|
1595
|
+
"points_to": hex(target),
|
|
1596
|
+
"pointer_value": hex(pointer_value),
|
|
1597
|
+
}
|
|
1598
|
+
)
|
|
1599
|
+
if len(candidates) >= max_results:
|
|
1600
|
+
return candidates, scanned, "max_results"
|
|
1601
|
+
offset += read_size
|
|
1602
|
+
scanned += read_size
|
|
1603
|
+
if scanned >= max_scan_bytes:
|
|
1604
|
+
stopped_reason = "max_scan_bytes"
|
|
1605
|
+
return candidates, scanned, stopped_reason
|
|
1606
|
+
|
|
1607
|
+
@staticmethod
|
|
1608
|
+
def _module_bases(regions: list[MemoryRegion]) -> dict[str, int]:
|
|
1609
|
+
bases: dict[str, int] = {}
|
|
1610
|
+
for region in regions:
|
|
1611
|
+
if not region.path or region.path.startswith("["):
|
|
1612
|
+
continue
|
|
1613
|
+
bases[region.path] = min(region.start, bases.get(region.path, region.start))
|
|
1614
|
+
return bases
|
|
1615
|
+
|
|
1616
|
+
@staticmethod
|
|
1617
|
+
def _module_reference(
|
|
1618
|
+
address: int,
|
|
1619
|
+
region: MemoryRegion,
|
|
1620
|
+
module_bases: dict[str, int],
|
|
1621
|
+
) -> dict[str, Any] | None:
|
|
1622
|
+
if not region.path or region.path.startswith("["):
|
|
1623
|
+
return None
|
|
1624
|
+
base = module_bases.get(region.path, region.start)
|
|
1625
|
+
return {
|
|
1626
|
+
"path": region.path,
|
|
1627
|
+
"base": hex(base),
|
|
1628
|
+
"offset": hex(address - base),
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
def _resolve_base_address(
|
|
1632
|
+
self,
|
|
1633
|
+
pid: int,
|
|
1634
|
+
*,
|
|
1635
|
+
base_address: int | str | None,
|
|
1636
|
+
module_path: str | None,
|
|
1637
|
+
module_offset: int | str | None,
|
|
1638
|
+
) -> int:
|
|
1639
|
+
if base_address is not None:
|
|
1640
|
+
return parse_address(base_address)
|
|
1641
|
+
if not module_path or module_offset is None:
|
|
1642
|
+
raise ValueError("base_address or module_path+module_offset is required")
|
|
1643
|
+
offset = parse_address(module_offset)
|
|
1644
|
+
matches = [
|
|
1645
|
+
region
|
|
1646
|
+
for region in self._linux_regions(pid)
|
|
1647
|
+
if region.path and module_path in region.path
|
|
1648
|
+
]
|
|
1649
|
+
if not matches:
|
|
1650
|
+
raise ValueError(f"module not found: {module_path}")
|
|
1651
|
+
base = min(region.start for region in matches)
|
|
1652
|
+
return base + offset
|
|
1653
|
+
|
|
1654
|
+
@staticmethod
|
|
1655
|
+
def _result_entry(address: int, data: bytes, value_type: str, endian: str) -> dict[str, Any]:
|
|
1656
|
+
entry: dict[str, Any] = {
|
|
1657
|
+
"address": hex(address),
|
|
1658
|
+
"bytes_hex": data.hex(),
|
|
1659
|
+
}
|
|
1660
|
+
decoded = decode_value(value_type, data, endian=endian)
|
|
1661
|
+
if decoded is not None:
|
|
1662
|
+
entry["value"] = decoded
|
|
1663
|
+
return entry
|
|
1664
|
+
|
|
1665
|
+
@staticmethod
|
|
1666
|
+
def _matches_refine(
|
|
1667
|
+
*,
|
|
1668
|
+
comparison: str,
|
|
1669
|
+
old: bytes,
|
|
1670
|
+
current: bytes,
|
|
1671
|
+
expected: bytes | None,
|
|
1672
|
+
value_type: str,
|
|
1673
|
+
endian: str,
|
|
1674
|
+
) -> bool:
|
|
1675
|
+
if comparison == "equals":
|
|
1676
|
+
return expected is not None and current == expected
|
|
1677
|
+
if comparison == "changed":
|
|
1678
|
+
return current != old
|
|
1679
|
+
if comparison == "unchanged":
|
|
1680
|
+
return current == old
|
|
1681
|
+
old_value = decode_value(value_type, old, endian=endian)
|
|
1682
|
+
current_value = decode_value(value_type, current, endian=endian)
|
|
1683
|
+
if not isinstance(old_value, (int, float)) or not isinstance(current_value, (int, float)):
|
|
1684
|
+
return False
|
|
1685
|
+
if comparison == "increased":
|
|
1686
|
+
return current_value > old_value
|
|
1687
|
+
if comparison == "decreased":
|
|
1688
|
+
return current_value < old_value
|
|
1689
|
+
return False
|
|
1690
|
+
|
|
1691
|
+
@staticmethod
|
|
1692
|
+
def _freeze_snapshot(state: dict[str, Any], *, include_task: bool) -> dict[str, Any]:
|
|
1693
|
+
if include_task:
|
|
1694
|
+
return dict(state)
|
|
1695
|
+
return {key: value for key, value in state.items() if key != "task"}
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
def parse_address(value: int | str) -> int:
|
|
1699
|
+
if isinstance(value, int):
|
|
1700
|
+
return value
|
|
1701
|
+
text = str(value).strip()
|
|
1702
|
+
if not text:
|
|
1703
|
+
raise ValueError("address is required")
|
|
1704
|
+
return int(text, 0)
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
def parse_aob_pattern(pattern: str) -> list[int | None]:
|
|
1708
|
+
text = pattern.strip()
|
|
1709
|
+
if not text:
|
|
1710
|
+
return []
|
|
1711
|
+
if re.search(r"\s", text):
|
|
1712
|
+
tokens = re.split(r"[\s,]+", text)
|
|
1713
|
+
else:
|
|
1714
|
+
compact = text.replace("0x", "").replace("0X", "")
|
|
1715
|
+
if len(compact) % 2:
|
|
1716
|
+
raise ValueError("compact AOB pattern must contain an even number of hex/wildcard characters")
|
|
1717
|
+
tokens = [compact[i : i + 2] for i in range(0, len(compact), 2)]
|
|
1718
|
+
parsed: list[int | None] = []
|
|
1719
|
+
for token in tokens:
|
|
1720
|
+
if not token:
|
|
1721
|
+
continue
|
|
1722
|
+
token = token.strip()
|
|
1723
|
+
if set(token) <= {"?"}:
|
|
1724
|
+
parsed.append(None)
|
|
1725
|
+
continue
|
|
1726
|
+
if "?" in token:
|
|
1727
|
+
if len(token) != 2:
|
|
1728
|
+
raise ValueError(f"invalid wildcard token: {token}")
|
|
1729
|
+
parsed.append(None)
|
|
1730
|
+
continue
|
|
1731
|
+
parsed.append(int(token.removeprefix("0x").removeprefix("0X"), 16))
|
|
1732
|
+
return parsed
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
def find_aob_matches(data: bytes, pattern: list[int | None]) -> list[int]:
|
|
1736
|
+
if not pattern or len(data) < len(pattern):
|
|
1737
|
+
return []
|
|
1738
|
+
matches: list[int] = []
|
|
1739
|
+
width = len(pattern)
|
|
1740
|
+
first = pattern[0]
|
|
1741
|
+
start = 0
|
|
1742
|
+
while start <= len(data) - width:
|
|
1743
|
+
if first is None:
|
|
1744
|
+
index = start
|
|
1745
|
+
else:
|
|
1746
|
+
index = data.find(bytes([first]), start, len(data) - width + 1)
|
|
1747
|
+
if index == -1:
|
|
1748
|
+
break
|
|
1749
|
+
for pattern_index, expected in enumerate(pattern):
|
|
1750
|
+
if expected is not None and data[index + pattern_index] != expected:
|
|
1751
|
+
break
|
|
1752
|
+
else:
|
|
1753
|
+
matches.append(index)
|
|
1754
|
+
start = index + 1
|
|
1755
|
+
return matches
|
|
1756
|
+
|
|
1757
|
+
|
|
1758
|
+
def encode_value(
|
|
1759
|
+
value_type: str,
|
|
1760
|
+
*,
|
|
1761
|
+
value: Any = None,
|
|
1762
|
+
value_hex: str | None = None,
|
|
1763
|
+
endian: str = "little",
|
|
1764
|
+
) -> bytes:
|
|
1765
|
+
normalized = value_type.lower()
|
|
1766
|
+
if normalized == "bytes":
|
|
1767
|
+
raw = value_hex if value_hex is not None else str(value or "")
|
|
1768
|
+
cleaned = re.sub(r"0[xX]", "", raw.strip())
|
|
1769
|
+
invalid = re.search(r"[^0-9a-fA-F\s,;:_-]", cleaned)
|
|
1770
|
+
if invalid:
|
|
1771
|
+
raise ValueError(f"invalid hexadecimal character: {invalid.group(0)!r}")
|
|
1772
|
+
cleaned = re.sub(r"[\s,;:_-]", "", cleaned)
|
|
1773
|
+
if len(cleaned) % 2:
|
|
1774
|
+
raise ValueError("hexadecimal byte strings must contain an even number of digits")
|
|
1775
|
+
return bytes.fromhex(cleaned)
|
|
1776
|
+
if normalized == "string":
|
|
1777
|
+
return str(value or "").encode("utf-8")
|
|
1778
|
+
fmt = _NUMERIC_FORMATS.get(normalized)
|
|
1779
|
+
if not fmt:
|
|
1780
|
+
raise ValueError(f"unsupported value_type: {value_type}")
|
|
1781
|
+
prefix = "<" if endian.lower() == "little" else ">"
|
|
1782
|
+
if normalized.startswith("float"):
|
|
1783
|
+
typed_value: int | float = float(value)
|
|
1784
|
+
else:
|
|
1785
|
+
typed_value = int(str(value), 0)
|
|
1786
|
+
return struct.pack(prefix + fmt, typed_value)
|
|
1787
|
+
|
|
1788
|
+
|
|
1789
|
+
def decode_value(value_type: str, data: bytes, *, endian: str = "little") -> Any:
|
|
1790
|
+
normalized = value_type.lower()
|
|
1791
|
+
if normalized == "bytes":
|
|
1792
|
+
return data.hex()
|
|
1793
|
+
if normalized == "string":
|
|
1794
|
+
return data.decode("utf-8", errors="replace")
|
|
1795
|
+
fmt = _NUMERIC_FORMATS.get(normalized)
|
|
1796
|
+
if not fmt:
|
|
1797
|
+
return None
|
|
1798
|
+
prefix = "<" if endian.lower() == "little" else ">"
|
|
1799
|
+
size = struct.calcsize(prefix + fmt)
|
|
1800
|
+
if len(data) < size:
|
|
1801
|
+
return None
|
|
1802
|
+
return struct.unpack(prefix + fmt, data[:size])[0]
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
def _ascii_preview(data: bytes) -> str:
|
|
1806
|
+
return "".join(chr(b) if 32 <= b < 127 else "." for b in data)
|