-
Notifications
You must be signed in to change notification settings - Fork 186
Library Injection
Library injection lets you load your own code into a running process. This is useful for hooking functions, modifying behavior at a deeper level or adding custom functionality that can't be achieved with simple memory edits.
PINCE supports two types of library injection:
- .so files (shared objects): works on any Linux process, including WINE/Proton
- DLL files: only works on WINE/Proton processes
Open the Memory Viewer window and go to Tools menu:
-
Inject .so file: for any Linux process (native or WINE/Proton) -
Inject DLL file: for WINE/Proton processes only
To inject a shared object file:
- Make sure you're attached to a process
- Go to
Tools -> Inject .so file - Select your .so file from the file dialog (filter:
Shared object library (*.so)) - PINCE will call
dlopen()in the target process to load your library
If the injection succeeds, you'll see the message "The shared object (.so) file has been injected". Your library's constructor function (if you defined one with __attribute__((constructor))) will run automatically when loaded, since that's how dlopen() works.
If it fails, you'll see "Failed to inject the shared object (.so) file". Note that PINCE can't distinguish between "couldn't find dlopen" and "dlopen returned NULL because the .so file was invalid" so both cases show the same error.
PINCE first tries to call dlopen() (or __libc_dlopen_mode() as a fallback) in the target process via GDB. The call uses RTLD_NOW (flag 2) so all symbols are resolved immediately.
If GDB can't resolve these symbols (like in stripped binaries), PINCE has a fallback mechanism that:
- Scans
/proc/<pid>/mapsfor libc, libc-musl, ld-musl or libdl mappings - Parses the ELF file's dynamic symbol tables on disk to find the
dlopenaddress - Resolves sandboxed paths (Flatpak, Steam pressure-vessel) by going through
/proc/<pid>/rootso the file PINCE reads is the exact file the target process mapped - Manually calls
dlopenat the resolved address with a temporary interrupt
This works on both native Linux and WINE/Proton processes and handles both 32-bit and 64-bit ELF files (little and big endian).
To inject a Windows DLL:
- Make sure you're attached to a WINE or Proton process
- Go to
Tools -> Inject DLL file - Select your DLL file from the file dialog (filter:
Dynamic-link library (*.dll)) - PINCE will convert the path to WINE format and call
LoadLibraryWin the target process
If you try this on a native Linux process, you'll get the error "DLL injection is only supported for WINE/Proton processes".
If the injection succeeds, you'll see a message like "The DLL has been injected at 0x7f1234560000" showing the module handle address returned by LoadLibraryW.
DLL injection is more complex because WINE processes run Windows PE binaries and GDB can't resolve PE exports by symbol. The process:
- PINCE locates
kernel32.dllandntdll.dllin the process memory by scanning/proc/<pid>/maps - Parses the PE export tables in memory to find
LoadLibraryWin kernel32.dll. IfLoadLibraryWturns out to be an export forwarder (which happens in some WINE versions), PINCE falls back to resolving it fromkernelbase.dll - Also resolves
NtWaitForSingleObject(orNtDelayExecutionas fallback) from ntdll.dll - Resolves
mallocfrom the target's libc (using the same ELF parsing fallback as .so injection) to allocate a buffer for the path string - Finds a clean execution context by setting a temporary breakpoint at the entry of
NtWaitForSingleObjectorNtDelayExecution(these are hot ntdll syscall wrappers), letting the target run until it hits one (up to 10 seconds), then callingLoadLibraryWfrom that context. This is necessary because the call needs to run from a clean PE context, not a thread parked mid-syscall - Converts your Linux path to WINE format:
Z:+ path with/replaced by\, encoded as UTF-16LE with a null terminator (e.g./home/user/mod.dllbecomesZ:\home\user\mod.dll) - Allocates memory in the process via
malloc, writes the UTF-16LE path string into it, then callsLoadLibraryWwith a pointer to that buffer - The call uses different argument signatures for 32-bit vs 64-bit targets (64-bit uses a 4-arg calling convention, 32-bit uses single-arg stdcall)
During injection, PINCE sets a driving_inferior flag that suppresses UI stop/run notifications and blocks user pause/continue/step controls so nothing interferes with the process.
- DLL injection only works on WINE/Proton processes. If you try to inject a DLL into a native Linux process, you'll get an error.
- Your library must be compatible with the target process architecture (32-bit or 64-bit).
- The library file must be accessible from the target process. For sandboxed environments, make sure the path is valid inside the sandbox.
-
DLL injection assumes the default WINE prefix where the
Z:drive maps to/. If you remapped or removed theZ:drive in your WINE prefix, DLL injection won't find your file. -
DLL injection requires both kernel32.dll and ntdll.dll to be mapped in the target, plus a resolvable
mallocin the target's libc. -
The clean context wait can time out. If the target doesn't call
NtWaitForSingleObjectorNtDelayExecutionwithin 10 seconds, the injection fails with a generic error. -
No file existence check is done before injection. If the path is wrong,
dlopenorLoadLibraryWsimply returns NULL and you get a generic failure message.
Common reasons to use library injection:
- Function hooking: Replace or wrap game functions with your own code
- Custom allocators: Intercept memory allocation calls
- Logging: Add debug output to specific functions
- Patches: Apply complex multi-step modifications atomically
For simpler patches like NOPing instructions or basic code injection, check out Modifying Game Code or Libpince Engine instead, which don't require creating external library files.