Skip to content

Commit e0c676b

Browse files
Merge pull request #3 from ChristopherProject/nextgen
Add Linux backend behind a NativeAccess abstraction
2 parents 5abdf49 + 560e4da commit e0c676b

10 files changed

Lines changed: 606 additions & 140 deletions

File tree

README.md

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# Mem4J — Memory Manipulation Library for Java
22

3-
Mem4J is a Java library that exposes Windows process memory primitives through [JNA](https://github.com/java-native-access/jna). It lets you attach to a running process, resolve module base addresses, follow pointer chains, read and write typed values, and locate addresses by byte signatures — entirely from Java, without writing C++ or maintaining a JNI bridge.
3+
Mem4J is a Java library that exposes process memory primitives — attaching to a running process, resolving module base addresses, following pointer chains, reading and writing typed values, and locating addresses by byte signatures — entirely from Java, without writing C++ or maintaining a JNI bridge.
44

5-
The library wraps the Win32 APIs `OpenProcess`, `ReadProcessMemory`, `WriteProcessMemory`, `CreateToolhelp32Snapshot`, `Module32First/NextW`, and `Process32NextW` behind a small, opinionated API centered on a `Pointer` abstraction.
5+
It runs on **both Windows and Linux** behind the same `Pointer` / `Memory` API. The platform-specific layer is selected at runtime via a `NativeAccess` abstraction:
6+
7+
- On **Windows** it wraps the Win32 APIs `OpenProcess`, `ReadProcessMemory`, `WriteProcessMemory`, `CreateToolhelp32Snapshot`, `Module32First/NextW`, and `Process32NextW` through [JNA](https://github.com/java-native-access/jna).
8+
- On **Linux** it uses `/proc/<pid>/maps` for module discovery and `/proc/<pid>/mem` for memory I/O. Process lookup is performed via `/proc/<pid>/comm` and the `/proc/<pid>/exe` symlink.
69

710
---
811

@@ -22,9 +25,9 @@ The library wraps the Win32 APIs `OpenProcess`, `ReadProcessMemory`, `WriteProce
2225
| Component | Version / Note |
2326
|-------------------|---------------------------------------------------------------|
2427
| Java | **11 or higher** (uses `ProcessHandle`, available since Java 9; project targets Java 11) |
25-
| Operating system | **Windows only** (uses `kernel32.dll`, `user32.dll`, `shell32.dll`) |
26-
| Architecture | The JVM bitness **must match** the target process. A 32-bit JVM cannot read/write a 64-bit process and vice versa`ReadProcessMemory`/`WriteProcessMemory` will fail. Use a 64-bit JDK against 64-bit targets. |
27-
| Privileges | **Administrator** (the library aborts otherwise via `Shell32.IsUserAnAdmin`) |
28+
| Operating system | **Windows** (`kernel32.dll`, `user32.dll`, `shell32.dll`) **or Linux** (`/proc/<pid>/{maps,mem,comm,exe}` + `libc` for `geteuid`) |
29+
| Architecture | The JVM bitness **must match** the target process. A 32-bit JVM cannot read/write a 64-bit process and vice versa. Use a 64-bit JDK against 64-bit targets. |
30+
| Privileges | **Windows:** Administrator (checked via `Shell32.IsUserAnAdmin`). **Linux:** `euid == 0` (root) or the JVM granted `CAP_SYS_PTRACE`. The library aborts otherwise. |
2831
| Runtime deps | `net.java.dev.jna:jna:5.12.1`, `net.java.dev.jna:jna-platform:5.12.1` |
2932

3033
---
@@ -72,6 +75,20 @@ You can also pin to a branch (e.g. `master-SNAPSHOT`) or a specific commit hash
7275

7376
---
7477

78+
## Architecture
79+
80+
Platform dispatch is centralised in `it.adrian.code.platform.NativeAccess`. The first call to `NativeAccess.get()` inspects `com.sun.jna.Platform` and reflectively loads exactly one backend, so the unused backend's classes (and its native libraries) are never initialised:
81+
82+
```
83+
NativeAccess (abstract)
84+
├── WindowsAccess → kernel32 / user32 / shell32 via JNA
85+
└── LinuxAccess → /proc/<pid>/maps, /proc/<pid>/mem, libc geteuid
86+
```
87+
88+
`Pointer` and `Memory` route all reads, writes, process lookup, and privilege checks through this interface, so the same call sites work on both platforms. The Windows-specific `ProcessUtil.getModule`, `Shell32Util`, `SignatureManager` and `SignatureUtil` remain available unchanged for existing Windows callers.
89+
90+
---
91+
7592
## Quick start
7693

7794
```java
@@ -81,33 +98,39 @@ import it.adrian.code.memory.Pointer;
8198
public class Example {
8299
public static void main(String[] args) {
83100
// 1. Attach to the target process by executable name.
101+
// Windows: "notepad.exe"; Linux: the binary name as in /proc/<pid>/comm (e.g. "firefox").
84102
Pointer base = Pointer.getBaseAddress("notepad.exe");
85103

86104
// 2. Read an int 0x1234 bytes past the module base.
87105
int value = Memory.readMemory(base, 0x1234L, Integer.class);
88-
System.out.println("Value at notepad.exe+0x1234 = " + value);
106+
System.out.println("Value at +0x1234 = " + value);
89107

90108
// 3. Write a new int back to the same location.
91109
Memory.writeMemory(base, 0x1234L, 42, Integer.class);
92110
}
93111
}
94112
```
95113

96-
> **Run this with Administrator privileges.** Without them the library shows a `MessageBox` and calls `System.exit(-1)`.
114+
> **Privileges required.** On Windows the library aborts via `MessageBox` and `System.exit(-1)` without Administrator rights. On Linux it prints to stderr and exits unless `euid == 0` or the JVM has `CAP_SYS_PTRACE`.
97115
98116
---
99117

100118
## Usage
101119

102120
### Attaching to a process
103121

104-
`Pointer.getBaseAddress(processName)` opens a handle with `PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION` (`0x0010 | 0x0020 | 0x0008`) and resolves the base address of the main module that matches `processName`:
122+
`Pointer.getBaseAddress(processName)` resolves the PID and the main module's base address for the named target. The mechanism is platform-specific:
123+
124+
- **Windows:** opens a handle via `OpenProcess` with `PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION` (`0x0010 | 0x0020 | 0x0008`) and locates the module through `CreateToolhelp32Snapshot` + `Module32First/NextW`. Match is against `MODULEENTRY32W.szModule` (e.g. `"game.exe"`).
125+
- **Linux:** scans `/proc/*/comm` and the `/proc/*/exe` symlink basename to find the PID, then opens `/proc/<pid>/mem` for r/w. The module base is the lowest start address in `/proc/<pid>/maps` whose pathname basename equals the given name (or whose full path matches it).
105126

106127
```java
107-
Pointer base = Pointer.getBaseAddress("game.exe");
128+
Pointer base = Pointer.getBaseAddress("game.exe"); // Windows
129+
// or
130+
Pointer base = Pointer.getBaseAddress("game"); // Linux binary name
108131
```
109132

110-
If the process cannot be found the library opens a `MessageBox` and exits. The returned `Pointer` carries an internal `offset` initialised to `0`.
133+
If the process cannot be found the library aborts (MessageBox on Windows, stderr on Linux) and calls `System.exit(-1)`. The returned `Pointer` carries an internal `offset` initialised to `0`.
111134

112135
### Reading and writing typed values
113136

@@ -155,6 +178,8 @@ int hp = Memory.readMemory(p, 0L, Integer.class);
155178

156179
### Signature (AOB) scanning
157180

181+
> ⚠️ **Windows-only.** `SignatureManager` and `SignatureUtil` are coupled to `WinNT.HANDLE`/`Kernel32.ReadProcessMemory`. The cross-platform `Pointer`/`Memory` APIs above work on Linux; AOB scanning currently does not.
182+
158183
When offsets shift between builds, byte signatures are more stable. `SignatureManager` scans the target module's address range for a pattern and returns the relative offset of the matched address:
159184

160185
```java
@@ -184,12 +209,16 @@ The mask uses `'x'` for "must match exactly" and any other character (typically
184209
185210
### Utilities
186211

187-
| Class / method | Purpose |
188-
|-------------------------------------------------|-------------------------------------------------------------------------|
189-
| `ProcessUtil.getProcessPidByName(String)` | Returns the PID of the first process whose `szExeFile` equals the name. |
190-
| `ProcessUtil.getModule(int pid, String name)` | Returns the `MODULEENTRY32W` for the named module (case-insensitive). |
191-
| `Shell32Util.isUserWindowsAdmin()` | Returns `true` if the current process has Administrator rights. |
192-
| `Pointer.getModuleBaseAddress(int pid, String)` | Static helper used internally; resolves a module base via Tool Help. |
212+
| Class / method | Platform | Purpose |
213+
|-------------------------------------------------|----------|-------------------------------------------------------------------------|
214+
| `NativeAccess.get()` | both | Returns the platform-specific backend (`WindowsAccess` or `LinuxAccess`). |
215+
| `NativeAccess.findPidByName(String)` | both | First PID whose executable name matches. |
216+
| `NativeAccess.getModuleBaseAddress(pid, name)` | both | Base address of a loaded module / mapped binary. |
217+
| `NativeAccess.getModuleSize(pid, name)` | both | Mapped size of the module (max end − min start across mappings on Linux). |
218+
| `NativeAccess.isPrivileged()` | both | Admin on Windows, `euid == 0` on Linux. |
219+
| `ProcessUtil.getProcessPidByName(String)` | both | Thin wrapper around `NativeAccess.findPidByName`. |
220+
| `ProcessUtil.getModule(int pid, String name)` | Windows | Returns the `MODULEENTRY32W` for the named module (case-insensitive). Throws on Linux. |
221+
| `Shell32Util.isUserWindowsAdmin()` | Windows | Returns `true` if the current process has Administrator rights; `false` on Linux. |
193222

194223
---
195224

@@ -243,11 +272,12 @@ The read/write primitives map to fixed-width writes/reads in the target process,
243272

244273
## Limitations & caveats
245274

246-
- **Windows-only.** The library directly imports `kernel32`/`user32`/`shell32`. There is no Linux/macOS fallback.
275+
- **macOS is not supported.** Only Windows and Linux backends ship. The factory throws `UnsupportedOperationException` on other platforms.
247276
- **Bitness must match.** A 32-bit JVM cannot operate on a 64-bit target (or vice versa). Use the appropriate JDK distribution.
248-
- **No anti-cheat / kernel bypass.** Memory access is performed through the standard documented Win32 API. Targets protected by anti-tamper drivers or Protected Process Light (PPL) will reject `OpenProcess` with `ERROR_ACCESS_DENIED`.
249-
- **Process attachment is by executable name only.** If two processes share the same `szExeFile`, the first match wins.
277+
- **No anti-cheat / kernel bypass.** Memory access goes through documented OS APIs. On Windows, targets protected by anti-tamper drivers or Protected Process Light (PPL) reject `OpenProcess` with `ERROR_ACCESS_DENIED`. On Linux, processes marked non-dumpable or owned by another user with no `CAP_SYS_PTRACE` cannot be opened.
278+
- **Process attachment is by executable name only.** If two processes share the same name, the first match wins.
250279
- **`indirect64()` assumes a 64-bit pointer.** There is no `indirect32()` variant; on 32-bit targets you would need to extend the API.
280+
- **AOB scanning is Windows-only.** `SignatureManager` / `SignatureUtil` use `WinNT.HANDLE` directly. A cross-platform implementation on top of `NativeAccess` is on the roadmap.
251281
- **The library calls `System.exit(-1)`** on missing privileges or missing process. This is intentional for the typical "trainer" use case but may be inconvenient when embedding Mem4J inside a larger application.
252282

253283
---
Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
package it.adrian.code;
22

3-
import it.adrian.code.interfaces.User32;
43
import it.adrian.code.memory.Pointer;
5-
import it.adrian.code.utilities.Shell32Util;
4+
import it.adrian.code.platform.NativeAccess;
65

76
public class Memory {
87

98
/**
10-
* Legge un valore di tipo specificato dalla memoria del processo remoto all'indirizzo ottenuto sommando l'offset specificato all'indirizzo base.
9+
* Reads a value of the specified type from the remote process at
10+
* {@code baseAddr + offset}.
1111
*
12-
* @param baseAddr l'indirizzo base a cui aggiungere l'offset per ottenere l'indirizzo finale di lettura.
13-
* @param offset l'offset da sommare all'indirizzo base per ottenere l'indirizzo finale di lettura.
14-
* @param type il tipo di dato da leggere (Integer, Long o Float).
15-
* @return il valore letto dalla memoria del processo remoto di tipo specificato.
16-
* @throws IllegalArgumentException se il tipo di dato specificato non è supportato.
12+
* @param baseAddr the base pointer obtained via {@link Pointer#getBaseAddress(String)}.
13+
* @param offset byte offset from the base address.
14+
* @param type {@code Integer.class}, {@code Long.class}, {@code Float.class} or {@code Double.class}.
15+
* @return the value read from the remote process.
16+
* @throws IllegalArgumentException if the type is unsupported.
1717
*/
1818
public static <T> T readMemory(Pointer baseAddr, long offset, Class<T> type) {
19-
if (!Shell32Util.isUserWindowsAdmin()) {
20-
User32.INSTANCE.MessageBox(null, "THIS REQUIRE ADMINISTRATION PERMISSIONS", "Warining!?!", User32.MB_OK | User32.MB_ICONWARNING);
21-
System.exit(-1);
19+
NativeAccess na = NativeAccess.get();
20+
if (!na.isPrivileged()) {
21+
na.abortMissingPrivileges();
2222
}
2323
int offsetAsInt = (int) offset;
2424
Pointer finalPtr = baseAddr.copy().add(offsetAsInt);
@@ -27,29 +27,29 @@ public static <T> T readMemory(Pointer baseAddr, long offset, Class<T> type) {
2727
return type.cast(finalPtr.readInt());
2828
} else if (type == Long.class) {
2929
return type.cast(finalPtr.readLong());
30-
}else if (type == Double.class) {
30+
} else if (type == Double.class) {
3131
return type.cast(finalPtr.readDouble());
32-
}
33-
else if (type == Float.class) {
32+
} else if (type == Float.class) {
3433
return type.cast(finalPtr.readFloat());
3534
} else {
3635
throw new IllegalArgumentException("Unsupported data type");
3736
}
3837
}
3938

4039
/**
41-
* Scrive un valore di tipo specificato nella memoria del processo remoto all'indirizzo ottenuto sommando l'offset specificato all'indirizzo base.
40+
* Writes a value of the specified type to the remote process at
41+
* {@code baseAddr + offset}.
4242
*
43-
* @param baseAddr l'indirizzo base a cui aggiungere l'offset per ottenere l'indirizzo finale di scrittura.
44-
* @param offset l'offset da sommare all'indirizzo base per ottenere l'indirizzo finale di scrittura.
45-
* @param value il valore da scrivere nella memoria del processo remoto.
46-
* @param type il tipo di dato del valore da scrivere (Integer, Long, Float o Double).
47-
* @throws IllegalArgumentException se il tipo di dato specificato non è supportato.
43+
* @param baseAddr the base pointer obtained via {@link Pointer#getBaseAddress(String)}.
44+
* @param offset byte offset from the base address.
45+
* @param value value to write.
46+
* @param type {@code Integer.class}, {@code Long.class}, {@code Float.class} or {@code Double.class}.
47+
* @throws IllegalArgumentException if the type is unsupported.
4848
*/
4949
public static <T> void writeMemory(Pointer baseAddr, long offset, T value, Class<T> type) {
50-
if (!Shell32Util.isUserWindowsAdmin()) {
51-
User32.INSTANCE.MessageBox(null, "THIS REQUIRE ADMINISTRATION PERMISSIONS", "Warining!?!", User32.MB_OK | User32.MB_ICONWARNING);
52-
System.exit(-1);
50+
NativeAccess na = NativeAccess.get();
51+
if (!na.isPrivileged()) {
52+
na.abortMissingPrivileges();
5353
}
5454
int offsetAsInt = (int) offset;
5555
Pointer finalPtr = baseAddr.copy().add(offsetAsInt);
@@ -66,4 +66,4 @@ public static <T> void writeMemory(Pointer baseAddr, long offset, T value, Class
6666
throw new IllegalArgumentException("Unsupported data type");
6767
}
6868
}
69-
}
69+
}

0 commit comments

Comments
 (0)