Pull requests are welcomed.
- Try to run the given injection techniques code.
- Understand how each technique works
- Understand the attack vector and the different parts (stages) of the chain
(i.e the bridgehead shellcode, injection to process memory,LPE, when to create a new process etc.) - Describe the need for a custom statically PIC compiled elf (Shared object library) loader shellcode.
- Injection vs patching at runtime?
- Implement / improve it by yourself.
This repository is a research index, not a copy-paste implementation guide. The
useful question for modern Android is no longer only "how do I call dlopen in
another process?". The harder questions are:
- Which process lifecycle point do you control?
- Which SELinux/domain and ptrace constraints apply?
- Who maps the library: Android's linker or a custom loader?
- How deterministic does the mapped address need to be?
- How will native code cross into ART/JNI/DEX code once it is resident?
The notes below assume an authorized post-exploitation research setting. When this document says "RCE" in the Android target-entry discussion, read that as shorthand for a completed chain: native code execution already exists, privilege escalation to a useful system/root-equivalent context has already happened, and SELinux/domain restrictions have either been bypassed, changed by policy, or are otherwise not the blocker being studied. This repository is about what the loader/injector does after that point.
- For a modern zygote or
system_serverlifecycle model, study ReZygisk before old BinderJack-style examples. Its interesting part is not the module API; it is the early zygote tracer, custom loader, and hooks around app/system-server specialization. - For late injection into an already-running, attachable process, study AndKittyInjector. It is a more complete modern ptrace-based injector than the older examples listed below.
- For linker namespace and symbol-resolution work after code is already inside the target process, study xDL.
- For linker-managed placement, do not skip
android_dlextinfo. TheANDROID_DLEXT_RESERVED_ADDRESS,ANDROID_DLEXT_RESERVED_ADDRESS_HINT, andANDROID_DLEXT_RESERVED_ADDRESS_RECURSIVEflags are the public Android interface for asking the linker to use caller-provided address space. - For true custom ELF loading, compare against Chromium/NDK Crazy Linker and
AOSP bionic's linker. For in-memory linker-mediated loading, compare against
LibcoreSyscall's
DlExtLibraryLoader. - For native or ART hooks after residency, study ShadowHook, ByteHook, and LSPlant. These are not process injectors by themselves.
- Treat injectvm-binderjack and simple ptrace examples as historical references. They are useful for understanding the idea, but they do not model modern Android linker namespaces, zygote timing, ART changes, or current policy boundaries well enough.
Think about process injection as a chain of separate problems:
- Target entry: how execution first crosses into the target process. Common families are late ptrace attach, early zygote tracing, preload/module frameworks, or already-running in-process code.
- Remote memory: how scratch memory, file bytes, and final executable mappings appear in the target address space.
- ELF loading: whether Android's linker performs
dlopen/android_dlopen_ext, or whether a custom loader mapsPT_LOADsegments and applies relocations itself. - Symbol discovery: how addresses are located in the target, usually from
/proc/<pid>/maps, ELF parsing, local-to-remote offsets for the same mapped object, linker metadata, or ART/runtime exports. - Execution handoff: whether the injected library starts through an
exported function,
JNI_OnLoad, a zygote module callback, a JNI native-method replacement, or an ART hook. - Managed-code bridge: native residency is not enough for DEX execution.
DEX bytes still need an ART-visible loading path, a recoverable
JavaVM *, a current-threadJNIEnv *, a class loader context, and compatibility with hidden API and process lifecycle restrictions.
The address where an injected shared library lands is determined by the loading path, not just by the fact that a remote call happened.
System linker path
When the target process executes dlopen or android_dlopen_ext, Android's
linker owns the final mapping choices, relocation processing, dependency
resolution, and constructor calls. A memfd-backed load changes the backing object
and avoids a normal filesystem path, but it does not by itself make the final
base address deterministic. ASLR, existing VMAs, linker namespace rules, and
architecture-specific loader behavior still matter.
This is the path used by many late injectors. For example, AndKittyInjector
resolves remote dlopen / __loader_dlopen and can use memfd plus
ANDROID_DLEXT_USE_LIBRARY_FD through android_dlopen_ext.
android_dlextinfo reserved-address path
android_dlopen_ext accepts Android-specific loading options through
android_dlextinfo. This is the important middle ground between "let the linker
choose everything" and "write a full custom ELF loader".
The placement-relevant fields are reserved_addr and reserved_size; they only
matter when paired with the reserved-address flags below.
This means a remote mmap call can matter to final library placement only if
the reserved region it creates is then handed to the target process linker via
android_dlopen_ext and android_dlextinfo. A scratch mmap used only for
strings, temporary buffers, or RPC arguments does not constrain where dlopen
maps the final shared object.
Injector process versus target process
Actions in the injector process do not directly reserve memory, change linker
state, or choose load addresses in the target process. Android processes have
separate virtual address spaces and separate dynamic-linker state. A local
dlopen / android_dlopen_ext experiment can help identify ABI, API-level
behavior, ELF size, dependency closure, SONAME, relocation style, and symbol
offsets for the same on-disk library, but it does not make the target process
pick the same base address.
The useful relationship is indirect:
- Static metadata can be learned locally. Segment spans, alignment,
DT_NEEDEDdependencies, exported symbol offsets, and RELRO size are properties of the ELF files. Those values can help estimate how large a target-side reservation would need to be. - Absolute addresses are process-local. A
dlsymresult in the injector is not a target address. At most, if the exact same library build is mapped in both processes, the symbol offset relative to that library's load bias can be reused after finding the target's own mapping. - Post-fork local loads do not propagate. Loading a library inside an app process after it forked from zygote affects that process only.
- Pre-fork zygote state can propagate. Mappings and reserved holes created
in zygote before fork can be inherited by app processes and
system_server. WebView's reserved-address/RELRO design relies on this kind of controlled lifecycle, not on a random injector process influencing another process after the fact. - Shared RELRO is explicit. A RELRO file created in one process is useful to
another only when the library and relevant dependencies are loaded at the same
addresses and the target load uses the matching
android_dlextinfoflags.
So the rule is: local actions can inform a target-side loading plan, and
pre-fork zygote actions can shape descendants, but target layout is affected
only by target-side mappings, inherited mappings, and the android_dlopen_ext
arguments actually used by the target process.
Other android_dlextinfo flags solve adjacent problems. Read them as linker
policy inputs, not as a generic remote-memory API:
| Flag | What it changes | Important caveat |
|---|---|---|
ANDROID_DLEXT_RESERVED_ADDRESS |
Requires the linker to load into the caller-supplied reserved range. | The range must already exist in the target process and must be large enough, or loading fails. |
ANDROID_DLEXT_RESERVED_ADDRESS_HINT |
Treats the caller-supplied range as preferred placement. | If the range is unsuitable, the linker can ignore the hint and choose another address. |
ANDROID_DLEXT_RESERVED_ADDRESS_RECURSIVE |
Extends reserved-address and RELRO behavior to newly loaded dependencies. | reserved_size must cover the main library plus new dependencies. Existing already-loaded dependencies are not included, so reproducibility depends on the target's prior load state. |
ANDROID_DLEXT_USE_LIBRARY_FD |
Reads the library from library_fd instead of opening it by path. |
The filename argument still identifies the library to the linker. |
ANDROID_DLEXT_USE_LIBRARY_FD_OFFSET |
Starts reading from library_fd_offset. |
Only valid together with ANDROID_DLEXT_USE_LIBRARY_FD; useful for embedded libraries such as uncompressed ZIP entries. |
ANDROID_DLEXT_FORCE_LOAD |
Avoids the normal already-loaded check based on stat(2). |
It can load another file with the same filename, but dependency resolution can still prefer the first library when DT_SONAME overlaps. It is not a placement control. |
ANDROID_DLEXT_USE_NAMESPACE |
Requests loading in library_namespace. |
Marked internal in the public docs because the NDK does not expose a namespace API. |
ANDROID_DLEXT_WRITE_RELRO |
Writes relocated GNU RELRO pages to relro_fd. |
Useful only when another process can later map the same library at the same address. Implies ANDROID_DLEXT_USE_RELRO. |
ANDROID_DLEXT_USE_RELRO |
Reuses identical relocated RELRO pages from relro_fd. |
Mostly useful for WebView-style shared-library layouts where address placement and dependency state are controlled. |
The recursive reserved-address mode is the closest public linker feature to
deterministic multi-library placement: newly loaded dependencies are placed
consecutively from reserved_addr in DT_NEEDED order. That determinism is
conditional. It depends on the reserved region being large enough and on the set
of dependencies that were not already loaded being stable.
RELRO sharing is not placement
ANDROID_DLEXT_WRITE_RELRO and ANDROID_DLEXT_USE_RELRO are often seen next to
reserved-address loading, but they solve a different problem. WRITE_RELRO
serializes the already-relocated GNU RELRO pages to relro_fd. USE_RELRO
later compares the target process's already-relocated RELRO pages with that file
and maps file-backed pages over the matching page ranges.
That means RELRO sharing depends on layout sameness; it does not create layout
sameness. If the library or its newly loaded dependencies land at different base
addresses, many RELRO pages can differ because they contain relocated absolute
pointers. The linker can then skip the non-matching pages and still complete the
load. So USE_RELRO by itself is not a strict deterministic-placement check and
not a way to force the linker to choose the original address.
The deterministic WebView-style pattern is the combination:
- reserve a sufficiently large address range at a controlled lifecycle point;
- load once with
ANDROID_DLEXT_RESERVED_ADDRESS,ANDROID_DLEXT_RESERVED_ADDRESS_RECURSIVE, andANDROID_DLEXT_WRITE_RELRO; - load later with the same reserved range and
ANDROID_DLEXT_USE_RELRO; - keep the set and order of newly loaded dependencies consistent.
In that pattern, reserved-address loading is the placement mechanism and RELRO is the memory-sharing/result-reuse mechanism. RELRO can reveal partial sameness because identical pages are remapped from the file, but it should not be treated as the thing that makes the layout deterministic.
Choosing a reserved range
There is no universal "close to 100%" fixed start address for a late arbitrary target. The reliable primitive is not guessing a magic hole; it is creating the reservation in the target address space and then giving the exact returned range to the target linker.
For one already-running target process, the most reliable model is:
- create a target-side anonymous
PROT_NONEreservation large enough for the intended linker-managed load; - let the target kernel choose the address rather than hard-coding one;
- keep that reservation alive until
android_dlopen_extconsumes it throughreserved_addr/reserved_size; - treat the returned address as valid only for that target process and that moment in time.
That can make the range "available now" in the target, but it does not make the address deterministic across different processes or launches. If the requirement is the same address across a family of processes, the stronger design is pre-fork reservation in zygote or another controlled lifecycle point. That is why WebView reserves in zygote instead of trying to discover a perfect hole after every app process already exists.
Picking a fixed address by reading /proc/<pid>/maps and looking for a large
gap is only a heuristic. It can work in quiet processes, but it has race and
portability problems: other target threads can allocate between observation and
reservation, ART/JIT/native allocations can change the VMA layout, ASLR policies
vary, and 32-bit processes have much less room. If fixed placement must be
attempted, a non-clobbering reservation primitive is preferable. On Linux,
MAP_FIXED_NOREPLACE was added for this kind of atomic "use this exact range or
fail" behavior; older kernels may not honor it in the same way, so robust code
must verify that the returned address is actually the requested address. Plain
MAP_FIXED is dangerous for this purpose because overlap can discard existing
mappings.
"Big enough" also has a precise meaning. For the main ELF, start with the span
of all PT_LOAD segments after page alignment, not the file size. Bionic's
linker computes this load span from the program headers before reserving address
space. On newer devices, page-size migration and 16 KiB compatibility can add
padding. For ANDROID_DLEXT_RESERVED_ADDRESS_RECURSIVE, the reservation must
also cover every dependency that will be newly loaded, in the order the linker
will load them. Dependencies already present in the target do not consume that
reserved range, which is another reason that reproducibility depends on the
target's prior linker state.
On 64-bit, oversized reservations are often practical enough that production code can reserve generously; current WebView uses a large zygote reservation for that reason. On 32-bit, large reservations are much more likely to fail or starve the process, so a precise dependency/load-span estimate matters more.
Known public uses of reserved-address flags
The canonical production use is Android WebView, not a public injector. WebView reserves address space in the zygote, creates a RELRO file by loading the native library into that region, and later loads the same library in WebView processes with the same reserved region plus the RELRO file. Current AOSP loader code uses combinations of:
ANDROID_DLEXT_RESERVED_ADDRESSANDROID_DLEXT_RESERVED_ADDRESS_RECURSIVEANDROID_DLEXT_WRITE_RELROANDROID_DLEXT_USE_RELROANDROID_DLEXT_USE_NAMESPACE
That is exactly the reliability model these flags were designed for: controlled
multi-process loading where the loader controls the reserved address range,
namespace, RELRO file, and dependency set. It is not a general guarantee that an
arbitrary late injector can force deterministic placement with one remote
mmap.
Effects and implications of the WebView pattern
WebView is useful to study because it shows the intended end-to-end contract for reserved-address plus RELRO loading:
- The reservation is inherited, not guessed. The zygote creates a large
anonymous
PROT_NONEreservation before forking app processes. Descendants inherit the same virtual address hole, so later WebView loads can ask the linker to consume that known range. This is much stronger than scanning a post-fork process for a convenient gap. - RELRO is generated from an address-specific load. A separate RELRO creator
process loads the WebView library into the reserved range with
ANDROID_DLEXT_WRITE_RELRO. The generated file contains relocated read-only pages whose contents only match future loads if the relevant layout and dependency state match. - Later app loads reuse, not relocate-from, the RELRO file. App processes
load the same library with the same reservation and
ANDROID_DLEXT_USE_RELRO. The linker still performs a real load and relocation; the RELRO file lets it replace matching read-only relocated pages with file-backed shared pages. - The practical win is memory sharing and predictable layout. Many apps use WebView, so sharing identical relocated RELRO pages reduces per-process memory cost. Predictable placement is what makes that sharing possible.
- The cost is address-space commitment. The reserved hole must be large enough for the main library and newly loaded dependencies. Android can reserve generously on 64-bit, but the same idea is much more constrained on 32-bit.
- The dependency set matters. Recursive reserved-address loading applies to newly loaded dependencies. If a dependency is already loaded in one process but not another, the reserved-range consumption and RELRO contents can differ.
- Failure degrades the optimization or the strict load. If strict reserved placement cannot be satisfied, the linker-managed load can fail. If RELRO pages do not match, RELRO sharing is reduced or skipped for those pages rather than becoming a magic placement fix.
For injection research, the implication is specific: if code is running only in a normal post-fork app process, it cannot make unrelated target processes inherit that app's local reservation. To get WebView-like determinism across future targets, the reservation has to be created in a common ancestor such as zygote, or each target must be handled independently with its own target-side reservation.
Bionic's own dlext_test.cpp also exercises the behavior directly:
RESERVED_ADDRESS loads inside a pre-reserved range, RESERVED_ADDRESS_HINT
falls back when the range is unsuitable, too-small strict reservations fail, and
recursive reserved-address loads are tested with dependencies and RELRO sharing.
Chromium's historical Crazy Linker is adjacent but not the same interface: it
implemented its own fixed-address loading and RELRO sharing before modern
WebView/Trichrome paths relied more on android_dlopen_ext. The idea is similar:
prearrange enough address space so multiple processes can load the same native
code layout and share RELRO pages.
In the public injector-style projects checked for this repository, this is less
common. AndKittyInjector's memfd path uses ANDROID_DLEXT_USE_LIBRARY_FD, not
the reserved-address flags. LibcoreSyscall's DlExtLibraryLoader builds an
android_dlextinfo and exposes extension flags, but its main documented value is
in-memory linker-mediated loading, not a WebView-style deterministic reserved
layout. ReZygisk goes the other direction with custom loader behavior rather
than relying on android_dlextinfo reserved placement.
In other words: android_dlextinfo can affect the target layout, but it affects
the linker's mapping decision. It does not retroactively change the behavior of
an unrelated remote allocation.
Remote syscall or RPC path
Remote calls, ptrace register manipulation, and remote syscalls are transports.
They can allocate scratch buffers or reserve address ranges in the target, but
if the final handoff is still plain dlopen, the linker still controls the
library mapping. To make the remote allocation influence a linker-managed load,
the loader path must carry that reserved region through android_dlextinfo.
This is why "I can call remote mmap" and "I can choose exactly where the final
.so loads" are different claims.
Custom loader path
A custom ELF loader can make placement more deterministic because it can reserve
an address range and map PT_LOAD segments relative to a chosen load bias. That
comes with real complexity: relocations, imported symbols, TLS, init arrays,
memory protections, RELRO, namespace assumptions, and Android/architecture
differences. ReZygisk's remote CSOLoader path is a much more relevant modern
example of this direction than older "just call remote dlopen" projects.
Be strict about terminology. A real custom ELF loader is not just "load bytes
from memory" and is not just a wrapper around dlopen. In the strict sense it
does most of the linker's job itself:
- parse ELF headers and program headers;
- reserve address space and map
PT_LOADsegments with the right protections; - parse
PT_DYNAMIC; - load or locate dependencies;
- resolve imported symbols;
- apply
REL,RELA,RELR, PLT/GOT, and architecture-specific relocations; - set final memory protections, including RELRO where relevant;
- run preinit/init/init-array code in the correct order;
- bridge into JNI when the payload expects
JNI_OnLoad.
Useful examples fall into three different buckets:
| Project | Category | Why it matters |
|---|---|---|
| Chromium / NDK Crazy Linker | Strict custom linker, historical | Maps shared libraries itself, supports fixed-address and file-offset loads, handles dependencies/relocations, and implements RELRO sharing. Old, but one of the best public Android custom-linker references. |
ReZygisk remote_csoloader.c |
Purpose-built remote loader | Not a general replacement for Android's linker, but relevant because it shows custom loading as part of zygote/process residency instead of a normal local dlopen. |
LibcoreSyscall DlExtLibraryLoader |
In-memory, linker-mediated loader | Strong modern reference for loading a .so from memory using Java/native-memory tricks and android_dlopen_ext. It is useful, but it is not a full custom linker because Android's linker still performs the final load/relocation work. |
| AOSP bionic linker | Reference implementation | Not custom, but this is the behavior custom loaders are trying to reproduce or intentionally avoid: segment mapping, relocation, namespaces, TLS, constructors, RELRO, and linker bookkeeping. |
Python2 can appear at runtime if it is frozen into a native executable or if a
CPython runtime is embedded. It can also use native ELF libraries or bindings,
such as libelf/elfutils-style parsers, if those dependencies are bundled or
otherwise loadable in the target process. So the language is not the deciding
factor.
The deciding factor is where the real loader semantics live. If frozen Python
only coordinates native helpers, calls a bundled C extension, or hands bytes to
Android's linker, it is a Python-fronted loader or orchestrator. If the embedded
runtime itself parses the ELF, chooses mappings, drives target mmap/mprotect
or equivalent primitives, applies architecture relocations, handles symbol
resolution, and runs init/JNI entry points inside the target process, then it is
fair to call it a Python-implemented custom loader.
In practice, C/C++, Java/Kotlin with native memory primitives, or a small assembly/shellcode stub are more common for the in-target loader core because they avoid shipping a large interpreter, reduce dependency/bootstrap problems, and make architecture-specific relocation, TLS, cache maintenance, and signal or thread-state constraints easier to control. Python or Python2 is more often seen around the loader as tooling, a packer/generator, an exploit orchestrator, or a frozen carrier.
The quick filter is simple: if a project does not map PT_LOAD, parse
PT_DYNAMIC, resolve symbols, apply relocations, set segment protections, run
init arrays, and handle JNI_OnLoad when JNI is expected, it is probably not a
strict custom Android ELF loader. It may still be a very useful dlopen,
android_dlopen_ext, memfd, in-memory loading, or hook framework.
Do not assume that a symbol address from the injector process is valid in the target process. The common pattern is:
- find the library mapping in the target process;
- identify the equivalent local library or parse the target ELF directly;
- compute symbol offsets relative to the mapped object, then rebase those offsets onto the target mapping;
- account for Android version, APEX paths, linker namespace behavior, stripped symbols, and architecture.
For ART/JNI work, modern projects tend to avoid the old direct
android::AndroidRuntime::mJavaVM assumption. A more portable in-process pattern
is to recover a JavaVM * through exported JNI/ART entry points when available,
then obtain a JNIEnv * for the current thread with GetEnv or
AttachCurrentThread. Another valid pattern is to run at a lifecycle point where
Android passes a current-thread JNIEnv * into the hook or callback.
AndKittyInjector, for example, looks for JNI_GetCreatedJavaVMs in the target's
libart.so and then invokes JNI_OnLoad if it can recover a valid JavaVM.
For linker namespace and symbol discovery once code is resident, xDL is a useful source-level reference because it contains Android-version-specific handling for linker internals and caller-address-sensitive loads.
Writing DEX bytes into a target process is only storage. It is not the same as loading Java/Kotlin code into ART.
A managed-code bridge normally needs:
- native code already running in the target process;
- a valid
JavaVM *plus aJNIEnv *for the current attached thread; - a compatible class loader strategy, such as a process-appropriate loader or an in-memory loader on Android versions that support it;
- awareness of hidden API restrictions, process classpath, target UID/domain, and whether the process is pre-specialization, post-specialization, or already running normal framework code.
For zygote and system_server, the lifecycle point matters more than the DEX
container. Code that runs before specialization has different visibility,
permissions, file descriptors, mount namespace, and classloader assumptions than
code injected into a long-running process.
| Project | What to study | Why it matters |
|---|---|---|
| ReZygisk | Early zygote tracing, custom remote loader, zygote JNI hooks, app/server specialize callbacks | Best modern public source for understanding zygote and system_server lifecycle injection concepts. |
| AndKittyInjector | Late ptrace attach, remote function/syscall calls, memfd + android_dlopen_ext, JNI_OnLoad handoff |
Stronger modern baseline for direct injection into an already-running attachable process. Its current memfd path uses ANDROID_DLEXT_USE_LIBRARY_FD; reserved-address android_dlextinfo behavior is a separate placement concern. |
| Chromium / NDK Crazy Linker | Custom ELF mapping, dependency loading, relocation, fixed-address loading, RELRO sharing | Best historical public Android custom linker to compare against when evaluating "custom loader" claims. |
LibcoreSyscall DlExtLibraryLoader |
In-memory .so loading, Java native-memory primitives, android_dlopen_ext, manual JNI_OnLoad |
Useful modern in-process loader reference; not a remote injector and not a strict custom linker. |
| AOSP bionic linker | Android's real linker implementation | Reference behavior for namespaces, relocation, TLS, RELRO, constructors, and linker-managed mapping. |
| xDL | Linker namespace bypass concepts, enhanced dlopen / dlsym, dl_iterate_phdr handling |
Useful after residency for locating libraries and symbols under modern Android linker behavior. |
| ShadowHook | Inline native hooks | Post-injection native instrumentation, not target-entry by itself. |
| ByteHook | PLT/GOT hooks and dlopen monitoring |
Post-injection hook management and dynamic library load monitoring. |
| LSPlant | ART method hooking | Managed/ART behavior after native code is already resident. |
| AndroidPtraceInject | Educational ptrace mmap + dlopen flow |
Good for learning the classic shape, weak as a modern Android design. |
| injectvm-binderjack | Historical system_server experiment, AndroidRuntime::mJavaVM, Binder swapping |
Valuable historical context, but not a modern baseline. |
This section is intentionally source-level and comparative. It is meant to help readers understand project design choices and prerequisites, not to provide an ordered privileged-process injection recipe.
ReZygisk
- Target-entry trick: monitors process birth from a root/module context, traces
initfork/exec events, detectsapp_process/ zygote startup, then hands the new zygote process to an ABI-matching tracer. - Memory/loading trick: maps its zygisk library during early process startup
using a custom remote CSOLoader path. This is closer to a purpose-built ELF
mapper than to a simple remote
dlopen. - RPC trick: uses ptrace register control,
process_vm_readv/process_vm_writev, remote syscalls, and a controlled return/fault point to regain tracer control after target-side execution. - ART/JNI trick: once resident, it initializes JNI hooks from inside the process.
It looks up
JNI_GetCreatedJavaVMs, obtainsJavaVM, callsGetEnvfor the current thread, and also hooks zygote native methods where Android naturally passes a current-threadJNIEnv *. - Lifecycle trick: hooks variants of
nativeForkAndSpecialize,nativeSpecializeAppProcess, andnativeForkSystemServer, giving it pre/post app and server specialization callbacks. - Main prerequisites: root/module environment, ability to trace
init/zygote, compatible ABI, writable module files, SELinux policy support, and ability to restart or catch zygote early enough.
AndKittyInjector
- Target-entry trick: attaches to an already-running target PID with
PTRACE_SEIZEwhere possible, falling back toPTRACE_ATTACH. - Memory/loading trick: uses remote scratch memory and remote calls into the
target linker. It can load by path, or copy the library into a target-side
memfd and invoke
android_dlopen_extwithANDROID_DLEXT_USE_LIBRARY_FD. - RPC trick: builds remote function calls by saving target registers, placing arguments in target registers/stack according to ABI, setting PC to the target function, waiting for completion, then restoring state.
- Symbol trick: scans the target maps/ELF objects, resolves
dlopen,android_dlopen_ext,dlsym,dlclose, and linker symbols in the target address space instead of reusing local addresses directly. - ART/JNI trick: after the library is mapped, it finds
JNI_GetCreatedJavaVMsin targetlibart.so, remote-calls it to recoverJavaVM, then calls the injected library'sJNI_OnLoad(JavaVM *, key). The library can then obtain its own current-threadJNIEnv *through normal JNI rules. - Main prerequisites: native code execution as a process that may ptrace the target, matching 32/64-bit ABI, target readable maps/memory, target linker symbols available enough for the chosen path, SELinux/domain permission, and a target process that can safely be interrupted.
injectvm-binderjack
- Target-entry trick: classic ptrace attach to a target such as
system_server, followed by remote calls into target libc/linker functions. - Memory/loading trick: allocates target memory with remote
calloc, writes strings/parameters, then remote-callsdlopen. - VM trick: resolves
android::AndroidRuntime::mJavaVMfromlibandroid_runtime.soin the injector process, rebases that symbol into the target process, and passes the resultingJavaVM **to an exported payload function. - Managed-code trick: the payload calls
AttachCurrentThreadto obtain a current-threadJNIEnv *, creates or reuses a class loader, loads Java classes, and registers native methods for its Binder experiment. - Why historical: this depends on old framework internals, linker namespace assumptions, Binder object layout assumptions, and a rooted shell-style execution model. It is useful for learning, not as a modern baseline.
AndroidPtraceInject
- Target-entry trick: direct
PTRACE_ATTACHto a target PID. - Memory/loading trick: remote-calls
mmap, writes a library path into target memory, remote-callsdlopen, then optionally remote-calls a symbol resolved withdlsym. - ART/JNI trick: none built in. It is a native-loader example, not a modern ART/DEX bridge.
- Main prerequisites: ptrace permission, ABI compatibility, permissive-enough SELinux/domain state, a path the target linker can load, and tolerance for stopping the target process.
xDL, ShadowHook, ByteHook, LSPlant
- Target-entry trick: none. These libraries assume code is already resident in the process.
- xDL helps with linker namespace and symbol lookup once resident.
- ShadowHook and ByteHook help with native inline/PLT/GOT hooks once resident.
- LSPlant helps with ART method hooks once resident and initialized from a thread
that has a valid
JNIEnv *, plus ART symbol resolution support.
On modern Android, "find the Dalvik VM in memory" is usually the wrong mental
model. Modern Android uses ART, and JNIEnv * is thread-local. A stable global
JNIEnv * is not normally what these projects recover or store.
The usual patterns are:
- recover or receive a
JavaVM *, then callGetEnvorAttachCurrentThreadwhenever a specific thread needs its own validJNIEnv *; - run inside a native JNI method hook where Android already passed
JNIEnv *; - call
JNI_OnLoadin the injected library with a validJavaVM *, so the library can initialize using normal JNI conventions.
For DEX loading, native memory writes are only the first step. The target process
still needs an ART-visible loading path, a suitable class loader, and a thread
attached to the VM. BinderJack demonstrates the older PathClassLoader style;
modern in-memory loading or zygote-time loading has different constraints and
depends strongly on Android version, hidden API policy, and target process
lifecycle.
Physical access is not the core technical requirement. Equivalent post-chain execution context is.
For this document's baseline, assume the earlier chain has already produced the capabilities normally provided by adb/root shell, a Magisk/KernelSU/APatch module context, or a privileged native process: native execution, system/root-equivalent privilege, and enough SELinux/domain control to trace, map, write, or restart the processes being discussed. A non-physical case is therefore in scope only after the authorized exploit chain has already reached that state.
Important boundaries:
- If you deliberately narrow the assumption back down to only unprivileged
app-process RCE, the rest of these notes no longer apply directly: that state
is not enough to ptrace
zygote,system_server, or arbitrary higher-privileged processes on a production Android device. - Late ptrace injectors require permission to trace/read/write the target process and must match target ABI.
- Zygote lifecycle approaches assume root/module-level or equivalent control and are easier when the device can restart zygote or reboot.
- Libraries such as xDL, ShadowHook, ByteHook, and LSPlant do not solve target entry. They only become useful after native code is already resident.
Commercial Android spyware seen in public reporting should be compared as an end-to-end system, not as a standalone injector. The implant loader is only one stage. A typical remote 0-click or 1-click operation has at least four layers:
- Delivery and exploit chain: link, browser, messaging, network injection, or another remote vector that obtains initial code execution.
- Sandbox escape / privilege escalation: enough privilege to cross from the first compromised process into a useful Android security context.
- Loader / stager: code that prepares memory, process residency, IPC, and persistence conditions for the implant.
- Implant framework: long-lived surveillance logic, module loading, command/control, data collection, anti-forensics, and update handling.
That is why comparing an open-source injector directly to commercial spyware can be misleading. Projects like AndKittyInjector mainly model stage 3 after the operator already has equivalent local privilege. ReZygisk models a zygote-aware loader/lifecycle framework, but it assumes a module/root environment rather than bringing its own remote exploit chain.
Older commercial examples
- Hacking Team RCS Android is a useful historical comparison for older Android
tradecraft, but not because it is a modern zygote injector. Citizen Lab's
public analysis describes a malicious APK seeded through social links, a
legacy rooting exploit check, and a dropped privileged helper named
rilcapthat behaved like a hidden root command bridge. Its relevance here is the operator mindset: once code is on-device, the loader/helper exists to create durable privilege and broad control. It is less useful as a modern process-injection baseline. - Pegasus for Android / Chrysaor, as documented by Lookout and Google, is an example where the Android side differed from the iOS zero-day story. Public reporting said it used a known rooting technique and had a permissions-based fallback if rooting failed. For this repository, the lesson is that commercial Android implants often optimize for operational reliability across device states, not only for the most elegant memory loader.
- Cytrox / Intellexa Predator is the more relevant modern comparison. Google TAG reported exploit chains delivering ALIEN, a loader associated with PREDATOR, after browser and Android/kernel stages. TAG described ALIEN as living inside multiple privileged processes and receiving commands from PREDATOR over IPC. Cisco Talos later assessed ALIEN as more than a simple loader: it helped set up low-level capabilities used by the spyware and interacted with privileged Android process contexts.
Loader/injector lens
| Reported family | Publicly visible loader shape | Closest public research analogue | Important mismatch |
|---|---|---|---|
| Hacking Team RCS Android | APK-stage implant plus root/persistence helper; privilege is converted into command execution and filesystem/system control. | Classic Android root-era tooling, not a specific project in this repo. | It models old persistence/helper design more than cross-process ELF loading into ART-era privileged services. |
| Pegasus for Android / Chrysaor | Root attempt plus permissions fallback, with implant behavior continuing when full privilege is unavailable. | Access-model comparison only. | Public reporting is more useful for operational fallback design than for loader internals. |
| Cytrox / Intellexa Predator | ALIEN acts as loader/worker around PREDATOR, with privileged-process residency, binder/IPC coordination, and zygote/process-context awareness. | ReZygisk for lifecycle shape, AndKittyInjector for late native loading concepts, ByteHook/xHook-style libraries for in-process hooks. | Commercial chain includes remote exploit delivery, sandbox escape, privilege escalation, stealth, and implant orchestration that public research injectors deliberately do not implement. |
The most useful comparison is therefore architectural:
- Hacking Team-era Android tradecraft often looks like "gain root, drop helper, persist, expose privileged operations".
- Simple open-source injectors look like "given enough local privilege, map or
dlopenthis shared library in another process". - Predator-era reporting looks like "use exploit-chain output to place a loader inside privileged Android processes, then coordinate a modular implant around process privileges and IPC".
That last category is where ReZygisk is conceptually informative despite being a root/module project: it demonstrates why zygote timing, process specialization, JNI availability, and multi-process lifecycle hooks matter. It still does not replace the exploit-chain and privilege-transition layers needed for a remote 0-click or 1-click case.
How this maps to the open-source projects
| Commercial spyware need | Closest public research analogue | Gap |
|---|---|---|
| Remote delivery and initial RCE | None in this repo | This belongs to exploit-chain research, not ELF loader research. |
| Sandbox escape / LPE / SELinux-domain crossing | None directly | Public injectors usually assume this is already solved. |
| Late native library load into an attachable process | AndKittyInjector, AndroidPtraceInject | Requires ptrace/memory permissions and compatible ABI. |
| Zygote-aware process lifecycle control | ReZygisk | Assumes root/module control instead of a remote exploit chain. |
| Linker namespace and symbol discovery after residency | xDL | Does not provide target entry. |
| Native/ART hooks after residency | ShadowHook, ByteHook, LSPlant | Does not provide target entry or persistence. |
| Multi-process implant coordination | ReZygisk is conceptually closer than late ptrace injectors | Spyware implementations add stealth, IPC, update, collection, and anti-forensics. |
Remote 0-click / 1-click prerequisite reality
For a non-physical case, the loader is not the hard starting point. The hard starting point is obtaining a process and privilege state where the loader is allowed to do anything useful. Public reporting on commercial spyware repeatedly shows exploit chains combining browser or app RCE, sandbox escape, and Android kernel or privilege bugs before an implant loader can operate in privileged contexts. Without that earlier chain, a loader that works from adb/root or a Magisk-style module does not become remotely deployable.
Keep this distinction clear:
- Exploit chain: gets code execution and privileges.
- Loader/injector: places and starts code in the chosen process.
- Implant: performs long-lived collection, control, and update behavior.
This repository is mainly about the second layer and the Android runtime/linker internals around it.
It does make sense to compare modern iOS in-the-wild chains, but only at the architecture layer. The platform primitives are different enough that iOS code should not be treated as an Android implementation reference.
DarkSword is useful here because public reporting and the recovered source show the same high-level pattern seen in serious Android spyware:
- get initial browser/JIT/runtime execution;
- escape the first sandbox;
- obtain stronger process or kernel primitives;
- choose target processes for their security context;
- inject a higher-level runtime/payload into those processes;
- run task-specific modules from inside the chosen process context.
The closest Android analogy is not "DarkSword equals dlopen". The better
analogy is "DarkSword's loader layer equals a privileged-process residency and
runtime execution layer", similar in role to the ALIEN/PREDATOR relationship
reported on Android.
What DarkSword shows
- Runtime loading instead of only Mach-O loading:
pe_main.jsloads JavaScriptCore into a remote target, creates aJSContext, wires a native-call bridge, and evaluates injected JavaScript inside that target process. - Process-context selection: payloads are placed into processes such as
configd,wifid,securityd,UserEventAgent, and a main agent process because those processes have useful sandbox, entitlement, filesystem, or data-access properties. - Remote-call machinery: the code builds target-side function calls from Mach task/thread knowledge, exception-port handling, thread-state manipulation, PAC-aware address signing, and scratch memory in the remote task.
- Sandbox handoff:
launchdand sandbox-extension logic are used as part of moving useful access into remote tasks. - Staged chain design: Google GTIG reported DarkSword as a full iOS exploit chain, with WebKit/JavaScriptCore RCE, GPU-process and sandbox stages, XNU privilege escalation, and JavaScript final payloads.
How that differs from Android
| Question | Android focus in this repo | iOS DarkSword-style focus |
|---|---|---|
| Process primitive | ptrace, /proc/<pid>/maps, process_vm_*, SELinux/domain checks, zygote lifecycle |
Mach task/thread objects, exception ports, launchd, sandbox extensions, PAC-signed thread state |
| Loader/runtime | Android linker, android_dlopen_ext, custom ELF loader, ART/JNI/DEX bridge |
dyld/dlopen, JavaScriptCore injection, Objective-C runtime, remote JSContext execution |
| Address control | Linker choice, android_dlextinfo reserved ranges, or custom ELF mapping |
Mach VM mappings and task memory primitives; final payload may be JS rather than a native image |
| Managed-code bridge | Recover JavaVM *, obtain per-thread JNIEnv *, load DEX/classes, handle ART constraints |
Create or reuse JSC/Objective-C runtime objects and evaluate JavaScript in the selected process |
| Process choice | UID, SELinux domain, zygote/system_server/app lifecycle | sandbox profile, entitlement/data access, daemon role, launchd-managed process context |
So the comparison is valuable because it highlights what modern commercial chains optimize for: not just "load a library", but "turn exploit-chain output into a reusable remote-call/runtime substrate, then run modules inside processes whose existing privileges solve the data-access problem". That lesson transfers cleanly to Android. The API details do not.
These are useful starting points for reading the actual implementations:
- ReZygisk monitor and handoff from
init/app_process: monitor.c - ReZygisk early zygote injection and return-to-entry logic: ptracer.c
- ReZygisk custom remote ELF mapping path: remote_csoloader.c
- ReZygisk zygote JNI hooks for app and system-server specialization: jni_hooks.h
- ReZygisk
JavaVMrecovery and current-threadJNIEnvsetup before JNI hook setup: hook.c - Crazy Linker public API and feature summary: crazy_linker.h
- Crazy Linker
PT_LOADmapping and fixed-address load logic: crazy_linker_elf_loader.cpp - Crazy Linker relocation implementation: crazy_linker_elf_relocator.cpp
- Chromium notes on Android native libraries, Crazy Linker, and RELRO sharing: android_native_libraries.md
- LibcoreSyscall in-memory
android_dlopen_extloader: DlExtLibraryLoader.java - LibcoreSyscall ELF view and symbol parsing: ElfView.java
- LibcoreSyscall manual
JNI_OnLoadexample: TestNativeLoader.java - AOSP bionic linker segment mapping and relocation reference: linker_phdr.cpp and linker_relocate.cpp
- Bionic linker load-span calculation and caller-supplied reservation handling: linker_phdr.cpp
- Bionic GNU RELRO serialize/map behavior used by
WRITE_RELROandUSE_RELRO: linker_phdr.cpp - Linux
mmap(2)notes onMAP_FIXED,MAP_FIXED_NOREPLACE, and kernel-chosen addresses: mmap(2) - AOSP WebView Java-side address-space reservation and RELRO orchestration: WebViewLibraryLoader.java
- AOSP WebView native loader using reserved-address, recursive, namespace, and
RELRO flags with
android_dlopen_ext: native/webview/loader/loader.cpp - Bionic tests for reserved-address, hint, recursive, and RELRO-sharing behavior: dlext_test.cpp
- AndKittyInjector ptrace attach and ABI sanity checks: main.cpp
- AndKittyInjector remote linker symbol resolution: KittyInjector.cpp
- AndKittyInjector memfd +
android_dlopen_extpath: KittyInjector.cpp - AndKittyInjector target
JavaVMdiscovery andJNI_OnLoadhandoff: KittyInjector.cpp - BinderJack remote call primitive: inject.cpp
- BinderJack VM lookup rationale: README.md
- Google TAG on Cytrox/Predator Android exploit chains and ALIEN/PREDATOR: Protecting Android users from 0-Day attacks
- Cisco Talos on ALIEN/PREDATOR implant architecture: Mercenary mayhem
- Lookout on Pegasus for Android / Chrysaor: Pegasus for Android
- Citizen Lab on Hacking Team RCS Android: Hacking Team's Tradecraft and Android Implant
- Google GTIG on DarkSword's iOS exploit chain and payload staging: The Proliferation of DarkSword
- DarkSword recovered
pe_main.jsprocess/runtime injection layer: pe_main.js - xDL Android-version-specific linker internals: xdl_linker.c
- xDL caller-address-sensitive force-load path: xdl_linker.c
- ByteHook dynamic loader monitoring across Android versions: bh_dl_monitor.c
- Android NDK
android_dlextinfostructure and flag semantics: android_dlextinfo and Dynamic Linker flags - BinderJack historical
system_serverinjection path: inject.cpp
- Is the target already attachable, or do you need to win an earlier lifecycle point such as zygote startup?
- Does the design require deterministic base addresses, or only reliable symbol rebasing after load?
- If deterministic placement matters, is linker-managed reserved-address loading enough, or do dependencies/relocations force a custom loader design?
- Is
dlopenenough, or do linker namespace and backing-file constraints requireandroid_dlopen_ext, memfd, or a custom loader? - Are you trying to run native code only, or eventually bridge into ART/DEX?
- Which assumptions break across Android version, architecture, vendor build, APEX layout, and SELinux domain?
- Linkers & Loaders by John R. Levine (1999)
- Using procfs to execute ELF without touching the disk
- The Nexus between Static and Position Independent Code
- Enabling SHELF Loading in Chrome for fun and profit
- General Linux Process injection techniques
- ARM: SamyGOso Next-Gen
- Based on 2014 ARM: HideAndroidEmulator ADBI Hook System Call
- Reflective Injection for Linux
TODO: how likely is it that the process you wish to inject to has already ptraced (attached) itself?, what would you do in such scenario?
- Linux ptrace introduction AKA injecting into sshd for fun - XPN InfoSec Blog
- Linux Kernel Dirty COW PTRACE_POKEDATA Privilege Escalation - exploit database | Vulners.com
- Code search results on GitHub (ProcDump for Linux - ptrace)
- HookProcessEvent: PtraceInject.h at main · Jingle-BF/HookProcessEvent
- Code search results on GitHub (PTRACE_SETREGSET, NT_PRSTATUS, PTRACE_SETREGS, CPSR_T_MASK)
- W3ndige/linux-process-injection: Proof of concept for injecting simple shellcode via ptrace
- Ptrace pokedata Input/output error in memory injection - Stack Overflow
- Ptrace(PTRACE_PEEKDATA, ...) error: data dump - Stack Overflow Watch for ptrace alignment issues?
-
ElfMaster - ELF Internals projects (Injection, Patching etc.)
-
DEF CON 31 - Revolutionizing ELF binary patching w Shiva - ElfMaster
- Riru (C++)
- Riru Project
- Inject into zygote process (see also Zygisk project)
- Modern Linker JNI (Chromium)
- dlext.h
- dlext_test.cpp
- dlfcn.cpp
- WebView Java loader
- WebView native loader
- oat_file.cc