Modular binary injection framework
ezinject is a lightweight and flexible binary injection framework. it can be thought as a lightweight and less featured version of frida.
It's main and primary goal is to load a user module (.dll, .so, .dylib) inside a target process. These modules can augment ezinject by providing additional features, such as hooks, scripting languages, RPC servers, and so on. They can also be written in multiple languages such as C, C++, Rust, etc... as long as the ABI is respected.
NOTE: ezinject core is purposedly small, and only implements the "kernel-mode" (debugger) features it needs to run the "user-mode" program, aka the user module.
It requires no dependencies other than the OS C library (capstone is optionally used only by user modules)
Porting ezinejct is simple: No assembly code is required other than a few inline assembly statements, and an abstraction layer separates multiple OSes implementations.
As proof, it has been ported and battle-tested on a wild variety of Linux devices such as:
- Asus DSL-N55U D1, a Mips BE DSL modem running uClibc and Linux 2.6
- ADB/DLink DVA-5592, an ARM v7 FPU-less xDSL modem running uClibc and Linux 3.0
- Samsung GT-i9003 (latona), an Android 2.3 smartphone running Linux 2.6
- Samsung GT-i9070 (janice), running Android 4
- Samsung GT-i9300 (smdk4x12), running Android 7
- pocophone F1 (beryllium), running Android 10
- TomTom GO 910, a standalone GPS nav running glibc 2.3 and Linux 2.6
as well as wildly different (both POSIX and non-POSIX OSes) such as
- Windows
- FreeBSD
- Darwin (macOS)
- hooks example (libdummy)
- run Python scripts (pyloader)
- run PHP scripts (php)
- run .NET programs (mono, dotnetcore, EzDotNet)
- turn the remote process into an RPC (ezinject-webapi)
and so on...
Shared modules must implement the following 2 functions:
This function is provided to let the user control the module lifecycle.
For example, by setting user->persist
to 1, the module can be kept persistent in memory once lib_main
returns.
This function should always return 0 to signal success.
It works just like a typical main
function in C.
argv[0]
holds the name of the module, while argv[1]
onwards are user arguments that you passed to the ezinject
binary (they are passed to the module as user supplied arguments)
NOTE: lib_main
is executed synchronously, and ezinject
will not complete until this function has returned. If you need to perform background work, you will need to make a copy of argv
(important, as it will be freed upon return) and start a new thread.
See the php module for an example
-
Linux:
- arm (arm+thumb)
- aarch64
- mips
- x86
- amd64
-
Windows: x86, x64
-
FreeBSD: x86, x64
-
Darwin: x64
- Linux
- glibc
- uClibc (tested on ARM, MIPS)
- Android (tested on Android 2.x - 10.x)
- FreeBSD (tested on FreeBSD 12)
- Windows
- Win32 (tested on Windows 95)
- NT (tested on Windows NT 3.51, Windows XP, Windows 10 64bit)
- Darwin (tested on macOS 11)
ezinject implements a single instrumentation primitive: remote calls
We proceed as following:
- Create a remote memory segment that will hold the payload
- If using shared memory, use remote syscalls to attach the shared memory in the target process
- Invoke the payload remotely.
The stack at entry will contain a pointer to the context, and a pointer to the function to call.
- The payload pops the parameters and the function to call from the stack, then calls the function in C (thus emitting a proper call with a stack frame)
- The payload implementation creates a mutex/event, then opens the target library and awaits for the thread to be created.
- The ezinject's crt (linked in the library) creates a local copy of the context, then creates a new thread.
- The crt signals that the thread is ready to be awaited
- The newly created thread prepares argv, then invokes
lib_preinit
andlib_main
functions in the library - The user code is invoked. It can call any function inside the target, replace or hook functions (with libhooker as part of the CRT, in userland)
The following is an example on Debian and derivates, needs to be adjusted for each platform.
- Install dependencies
- build-essential
- cmake
- libcapstone-dev
- pkg-config
- Build the project
./build.sh
On Terminal 1
$ cd build/samples/dummy
$ ./target
On Terminal 2
$ cd build
$ sudo ./ezinject `pidof target` samples/dummy/libdummy.so
Expected output
return1() = 1
changes to
return1() = 13370
echo "print('hello ' * 3 + 'from python')" > hello.py
export EZPY=`python -c "import sys; import os; print(os.pathsep.join(sys.path))"`
echo "python path: $EZPY"
Find libpython:
find /usr/lib -name "libpython*"
Put correct libpython and paths in example below:
sudo ./ezinject `pidof target` samples/pyloader/libpyloader.so /usr/lib/x86_64-linux-gnu/libpython2.7.so.1 /usr/lib/python2.7 "$EZPY" hello.py
This project has initially been created for the openlgtv community, of which i'm a member. It has since evolved to be a generic tool
Thanks to all members of the openlgtv and webosbrew community for supporting the development by adopting and testing ezinject.
Special thanks to:
- irsl, for the initial work on libhooker, which inspired me to get involved
- mudkip908, for the preliminar ezinject proof of concept and code review
If you would like to support the ezinject development, you can