| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
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, scanning byte signatures, querying and changing page protection — entirely from Java, without writing C++ or maintaining a JNI bridge.
It runs on both Windows and Linux behind the same Pointer / Memory API. The platform-specific layer is selected at runtime by a NativeAccess abstraction:
| Component | Version / Note |
|---|---|
| Java | 11 or higher (uses ProcessHandle, available since Java 9; project targets Java 11) |
| 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 throws PrivilegeException otherwise. |
| Runtime deps | net.java.dev.jna:jna:5.12.1, net.java.dev.jna:jna-platform:5.12.1 |
Mem4J is published through JitPack, which builds artifacts directly from this GitHub repository on demand.
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.christopherproject</groupId>
<artifactId>Mem4J</artifactId>
<version>1.0.2</version>
</dependency>
</dependencies>For Gradle:
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
implementation 'com.github.christopherproject:Mem4J:1.0.2'
}You can also pin to a branch (e.g. master-SNAPSHOT) or a specific commit hash — see the JitPack docs for details.
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
│ (Virtual{Protect,Alloc,Free,Query}Ex for memory protection)
└── LinuxAccess → /proc/<pid>/{maps,mem,comm,exe}, libc geteuid
(ptrace syscall injection for mprotect / mmap / munmap on x86_64)
Pointer, Memory, ProcessUtil.listModules, SignatureManager and SignatureUtil route every read, write, lookup, privilege check, AOB scan, protection query and allocation through this interface, so the same call sites work on both platforms. The remaining Windows-specific helpers (ProcessUtil.getModule, Shell32Util, and the legacy WinNT.HANDLE-based overloads of SignatureManager / SignatureUtil / Pointer's constructor) are kept as @Deprecated shims for existing Windows callers.
The same code works on Windows and Linux — only the process name differs (Windows wants the .exe, Linux wants whatever appears in /proc/<pid>/comm).
Windows (run as Administrator):
import it.adrian.code.Memory;
import it.adrian.code.memory.Pointer;
public class WindowsExample {
public static void main(String[] args) {
// try-with-resources releases the OS handle on exit.
try (Pointer base = Pointer.getBaseAddress("notepad.exe")) {
// 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);
// Write a new int back to the same location.
Memory.writeMemory(base, 0x1234L, 42, Integer.class);
}
}
}Linux (run as root, or grant the JVM CAP_SYS_PTRACE — see Linux notes below):
import it.adrian.code.Memory;
import it.adrian.code.memory.Pointer;
public class LinuxExample {
public static void main(String[] args) {
// try-with-resources closes /proc/<pid>/mem on exit.
try (Pointer base = Pointer.getBaseAddress("firefox")) {
// Read an int 0x1234 bytes past the main binary's base address.
int value = Memory.readMemory(base, 0x1234L, Integer.class);
System.out.println("Value at firefox+0x1234 = " + value);
// Write a new int back to the same location.
Memory.writeMemory(base, 0x1234L, 42, Integer.class);
}
}
}Privileges required. On Windows the library throws PrivilegeException without Administrator rights. On Linux it does the same unless euid == 0 or the JVM has CAP_SYS_PTRACE. The process/module lookup throws ProcessNotFoundException / ModuleNotFoundException. All of these extend Mem4JException (a RuntimeException) so a single catch is enough.
All the snippets below show only the body that goes inside
try (Pointer base = Pointer.getBaseAddress(/* "game.exe" on Windows, "game" on Linux */)) {
// …snippet here…
}so the OS handle is always released when you leave the block.
Pointer.getBaseAddress(processName) resolves the PID and the main module's base address. The mechanism is platform-specific:
If no process matches, ProcessNotFoundException is thrown. If the process is found but its main module is not visible (e.g. the JVM lacks permission to read its mappings), ModuleNotFoundException is thrown.
When several processes share the same executable name, attach by PID directly:
int pid = pickRightInstance(); // your own disambiguation logic
try (Pointer base = Pointer.getBaseAddress("game.exe", pid)) {
// …
}The single-argument overload (name only) is preserved for the common single-instance case and resolves the PID via the OS process list.
Memory.readMemory / Memory.writeMemory are the high-level entry points. They take a base Pointer, an offset in bytes, and the target type:
int hp = Memory.readMemory(base, 0x00ABCDEFL, Integer.class);
long xp = Memory.readMemory(base, 0x00ABCDF8L, Long.class);
float speed = Memory.readMemory(base, 0x00ABCE00L, Float.class);
double scale = Memory.readMemory(base, 0x00ABCE10L, Double.class);
Memory.writeMemory(base, 0x00ABCDEFL, 9999, Integer.class);
Memory.writeMemory(base, 0x00ABCDF8L, 100_000L, Long.class);
Memory.writeMemory(base, 0x00ABCE00L, 12.5f, Float.class);
Memory.writeMemory(base, 0x00ABCE10L, 0.75d, Double.class);Supported types: Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class. Any other type throws IllegalArgumentException. Failed reads throw MemoryAccessException (e.g. unmapped page, insufficient page protection).
The offset parameter is honoured for its full long range — earlier versions silently truncated it to 32 bits. Internally each call does baseAddr.copy().add(offset) so the supplied base is not mutated between calls.
Pointer p = base.copy().add(0x1000);
byte[] header = p.readBytes(64);
String name = p.copy().add(0x100).readString(32); // UTF-8, NUL-terminated
String wide = p.copy().add(0x100).readString(32, StandardCharsets.UTF_16LE);
p.copy().add(0x200).writeBytes(new byte[]{ 0x48, 0x65, 0x6C, 0x6C, 0x6F });
p.copy().add(0x200).writeString("Hello");By default a Pointer decodes little-endian. To target a big-endian process (e.g. ARM), call withByteOrder once:
int value = base.copy().add(0x1234).withByteOrder(ByteOrder.BIG_ENDIAN).readInt();Real-world targets often expose data through pointer chains like module+0x123456 → +0x10 → +0x20 → value. Pointer lets you express that path:
Pointer p = base.copy()
.add(0x123456) // module+0x123456
.indirect64() // dereference the 64-bit pointer
.add(0x10) // +0x10
.indirect64() // dereference again
.add(0x20); // +0x20
int hp = Memory.readMemory(p, 0L, Integer.class);| Method | Effect |
|---|---|
| copy() | Returns a new Pointer sharing handle, base, offset and byte order. Bumps the session reference count so close() on any sibling is safe. |
| add(long) | Adds bytes to the current offset and returns this (mutable, fluent). Accepts the full long range. |
| indirect64() | Reads a 64-bit pointer at the current address, replaces the base with that value, and resets the offset to 0. |
| indirect32() | Same as indirect64() but reads a zero-extended 32-bit pointer — use against 32-bit targets. |
| withByteOrder() | Switch this pointer's endianness for subsequent reads/writes. |
| force() | Returns a sibling whose writes bypass page protection (see below). |
| close() | Decrement the session refcount; the OS handle / fd is released when the last live Pointer is closed. Idempotent. |
| toString() | Pretty-prints as module[0xBASE]+0xOFFSET => 0xFINAL. |
When offsets shift between builds, byte signatures are more stable. SignatureManager scans the target module's address range for a pattern and returns the offset of the resolved address relative to the module base — cross-platform:
byte[] pattern = new byte[] {
(byte) 0x48, (byte) 0x8B, 0x00, 0x00, (byte) 0x05, 0x00, 0x00, 0x00, (byte) 0xC3
};
String mask = "xx??x???x";
SignatureManager sm = new SignatureManager(base);
long relativeOffset = sm.getPtrFromSignature(base.getBaseAddressValue(), pattern, mask);
int value = Memory.readMemory(base, relativeOffset, Integer.class);The mask uses 'x' for "must match exactly" and any other character (typically '?') for "wildcard". getPtrFromSignature interprets the matched site as a mov/lea-style RIP-relative instruction: it reads the 4-byte displacement at match+3, then computes match + displacement + 7, returning the final address as an offset relative to the module base. Unlike older releases, SignatureManager no longer closes the underlying handle — the caller owns the lifecycle through the Pointer.
By default WriteProcessMemory (Windows) and /proc/<pid>/mem (Linux) handle most pages transparently, but writing into a PAGE_EXECUTE_READ section on Windows usually fails. Use Pointer.force() to bypass that:
byte[] nopSled = { (byte)0x90, (byte)0x90, (byte)0x90, (byte)0x90, (byte)0x90 };
base.copy().add(0x1234).force().writeBytes(nopSled);
// Windows: pages are temporarily flipped to PAGE_EXECUTE_READWRITE, then restored.
// Linux: /proc/<pid>/mem already ignores page protection for CAP_SYS_PTRACE callers,
// so force() is a no-op.NativeAccess na = NativeAccess.get();
// Make 4 KiB at base+0x1000 writable+executable for a hook.
base.copy().add(0x1000).protect(0x1000, MemoryProtection.READ_WRITE_EXECUTE);
// Query the current protection of any address.
MemoryProtection prot = na.queryProtection(base.getSession(), base.getBaseAddressValue() + 0x2000);
// Allocate a remote 4 KiB block for a code cave.
long cave = na.allocate(base.getSession(), 0x1000, MemoryProtection.READ_WRITE_EXECUTE);
na.writeMemory(base.getSession(), cave, shellcode, shellcode.length);
// …
na.free(base.getSession(), cave, 0);Implementation:
⚠️ ptrace injection requires CAP_SYS_PTRACE (or root) and the same Yama ptrace_scope constraints already documented in Platform notes — Linux.
Pointer is reference-counted. Every copy() retains a new reference on the underlying ProcessSession; every close() releases one. The OS handle (Windows) or /proc/<pid>/mem file descriptor (Linux) is only torn down when the last live Pointer is closed, so it is safe to:
The recommended multi-threaded pattern is one root Pointer per attach, with each worker thread taking a private copy() to drive its own offset / force() / byte-order state without interfering with the others:
try (Pointer root = Pointer.getBaseAddress("game.exe")) {
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int slot = 0; slot < 4; slot++) {
final int s = slot;
pool.submit(() -> {
try (Pointer view = root.copy()) {
int hp = Memory.readMemory(view, slot(s).hpOffset(), Integer.class);
// ...
}
});
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.MINUTES);
}The mutating fluent methods (add, indirect64, indirect32, withByteOrder, force) all operate on a single Pointer instance — don't share that instance across threads, give each thread its own copy.
| Class / method | Platform | Purpose |
|---|---|---|
| NativeAccess.get() | both | Returns the platform-specific backend (WindowsAccess or LinuxAccess). |
| Pointer.getBaseAddress(String) | both | Attach by executable name; first match wins (ProcessNotFoundException on miss). |
| Pointer.getBaseAddress(String, int pid) | both | Attach by name + explicit PID; skips process-list lookup. Use it when multiple processes share the same executable name. |
| 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.listModules(int pid) | both | Every loaded module / mapped binary as List<ModuleInfo>. |
| NativeAccess.queryProtection(session, addr) | both | Current page protection at the address; reads /proc/<pid>/maps on Linux. |
| NativeAccess.protect / allocate / free | both | VirtualProtectEx / VirtualAllocEx / VirtualFreeEx on Windows; mprotect(2) / mmap(2) / munmap(2) injected via ptrace on Linux x86_64. |
| NativeAccess.isPrivileged() | both | Admin on Windows, euid == 0 on Linux. |
| Pointer.force() | both | Returns a sibling pointer whose writes flip protection around them on Windows; no-op on Linux (/proc/<pid>/mem already bypasses protection). |
| ProcessUtil.getProcessPidByName(String) | both | Thin wrapper around NativeAccess.findPidByName. |
| ProcessUtil.listModules(int pid) | both | Thin wrapper around NativeAccess.listModules. |
| ProcessUtil.getModule(int pid, String name) | Windows | Deprecated. Returns the raw MODULEENTRY32W. Throws on Linux. |
| Shell32Util.isUserWindowsAdmin() | Windows | Returns true if the current process has Administrator rights; false on Linux. |
Run as root, or grant the JVM CAP_SYS_PTRACE:
sudo setcap cap_sys_ptrace+ep "$(realpath "$(which java)")"Otherwise /proc/<pid>/mem cannot be opened for processes you don't own and you get a PrivilegeException.
Many distros set kernel.yama.ptrace_scope = 1. To attach to a non-child process, either run as root or temporarily lower it:
sudo sysctl kernel.yama.ptrace_scope=0The process name is matched against /proc/<pid>/comm (truncated to 15 chars) first, then against the basename of /proc/<pid>/exe. If two processes share the same name, the first match wins.
A self-contained recipe — reading the ELF magic of any running java process (a sanity check that the Linux backend actually works on your box):
try (Pointer base = Pointer.getBaseAddress("java")) {
byte[] magic = base.readBytes(4); // → 7F 45 4C 46 ("\x7FELF")
for (ModuleInfo m : ProcessUtil.listModules(base.getSession().pid)) {
System.out.printf("0x%016x %s%n", m.baseAddress(), m.path());
}
}Following a 4-level pointer chain inside a 64-bit Linux target (Cheat-Engine style):
try (Pointer base = Pointer.getBaseAddress("Hollow_Knight.x86_64")) {
Pointer hp = base.copy()
.add(0x01F2C720)
.indirect64().add(0xB0)
.indirect64().add(0x28)
.indirect64().add(0x1C);
System.out.println("HP = " + hp.readInt());
}Memory
static <T> T readMemory(Pointer base, long offset, Class<T> type)
static <T> void writeMemory(Pointer base, long offset, T value, Class<T> type)
// T ∈ { Byte, Short, Integer, Long, Float, Double }
Pointer (implements AutoCloseable)
static Pointer getBaseAddress(String processName) // PID resolved automatically
static Pointer getBaseAddress(String processName, int pid) // disambiguate by PID
Pointer copy() // sibling, shares the OS handle
Pointer add(long bytes) // fluent, mutates this
Pointer indirect64() // deref 64-bit pointer
Pointer indirect32() // deref 32-bit pointer (zero-extended)
Pointer withByteOrder(ByteOrder order)
Pointer force() // bypass page protection on writes
byte / short / int / long / float / double read*()
boolean write*(value)
byte[] readBytes(int len) boolean writeBytes(byte[])
String readString(int max [, Charset]) boolean writeString(String [, Charset])
com.sun.jna.Memory getMemory(int size) // raw JNA buffer copy
boolean protect(long size, MemoryProtection) // delegates to NativeAccess
ProcessSession getSession()
long getBaseAddressValue()
long getOffset()
void close() // releases the session
NativeAccess
static NativeAccess get() // lazy, picks one backend
int findPidByName(String)
long getModuleBaseAddress(int pid, String name)
long getModuleSize(int pid, String name)
List<ModuleInfo> listModules(int pid)
ProcessSession openProcess(int pid)
boolean readMemory / writeMemory(session, address, byte[], length)
MemoryProtection queryProtection(session, address) // /proc/<pid>/maps on Linux
boolean protect(session, address, size, MemoryProtection) // ptrace inject on Linux x86_64
long allocate(session, size, MemoryProtection) // ptrace inject on Linux x86_64
boolean free(session, address, size) // ptrace inject on Linux x86_64
void closeSession(ProcessSession)
boolean isPrivileged()
void ensurePrivileged() // throws PrivilegeException
void throwProcessNotFound(String name) // helper for backends
SignatureManager(Pointer) // cross-platform
SignatureManager(ProcessSession, String moduleName) // cross-platform
SignatureManager(WinNT.HANDLE, String, int) // @Deprecated, Windows shim
long getPtrFromSignature(long moduleBaseAddress, byte[] sig, String mask)
SignatureUtil
static long findSignature(ProcessSession session, long start, long size, byte[] sig, String mask)
static int readInt(ProcessSession session, long address)
// @Deprecated WinNT.HANDLE-based overloads kept for Windows callers
ProcessUtil
static int getProcessPidByName(String name)
static List<ModuleInfo> listModules(int pid)
static MODULEENTRY32W getModule(int pid, String name) // @Deprecated, Windows-only
Shell32Util
static boolean isUserWindowsAdmin() // false on Linux
Exceptions (it.adrian.code.exceptions)
Mem4JException // root, extends RuntimeException
├── PrivilegeException
├── ProcessNotFoundException
├── ModuleNotFoundException
└── MemoryAccessException
The read/write primitives map to fixed-width writes/reads in the target process, following the Java Language Specification §4.2.1:
| Java type | Bytes written/read |
|---|---|
| byte | 1 |
| short | 2 |
| int | 4 |
| long | 8 |
| float | 4 |
| double | 8 |
git clone https://github.com/ChristopherProject/Mem4J.git
cd Mem4J
mvn -B packageArtifacts land in target/: the runtime jar, a sources jar and a Javadoc jar (the last two so IDEs of downstream consumers can show docs and step into Mem4J sources). CI runs the same mvn -B package on both ubuntu-latest and windows-latest for every push and pull request targeting master (see .github/workflows/maven.yml).
Mem4J ships a JUnit 5 integration test suite under src/test/java/it/adrian/code/Mem4JTests.java. Tests exercise the active backend against the running JVM (and a short-lived sleep child for the ptrace injection round-trip) — there is no mock layer, every assertion is end-to-end against real kernel memory.
mvn -B testThe suite is privilege-aware:
Counted today: 11 tests, 1 skipped (the ptrace round-trip). All other Linux integration tests pass on a privileged JVM.
For local development on Linux:
sudo mvn -B test
# or, without sudo, after granting CAP_SYS_PTRACE to the JVM once
sudo setcap cap_sys_ptrace+ep "$(realpath "$(which java)")"
mvn -B testCI runs mvn -B test on both ubuntu-latest and windows-latest, so the matrix exercises whichever backend matches the runner.
| Back | FazBrowse Home | New Git URL |