Library for injecting a shared library into a Linux, Windows and MacOS process
Caution
Due to a shift in my personal interests, this repository is no longer maintained and has been archived. If you're looking for a successor project, please check the network graph to see if any forks have continued development.
Warning
Don't use this in production environments. It may stop target processes forever. See Caveats.
I was inspired by linux-inject and the basic idea came from it.
However the way to call __libc_dlopen_mode in libc.so.6 is
thoroughly different.
- linux-injectwrites about 80 bytes of code to the target process on x86_64. This writes only 4 ~ 16 bytes.
- linux-injectwrites code at the firstly found executable region of memory, which may be referred by other threads. This writes it at the entry point of- libc.so.6, which will be referred by nobody unless the libc itself is executed as a program.
Windows version is also here. It uses well-known CreateRemoteThread+LoadLibrary
technique to load a DLL into another process with some improvements.
- It gets Win32 error messages when LoadLibraryfails by copying assembly code into the target process.
- It can inject a 32-bit dll into a 32-bit process from x64 processes by checking the export entries in 32-bit kernel32.dll.
Note: It may work on Windows on ARM though I have not tested it because I have no ARM machines. Let me know if it really works.
The injector connects to the target process using task_for_pid and creates a mach-thread. If dlopen is called in this thread, the target process will fail with an error, however, it is possible to create another thread using pthread_create_from_mach_thread function for Mac >= 10.12 or pthread_create otherwise. In the created thread, the code for loading the library is executed. The second thread is created when injector_inject is called and terminated when injector_detach is called.
$ git clone https://github.com/kubo/injector.git
$ cd injector
$ makeThe make command creates:
| filename | - | 
|---|---|
| src/linux/libinjector.a | a static library | 
| src/linux/libinjector.so | a shared library | 
| cmd/injector | a command line program linked with the static library | 
Open a Visual Studio command prompt and run the following commands:
$ git clone https://github.com/kubo/injector.git # Or use any other tool
$ cd injector
$ nmake -f Makefile.win32The nmake command creates:
| filename | - | 
|---|---|
| src/windows/injector-static.lib | a static library (release build) | 
| src/windows/injector.dll | a shared library (release build) | 
| src/windows/injector.lib | an import library for injector.dll | 
| src/windows/injectord-static.lib | a static library (debug build) | 
| src/windows/injectord.dll | a shared library (debug build) | 
| src/windows/injectord.lib | an import library for injectord.dll | 
| cmd/injector.exe | a command line program linked the static library (release build) | 
On MSYS2:
$ git clone https://github.com/kubo/injector.git
$ cd injector
$ CC=gcc makeCross-compilation on Linux:
$ git clone https://github.com/kubo/injector.git
$ cd injector
$ CC=x86_64-w64-mingw32-gcc OS=Windows_NT makeThe environment variable OS=Windows_NT must be set on Linux.
$ git clone https://github.com/TheOiseth/injector.git
$ cd injector
$ makeThe make command creates:
| filename | - | 
|---|---|
| src/macos/libinjector.a | a static library | 
| src/macos/libinjector.dylib | a shared library | 
| cmd/injector | a command line program linked with the static library | 
Important: in order for the injector process to connect to another process using task_for_pid, it is necessary to disable SIP or sign the injector with a self-signed certificate with debugging permission, for this:
$ cd cmd/macos-sign
$ chmod +x genkey.sh
$ ./genkey.sh
$ chmod +x sign.sh
$ ./sign.shIf injector still does not work after signing, reboot the system.
#include <injector.h>
...
    injector_t *injector;
    void *handle;
    /* attach to a process whose process id is 1234. */
    if (injector_attach(&injector, 1234) != 0) {
        printf("ATTACH ERROR: %s\n", injector_error());
        return;
    }
    /* inject a shared library into the process. */
    if (injector_inject(injector, "/path/to/shared/library", NULL) != 0) {
        printf("INJECT ERROR: %s\n", injector_error());
    }
    /* inject another shared library. */
    if (injector_inject(injector, "/path/to/another/shared/library", &handle) != 0) {
        printf("INJECT ERROR: %s\n", injector_error());
    }
...
    /* uninject the second shared library. */
    if (injector_uninject(injector, handle) != 0) {
        printf("UNINJECT ERROR: %s\n", injector_error());
    }
    /* cleanup */
    injector_detach(injector);See Usage section and Sample section in linux-inject and substitute
