memscope-mcp 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.
- memscope_mcp/__init__.py +10 -0
- memscope_mcp/_contrib/__init__.py +0 -0
- memscope_mcp/_contrib/plugins/__init__.py +0 -0
- memscope_mcp/_contrib/plugins/il2cpp.py +287 -0
- memscope_mcp/_contrib/plugins/netcap.py +2081 -0
- memscope_mcp/cli.py +135 -0
- memscope_mcp/extensions/__init__.py +5 -0
- memscope_mcp/extensions/base.py +82 -0
- memscope_mcp/extensions/bootstrap.py +83 -0
- memscope_mcp/extensions/core/__init__.py +24 -0
- memscope_mcp/extensions/core/execution.py +69 -0
- memscope_mcp/extensions/core/general.py +115 -0
- memscope_mcp/extensions/core/hooking.py +115 -0
- memscope_mcp/extensions/core/memory.py +207 -0
- memscope_mcp/extensions/core/module_scan.py +164 -0
- memscope_mcp/extensions/core/network.py +32 -0
- memscope_mcp/extensions/core/process.py +163 -0
- memscope_mcp/instructions/__init__.py +31 -0
- memscope_mcp/instructions/base.py +50 -0
- memscope_mcp/paths.py +28 -0
- memscope_mcp/plugins/__init__.py +135 -0
- memscope_mcp/server.py +580 -0
- memscope_mcp/session.py +810 -0
- memscope_mcp/tools/__init__.py +1 -0
- memscope_mcp/tools/execute.py +569 -0
- memscope_mcp/tools/hooking.py +774 -0
- memscope_mcp/tools/lua/__init__.py +5 -0
- memscope_mcp/tools/lua/code_execution.py +444 -0
- memscope_mcp/tools/lua/comparisons.py +146 -0
- memscope_mcp/tools/lua/engine.py +406 -0
- memscope_mcp/tools/lua/hooking.py +199 -0
- memscope_mcp/tools/lua/memory_read.py +345 -0
- memscope_mcp/tools/lua/memory_write.py +192 -0
- memscope_mcp/tools/lua/modules.py +130 -0
- memscope_mcp/tools/lua/network.py +125 -0
- memscope_mcp/tools/lua/process_info.py +584 -0
- memscope_mcp/tools/lua/scanning_helpers.py +210 -0
- memscope_mcp/tools/lua/struct_helpers.py +166 -0
- memscope_mcp/tools/lua/utilities.py +157 -0
- memscope_mcp/tools/lua_engine.py +5 -0
- memscope_mcp/tools/lua_scripts.py +169 -0
- memscope_mcp/tools/memory.py +177 -0
- memscope_mcp/tools/pointers.py +194 -0
- memscope_mcp/tools/scanning.py +519 -0
- memscope_mcp/tools/types.py +479 -0
- memscope_mcp/utils/__init__.py +1 -0
- memscope_mcp/utils/disasm.py +500 -0
- memscope_mcp/utils/heuristics.py +167 -0
- memscope_mcp/utils/logger.py +184 -0
- memscope_mcp/utils/memory_utils.py +161 -0
- memscope_mcp/utils/pattern.py +120 -0
- memscope_mcp/utils/pe.py +156 -0
- memscope_mcp/utils/peb.py +363 -0
- memscope_mcp/utils/shellcode.py +843 -0
- memscope_mcp-0.1.0.dist-info/METADATA +332 -0
- memscope_mcp-0.1.0.dist-info/RECORD +59 -0
- memscope_mcp-0.1.0.dist-info/WHEEL +4 -0
- memscope_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- memscope_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
memscope_mcp/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""memscope-mcp: a Model Context Protocol server for memory research on Windows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
if sys.platform != "win32":
|
|
8
|
+
raise RuntimeError(
|
|
9
|
+
f"memscope-mcp requires Windows (sys.platform == 'win32'); detected sys.platform={sys.platform!r}"
|
|
10
|
+
)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""IL2CPP runtime structure reader.
|
|
2
|
+
|
|
3
|
+
Reference plugin demonstrating runtime-structure helpers for Unity's IL2CPP
|
|
4
|
+
runtime. Useful for reverse engineering Unity-packaged binaries, malware
|
|
5
|
+
analysis of Unity-bundled payloads, and security research on IL2CPP apps.
|
|
6
|
+
|
|
7
|
+
Provides Lua functions for reading IL2CPP strings, arrays, lists, and dictionaries.
|
|
8
|
+
Activate by copying this file to the plugins/ directory.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from memscope_mcp.plugins import PluginBase
|
|
14
|
+
from memscope_mcp.session import SESSION
|
|
15
|
+
from memscope_mcp.utils.memory_utils import is_valid_pointer
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IL2CppPlugin(PluginBase):
|
|
19
|
+
"""Unity IL2CPP runtime structure helpers."""
|
|
20
|
+
|
|
21
|
+
name = "il2cpp"
|
|
22
|
+
description = "Unity IL2CPP runtime structure helpers"
|
|
23
|
+
|
|
24
|
+
instructions = """
|
|
25
|
+
## IL2CPP Helpers
|
|
26
|
+
Unity IL2CPP applications compile C# to C++. These helpers work with IL2CPP's runtime structures.
|
|
27
|
+
|
|
28
|
+
**Note:** The core `read` tool handles primitives and composite types only. Use the Lua
|
|
29
|
+
helpers below for IL2CPP-specific structures (strings, arrays, lists, dictionaries).
|
|
30
|
+
|
|
31
|
+
### IL2CPP String
|
|
32
|
+
```lua
|
|
33
|
+
readIL2CppString(addr) -- UTF-16 string at object+0x14, length at +0x10
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### IL2CPP Array
|
|
37
|
+
Layout: `+0x18` = length (uint64), `+0x20` = data start
|
|
38
|
+
```lua
|
|
39
|
+
local arr = readIL2CppArray(addr, "ptr", 50) -- Read up to 50 pointer elements
|
|
40
|
+
-- Returns: {length=N, elements={...}}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### IL2CPP List<T>
|
|
44
|
+
Layout: `+0x10` = items array ptr, `+0x18` = count (int32)
|
|
45
|
+
```lua
|
|
46
|
+
local list = readIL2CppList(addr, 50) -- Read up to 50 elements
|
|
47
|
+
-- Returns: {count=N, items={...}}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### IL2CPP Dictionary<K,V>
|
|
51
|
+
Layout: `+0x18` = entries array, `+0x20` = count
|
|
52
|
+
```lua
|
|
53
|
+
local dict = readIL2CppDict(addr, "int32", "ptr", 50)
|
|
54
|
+
-- Returns: {count=N, entries={{key=K, value=V}, ...}}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Thread Attachment
|
|
58
|
+
IL2CPP API calls CRASH without thread attachment. The attachment is thread-local,
|
|
59
|
+
so you MUST use call_sequence to run attach + API calls in the same thread:
|
|
60
|
+
|
|
61
|
+
```lua
|
|
62
|
+
-- Resolve il2cpp_thread_attach and the appdomain pointer dynamically
|
|
63
|
+
-- (offsets vary by build; discover via pattern scan or exports).
|
|
64
|
+
local attach_func = getAddress("GameAssembly.dll+0x<thread_attach_offset>")
|
|
65
|
+
local domain = readPointer(getAddress("GameAssembly.dll+0x<domain_offset>"))
|
|
66
|
+
|
|
67
|
+
callSequence({
|
|
68
|
+
{address=attach_func, args={domain}}, -- Attach this thread
|
|
69
|
+
{address=api_func, args={...}} -- Now safe to call
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Common IL2CPP Offsets
|
|
74
|
+
These are typical but may vary by Unity version:
|
|
75
|
+
|
|
76
|
+
| Structure | Field | Offset |
|
|
77
|
+
|-----------|-------|--------|
|
|
78
|
+
| Il2CppString | length | +0x10 |
|
|
79
|
+
| Il2CppString | chars | +0x14 |
|
|
80
|
+
| Il2CppArray | max_length | +0x18 |
|
|
81
|
+
| Il2CppArray | data | +0x20 |
|
|
82
|
+
| List<T> | _items | +0x10 |
|
|
83
|
+
| List<T> | _size | +0x18 |
|
|
84
|
+
| Dictionary | entries | +0x18 |
|
|
85
|
+
| Dictionary | count | +0x20 |
|
|
86
|
+
|
|
87
|
+
### Finding IL2CPP Structures
|
|
88
|
+
Use pattern scans to find IL2CPP runtime functions, then call them:
|
|
89
|
+
```lua
|
|
90
|
+
-- Example: Find class by name
|
|
91
|
+
local class_from_name = AOBScanModule("GameAssembly.dll", "48 89 5C 24 08 57 48 83 EC 20 ...")[1]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Scripts should discover offsets dynamically rather than hardcoding them,
|
|
95
|
+
as they change between Unity/IL2CPP versions.
|
|
96
|
+
""".strip()
|
|
97
|
+
|
|
98
|
+
def register(self, engine) -> dict[str, callable]:
|
|
99
|
+
self.table = engine.lua.table
|
|
100
|
+
return {
|
|
101
|
+
"readUnityString": self._read_string,
|
|
102
|
+
"readIL2CppString": self._read_string,
|
|
103
|
+
"readListCount": self._read_list_count,
|
|
104
|
+
"readListElement": self._read_list_element,
|
|
105
|
+
"readDictCount": self._read_dict_count,
|
|
106
|
+
"readIL2CppArray": lambda addr, elem_type="ptr", limit=50: self._read_array(addr, elem_type, limit),
|
|
107
|
+
"readIL2CppList": lambda addr, limit=50: self._read_list(addr, limit),
|
|
108
|
+
"readIL2CppDict": lambda addr, key_type="int32", val_type="ptr", limit=50: self._read_dict(
|
|
109
|
+
addr, key_type, val_type, limit
|
|
110
|
+
),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# =========================================================================
|
|
114
|
+
# String
|
|
115
|
+
# =========================================================================
|
|
116
|
+
|
|
117
|
+
def _read_string(self, address) -> Optional[str]:
|
|
118
|
+
"""Read IL2CPP string (UTF-16 at addr+0x14, length at +0x10)."""
|
|
119
|
+
try:
|
|
120
|
+
addr = int(address)
|
|
121
|
+
length = SESSION.read_int32(addr + 0x10)
|
|
122
|
+
if length <= 0 or length > 4096:
|
|
123
|
+
return ""
|
|
124
|
+
raw = SESSION.read_bytes(addr + 0x14, length * 2)
|
|
125
|
+
return raw.decode("utf-16-le", errors="replace")
|
|
126
|
+
except:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
# =========================================================================
|
|
130
|
+
# List
|
|
131
|
+
# =========================================================================
|
|
132
|
+
|
|
133
|
+
def _read_list_count(self, address) -> Optional[int]:
|
|
134
|
+
"""Read IL2CPP List<T> count (at +0x18)."""
|
|
135
|
+
try:
|
|
136
|
+
return SESSION.read_int32(int(address) + 0x18)
|
|
137
|
+
except:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
def _read_list_element(self, address, index) -> Optional[int]:
|
|
141
|
+
"""Read pointer element from IL2CPP List<T>."""
|
|
142
|
+
try:
|
|
143
|
+
addr = int(address)
|
|
144
|
+
items_ptr = SESSION.read_ptr(addr + 0x10)
|
|
145
|
+
if not is_valid_pointer(items_ptr):
|
|
146
|
+
return None
|
|
147
|
+
elem_addr = items_ptr + 0x20 + (int(index) * 8)
|
|
148
|
+
return SESSION.read_ptr(elem_addr)
|
|
149
|
+
except:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
def _read_list(self, address, limit: int = 50):
|
|
153
|
+
"""Read IL2CPP List<T>. Layout: +0x10 = items ptr, +0x18 = count."""
|
|
154
|
+
try:
|
|
155
|
+
addr = int(address)
|
|
156
|
+
items_ptr = SESSION.read_ptr(addr + 0x10)
|
|
157
|
+
count = SESSION.read_int32(addr + 0x18)
|
|
158
|
+
|
|
159
|
+
if not is_valid_pointer(items_ptr) or count is None or count < 0:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
data_start = items_ptr + 0x20
|
|
163
|
+
items = []
|
|
164
|
+
read_count = min(count, limit)
|
|
165
|
+
|
|
166
|
+
for i in range(read_count):
|
|
167
|
+
val = SESSION.read_ptr(data_start + (i * 8))
|
|
168
|
+
items.append(val)
|
|
169
|
+
|
|
170
|
+
result = self.table()
|
|
171
|
+
result["count"] = count
|
|
172
|
+
result["items"] = self.table(*items)
|
|
173
|
+
return result
|
|
174
|
+
except:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
# =========================================================================
|
|
178
|
+
# Array
|
|
179
|
+
# =========================================================================
|
|
180
|
+
|
|
181
|
+
def _read_array(self, address, element_type: str = "ptr", limit: int = 50):
|
|
182
|
+
"""Read IL2CPP array. Layout: +0x18 = length, +0x20 = data start."""
|
|
183
|
+
try:
|
|
184
|
+
addr = int(address)
|
|
185
|
+
length = SESSION.read_int32(addr + 0x18)
|
|
186
|
+
if length is None or length < 0:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
data_start = addr + 0x20
|
|
190
|
+
count = min(length, limit)
|
|
191
|
+
|
|
192
|
+
elem_size = 8
|
|
193
|
+
if element_type in ("int32", "float"):
|
|
194
|
+
elem_size = 4
|
|
195
|
+
elif element_type == "byte":
|
|
196
|
+
elem_size = 1
|
|
197
|
+
|
|
198
|
+
elements = []
|
|
199
|
+
for i in range(count):
|
|
200
|
+
elem_addr = data_start + (i * elem_size)
|
|
201
|
+
if element_type in ("ptr", "pointer"):
|
|
202
|
+
val = SESSION.read_ptr(elem_addr)
|
|
203
|
+
elif element_type == "int32":
|
|
204
|
+
val = SESSION.read_int32(elem_addr)
|
|
205
|
+
elif element_type == "float":
|
|
206
|
+
val = SESSION.read_float(elem_addr)
|
|
207
|
+
elif element_type == "byte":
|
|
208
|
+
data = SESSION.read_bytes(elem_addr, 1)
|
|
209
|
+
val = data[0] if data else None
|
|
210
|
+
else:
|
|
211
|
+
val = SESSION.read_ptr(elem_addr)
|
|
212
|
+
elements.append(val)
|
|
213
|
+
|
|
214
|
+
result = self.table()
|
|
215
|
+
result["length"] = length
|
|
216
|
+
result["elements"] = self.table(*elements)
|
|
217
|
+
return result
|
|
218
|
+
except:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
# =========================================================================
|
|
222
|
+
# Dictionary
|
|
223
|
+
# =========================================================================
|
|
224
|
+
|
|
225
|
+
def _read_dict_count(self, address) -> Optional[int]:
|
|
226
|
+
"""Read IL2CPP Dictionary count (at +0x20)."""
|
|
227
|
+
try:
|
|
228
|
+
return SESSION.read_int32(int(address) + 0x20)
|
|
229
|
+
except:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
def _read_dict(self, address, key_type: str = "int32", value_type: str = "ptr", limit: int = 50):
|
|
233
|
+
"""Read IL2CPP Dictionary. Layout: +0x18 = entries, +0x20 = count."""
|
|
234
|
+
try:
|
|
235
|
+
addr = int(address)
|
|
236
|
+
entries_ptr = SESSION.read_ptr(addr + 0x18)
|
|
237
|
+
count = SESSION.read_int32(addr + 0x20)
|
|
238
|
+
|
|
239
|
+
if not is_valid_pointer(entries_ptr) or count is None or count < 0:
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
data_start = entries_ptr + 0x20
|
|
243
|
+
entry_size = 24 # hashCode(4) + next(4) + key(8) + value(8)
|
|
244
|
+
|
|
245
|
+
entries = []
|
|
246
|
+
valid_count = 0
|
|
247
|
+
|
|
248
|
+
for i in range(count + 100):
|
|
249
|
+
if valid_count >= limit:
|
|
250
|
+
break
|
|
251
|
+
|
|
252
|
+
entry_addr = data_start + (i * entry_size)
|
|
253
|
+
hash_code = SESSION.read_int32(entry_addr)
|
|
254
|
+
|
|
255
|
+
if hash_code is None or hash_code < 0:
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
key_addr = entry_addr + 8
|
|
259
|
+
value_addr = entry_addr + 16
|
|
260
|
+
|
|
261
|
+
key = self._read_typed_value(key_addr, key_type)
|
|
262
|
+
value = self._read_typed_value(value_addr, value_type)
|
|
263
|
+
|
|
264
|
+
entry = self.table()
|
|
265
|
+
entry["key"] = key
|
|
266
|
+
entry["value"] = value
|
|
267
|
+
entries.append(entry)
|
|
268
|
+
valid_count += 1
|
|
269
|
+
|
|
270
|
+
result = self.table()
|
|
271
|
+
result["count"] = count
|
|
272
|
+
result["entries"] = self.table(*entries)
|
|
273
|
+
return result
|
|
274
|
+
except:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
def _read_typed_value(self, addr: int, type_name: str):
|
|
278
|
+
"""Read a value based on type name."""
|
|
279
|
+
if type_name == "int32":
|
|
280
|
+
return SESSION.read_int32(addr)
|
|
281
|
+
elif type_name == "float":
|
|
282
|
+
return SESSION.read_float(addr)
|
|
283
|
+
elif type_name == "string":
|
|
284
|
+
ptr = SESSION.read_ptr(addr)
|
|
285
|
+
return self._read_string(ptr) if is_valid_pointer(ptr) else None
|
|
286
|
+
else:
|
|
287
|
+
return SESSION.read_ptr(addr)
|