Home

Awesome

Injector

tests Static Badge

Library for injecting a shared library into a Linux, Windows and MacOS process

Linux

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.

Windows

Windows version is also here. It uses well-known CreateRemoteThread+LoadLibrary technique to load a DLL into another process with some improvements.

  1. It gets Win32 error messages when LoadLibrary fails by copying assembly code into the target process.
  2. 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.

MacOS

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.

Compilation

Linux

$ git clone https://github.com/kubo/injector.git
$ cd injector
$ make

The make command creates:

filename-
src/linux/libinjector.aa static library
src/linux/libinjector.soa shared library
cmd/injectora command line program linked with the static library

Windows (MSVC)

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

The nmake command creates:

filename-
src/windows/injector-static.liba static library (release build)
src/windows/injector.dlla shared library (release build)
src/windows/injector.liban import library for injector.dll
src/windows/injectord-static.liba static library (debug build)
src/windows/injectord.dlla shared library (debug build)
src/windows/injectord.liban import library for injectord.dll
cmd/injector.exea command line program linked the static library (release build)

Windows (mingw-w64)

On MSYS2:

$ git clone https://github.com/kubo/injector.git
$ cd injector
$ CC=gcc make

Cross-compilation on Linux:

$ git clone https://github.com/kubo/injector.git
$ cd injector
$ CC=x86_64-w64-mingw32-gcc OS=Windows_NT make

The environment variable OS=Windows_NT must be set on Linux.

MacOS

$ git clone https://github.com/TheOiseth/injector.git
$ cd injector
$ make

The make command creates:

filename-
src/macos/libinjector.aa static library
src/macos/libinjector.dyliba shared library
cmd/injectora 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.sh

If injector still does not work after signing, reboot the system.

Usage

C API

#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);

Command line program

See Usage section and Sample section in linux-inject and substitute inject with injector in the page.

Tested Architectures

Linux

x86_64:

injector process \ target processx86_64i386x32(*1)
x86_64:smiley: success(*2):smiley: success(*3):smiley: success(*6)
i386:skull: failure(*4):smiley: success(*3):skull: failure(*5)
x32(*1):skull: failure(*4):smiley: success(*6):skull: 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 processarm64armhfarmel
arm64:smiley: success:smiley: success:smiley: success
armhf:skull: failure(*1):smiley: success:smiley: success
armel:skull: failure(*1):smiley: success:smiley: success

*1: failure with 64-bit target process isn't supported by 32-bit process.

MIPS:

injector process \ target processmips64elmipsel (n32)mipsel (o32)
mips64el:smiley: success (*1):smiley: success (*1):smiley: success (*1)
mipsel (n32):skull: failure(*2):smiley: success (*1):smiley: success (*1)
mipsel (o32):skull: failure(*2):smiley: success (*1):smiley: 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:

RISC-V:

Windows

Windows x64:

injector process \ target processx64x86
x64:smiley: success(*2):smiley: success(*2)
x86:skull: failure(*1):smiley: 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 processarm64arm64ecx64x86arm32
arm64:smiley: success:skull: failure:skull: failure:skull: failure:smiley: success
arm64ec:skull: failure:smiley: success:smiley: success:skull: failure:skull: failure
x64:skull: failure:smiley: success:smiley: success:skull: failure:skull: failure
x86:skull: failure:skull: failure:skull: failure:smiley: success:skull: failure
arm32:skull: failure:skull: failure:skull: failure:skull: failure:smiley: success

Wine (on Linux x86_64):

injector process \ target processx64x86
x64:smiley: success:skull: failure
x86:skull: failure:smiley: success

MacOS

injector process \ target processx64arm64
x64:smiley: success(*1):skull: failure(*2)
arm64:skull: failure(*3):smiley: 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.

Caveats

The following restrictions are only on Linux.

Injector doesn't work where ptrace() is disallowed.

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.

License

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.