From 560e4da3aca4ee54f932896faf1ae805e08d6448 Mon Sep 17 00:00:00 2001 From: Chris <61153610+ChristopherProject@users.noreply.github.com> Date: Sat, 16 May 2026 02:05:19 +0000 Subject: [PATCH] Add Linux backend behind a NativeAccess abstraction Platform dispatch is centralised in it.adrian.code.platform.NativeAccess, which reflectively loads either WindowsAccess (kernel32/user32/shell32 via JNA) or LinuxAccess (/proc//{maps,mem,comm,exe} + libc geteuid) so the unused backend's native libraries are never initialised. Pointer, Memory and ProcessUtil.getProcessPidByName route through this interface and work on both platforms. The pre-existing Windows-only helpers (ProcessUtil.getModule, Shell32Util, SignatureManager, SignatureUtil) and the WinNT.HANDLE-based Pointer constructor are kept for backward compatibility. README updated with the dual-platform requirements, Linux examples, architecture overview and revised caveats. --- README.md | 68 ++++-- src/main/java/it/adrian/code/Memory.java | 48 ++-- .../java/it/adrian/code/memory/Pointer.java | 155 ++++++------- .../it/adrian/code/platform/NativeAccess.java | 59 +++++ .../adrian/code/platform/ProcessSession.java | 10 + .../code/platform/linux/LinuxAccess.java | 215 ++++++++++++++++++ .../platform/linux/LinuxProcessSession.java | 20 ++ .../code/platform/windows/WindowsAccess.java | 131 +++++++++++ .../windows/WindowsProcessSession.java | 14 ++ .../it/adrian/code/utilities/ProcessUtil.java | 26 +-- 10 files changed, 606 insertions(+), 140 deletions(-) create mode 100644 src/main/java/it/adrian/code/platform/NativeAccess.java create mode 100644 src/main/java/it/adrian/code/platform/ProcessSession.java create mode 100644 src/main/java/it/adrian/code/platform/linux/LinuxAccess.java create mode 100644 src/main/java/it/adrian/code/platform/linux/LinuxProcessSession.java create mode 100644 src/main/java/it/adrian/code/platform/windows/WindowsAccess.java create mode 100644 src/main/java/it/adrian/code/platform/windows/WindowsProcessSession.java diff --git a/README.md b/README.md index 2bde4a8..b76b00d 100644 --- a/README.md +++ b/README.md @@ -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//maps` for module discovery and `/proc//mem` for memory I/O. Process lookup is performed via `/proc//comm` and the `/proc//exe` symlink. --- @@ -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//{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` | --- @@ -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//maps, /proc//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 @@ -81,11 +98,12 @@ 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//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); @@ -93,7 +111,7 @@ public class Example { } ``` -> **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`. --- @@ -101,13 +119,18 @@ public class Example { ### 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//mem` for r/w. The module base is the lowest start address in `/proc//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 @@ -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 @@ -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. | --- @@ -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. --- diff --git a/src/main/java/it/adrian/code/Memory.java b/src/main/java/it/adrian/code/Memory.java index a06fb3f..6729b13 100644 --- a/src/main/java/it/adrian/code/Memory.java +++ b/src/main/java/it/adrian/code/Memory.java @@ -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 readMemory(Pointer baseAddr, long offset, Class 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); @@ -27,10 +27,9 @@ public static T readMemory(Pointer baseAddr, long offset, Class 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"); @@ -38,18 +37,19 @@ else if (type == Float.class) { } /** - * 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 void writeMemory(Pointer baseAddr, long offset, T value, Class 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); @@ -66,4 +66,4 @@ public static void writeMemory(Pointer baseAddr, long offset, T value, Class throw new IllegalArgumentException("Unsupported data type"); } } -} \ No newline at end of file +} diff --git a/src/main/java/it/adrian/code/memory/Pointer.java b/src/main/java/it/adrian/code/memory/Pointer.java index c8539f4..e623926 100644 --- a/src/main/java/it/adrian/code/memory/Pointer.java +++ b/src/main/java/it/adrian/code/memory/Pointer.java @@ -1,71 +1,60 @@ package it.adrian.code.memory; import com.sun.jna.Memory; -import com.sun.jna.platform.win32.Tlhelp32; -import com.sun.jna.platform.win32.WinDef; import com.sun.jna.platform.win32.WinNT; -import com.sun.jna.ptr.IntByReference; -import it.adrian.code.interfaces.Kernel32; -import it.adrian.code.interfaces.User32; +import it.adrian.code.platform.NativeAccess; +import it.adrian.code.platform.ProcessSession; +import it.adrian.code.platform.windows.WindowsProcessSession; -import java.util.Optional; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; public class Pointer { - private final WinNT.HANDLE handle; + private final ProcessSession session; public String processName; public String moduleName; - private com.sun.jna.Pointer baseAddress; + private long baseAddress; private long offset; - public Pointer(WinNT.HANDLE handle, com.sun.jna.Pointer baseAddress) { - this.handle = handle; + public Pointer(ProcessSession session, long baseAddress) { + this.session = session; this.baseAddress = baseAddress; this.offset = 0L; } + /** + * Backward-compatible constructor for Windows callers that already have a + * {@code WinNT.HANDLE} and a JNA pointer. + */ + public Pointer(WinNT.HANDLE handle, com.sun.jna.Pointer baseAddress) { + this(new WindowsProcessSession(0, handle), + baseAddress == null ? 0L : com.sun.jna.Pointer.nativeValue(baseAddress)); + } + public static Pointer getBaseAddress(String processName) { - Optional processHandle = ProcessHandle.allProcesses().filter(p -> p.info().command().orElse("").endsWith(processName)).findFirst(); - Optional pidOptional = processHandle.map(ProcessHandle::pid); - int pid = 0; - if (pidOptional.isPresent()) { - pid = Math.toIntExact(pidOptional.get()); - } - else { - try { - User32.INSTANCE.MessageBox(null, "PROCESS TO ATTACH NOT FOUND", "Warning!?!", User32.MB_OK | User32.MB_ICONWARNING); - System.exit(-1); - } catch (Throwable e) { - //e.printStackTrace(); - } + NativeAccess na = NativeAccess.get(); + int pid = na.findPidByName(processName); + if (pid == 0) { + na.abortProcessNotFound(processName); + return null; } - int accessRight = 0x0010 | 0x0020 | 0x0008; - WinNT.HANDLE handle = Kernel32.INSTANCE.OpenProcess(accessRight, false, pid); - com.sun.jna.Pointer baseAddress = getModuleBaseAddress(pid, processName); - Pointer ptr = new Pointer(handle, baseAddress); + ProcessSession session = na.openProcess(pid); + long base = na.getModuleBaseAddress(pid, processName); + Pointer ptr = new Pointer(session, base); ptr.processName = processName; ptr.moduleName = processName; return ptr; } + /** + * @deprecated Use {@link #getBaseAddress(String)}; this Windows-only helper + * is preserved for backward compatibility. + */ + @Deprecated public static com.sun.jna.Pointer getModuleBaseAddress(int pid, String moduleName) { - com.sun.jna.Pointer baseAddress = null; - WinNT.HANDLE snapshot = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPMODULE, new WinDef.DWORD(pid)); - try { - Tlhelp32.MODULEENTRY32W module = new Tlhelp32.MODULEENTRY32W(); - if (Kernel32.INSTANCE.Module32FirstW(snapshot, module)) { - do { - if (moduleName.equals(module.szModule())) { - baseAddress = module.modBaseAddr; - break; - } - } while (Kernel32.INSTANCE.Module32NextW(snapshot, module)); - } - } finally { - Kernel32.INSTANCE.CloseHandle(snapshot); - } - - return baseAddress; + long addr = NativeAccess.get().getModuleBaseAddress(pid, moduleName); + return addr == 0L ? null : new com.sun.jna.Pointer(addr); } public Pointer add(int val) { @@ -74,69 +63,56 @@ public Pointer add(int val) { } public long readLong() { - Memory memory = getMemory(8); - return memory.getLong(0); + return ByteBuffer.wrap(read(8)).order(ByteOrder.LITTLE_ENDIAN).getLong(); } public double readDouble() { - Memory memory = getMemory(8); - return memory.getDouble(0); + return ByteBuffer.wrap(read(8)).order(ByteOrder.LITTLE_ENDIAN).getDouble(); } - public float readFloat() { - Memory memory = getMemory(4); - return memory.getFloat(0); + return ByteBuffer.wrap(read(4)).order(ByteOrder.LITTLE_ENDIAN).getFloat(); } public int readInt() { - Memory memory = getMemory(4); - return memory.getInt(0); + return ByteBuffer.wrap(read(4)).order(ByteOrder.LITTLE_ENDIAN).getInt(); + } + + private byte[] read(int length) { + byte[] buffer = new byte[length]; + NativeAccess.get().readMemory(session, baseAddress + offset, buffer, length); + return buffer; } public Memory getMemory(int size) { - Memory memory = new Memory(size); - com.sun.jna.Pointer src = baseAddress.share(offset); - Kernel32.INSTANCE.ReadProcessMemory(handle, src, memory, size, null); - return memory; + byte[] buffer = read(size); + Memory mem = new Memory(size); + mem.write(0, buffer, 0, size); + return mem; } public boolean writeFloat(float value) { - Memory memory = new Memory(4); - memory.setFloat(0, value); - com.sun.jna.Pointer src = baseAddress.share(offset); - IntByReference intRef = new IntByReference(); - return Kernel32.INSTANCE.WriteProcessMemory(handle, src, memory, 4, intRef); + byte[] b = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putFloat(value).array(); + return NativeAccess.get().writeMemory(session, baseAddress + offset, b, 4); } public boolean writeDouble(double value) { - Memory memory = new Memory(8); - memory.setDouble(0, value); - com.sun.jna.Pointer src = baseAddress.share(offset); - IntByReference intRef = new IntByReference(); - return Kernel32.INSTANCE.WriteProcessMemory(handle, src, memory, 8, intRef); + byte[] b = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putDouble(value).array(); + return NativeAccess.get().writeMemory(session, baseAddress + offset, b, 8); } public boolean writeLong(long value) { - Memory memory = new Memory(8); - memory.setLong(0, value); - com.sun.jna.Pointer src = baseAddress.share(offset); - IntByReference intRef = new IntByReference(); - boolean res = Kernel32.INSTANCE.WriteProcessMemory(handle, src, memory, 8, intRef); - return res; + byte[] b = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).array(); + return NativeAccess.get().writeMemory(session, baseAddress + offset, b, 8); } public boolean writeInt(int value) { - Memory memory = new Memory(4); - memory.setInt(0, value); - com.sun.jna.Pointer src = baseAddress.share(offset); - IntByReference intRef = new IntByReference(); - boolean res = Kernel32.INSTANCE.WriteProcessMemory(handle, src, memory, 4, intRef); - return res; + byte[] b = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(value).array(); + return NativeAccess.get().writeMemory(session, baseAddress + offset, b, 4); } public Pointer copy() { - Pointer ptr = new Pointer(handle, baseAddress); + Pointer ptr = new Pointer(session, baseAddress); ptr.offset = offset; ptr.moduleName = moduleName; ptr.processName = processName; @@ -144,13 +120,26 @@ public Pointer copy() { } public Pointer indirect64() { - baseAddress = new com.sun.jna.Pointer(readLong()); + baseAddress = readLong(); offset = 0; return this; } + public ProcessSession getSession() { + return session; + } + + public long getBaseAddressValue() { + return baseAddress; + } + + public long getOffset() { + return offset; + } + @Override public String toString() { - return moduleName + "[" + String.format("%#08x", com.sun.jna.Pointer.nativeValue(baseAddress)) + "]+0x" + Long.toHexString(offset) + " => 0x" + Long.toHexString(com.sun.jna.Pointer.nativeValue(baseAddress) + offset); + return moduleName + "[" + String.format("%#08x", baseAddress) + "]+0x" + + Long.toHexString(offset) + " => 0x" + Long.toHexString(baseAddress + offset); } -} \ No newline at end of file +} diff --git a/src/main/java/it/adrian/code/platform/NativeAccess.java b/src/main/java/it/adrian/code/platform/NativeAccess.java new file mode 100644 index 0000000..bac5041 --- /dev/null +++ b/src/main/java/it/adrian/code/platform/NativeAccess.java @@ -0,0 +1,59 @@ +package it.adrian.code.platform; + +import com.sun.jna.Platform; + +public abstract class NativeAccess { + + private static volatile NativeAccess instance; + + public static NativeAccess get() { + NativeAccess local = instance; + if (local == null) { + synchronized (NativeAccess.class) { + local = instance; + if (local == null) { + local = create(); + instance = local; + } + } + } + return local; + } + + private static NativeAccess create() { + String backend; + if (Platform.isWindows()) { + backend = "it.adrian.code.platform.windows.WindowsAccess"; + } else if (Platform.isLinux()) { + backend = "it.adrian.code.platform.linux.LinuxAccess"; + } else { + throw new UnsupportedOperationException( + "Mem4J does not support OS: " + System.getProperty("os.name")); + } + try { + return (NativeAccess) Class.forName(backend).getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Failed to load Mem4J backend " + backend, e); + } + } + + public abstract int findPidByName(String processName); + + public abstract long getModuleBaseAddress(int pid, String moduleName); + + public abstract long getModuleSize(int pid, String moduleName); + + public abstract ProcessSession openProcess(int pid); + + public abstract boolean readMemory(ProcessSession session, long address, byte[] buffer, int length); + + public abstract boolean writeMemory(ProcessSession session, long address, byte[] buffer, int length); + + public abstract void closeSession(ProcessSession session); + + public abstract boolean isPrivileged(); + + public abstract void abortMissingPrivileges(); + + public abstract void abortProcessNotFound(String processName); +} diff --git a/src/main/java/it/adrian/code/platform/ProcessSession.java b/src/main/java/it/adrian/code/platform/ProcessSession.java new file mode 100644 index 0000000..4964917 --- /dev/null +++ b/src/main/java/it/adrian/code/platform/ProcessSession.java @@ -0,0 +1,10 @@ +package it.adrian.code.platform; + +public abstract class ProcessSession { + + public final int pid; + + protected ProcessSession(int pid) { + this.pid = pid; + } +} diff --git a/src/main/java/it/adrian/code/platform/linux/LinuxAccess.java b/src/main/java/it/adrian/code/platform/linux/LinuxAccess.java new file mode 100644 index 0000000..2bebc08 --- /dev/null +++ b/src/main/java/it/adrian/code/platform/linux/LinuxAccess.java @@ -0,0 +1,215 @@ +package it.adrian.code.platform.linux; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import it.adrian.code.platform.NativeAccess; +import it.adrian.code.platform.ProcessSession; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; + +public class LinuxAccess extends NativeAccess { + + public interface LibC extends Library { + int geteuid(); + } + + private static final class LibCHolder { + static final LibC INSTANCE = Native.load("c", LibC.class); + } + + @Override + public int findPidByName(String processName) { + Path procRoot = Paths.get("/proc"); + if (!Files.isDirectory(procRoot)) { + return 0; + } + try (Stream entries = Files.list(procRoot)) { + return entries + .filter(LinuxAccess::isPidDir) + .filter(dir -> matches(dir, processName)) + .mapToInt(dir -> Integer.parseInt(dir.getFileName().toString())) + .findFirst() + .orElse(0); + } catch (IOException e) { + return 0; + } + } + + private static boolean isPidDir(Path p) { + String name = p.getFileName().toString(); + if (name.isEmpty()) return false; + for (int i = 0; i < name.length(); i++) { + if (!Character.isDigit(name.charAt(i))) return false; + } + return true; + } + + private static boolean matches(Path procDir, String target) { + try { + Path comm = procDir.resolve("comm"); + if (Files.isReadable(comm)) { + String value = new String(Files.readAllBytes(comm)).trim(); + if (value.equals(target)) return true; + } + } catch (IOException ignored) { + } + try { + Path exe = procDir.resolve("exe"); + if (Files.isSymbolicLink(exe)) { + Path linkTarget = Files.readSymbolicLink(exe); + Path basename = linkTarget.getFileName(); + if (basename != null && basename.toString().equals(target)) return true; + } + } catch (IOException | SecurityException ignored) { + } + return false; + } + + @Override + public long getModuleBaseAddress(int pid, String moduleName) { + long min = Long.MAX_VALUE; + try (BufferedReader reader = Files.newBufferedReader(Paths.get("/proc/" + pid + "/maps"))) { + String line; + while ((line = reader.readLine()) != null) { + MapEntry entry = parseMapLine(line); + if (entry == null || entry.path == null) continue; + if (matchesModule(entry.path, moduleName)) { + if (entry.start < min) min = entry.start; + } + } + } catch (IOException ignored) { + return 0L; + } + return min == Long.MAX_VALUE ? 0L : min; + } + + @Override + public long getModuleSize(int pid, String moduleName) { + long min = Long.MAX_VALUE; + long max = 0L; + try (BufferedReader reader = Files.newBufferedReader(Paths.get("/proc/" + pid + "/maps"))) { + String line; + while ((line = reader.readLine()) != null) { + MapEntry entry = parseMapLine(line); + if (entry == null || entry.path == null) continue; + if (matchesModule(entry.path, moduleName)) { + if (entry.start < min) min = entry.start; + if (entry.end > max) max = entry.end; + } + } + } catch (IOException ignored) { + return 0L; + } + return min == Long.MAX_VALUE ? 0L : max - min; + } + + private static boolean matchesModule(String fullPath, String moduleName) { + if (fullPath.equals(moduleName)) return true; + int slash = fullPath.lastIndexOf('/'); + String basename = slash < 0 ? fullPath : fullPath.substring(slash + 1); + return basename.equals(moduleName); + } + + private static MapEntry parseMapLine(String line) { + int dash = line.indexOf('-'); + int space = line.indexOf(' '); + if (dash <= 0 || space <= dash) return null; + try { + long start = Long.parseUnsignedLong(line.substring(0, dash), 16); + long end = Long.parseUnsignedLong(line.substring(dash + 1, space), 16); + int pathStart = line.indexOf('/'); + String path = pathStart < 0 ? null : line.substring(pathStart).trim(); + if (path != null && path.isEmpty()) path = null; + return new MapEntry(start, end, path); + } catch (NumberFormatException e) { + return null; + } + } + + private static final class MapEntry { + final long start; + final long end; + final String path; + + MapEntry(long start, long end, String path) { + this.start = start; + this.end = end; + this.path = path; + } + } + + @Override + public ProcessSession openProcess(int pid) { + try { + return new LinuxProcessSession(pid); + } catch (IOException e) { + return null; + } + } + + @Override + public boolean readMemory(ProcessSession session, long address, byte[] buffer, int length) { + LinuxProcessSession ls = (LinuxProcessSession) session; + if (ls == null) return false; + try { + synchronized (ls.mem) { + ls.mem.seek(address); + ls.mem.readFully(buffer, 0, length); + } + return true; + } catch (IOException e) { + return false; + } + } + + @Override + public boolean writeMemory(ProcessSession session, long address, byte[] buffer, int length) { + LinuxProcessSession ls = (LinuxProcessSession) session; + if (ls == null) return false; + try { + synchronized (ls.mem) { + ls.mem.seek(address); + ls.mem.write(buffer, 0, length); + } + return true; + } catch (IOException e) { + return false; + } + } + + @Override + public void closeSession(ProcessSession session) { + if (session == null) return; + try { + ((LinuxProcessSession) session).close(); + } catch (IOException ignored) { + } + } + + @Override + public boolean isPrivileged() { + try { + return LibCHolder.INSTANCE.geteuid() == 0; + } catch (Throwable t) { + return "root".equals(System.getProperty("user.name")); + } + } + + @Override + public void abortMissingPrivileges() { + System.err.println("Mem4J: this operation requires elevated privileges " + + "(run as root or grant CAP_SYS_PTRACE to the JVM)."); + System.exit(-1); + } + + @Override + public void abortProcessNotFound(String processName) { + System.err.println("Mem4J: process to attach not found: " + processName); + System.exit(-1); + } +} diff --git a/src/main/java/it/adrian/code/platform/linux/LinuxProcessSession.java b/src/main/java/it/adrian/code/platform/linux/LinuxProcessSession.java new file mode 100644 index 0000000..e4cd82a --- /dev/null +++ b/src/main/java/it/adrian/code/platform/linux/LinuxProcessSession.java @@ -0,0 +1,20 @@ +package it.adrian.code.platform.linux; + +import it.adrian.code.platform.ProcessSession; + +import java.io.IOException; +import java.io.RandomAccessFile; + +public class LinuxProcessSession extends ProcessSession { + + final RandomAccessFile mem; + + LinuxProcessSession(int pid) throws IOException { + super(pid); + this.mem = new RandomAccessFile("/proc/" + pid + "/mem", "rw"); + } + + void close() throws IOException { + mem.close(); + } +} diff --git a/src/main/java/it/adrian/code/platform/windows/WindowsAccess.java b/src/main/java/it/adrian/code/platform/windows/WindowsAccess.java new file mode 100644 index 0000000..522701e --- /dev/null +++ b/src/main/java/it/adrian/code/platform/windows/WindowsAccess.java @@ -0,0 +1,131 @@ +package it.adrian.code.platform.windows; + +import com.sun.jna.Memory; +import com.sun.jna.Native; +import com.sun.jna.platform.win32.Tlhelp32; +import com.sun.jna.platform.win32.WinDef; +import com.sun.jna.platform.win32.WinNT; +import com.sun.jna.ptr.IntByReference; +import it.adrian.code.interfaces.Kernel32; +import it.adrian.code.interfaces.User32; +import it.adrian.code.platform.NativeAccess; +import it.adrian.code.platform.ProcessSession; +import it.adrian.code.utilities.Shell32Util; + +public class WindowsAccess extends NativeAccess { + + private static final int PROCESS_VM_OPERATION = 0x0008; + private static final int PROCESS_VM_READ = 0x0010; + private static final int PROCESS_VM_WRITE = 0x0020; + + @Override + public int findPidByName(String processName) { + Tlhelp32.PROCESSENTRY32.ByReference entry = new Tlhelp32.PROCESSENTRY32.ByReference(); + WinNT.HANDLE snapshot = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPALL, new WinDef.DWORD(0)); + try { + while (Kernel32.INSTANCE.Process32NextW(snapshot, entry)) { + if (processName.equals(Native.toString(entry.szExeFile))) { + return entry.th32ProcessID.intValue(); + } + } + } finally { + Kernel32.INSTANCE.CloseHandle(snapshot); + } + return 0; + } + + @Override + public long getModuleBaseAddress(int pid, String moduleName) { + WinNT.HANDLE snapshot = Kernel32.INSTANCE.CreateToolhelp32Snapshot( + Kernel32.TH32CS_SNAPMODULE, new WinDef.DWORD(pid)); + try { + Tlhelp32.MODULEENTRY32W module = new Tlhelp32.MODULEENTRY32W(); + if (Kernel32.INSTANCE.Module32FirstW(snapshot, module)) { + do { + if (moduleName.equals(module.szModule())) { + return com.sun.jna.Pointer.nativeValue(module.modBaseAddr); + } + } while (Kernel32.INSTANCE.Module32NextW(snapshot, module)); + } + } finally { + Kernel32.INSTANCE.CloseHandle(snapshot); + } + return 0L; + } + + @Override + public long getModuleSize(int pid, String moduleName) { + WinNT.HANDLE snapshot = Kernel32.INSTANCE.CreateToolhelp32Snapshot( + Kernel32.TH32CS_SNAPMODULE, new WinDef.DWORD(pid)); + try { + Tlhelp32.MODULEENTRY32W module = new Tlhelp32.MODULEENTRY32W(); + if (Kernel32.INSTANCE.Module32FirstW(snapshot, module)) { + do { + if (moduleName.equals(module.szModule())) { + return module.modBaseSize.longValue(); + } + } while (Kernel32.INSTANCE.Module32NextW(snapshot, module)); + } + } finally { + Kernel32.INSTANCE.CloseHandle(snapshot); + } + return 0L; + } + + @Override + public ProcessSession openProcess(int pid) { + WinNT.HANDLE handle = Kernel32.INSTANCE.OpenProcess( + PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION, false, pid); + return new WindowsProcessSession(pid, handle); + } + + @Override + public boolean readMemory(ProcessSession session, long address, byte[] buffer, int length) { + WinNT.HANDLE handle = ((WindowsProcessSession) session).handle; + Memory mem = new Memory(length); + if (!Kernel32.INSTANCE.ReadProcessMemory(handle, new com.sun.jna.Pointer(address), mem, length, null)) { + return false; + } + mem.read(0, buffer, 0, length); + return true; + } + + @Override + public boolean writeMemory(ProcessSession session, long address, byte[] buffer, int length) { + WinNT.HANDLE handle = ((WindowsProcessSession) session).handle; + Memory mem = new Memory(length); + mem.write(0, buffer, 0, length); + IntByReference written = new IntByReference(); + return Kernel32.INSTANCE.WriteProcessMemory(handle, new com.sun.jna.Pointer(address), mem, length, written); + } + + @Override + public void closeSession(ProcessSession session) { + Kernel32.INSTANCE.CloseHandle(((WindowsProcessSession) session).handle); + } + + @Override + public boolean isPrivileged() { + return Shell32Util.isUserWindowsAdmin(); + } + + @Override + public void abortMissingPrivileges() { + try { + User32.INSTANCE.MessageBox(null, "THIS REQUIRES ADMINISTRATION PERMISSIONS", "Warning", + User32.MB_OK | User32.MB_ICONWARNING); + } catch (Throwable ignored) { + } + System.exit(-1); + } + + @Override + public void abortProcessNotFound(String processName) { + try { + User32.INSTANCE.MessageBox(null, "PROCESS TO ATTACH NOT FOUND: " + processName, "Warning", + User32.MB_OK | User32.MB_ICONWARNING); + } catch (Throwable ignored) { + } + System.exit(-1); + } +} diff --git a/src/main/java/it/adrian/code/platform/windows/WindowsProcessSession.java b/src/main/java/it/adrian/code/platform/windows/WindowsProcessSession.java new file mode 100644 index 0000000..2e62a75 --- /dev/null +++ b/src/main/java/it/adrian/code/platform/windows/WindowsProcessSession.java @@ -0,0 +1,14 @@ +package it.adrian.code.platform.windows; + +import com.sun.jna.platform.win32.WinNT; +import it.adrian.code.platform.ProcessSession; + +public class WindowsProcessSession extends ProcessSession { + + public final WinNT.HANDLE handle; + + public WindowsProcessSession(int pid, WinNT.HANDLE handle) { + super(pid); + this.handle = handle; + } +} diff --git a/src/main/java/it/adrian/code/utilities/ProcessUtil.java b/src/main/java/it/adrian/code/utilities/ProcessUtil.java index 5e9ade8..773abb0 100644 --- a/src/main/java/it/adrian/code/utilities/ProcessUtil.java +++ b/src/main/java/it/adrian/code/utilities/ProcessUtil.java @@ -5,10 +5,20 @@ import com.sun.jna.platform.win32.WinDef; import com.sun.jna.platform.win32.WinNT; import it.adrian.code.interfaces.Kernel32; +import it.adrian.code.platform.NativeAccess; public class ProcessUtil { + /** + * Returns the module entry (Windows-only) for the named module of the given pid. + * On Linux this throws {@link UnsupportedOperationException} — use + * {@link NativeAccess#getModuleBaseAddress(int, String)} / {@link NativeAccess#getModuleSize(int, String)} instead. + */ public static Tlhelp32.MODULEENTRY32W getModule(int pid, String moduleName) { + if (!com.sun.jna.Platform.isWindows()) { + throw new UnsupportedOperationException( + "ProcessUtil.getModule is Windows-only; use NativeAccess.get().getModuleBaseAddress/Size on Linux."); + } WinNT.HANDLE snapshotModules = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Kernel32.TH32CS_SNAPMODULE, new WinDef.DWORD(pid)); WinNT.HANDLE snapshotModules32 = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Kernel32.TH32CS_SNAPMODULE32, new WinDef.DWORD(pid)); @@ -41,18 +51,6 @@ public static Tlhelp32.MODULEENTRY32W getModule(int pid, String moduleName) { } public static int getProcessPidByName(String pName) { - Tlhelp32.PROCESSENTRY32.ByReference entry = new Tlhelp32.PROCESSENTRY32.ByReference(); - WinNT.HANDLE snapshot = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPALL, new WinDef.DWORD(0)); - try { - while (Kernel32.INSTANCE.Process32NextW(snapshot, entry)) { - String processName = Native.toString(entry.szExeFile); - if (pName.equals(processName)) { - return entry.th32ProcessID.intValue(); - } - } - } finally { - Kernel32.INSTANCE.CloseHandle(snapshot); - } - return 0; + return NativeAccess.get().findPidByName(pName); } -} \ No newline at end of file +}