Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 49 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Mem4J — Memory Manipulation Library for Java

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.
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.

The library wraps the Win32 APIs `OpenProcess`, `ReadProcessMemory`, `WriteProcessMemory`, `CreateToolhelp32Snapshot`, `Module32First/NextW`, and `Process32NextW` behind a small, opinionated API centered on a `Pointer` abstraction.
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:

- On **Windows** it wraps the Win32 APIs `OpenProcess`, `ReadProcessMemory`, `WriteProcessMemory`, `CreateToolhelp32Snapshot`, `Module32First/NextW`, and `Process32NextW` through [JNA](https://github.com/java-native-access/jna).
- 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.

---

Expand All @@ -22,9 +25,9 @@ The library wraps the Win32 APIs `OpenProcess`, `ReadProcessMemory`, `WriteProce
| Component | Version / Note |
|-------------------|---------------------------------------------------------------|
| Java | **11 or higher** (uses `ProcessHandle`, available since Java 9; project targets Java 11) |
| Operating system | **Windows only** (uses `kernel32.dll`, `user32.dll`, `shell32.dll`) |
| 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. |
| Privileges | **Administrator** (the library aborts otherwise via `Shell32.IsUserAnAdmin`) |
| Operating system | **Windows** (`kernel32.dll`, `user32.dll`, `shell32.dll`) **or Linux** (`/proc/<pid>/{maps,mem,comm,exe}` + `libc` for `geteuid`) |
| 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. |
| Privileges | **Windows:** Administrator (checked via `Shell32.IsUserAnAdmin`). **Linux:** `euid == 0` (root) or the JVM granted `CAP_SYS_PTRACE`. The library aborts otherwise. |
| Runtime deps | `net.java.dev.jna:jna:5.12.1`, `net.java.dev.jna:jna-platform:5.12.1` |

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

---

## Architecture

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:

```
NativeAccess (abstract)
├── WindowsAccess → kernel32 / user32 / shell32 via JNA
└── LinuxAccess → /proc/<pid>/maps, /proc/<pid>/mem, libc geteuid
```

`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.

---

## Quick start

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

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

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

> **Run this with Administrator privileges.** Without them the library shows a `MessageBox` and calls `System.exit(-1)`.
> **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`.

---

## Usage

### Attaching to a process

`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`:
`Pointer.getBaseAddress(processName)` resolves the PID and the main module's base address for the named target. The mechanism is platform-specific:

- **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"`).
- **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).

```java
Pointer base = Pointer.getBaseAddress("game.exe");
Pointer base = Pointer.getBaseAddress("game.exe"); // Windows
// or
Pointer base = Pointer.getBaseAddress("game"); // Linux binary name
```

If the process cannot be found the library opens a `MessageBox` and exits. The returned `Pointer` carries an internal `offset` initialised to `0`.
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`.

### Reading and writing typed values

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

### Signature (AOB) scanning

> ⚠️ **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.

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:

```java
Expand Down Expand Up @@ -184,12 +209,16 @@ The mask uses `'x'` for "must match exactly" and any other character (typically

### Utilities

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

---

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

## Limitations & caveats

- **Windows-only.** The library directly imports `kernel32`/`user32`/`shell32`. There is no Linux/macOS fallback.
- **macOS is not supported.** Only Windows and Linux backends ship. The factory throws `UnsupportedOperationException` on other platforms.
- **Bitness must match.** A 32-bit JVM cannot operate on a 64-bit target (or vice versa). Use the appropriate JDK distribution.
- **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`.
- **Process attachment is by executable name only.** If two processes share the same `szExeFile`, the first match wins.
- **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.
- **Process attachment is by executable name only.** If two processes share the same name, the first match wins.
- **`indirect64()` assumes a 64-bit pointer.** There is no `indirect32()` variant; on 32-bit targets you would need to extend the API.
- **AOB scanning is Windows-only.** `SignatureManager` / `SignatureUtil` use `WinNT.HANDLE` directly. A cross-platform implementation on top of `NativeAccess` is on the roadmap.
- **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.

---
Expand Down
48 changes: 24 additions & 24 deletions src/main/java/it/adrian/code/Memory.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
package it.adrian.code;

import it.adrian.code.interfaces.User32;
import it.adrian.code.memory.Pointer;
import it.adrian.code.utilities.Shell32Util;
import it.adrian.code.platform.NativeAccess;

public class Memory {

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

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