如果你正在用Android应用程序的本地方法为Android编写native/JNI
代码,那么你会发现,这些JNI
方法都需要传递Dalvik VM
实例作为第一个参数。如,你需要用创建jstring
和其他的Java
对象、查找类或变量等。你从native
代码实例化VM
是不正常的,因为在大多数时候,如果你用Java Native Interface(JNI)接口从Java层调用Native
层中的代码时,你并不需要在Native
代码中自己初始化一个Dalvik VM
实例。但是,你在进行逆向或者写exploit时,你可能总是要钻研各种意想不到的情况。
我最近需要从native代码创建一个VM
,来将Java
对象参数传递给JNI
接口函数。在这篇文章中,我想分享一下我想出来的内容,以及我如何在这个方法上解决的问题。
官方文档地址:How to Create a JVM Instance in JNI。但是,这种方法在Android上面已经不能正常运行了,因为jint JNI_CreateJavaVM(JavaVM**, JNIEnv**, void*)
函数没有导出函数,无法直接调用。如果你不熟悉这个方法的话,名字是一个重要的线索,你可以根据它的名字在你的Android NDK目录下的jni.h
文件中查看自己的函数有没有导出。我的jni.h
文件位于android-sdk/ ndk-bundle/platforms/android-9/arch-x86/usr/include/jni.h
。相关代码:
#if 0 / *在实践中,这些不会被NDK导出,所以不要声明它们* / jint JNI_GetDefaultJavaVMInitArgs(void*); jint JNI_CreateJavaVM(JavaVM**, JNIEnv**, void*); jint JNI_GetCreatedJavaVMs(JavaVM**, jsize, jsize*); #endif
如果你尝试编译代码,你可能会看到下面的错误:
warning: implicit declaration of function 'JNI_CreateJavaVM' is invalid in C99 [-Wimplicit-function-declaration]
官方文档中仍然可以帮助我们了解API函数以及它们的选项和参数的作用。但是,如果我们要在Android中使用这个方法,我们必须明确从某个库中调用加载这个方法。
该代码显示的是一个重要的细节是如何设置VM的类路径。内容如下:
JavaVMOption jvmopt[1]; jvmopt[0].optionString = "-Djava.class.path=" + ".";
这会将类路径设置为当前目录(.
)。如果你希望VM访问系统或者应用程序的类,你就必须这样设置的。一些早期实验表明,将该值设置到目录不会起作用。我尝试设置它为/data/local/tmp
,并放置了一个DEX
文件,这含有DEX
文件和APK
文件的jar
包。唯一的选择是设置jar
、dex
或apk
的完整路径,上述选项才起作用。奇怪的是,当在类路径中没有一个有效的文件时,系统类(如java.lang.String
)都不能访问。也就是说,除非类路径中至少有一个文件,否则(*env)->FindClass(env, "java.lang.String")
返回0,即使在框架中定义了java.lang.String
类都无法访问。
我们测试一下,将一个apk
文件push
到模拟器或设备中。
adb push shim_app.apk /data/local/tmp
将其用于你的JavaVMOption
:
JavaVMOption opt[2]; opt[0].optionString = "-Djava.class.path=/data/local/tmp/shim_app.apk"; opt[1].optionString = "-agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y"; // ... args.nOptions = 2;
你现在应该能够使用FindClass
函数加载系统和应用程序的类了。
如果你需要在你的虚拟机中加载本地库,例如在静态初始化器中加载一个库文件,你可以使用optionString = "-Djava.library.path=/data/local/tmp"
这样的路径。这有一个样例代码。
你可以google搜索genius cyber-sleuth skills这种方法。这是一种从UniccUnlock.cpp中展示的另外一种创建虚拟机的类似技巧。我不敢说我完全理解了它在做什么,但get_transaction_code
部分吸引了我。这是它的作用:
看起来字段值用于检查设备是否已经解锁,或者可能检查解锁方法是否正常工作。我不确定。我只是想提取可以用来创建虚拟机的代码。
该方法看起来很好:通过libnativehelper.so
或libdvm.so
作为备份来加载相关的VM创建功能。然而,下面几行代码看起来很奇怪:
JniInvocation_ctor = (JniInvocation_ctor_t) dlsym(libnativehelper, "_ZN13JniInvocationC1Ev"); JniInvocation_dtor = (JniInvocation_dtor_t) dlsym(libnativehelper, "_ZN13JniInvocationD1Ev"); JniInvocation_Init = (JniInvocation_Init_t) dlsym(libnativehelper, "_ZN13JniInvocation4InitEPKc");
我没有找到这些功能的任何文档说明。不过想到了这些调用方法的人真聪明的。如果不调用这些函数,你就会得到下面奇怪的错误信息:
W/dalvikvm(5395): No implementation found for native Landroid/os/SystemProperties;.native_get: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; W/dalvikvm(5395): No implementation found for native Landroid/os/SystemProperties;.native_get: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; W/dalvikvm(5395): Exception Ljava/lang/UnsatisfiedLinkError; thrown while initializing Landroid/os/Build; A/libc(5395): Fatal signal 11 (SIGSEGV) at 0x0000000c (code=1), thread 5395 (create_vm)
除了这些奇怪的功能,使用这种方法对我来说很有用。但是,我想了解_ZN13JniInvocationC1Ev
功能做了什么和Android不同版本间是否可移植。我的直觉告诉我,该方法中的硬编码可能会导致在某些设备或者Android版本的不兼容性。
最终我在google到的Surfaceflinger
服务的源码中找到了我想要代码:DdmConnection.cpp。
默认查找了在libdvm.so
中的函数JNI_CreateJavaVM
。而不是调用_ZN13JniInvocation
方法,它看起来像屎调用了libandroid_runtime.so库
中的Java_com_android_internal_util_WithFramework_registerNatives
方法。registerNatives
方法的内容在此描述了。
而且它创建VM的选项很有趣:
opt.optionString = "-agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y";
这些选项在这篇文档中详细描述了。根据文档,它只允许调试JVM
时使用。似乎相当标准。
我注意到它JNI的版本是1_4
,但是我设置为1_6
了,因为样例代码中就是这样设置的。以下是jni.h
中支持的版本号:
#define JNI_VERSION_1_1 0x00010001 #define JNI_VERSION_1_2 0x00010002 #define JNI_VERSION_1_4 0x00010004 #define JNI_VERSION_1_6 0x00010006
最后,我使用了这种的方法,因为它来自谷歌,因为我认为它兼容性和完整性是最好的。
以下是创建虚拟机的最终的代码:
#include <dlfcn.h> #include <jni.h> typedef int (*JNI_CreateJavaVM_t)(void *, void *, void *); typedef jint (*registerNatives_t)(JNIEnv* env, jclass clazz); static int init_jvm(JavaVM **p_vm, JNIEnv **p_env) { //https://android.googlesource.com/platform/frameworks/native/+/ce3a0a5/services /surfaceflinger/DdmConnection.cpp JavaVMOption opt[4]; opt[0].optionString = "-Djava.class.path=/data/local/tmp/shim_app.apk"; opt[1].optionString = "-agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y"; opt[2].optionString = "-Djava.library.path=/data/local/tmp"; opt[3].optionString = "-verbose:jni"; // may want to remove this, it's noisy JavaVMInitArgs args; args.version = JNI_VERSION_1_6; args.options = opt; args.nOptions = 4; args.ignoreUnrecognized = JNI_FALSE; void *libdvm_dso = dlopen("libdvm.so", RTLD_NOW); void *libandroid_runtime_dso = dlopen("libandroid_runtime.so", RTLD_NOW); if (!libdvm_dso || !libandroid_runtime_dso) { return -1; } JNI_CreateJavaVM_t JNI_CreateJavaVM; JNI_CreateJavaVM = (JNI_CreateJavaVM_t) dlsym(libdvm_dso, "JNI_CreateJavaVM"); if (!JNI_CreateJavaVM) { return -2; } registerNatives_t registerNatives; registerNatives = (registerNatives_t) dlsym(libandroid_runtime_dso, "Java_com_android_internal_util_WithFramework_registerNatives"); if (!registerNatives) { return -3; } if (JNI_CreateJavaVM(&(*p_vm), &(*p_env), &args)) { return -4; } if (registerNatives(*p_env, 0)) { return -5; } return 0; }
下面是它的使用方法:
#include <stdlib.h> #include <stdio.h> JavaVM * vm = NULL; JNIEnv * env = NULL; int status = init_jvm( & vm, & env); if (status == 0) { printf("Initialization success (vm=%p, env=%p)\n", vm, env); } else { printf("Initialization failure (%i)\n", status); return -1; } jstring testy = (*env)->NewStringUTF(env, "this should work now!"); const char *str = (*env)->GetStringUTFChars(env, testy, NULL); printf("testy: %s\n", str);
现在,你有了Native
代码实例化Java VM
所需的一切。你还应该对你要使用的一些选项有一个完整的了解(或至少链接到文档)。要查看此技术的实际应用,请查看从命令行使用Java对象参数调用JNI函数。
*作者:Caleb Fenton,