ELFLoaderARM is an Android native module that loads an embedded ARM64 ELF image inside a selected application process through Zygisk. The module is implemented in C++17 and built with Android NDK ndk-build.
The loader is intended for controlled research and development environments where the target process, payload, and device are owned or explicitly authorized by the operator.
At runtime, the Zygisk module checks the app process name, starts a loader thread only for the configured target process, decodes the embedded payload from data.h, maps the ELF image manually, applies relocations, runs initialization routines, and transfers execution to the payload entry point.
The current source is configured for:
| Item | Value |
|---|---|
| Platform | Android / Zygisk |
| Architecture | ARM64 (arm64-v8a) |
| Language standard | C++17 |
| Build system | Gradle + Android NDK ndk-build |
| Android Gradle Plugin | 8.7.0 |
| NDK version | 24.0.8215888 |
| Compression | LZMA via bundled static liblzma.a |
| Payload storage | app/src/main/jni/data.h |
| Target process setting | pname in app/src/main/jni/URL.h |
Socket_Moduleis registered with Zygisk byREGISTER_ZYGISK_MODULE.onLoadstores the Zygisk API handle and Java VM reference.preAppSpecializecompares the processnice_nameagainstpname.postAppSpecializestartsload_elf_threadonly when the process matches.- The loader waits until
bin/linker64appears in/proc/self/maps. - The embedded
chdatabyte array is decrypted withxor_cipher. - The decrypted buffer is decompressed with
decompress_lzma. load_elfvalidates the ELF header, reserves memory, mapsPT_LOADsegments, handlesPT_TLS, resolves dynamic symbols, applies supported ARM64 relocations, and runs ELF init callbacks.- The loader calls the ELF entry point and then attempts to resolve and call
_Z6awakenv. - After loading completes, the module requests
DLCLOSE_MODULE_LIBRARYthrough the Zygisk API.
The manual loader supports the parts of the ELF format that are required by the embedded ARM64 payload:
PT_LOADsegment mapping with page alignment and final memory protections derived from ELF program header flags.PT_TLSallocation and thread-pointer setup for AArch64 throughTPIDR_EL0.- Dynamic dependency loading from
DT_NEEDEDentries usingdlopen. - Symbol lookup through the loaded image's dynamic symbol/string tables, falling back to
dlsym(RTLD_DEFAULT, ...). - Relocation handling for
R_AARCH64_RELATIVE,R_AARCH64_GLOB_DAT,R_AARCH64_JUMP_SLOT,R_AARCH64_ABS64, andR_AARCH64_IRELATIVE. - Execution of
DT_PREINIT_ARRAY,DT_INIT, andDT_INIT_ARRAYcallbacks before jumping to the entry point. - Optional finalization support through
DT_FINI_ARRAYandDT_FINIinunload_elf.
The loader does not implement a complete Android dynamic linker. Payloads should be tested against the exact relocation types, dependencies, TLS usage, and initialization behavior required by this implementation.
The payload is compiled into the module as char chdata[] in app/src/main/jni/data.h. The runtime expects this array to contain an LZMA-compressed ELF image encrypted with the same transform implemented by xor_cipher.
The loader currently decrypts with:
xor_cipher(elf_data, OBFUSCATE("System.Reflection"), false);When replacing the payload, generate bytes with the matching compression and encryption pipeline, then replace only the contents of chdata.
One supported preparation workflow is:
- Encrypt and compress the ARM64 ELF payload with FileCompressor.
- Convert the generated encrypted file to a C byte array with a file-to-hex converter such as tomeko.net's file to hex tool.
- Replace the
chdataarray inapp/src/main/jni/data.hwith the generated bytes.
app/src/main/jni/URL.h contains runtime configuration values:
const char* durl = OBFUSCATE("...");
const char* pname = OBFUSCATE("...");pname is used to restrict loading to a single process name. durl and the JNI HttpURLConnection helper are present in the source, but the current loader path uses the embedded data.h payload instead of downloading a payload.
Prerequisites:
- Android Studio or a Gradle environment capable of building Android projects.
- Android SDK with compile SDK 35.
- Android NDK 24.0.8215888.
- Static
liblzma.afor each enabled ABI underapp/src/main/jni/libraries/<abi>/.
Build the project with:
./gradlew assembleReleaseThe native module is built by app/src/main/jni/Android.mk as the shared library module zygisk. The Gradle configuration currently enables only arm64-v8a.
Copy the produced native shared object into the zygisk/ directory of a Magisk/Zygisk module using the ABI filename expected by Zygisk, typically:
zygisk/arm64-v8a.so
Deploy only to devices and applications where you have authorization to run injected native code.
LDEBUGis disabled by default. Define it inmain.cppto enable verbose Android log output.checkc()reads/system/etc/hostsand prevents loading if specific blocked strings are found.get_random_mem_size()and the JNI URL fetch helper are present but are not part of the current embedded-payload loading path.- The code uses obfuscated string macros and optional LLVM obfuscation flags in
Android.mk; those flags are currently commented out. - This repository contains bundled third-party components, including Zygisk API headers and liblzma headers/static libraries. Review their licenses before redistribution.
This project is licensed under the GNU General Public License v3.0. See LICENSE for the full license text.