0x01 前言

如果你正在用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接口函数。在这篇文章中,我想分享一下我想出来的内容,以及我如何在这个方法上解决的问题。

0x02 标准方法

官方文档地址: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包。唯一的选择是设置jardexapk的完整路径,上述选项才起作用。奇怪的是,当在类路径中没有一个有效的文件时,系统类(如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"这样的路径。这有一个样例代码

0x03 UniccUnlock方法

你可以google搜索genius cyber-sleuth skills这种方法。这是一种从UniccUnlock.cpp中展示的另外一种创建虚拟机的类似技巧。我不敢说我完全理解了它在做什么,但get_transaction_code部分吸引了我。这是它的作用:

看起来字段值用于检查设备是否已经解锁,或者可能检查解锁方法是否正常工作。我不确定。我只是想提取可以用来创建虚拟机的代码。

该方法看起来很好:通过libnativehelper.solibdvm.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版本的不兼容性。

0x04 Surfaceflinger 方法

最终我在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

最后,我使用了这种的方法,因为它来自谷歌,因为我认为它兼容性和完整性是最好的。

0x05 最终代码

以下是创建虚拟机的最终的代码:

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

0x06 结论

现在,你有了Native代码实例化Java VM所需的一切。你还应该对你要使用的一些选项有一个完整的了解(或至少链接到文档)。要查看此技术的实际应用,请查看从命令行使用Java对象参数调用JNI函数

 

*作者:Caleb Fenton

源链接

Hacking more

...