inject with injector in the page.
x86_64:
| injector process \ target process | x86_64 | i386 | x32(*1) | 
|---|---|---|---|
| x86_64 | π success(*2) | π success(*3) | π success(*6) | 
| i386 | π failure(*4) | π success(*3) | π failure(*5) | 
| x32(*1) | π failure(*4) | π success(*6) | π failure(*5) | 
*1: x32 ABI
*2: tested on github actions with both glibc and musl.
*3: tested on github actions with glibc.
*4: failure with 64-bit target process isn't supported by 32-bit process.
*5: failure with x32-ABI target process is supported only by x86_64.
*6: tested on a local machine. CONFIG_X86_X32 isn't enabled in github actions.
ARM:
| injector process \ target process | arm64 | armhf | armel | 
|---|---|---|---|
| arm64 | π success | π success | π success | 
| armhf | π failure(*1) | π success | π success | 
| armel | π failure(*1) | π success | π success | 
*1: failure with 64-bit target process isn't supported by 32-bit process.
MIPS:
| injector process \ target process | mips64el | mipsel (n32) | mipsel (o32) | 
|---|---|---|---|
| mips64el | π success (*1) | π success (*1) | π success (*1) | 
| mipsel (n32) | π failure(*2) | π success (*1) | π success (*1) | 
| mipsel (o32) | π failure(*2) | π success (*1) | π success (*1) | 
*1: tested on debian 11 mips64el on QEMU.
*2: failure with 64-bit target process isn't supported by 32-bit process.
PowerPC:
- ppc64le (tested on alpine 3.16.2 ppc64le on QEMU)
- powerpc (big endian) (tested on ubuntu 16.04 powerpc on QEMU
RISC-V:
- riscv64 (tested on Ubuntu 22.04.1 riscv64 on QEMU)
Windows x64:
| injector process \ target process | x64 | x86 | 
|---|---|---|
| x64 | π success(*2) | π success(*2) | 
| x86 | π failure(*1) | π success(*2) | 
*1: failure with x64 target process isn't supported by x86 process.
*2: tested on github actions
Windows 11 on Arm:
| injector process \ target process | arm64 | arm64ec | x64 | x86 | arm32 | 
|---|---|---|---|---|---|
| arm64 | π success | π failure | π failure | π failure | π success | 
| arm64ec | π failure | π success | π success | π failure | π failure | 
| x64 | π failure | π success | π success | π failure | π failure | 
| x86 | π failure | π failure | π failure | π success | π failure | 
| arm32 | π failure | π failure | π failure | π failure | π success | 
Wine (on Linux x86_64):
| injector process \ target process | x64 | x86 | 
|---|---|---|
| x64 | π success | π failure | 
| x86 | π failure | π success | 
| injector process \ target process | x64 | arm64 | 
|---|---|---|
| x64 | π success(*1) | π failure(*2) | 
| arm64 | π failure(*3) | π success | 
*1: failure with x86_64 target process isn't supported by x86_64 process on ARM64 machine. Tested on github actions.
*2: failure with arm64 target process isn't supported by x86_64 process.
*3: failure with x86_64 target process isn't supported by arm64 process.
The following restrictions are only on Linux.
Injector doesn't work where ptrace() is disallowed.
- Non-children processes (See Caveat about ptrace())
- Docker containers on docker version < 19.03 or linux kernel version < 4.8. You need to pass --cap-add=SYS_PTRACEtodocker runto allow it in the environments.
- Linux inside of UserLAnd (Android App) (See here)
Injector calls functions inside of a target process interrupted by ptrace().
If the target process is interrupted while holding a non-reentrant lock and
injector calls a function requiring the same lock, the process stops forever.
If the lock type is reentrant, the status guarded by the lock may become inconsistent.
As far as I checked, dlopen() internally calls malloc() requiring non-reentrant
locks. dlopen() also uses a reentrant lock to guard information about loaded files.
On Linux x86_64 injector_inject_in_cloned_thread in place of injector_inject
may be a solution of the locking issue. It calls dlopen() in a thread created by
clone(). Note that no wonder there are unexpected pitfalls because some resources
allocated in pthread_create() lack in the clone()-ed thread. Use it at
your own risk.
Files under include and src are licensed under LGPL 2.1 or later.
Files under cmd are licensed under GPL 2 or later.
Files under util are licensed under 2-clause BSD.