Library for injecting a shared library into a Linux, Windows and MacOS process
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-inject
writes about 80 bytes of code to the target process on x86_64. This writes only 4 ~ 16 bytes.linux-inject
writes code at the firstly found executable region of memory, which may be referred by other threads. This writes it at the entry point oflibc.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
LoadLibrary
fails 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
$ make
The 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.win32
The 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) |
$ git clone https://github.com/TheOiseth/injector.git
$ cd injector
$ make
The 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.sh
If 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
injector process \ target process x86_64 i386 x32(*1) x86_64 😃 success(*2) 😃 success(*3) 😃 success(*3) i386 💀 failure(*4) 😃 success(*3) 💀 failure(*5) x32(*1) 💀 failure(*4) 😃 success(*3) 💀 failure(*5) *1: x32 ABI
*2: tested on github actions with both glibc and musl.
*3: tested on github actions with glibc.
*4: failure with64-bit target process isn't supported by 32-bit process
.
*5: failure withx32-ABI target process is supported only by x86_64
. -
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 with64-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)
On x64 machine:
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
On arm machine:
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 |
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_PTRACE
todocker run
to 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.