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.
Files changed (59) hide show
  1. memscope_mcp/__init__.py +10 -0
  2. memscope_mcp/_contrib/__init__.py +0 -0
  3. memscope_mcp/_contrib/plugins/__init__.py +0 -0
  4. memscope_mcp/_contrib/plugins/il2cpp.py +287 -0
  5. memscope_mcp/_contrib/plugins/netcap.py +2081 -0
  6. memscope_mcp/cli.py +135 -0
  7. memscope_mcp/extensions/__init__.py +5 -0
  8. memscope_mcp/extensions/base.py +82 -0
  9. memscope_mcp/extensions/bootstrap.py +83 -0
  10. memscope_mcp/extensions/core/__init__.py +24 -0
  11. memscope_mcp/extensions/core/execution.py +69 -0
  12. memscope_mcp/extensions/core/general.py +115 -0
  13. memscope_mcp/extensions/core/hooking.py +115 -0
  14. memscope_mcp/extensions/core/memory.py +207 -0
  15. memscope_mcp/extensions/core/module_scan.py +164 -0
  16. memscope_mcp/extensions/core/network.py +32 -0
  17. memscope_mcp/extensions/core/process.py +163 -0
  18. memscope_mcp/instructions/__init__.py +31 -0
  19. memscope_mcp/instructions/base.py +50 -0
  20. memscope_mcp/paths.py +28 -0
  21. memscope_mcp/plugins/__init__.py +135 -0
  22. memscope_mcp/server.py +580 -0
  23. memscope_mcp/session.py +810 -0
  24. memscope_mcp/tools/__init__.py +1 -0
  25. memscope_mcp/tools/execute.py +569 -0
  26. memscope_mcp/tools/hooking.py +774 -0
  27. memscope_mcp/tools/lua/__init__.py +5 -0
  28. memscope_mcp/tools/lua/code_execution.py +444 -0
  29. memscope_mcp/tools/lua/comparisons.py +146 -0
  30. memscope_mcp/tools/lua/engine.py +406 -0
  31. memscope_mcp/tools/lua/hooking.py +199 -0
  32. memscope_mcp/tools/lua/memory_read.py +345 -0
  33. memscope_mcp/tools/lua/memory_write.py +192 -0
  34. memscope_mcp/tools/lua/modules.py +130 -0
  35. memscope_mcp/tools/lua/network.py +125 -0
  36. memscope_mcp/tools/lua/process_info.py +584 -0
  37. memscope_mcp/tools/lua/scanning_helpers.py +210 -0
  38. memscope_mcp/tools/lua/struct_helpers.py +166 -0
  39. memscope_mcp/tools/lua/utilities.py +157 -0
  40. memscope_mcp/tools/lua_engine.py +5 -0
  41. memscope_mcp/tools/lua_scripts.py +169 -0
  42. memscope_mcp/tools/memory.py +177 -0
  43. memscope_mcp/tools/pointers.py +194 -0
  44. memscope_mcp/tools/scanning.py +519 -0
  45. memscope_mcp/tools/types.py +479 -0
  46. memscope_mcp/utils/__init__.py +1 -0
  47. memscope_mcp/utils/disasm.py +500 -0
  48. memscope_mcp/utils/heuristics.py +167 -0
  49. memscope_mcp/utils/logger.py +184 -0
  50. memscope_mcp/utils/memory_utils.py +161 -0
  51. memscope_mcp/utils/pattern.py +120 -0
  52. memscope_mcp/utils/pe.py +156 -0
  53. memscope_mcp/utils/peb.py +363 -0
  54. memscope_mcp/utils/shellcode.py +843 -0
  55. memscope_mcp-0.1.0.dist-info/METADATA +332 -0
  56. memscope_mcp-0.1.0.dist-info/RECORD +59 -0
  57. memscope_mcp-0.1.0.dist-info/WHEEL +4 -0
  58. memscope_mcp-0.1.0.dist-info/entry_points.txt +2 -0
  59. memscope_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -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)