robloxmemoryapi 0.0.1__tar.gz
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.
- robloxmemoryapi-0.0.1/LICENSE.md +7 -0
- robloxmemoryapi-0.0.1/PKG-INFO +75 -0
- robloxmemoryapi-0.0.1/README.md +47 -0
- robloxmemoryapi-0.0.1/pyproject.toml +43 -0
- robloxmemoryapi-0.0.1/setup.cfg +4 -0
- robloxmemoryapi-0.0.1/src/robloxmemoryapi/__init__.py +87 -0
- robloxmemoryapi-0.0.1/src/robloxmemoryapi/data/offsets.json +6 -0
- robloxmemoryapi-0.0.1/src/robloxmemoryapi/utils/__init__.py +2 -0
- robloxmemoryapi-0.0.1/src/robloxmemoryapi/utils/memory.py +275 -0
- robloxmemoryapi-0.0.1/src/robloxmemoryapi/utils/offsets.py +46 -0
- robloxmemoryapi-0.0.1/src/robloxmemoryapi/utils/rbx/__init__.py +0 -0
- robloxmemoryapi-0.0.1/src/robloxmemoryapi/utils/rbx/datastructures.py +266 -0
- robloxmemoryapi-0.0.1/src/robloxmemoryapi/utils/rbx/instance.py +457 -0
- robloxmemoryapi-0.0.1/src/robloxmemoryapi.egg-info/PKG-INFO +75 -0
- robloxmemoryapi-0.0.1/src/robloxmemoryapi.egg-info/SOURCES.txt +16 -0
- robloxmemoryapi-0.0.1/src/robloxmemoryapi.egg-info/dependency_links.txt +1 -0
- robloxmemoryapi-0.0.1/src/robloxmemoryapi.egg-info/requires.txt +1 -0
- robloxmemoryapi-0.0.1/src/robloxmemoryapi.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 upio, mstudio45, master oogway
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robloxmemoryapi
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python Library that abstracts reading data from the Roblox DataModel
|
|
5
|
+
Author-email: upio <notpoiu@users.noreply.github.com>, mstudio45 <mstudio45@users.noreply.github.com>, ActualMasterOogway <ActualMasterOogway@users.noreply.github.com>
|
|
6
|
+
License: Copyright 2025 upio, mstudio45, master oogway
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
11
|
+
|
|
12
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
13
|
+
|
|
14
|
+
Project-URL: Homepage, https://github.com/notpoiu/RobloxMemoryAPI
|
|
15
|
+
Project-URL: Issues, https://github.com/notpoiu/RobloxMemoryAPI/issues
|
|
16
|
+
Keywords: roblox,memory,windows
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
20
|
+
Classifier: Development Status :: 3 - Alpha
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE.md
|
|
26
|
+
Requires-Dist: requests>=2.0
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# RobloxMemoryAPI
|
|
30
|
+
|
|
31
|
+
A Python library that is _hopefully stealthy_ and abstracts externally reading memory to get datamodel information from the roblox game client.
|
|
32
|
+
|
|
33
|
+
This was made by [upio](https://github.com/notpoiu), [mstudio45](https://github.com/mstudio45), and [Master Oogway](https://github.com/ActualMasterOogway) and created for the [Dig Macro](https://github.com/mstudio45/digmacro) project (external mode and not the computer vision mode).
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
PyPI:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install robloxmemoryapi
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Development (editable install from source):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
An example script can be found in [example.py](example.py). If running from the repo, use the editable install above so `import robloxmemoryapi` resolves the `src` package.
|
|
52
|
+
|
|
53
|
+
Import the library and create a client instance:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from robloxmemoryapi import RobloxGameClient
|
|
57
|
+
|
|
58
|
+
client = RobloxGameClient()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Access the data model:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
game = client.DataModel
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Get the local player's name:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
print("Player Name:", game.Players.LocalPlayer.Name)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# RobloxMemoryAPI
|
|
2
|
+
|
|
3
|
+
A Python library that is _hopefully stealthy_ and abstracts externally reading memory to get datamodel information from the roblox game client.
|
|
4
|
+
|
|
5
|
+
This was made by [upio](https://github.com/notpoiu), [mstudio45](https://github.com/mstudio45), and [Master Oogway](https://github.com/ActualMasterOogway) and created for the [Dig Macro](https://github.com/mstudio45/digmacro) project (external mode and not the computer vision mode).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
PyPI:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install robloxmemoryapi
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Development (editable install from source):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e .
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
An example script can be found in [example.py](example.py). If running from the repo, use the editable install above so `import robloxmemoryapi` resolves the `src` package.
|
|
24
|
+
|
|
25
|
+
Import the library and create a client instance:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from robloxmemoryapi import RobloxGameClient
|
|
29
|
+
|
|
30
|
+
client = RobloxGameClient()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Access the data model:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
game = client.DataModel
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Get the local player's name:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
print("Player Name:", game.Players.LocalPlayer.Name)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "robloxmemoryapi"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Python Library that abstracts reading data from the Roblox DataModel"
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { file = "LICENSE.md" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "upio", email = "notpoiu@users.noreply.github.com" },
|
|
14
|
+
{ name = "mstudio45", email = "mstudio45@users.noreply.github.com" },
|
|
15
|
+
{ name = "ActualMasterOogway", email = "ActualMasterOogway@users.noreply.github.com" },
|
|
16
|
+
]
|
|
17
|
+
keywords = ["roblox", "memory", "windows"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: Microsoft :: Windows",
|
|
22
|
+
"Development Status :: 3 - Alpha",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Topic :: Software Development :: Libraries",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"requests>=2.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/notpoiu/RobloxMemoryAPI"
|
|
32
|
+
Issues = "https://github.com/notpoiu/RobloxMemoryAPI/issues"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
package-dir = {"" = "src"}
|
|
36
|
+
include-package-data = true
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
robloxmemoryapi = ["data/*.json"]
|
|
43
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
__all__ = ["RobloxRandom", "RobloxGameClient", "__version__"]
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RobloxRandom:
|
|
9
|
+
MULT = 6364136223846793005
|
|
10
|
+
INC = 105
|
|
11
|
+
MASK64 = (1 << 64) - 1
|
|
12
|
+
|
|
13
|
+
def __init__(self, seed):
|
|
14
|
+
s = math.floor(seed)
|
|
15
|
+
|
|
16
|
+
self._state = 0
|
|
17
|
+
self._inc = RobloxRandom.INC
|
|
18
|
+
self._next_internal() # warm-up #1
|
|
19
|
+
self._state = (self._state + s) & RobloxRandom.MASK64
|
|
20
|
+
self._next_internal() # warm-up #2
|
|
21
|
+
|
|
22
|
+
def _next_internal(self):
|
|
23
|
+
old = self._state
|
|
24
|
+
self._state = (old * RobloxRandom.MULT + self._inc) & RobloxRandom.MASK64
|
|
25
|
+
x = ((old >> 18) ^ old) >> 27
|
|
26
|
+
r = old >> 59
|
|
27
|
+
return ((x >> r) | (x << ((32 - r) & 31))) & 0xFFFFFFFF
|
|
28
|
+
|
|
29
|
+
def _next_fraction64(self):
|
|
30
|
+
lo = self._next_internal()
|
|
31
|
+
hi = self._next_internal()
|
|
32
|
+
bits = (hi << 32) | lo
|
|
33
|
+
return bits / 2**64
|
|
34
|
+
|
|
35
|
+
def NextNumber(self, minimum=0.0, maximum=1.0):
|
|
36
|
+
frac = self._next_fraction64()
|
|
37
|
+
return minimum + frac * (maximum - minimum)
|
|
38
|
+
|
|
39
|
+
def NextInteger(self, a, b=None):
|
|
40
|
+
if b is None:
|
|
41
|
+
u = a
|
|
42
|
+
r = self._next_internal()
|
|
43
|
+
return ((u * r) >> 32) + 1
|
|
44
|
+
else:
|
|
45
|
+
lo, hi = (a, b) if a <= b else (b, a)
|
|
46
|
+
u = hi - lo + 1
|
|
47
|
+
r = self._next_internal()
|
|
48
|
+
return ((u * r) >> 32) + lo
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RobloxGameClient:
|
|
52
|
+
def __init__(self, pid: int = None, process_name: str = "RobloxPlayerBeta.exe"):
|
|
53
|
+
if platform.system() != "Windows":
|
|
54
|
+
self.failed = True
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
from .utils.memory import (
|
|
58
|
+
EvasiveProcess,
|
|
59
|
+
PROCESS_QUERY_INFORMATION,
|
|
60
|
+
PROCESS_VM_READ,
|
|
61
|
+
get_pid_by_name,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if pid is None:
|
|
65
|
+
self.pid = get_pid_by_name(process_name)
|
|
66
|
+
else:
|
|
67
|
+
self.pid = pid
|
|
68
|
+
|
|
69
|
+
if self.pid is None or self.pid == 0:
|
|
70
|
+
raise ValueError("Failed to get PID.")
|
|
71
|
+
|
|
72
|
+
self.memory_module = EvasiveProcess(self.pid, PROCESS_VM_READ | PROCESS_QUERY_INFORMATION)
|
|
73
|
+
self.failed = False
|
|
74
|
+
|
|
75
|
+
def close(self):
|
|
76
|
+
self.memory_module.close()
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def DataModel(self):
|
|
80
|
+
if platform.system() != "Windows":
|
|
81
|
+
raise RuntimeError("This module is only compatible with Windows.")
|
|
82
|
+
elif self.failed:
|
|
83
|
+
raise RuntimeError("There was an error while getting access to memory. Please try again later.")
|
|
84
|
+
|
|
85
|
+
from .utils.rbx.instance import DataModel
|
|
86
|
+
return DataModel(self.memory_module)
|
|
87
|
+
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import struct
|
|
3
|
+
from ctypes import wintypes
|
|
4
|
+
|
|
5
|
+
ntdll = ctypes.WinDLL('ntdll.dll')
|
|
6
|
+
psapi = ctypes.WinDLL('psapi.dll')
|
|
7
|
+
kernel32 = ctypes.WinDLL('kernel32.dll')
|
|
8
|
+
|
|
9
|
+
kernel32.VirtualAlloc.restype = wintypes.LPVOID
|
|
10
|
+
kernel32.VirtualAlloc.argtypes = [wintypes.LPVOID, ctypes.c_size_t, wintypes.DWORD, wintypes.DWORD]
|
|
11
|
+
kernel32.GetProcAddress.restype = wintypes.LPVOID
|
|
12
|
+
kernel32.GetProcAddress.argtypes = [wintypes.HMODULE, wintypes.LPCSTR]
|
|
13
|
+
|
|
14
|
+
NTSTATUS = wintypes.LONG
|
|
15
|
+
HANDLE = wintypes.HANDLE
|
|
16
|
+
DWORD = wintypes.DWORD
|
|
17
|
+
LPVOID = wintypes.LPVOID
|
|
18
|
+
HMODULE = wintypes.HMODULE
|
|
19
|
+
BOOL = wintypes.BOOL
|
|
20
|
+
|
|
21
|
+
PROCESS_QUERY_INFORMATION = 0x0400
|
|
22
|
+
PROCESS_VM_READ = 0x0010
|
|
23
|
+
LIST_MODULES_ALL = 0x03
|
|
24
|
+
STATUS_SUCCESS = 0
|
|
25
|
+
MEM_COMMIT = 0x1000
|
|
26
|
+
MEM_RESERVE = 0x2000
|
|
27
|
+
PAGE_EXECUTE_READWRITE = 0x40
|
|
28
|
+
NTDLL_HANDLE = ntdll._handle
|
|
29
|
+
|
|
30
|
+
class CLIENT_ID(ctypes.Structure):
|
|
31
|
+
_fields_ = [
|
|
32
|
+
("UniqueProcess", HANDLE),
|
|
33
|
+
("UniqueThread", HANDLE),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
class OBJECT_ATTRIBUTES(ctypes.Structure):
|
|
37
|
+
_fields_ = [
|
|
38
|
+
("Length", wintypes.ULONG),
|
|
39
|
+
("RootDirectory", HANDLE),
|
|
40
|
+
("ObjectName", LPVOID),
|
|
41
|
+
("Attributes", wintypes.ULONG),
|
|
42
|
+
("SecurityDescriptor", LPVOID),
|
|
43
|
+
("SecurityQualityOfService", LPVOID),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def get_syscall_number(function_name: str) -> int | None:
|
|
47
|
+
func_address = kernel32.GetProcAddress(NTDLL_HANDLE, function_name.encode('ascii'))
|
|
48
|
+
if not func_address:
|
|
49
|
+
return None
|
|
50
|
+
buffer = (ctypes.c_ubyte * 8).from_address(func_address)
|
|
51
|
+
if tuple(buffer[0:4]) == (0x4c, 0x8b, 0xd1, 0xb8):
|
|
52
|
+
return int.from_bytes(bytes(buffer[4:8]), 'little')
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def create_syscall_function(syscall_number, func_prototype):
|
|
56
|
+
assembly_stub = b'\x4C\x8B\xD1' + \
|
|
57
|
+
b'\xB8' + syscall_number.to_bytes(4, 'little') + \
|
|
58
|
+
b'\x0F\x05' + \
|
|
59
|
+
b'\xC3'
|
|
60
|
+
|
|
61
|
+
exec_mem = kernel32.VirtualAlloc(None, len(assembly_stub), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)
|
|
62
|
+
if not exec_mem:
|
|
63
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
64
|
+
ctypes.memmove(exec_mem, assembly_stub, len(assembly_stub))
|
|
65
|
+
return func_prototype(exec_mem)
|
|
66
|
+
|
|
67
|
+
NtOpenProcessProto = ctypes.WINFUNCTYPE(
|
|
68
|
+
NTSTATUS,
|
|
69
|
+
ctypes.POINTER(HANDLE),
|
|
70
|
+
DWORD,
|
|
71
|
+
ctypes.POINTER(OBJECT_ATTRIBUTES),
|
|
72
|
+
ctypes.POINTER(CLIENT_ID)
|
|
73
|
+
)
|
|
74
|
+
NtReadVirtualMemoryProto = ctypes.WINFUNCTYPE(
|
|
75
|
+
NTSTATUS,
|
|
76
|
+
HANDLE,
|
|
77
|
+
LPVOID,
|
|
78
|
+
LPVOID,
|
|
79
|
+
ctypes.c_ulong,
|
|
80
|
+
ctypes.POINTER(ctypes.c_ulong)
|
|
81
|
+
)
|
|
82
|
+
NtCloseProto = ctypes.WINFUNCTYPE(NTSTATUS, HANDLE)
|
|
83
|
+
|
|
84
|
+
syscall_id_open = get_syscall_number("NtOpenProcess")
|
|
85
|
+
syscall_id_read = get_syscall_number("NtReadVirtualMemory")
|
|
86
|
+
syscall_id_close = get_syscall_number("NtClose")
|
|
87
|
+
|
|
88
|
+
if not all([syscall_id_open, syscall_id_read, syscall_id_close]):
|
|
89
|
+
raise RuntimeError("Could not find required syscall numbers.")
|
|
90
|
+
|
|
91
|
+
nt_open_process_syscall = create_syscall_function(syscall_id_open, NtOpenProcessProto)
|
|
92
|
+
nt_read_virtual_memory_syscall = create_syscall_function(syscall_id_read, NtReadVirtualMemoryProto)
|
|
93
|
+
nt_close_syscall = create_syscall_function(syscall_id_close, NtCloseProto)
|
|
94
|
+
|
|
95
|
+
psapi.EnumProcessModulesEx.argtypes = [
|
|
96
|
+
HANDLE,
|
|
97
|
+
ctypes.POINTER(HMODULE),
|
|
98
|
+
DWORD,
|
|
99
|
+
ctypes.POINTER(DWORD),
|
|
100
|
+
DWORD
|
|
101
|
+
]
|
|
102
|
+
psapi.EnumProcessModulesEx.restype = BOOL
|
|
103
|
+
|
|
104
|
+
def _get_module_base(process_handle: HANDLE) -> int:
|
|
105
|
+
try:
|
|
106
|
+
modules_arr_size = 256
|
|
107
|
+
modules_arr = (HMODULE * modules_arr_size)()
|
|
108
|
+
needed = DWORD(0)
|
|
109
|
+
psapi.EnumProcessModulesEx(
|
|
110
|
+
process_handle,
|
|
111
|
+
modules_arr,
|
|
112
|
+
ctypes.sizeof(modules_arr),
|
|
113
|
+
ctypes.byref(needed),
|
|
114
|
+
LIST_MODULES_ALL
|
|
115
|
+
)
|
|
116
|
+
if needed.value > ctypes.sizeof(modules_arr):
|
|
117
|
+
new_size = needed.value // ctypes.sizeof(HMODULE)
|
|
118
|
+
modules_arr = (HMODULE * new_size)()
|
|
119
|
+
success = psapi.EnumProcessModulesEx(
|
|
120
|
+
process_handle,
|
|
121
|
+
modules_arr,
|
|
122
|
+
ctypes.sizeof(modules_arr),
|
|
123
|
+
ctypes.byref(needed),
|
|
124
|
+
LIST_MODULES_ALL
|
|
125
|
+
)
|
|
126
|
+
if not success:
|
|
127
|
+
return 0
|
|
128
|
+
if needed.value > 0:
|
|
129
|
+
return modules_arr[0] if modules_arr[0] else 0
|
|
130
|
+
return 0
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"An exception occurred in _get_module_base: {e}")
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
def get_pid_by_name(process_name: str) -> int:
|
|
136
|
+
class PROCESSENTRY32(ctypes.Structure):
|
|
137
|
+
_fields_ = [
|
|
138
|
+
("dwSize", DWORD),
|
|
139
|
+
("cntUsage", DWORD),
|
|
140
|
+
("th32ProcessID", DWORD),
|
|
141
|
+
("th32DefaultHeapID", ctypes.POINTER(wintypes.ULONG)),
|
|
142
|
+
("th32ModuleID", DWORD),
|
|
143
|
+
("cntThreads", DWORD),
|
|
144
|
+
("th32ParentProcessID", DWORD),
|
|
145
|
+
("pcPriClassBase", wintypes.LONG),
|
|
146
|
+
("dwFlags", DWORD),
|
|
147
|
+
("szExeFile", wintypes.CHAR * 260)
|
|
148
|
+
]
|
|
149
|
+
snapshot = kernel32.CreateToolhelp32Snapshot(2, 0)
|
|
150
|
+
if not snapshot or snapshot == wintypes.HANDLE(-1).value:
|
|
151
|
+
raise ctypes.WinError()
|
|
152
|
+
entry = PROCESSENTRY32()
|
|
153
|
+
entry.dwSize = ctypes.sizeof(PROCESSENTRY32)
|
|
154
|
+
try:
|
|
155
|
+
if not kernel32.Process32First(snapshot, ctypes.byref(entry)):
|
|
156
|
+
return 0
|
|
157
|
+
while True:
|
|
158
|
+
if entry.szExeFile.decode('utf-8', errors='ignore') == process_name:
|
|
159
|
+
return entry.th32ProcessID
|
|
160
|
+
if not kernel32.Process32Next(snapshot, ctypes.byref(entry)):
|
|
161
|
+
break
|
|
162
|
+
finally:
|
|
163
|
+
kernel32.CloseHandle(snapshot)
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
class EvasiveProcess:
|
|
167
|
+
def __init__(self, pid: int, access: DWORD):
|
|
168
|
+
self.pid = pid
|
|
169
|
+
self.access = access
|
|
170
|
+
self.handle = HANDLE(0)
|
|
171
|
+
self.base = 0
|
|
172
|
+
|
|
173
|
+
object_attributes = OBJECT_ATTRIBUTES()
|
|
174
|
+
client_id = CLIENT_ID()
|
|
175
|
+
client_id.UniqueProcess = HANDLE(pid)
|
|
176
|
+
object_attributes.Length = ctypes.sizeof(OBJECT_ATTRIBUTES)
|
|
177
|
+
|
|
178
|
+
status = nt_open_process_syscall(
|
|
179
|
+
ctypes.byref(self.handle),
|
|
180
|
+
access,
|
|
181
|
+
ctypes.byref(object_attributes),
|
|
182
|
+
ctypes.byref(client_id)
|
|
183
|
+
)
|
|
184
|
+
if status != STATUS_SUCCESS:
|
|
185
|
+
raise ctypes.WinError(f"NtOpenProcess failed with NTSTATUS: 0x{status:X}")
|
|
186
|
+
self.base = _get_module_base(self.handle)
|
|
187
|
+
if self.base == 0:
|
|
188
|
+
self.close()
|
|
189
|
+
raise ConnectionError("Failed to get module base address.")
|
|
190
|
+
|
|
191
|
+
def read(self, address: int, size: int) -> bytes:
|
|
192
|
+
if not self.handle or self.handle.value == 0:
|
|
193
|
+
raise ValueError("Process handle is not valid.")
|
|
194
|
+
buffer = ctypes.create_string_buffer(size)
|
|
195
|
+
bytes_read = ctypes.c_ulong(0)
|
|
196
|
+
status = nt_read_virtual_memory_syscall(
|
|
197
|
+
self.handle,
|
|
198
|
+
LPVOID(address),
|
|
199
|
+
buffer,
|
|
200
|
+
size,
|
|
201
|
+
ctypes.byref(bytes_read)
|
|
202
|
+
)
|
|
203
|
+
if status != STATUS_SUCCESS:
|
|
204
|
+
raise OSError(f"NtReadVirtualMemory failed with NTSTATUS: 0x{status:X}")
|
|
205
|
+
return buffer.raw[:bytes_read.value]
|
|
206
|
+
|
|
207
|
+
# numbers #
|
|
208
|
+
def read_int(self, address: int) -> int:
|
|
209
|
+
buffer = self.read(address, 4)
|
|
210
|
+
return int.from_bytes(buffer, 'little') if len(buffer) == 4 else 0
|
|
211
|
+
|
|
212
|
+
def read_long(self, address: int) -> int:
|
|
213
|
+
buffer = self.read(address, 8)
|
|
214
|
+
return int.from_bytes(buffer, 'little') if len(buffer) == 8 else 0
|
|
215
|
+
|
|
216
|
+
def read_double(self, address: int) -> float:
|
|
217
|
+
try:
|
|
218
|
+
double_bytes = self.read(address, 8)
|
|
219
|
+
return struct.unpack('<d', double_bytes)[0] if len(double_bytes) == 8 else 0.0
|
|
220
|
+
except (OSError, struct.error):
|
|
221
|
+
return 0.0
|
|
222
|
+
|
|
223
|
+
def read_float(self, address: int) -> float:
|
|
224
|
+
try:
|
|
225
|
+
float_bytes = self.read(address, 4)
|
|
226
|
+
return struct.unpack('f', float_bytes)[0] if len(float_bytes) == 4 else 0.0
|
|
227
|
+
except (OSError, struct.error):
|
|
228
|
+
return 0.0
|
|
229
|
+
|
|
230
|
+
def read_floats(self, address: int, amount: int):
|
|
231
|
+
try:
|
|
232
|
+
bulk_float_bytes = self.read(address, 4 * amount)
|
|
233
|
+
floats = []
|
|
234
|
+
for i in range(amount):
|
|
235
|
+
start_range = i * 4
|
|
236
|
+
float_bytes = bulk_float_bytes[start_range:start_range + 4]
|
|
237
|
+
|
|
238
|
+
if len(float_bytes) == 4:
|
|
239
|
+
floats.append(struct.unpack('f', float_bytes)[0])
|
|
240
|
+
else:
|
|
241
|
+
floats.append(0.0)
|
|
242
|
+
|
|
243
|
+
return floats
|
|
244
|
+
except (OSError, struct.error) as e:
|
|
245
|
+
return [0.0]
|
|
246
|
+
|
|
247
|
+
# bool #
|
|
248
|
+
def read_bool(self, address: int) -> bool:
|
|
249
|
+
try:
|
|
250
|
+
bool_byte = self.read(address, 1)
|
|
251
|
+
if not bool_byte: return False
|
|
252
|
+
return bool(int.from_bytes(bool_byte, 'little'))
|
|
253
|
+
except OSError:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
# string #
|
|
257
|
+
def read_raw_string(self, address: int, max_length: int = 256) -> str:
|
|
258
|
+
buffer = self.read(address, max_length)
|
|
259
|
+
null_pos = buffer.find(b'\x00')
|
|
260
|
+
valid_bytes = buffer[:null_pos] if null_pos != -1 else buffer
|
|
261
|
+
return valid_bytes.decode('utf-8', errors='ignore')
|
|
262
|
+
|
|
263
|
+
def read_string(self, address: int) -> str:
|
|
264
|
+
string_length = self.read_int(address + 0x10)
|
|
265
|
+
if string_length <= 15:
|
|
266
|
+
return self.read_raw_string(address, string_length)
|
|
267
|
+
else:
|
|
268
|
+
string_data_pointer = self.read_long(address)
|
|
269
|
+
return self.read_raw_string(string_data_pointer, string_length) if string_data_pointer else ""
|
|
270
|
+
|
|
271
|
+
#########
|
|
272
|
+
def close(self):
|
|
273
|
+
if self.handle and self.handle.value != 0:
|
|
274
|
+
nt_close_syscall(self.handle)
|
|
275
|
+
self.handle = HANDLE(0)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
from importlib import resources
|
|
4
|
+
|
|
5
|
+
Offsets = {}
|
|
6
|
+
def ParseOffsets(*DataSources):
|
|
7
|
+
for _, Data in enumerate(DataSources, start=1):
|
|
8
|
+
for OffsetName in Data:
|
|
9
|
+
try:
|
|
10
|
+
FormattedOffsetName = OffsetName.replace(" ", "")
|
|
11
|
+
OffsetHexadecimalValue = Data[OffsetName]
|
|
12
|
+
|
|
13
|
+
Offsets[FormattedOffsetName] = int(OffsetHexadecimalValue, 16)
|
|
14
|
+
except (ValueError, TypeError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
OffsetsRequest = requests.get("https://offsets.ntgetwritewatch.workers.dev/offsets.json")
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
LoadedOffsetsRequest = requests.get(
|
|
22
|
+
"https://raw.githubusercontent.com/notpoiu/RobloxMemoryAPI/refs/heads/main/src/robloxmemoryapi/data/offsets.json"
|
|
23
|
+
)
|
|
24
|
+
LoadedOffsetsRequest.raise_for_status()
|
|
25
|
+
|
|
26
|
+
LoadedOffsets = LoadedOffsetsRequest.json()
|
|
27
|
+
except Exception:
|
|
28
|
+
try:
|
|
29
|
+
with resources.files("robloxmemoryapi.data").joinpath("offsets.json").open("r", encoding="utf-8") as f:
|
|
30
|
+
LoadedOffsets = json.load(f)
|
|
31
|
+
except Exception:
|
|
32
|
+
# Fallback defaults
|
|
33
|
+
LoadedOffsets = {
|
|
34
|
+
"Text": "0xC10",
|
|
35
|
+
"Character": "0x328",
|
|
36
|
+
"PrimaryPart": "0x268",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
ParseOffsets(LoadedOffsets, OffsetsRequest.json())
|
|
40
|
+
|
|
41
|
+
# CFrame Offsets
|
|
42
|
+
RotationMatriciesLengthBytes = 3 * 3 * 4
|
|
43
|
+
|
|
44
|
+
Offsets["CameraCFrame"] = Offsets["CameraPos"] - RotationMatriciesLengthBytes
|
|
45
|
+
Offsets["CFrame"] = Offsets["Position"] - RotationMatriciesLengthBytes
|
|
46
|
+
|
|
File without changes
|