1.文章难易度【★★★
★
★
】
2.文章作者:penguin_wwy
3.本文参与i春秋社区原创文章奖励计划,未经许可禁止转载
4.阅读基础:熟悉Android虚拟机源码、so加载过程、Native编程
【预备~~~起】
前几天有人在群里问,ELF的可执行文件能不能调用so文件的JNI_OnLoad函数。这倒是一个有脑洞的想法,我尝试了一夜,就把尝试的过程记录下来。
【一二三四】
先从理论上分析一下可能性,对于so文件我们在代码里是可以dlopen函数打开,然后dlsym函数定位so文件中的函数地址执行调用的。也就是说只要我们可以解决参数问题,调用so文件中的任意函数都是可以的。
这是jni.h中JNI_OnLoad函数的声明,对于第二个参数我们可以不管。第一个参数是一个虚拟机实例。在正常的APK调用so的过程中,Java层会将自己的JavaVM传递到JNI_OnLoad中,通过JavaVM对象中的函数表中的GetEnv,可以获得JNIEnv对象,JNIEnv对象的函数表中保存了我们在编写so代码中经常用到的函数如NewStringUTF,该函数接受一个char *字符串转化为jstring字符串。也就是说,如果我们要正确转化Java层和Native层互相传递的参数,或者正确使用他们之间的相互调用,JNIEnv对象不能少,而JNIEnv对象依托于JavaVM,JavaVM同样需要正确的存在(传递到JNI_OnLoad当中)。 搞清楚了这一点,我们就知道目前的问题了,因为要正确调用JNIEnv中的函数表,所以JavaVM必须正确传递。
我们必须要有一个正确的JavaVM对象,而这是一个虚拟机实例,难道我要创建一个虚拟机?
那能不能借用其他进程的JavaVM呢,应该是可以的,这样就相当于被借用进程的Java层加载了这个so文件。只要有足够的权限应该可以办到。然而我并没有采用这种办法。
既然不借用,那就只能自己创建一个虚拟机了。但是创建之前必须搞清楚一点,我们需要这个JavaVM对象干嘛?从前面的分析知道,这个JavaVM对象之所以不可或缺是因为我们需要JNIEnv的对象,而需要JNIEnv对象的目的是为了正确调用JNIEnv对象中的那些函数。本质上我们是在使用那些函数,JavaVM和JNIEnv只是调用那些函数的桥梁而已。我们似乎可以有这么一个猜想:既然Native层需要JavaVM只是为了通过它获得函数表中的函数,那如果我们创建一个空的JavaVM(以及JNIEnv),然后将需要调用的函数注册了,也就是将JNIEnv函数表中的函数指针指向我们自己的函数,这样在调用函数的时候也就能正确执行。如何佐证这点呢?事实上,不管是加载过程还是加载成功后我们在Native层写的代码,从来没有检查过JNIEnv对象的完整性,也就是Android系统根本不关心这个JNIEnv是不是一个真正的JNIEnv对象,理论上做一个空对象是可行的。
我们通过Android源码看一下JNI_OnLoad中JavaVM对象是怎么来的
gDvmJni是一个全局变量,保存了与虚拟机相关的设置信息
可以看到gDvmJni.jniVm保存的是pVM
上面是pVM的创建过程。仿照这个过程,便可以创建一个空的JavaVM对象
【二二三四】
根据上面的分析,我们准备进行实验,验证之前的猜想。由上面得知,需要测试的是我们自己创建的空JavaVM对象能否顺利传入JNI_OnLoad并且通过本地的函数顺利执行。所以这里并没有直接用ELF加载so文件调用JNI_OnLoad,而是在APK中组建两个不同的so文件,取名为native-lib和main-lib,Java层通过API调用native-lib文件,然后在native-lib中创建空JavaVM对象,继而加载main-lib并调用。这样做一来可以利用AndroidStudio强大的调试能力,便于调试;二来可以调用Apk的JavaVM帮助我们实现部分功能。
MainActivity类,设置一个按钮,按下后调用native-lib中的strFromLib函数,该函数返回一个jstring,如果正确执行,则返回“jni_onload success”。
CMakeLists.txt中,准备两个so库文件
先看main-lib中的代码
gString是一个全局的字符串。如果执行成功,先调用NewStringUTF生成一个jstring,内容为jni_onload success,之后调用GetStringUTFChars转换成一个char *字符串,再拷贝到gString,然后通过ReleaseStringUTFChars将ptr收回。
JNI_OnLoad函数中调用了GetEnv,NewStringUTF,GetStringUTFChars,ReleaseStringUTFChars。这四个函数。
再看native-lib
为了方便之后的调用,准备一个新的结构,包含之后自己创建的JavaVM和JNIEnv,最后一个really变量是Apk中的真正的JNIEnv。ptr是newStruct对象的一个全局指针。
该函数用来创建我们之前分析提到的结构jniInvokeInterface和jniNativeInterface是JavaVM和JNIEnv中的函数表结构。这当中的函数指针设置为本地的函数,除了之前说到的四个函数,还有一个GetJavaVM
这五个函数完全按照jni.h中的声明实现,为了方便并没有完全自己实现,几个复杂的函数直接调用了really中对应的函数,毕竟我们的目的是看能否顺利执行,如何实现先不管。
然后调用JNI_OnLoad,并且获取全局变量gString,如果JNI_OnLoad执行成功,则返回正确的字符串“jni_onload success”
【三二三四】
之后看执行情况
从调试结果看,执行了我们自己定义的本地函数
最终结果成功。
【四二三四】
总结一下。最开始收到问题ELF文件能否调用so文件中的JNI_OnLoad函数。逐步分析:
(1)JavaVM参数必须传入
(2)传入的目的是为了JNIEnv中的functions,也就是函数表中的函数
(3)由(1)和(2)猜想是否只是需要函数表中的函数指针指向正确的地址,其余内容可以为空
(4)猜想(3)的佐证,加载过程和执行过程中没有对JNIEnv做完整性检查
(5)对猜想(3)进行验证,执行过程中成功调用了本地的函数
此外,在正常程序的调试过程中可以看到所有的函数指针指向的位置都在libdvm.so中,第一个参数都是JNIEnv。
在Dalvik虚拟机的源码解读中一直提到一个Java环境和JNI环境,其实所谓的环境最直观的便是能否直接使用相对应的函数指针。
2.文章作者:penguin_wwy
3.本文参与i春秋社区原创文章奖励计划,未经许可禁止转载
4.阅读基础:熟悉Android虚拟机源码、so加载过程、Native编程
【预备~~~起】
前几天有人在群里问,ELF的可执行文件能不能调用so文件的JNI_OnLoad函数。这倒是一个有脑洞的想法,我尝试了一夜,就把尝试的过程记录下来。
【一二三四】
先从理论上分析一下可能性,对于so文件我们在代码里是可以dlopen函数打开,然后dlsym函数定位so文件中的函数地址执行调用的。也就是说只要我们可以解决参数问题,调用so文件中的任意函数都是可以的。
[C++]
纯文本查看
复制代码
1
|
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm,
void
* reserved);
|
这是jni.h中JNI_OnLoad函数的声明,对于第二个参数我们可以不管。第一个参数是一个虚拟机实例。在正常的APK调用so的过程中,Java层会将自己的JavaVM传递到JNI_OnLoad中,通过JavaVM对象中的函数表中的GetEnv,可以获得JNIEnv对象,JNIEnv对象的函数表中保存了我们在编写so代码中经常用到的函数如NewStringUTF,该函数接受一个char *字符串转化为jstring字符串。也就是说,如果我们要正确转化Java层和Native层互相传递的参数,或者正确使用他们之间的相互调用,JNIEnv对象不能少,而JNIEnv对象依托于JavaVM,JavaVM同样需要正确的存在(传递到JNI_OnLoad当中)。 搞清楚了这一点,我们就知道目前的问题了,因为要正确调用JNIEnv中的函数表,所以JavaVM必须正确传递。
我们必须要有一个正确的JavaVM对象,而这是一个虚拟机实例,难道我要创建一个虚拟机?
那能不能借用其他进程的JavaVM呢,应该是可以的,这样就相当于被借用进程的Java层加载了这个so文件。只要有足够的权限应该可以办到。然而我并没有采用这种办法。
既然不借用,那就只能自己创建一个虚拟机了。但是创建之前必须搞清楚一点,我们需要这个JavaVM对象干嘛?从前面的分析知道,这个JavaVM对象之所以不可或缺是因为我们需要JNIEnv的对象,而需要JNIEnv对象的目的是为了正确调用JNIEnv对象中的那些函数。本质上我们是在使用那些函数,JavaVM和JNIEnv只是调用那些函数的桥梁而已。我们似乎可以有这么一个猜想:既然Native层需要JavaVM只是为了通过它获得函数表中的函数,那如果我们创建一个空的JavaVM(以及JNIEnv),然后将需要调用的函数注册了,也就是将JNIEnv函数表中的函数指针指向我们自己的函数,这样在调用函数的时候也就能正确执行。如何佐证这点呢?事实上,不管是加载过程还是加载成功后我们在Native层写的代码,从来没有检查过JNIEnv对象的完整性,也就是Android系统根本不关心这个JNIEnv是不是一个真正的JNIEnv对象,理论上做一个空对象是可行的。
我们通过Android源码看一下JNI_OnLoad中JavaVM对象是怎么来的
gDvmJni是一个全局变量,保存了与虚拟机相关的设置信息
可以看到gDvmJni.jniVm保存的是pVM
上面是pVM的创建过程。仿照这个过程,便可以创建一个空的JavaVM对象
【二二三四】
根据上面的分析,我们准备进行实验,验证之前的猜想。由上面得知,需要测试的是我们自己创建的空JavaVM对象能否顺利传入JNI_OnLoad并且通过本地的函数顺利执行。所以这里并没有直接用ELF加载so文件调用JNI_OnLoad,而是在APK中组建两个不同的so文件,取名为native-lib和main-lib,Java层通过API调用native-lib文件,然后在native-lib中创建空JavaVM对象,继而加载main-lib并调用。这样做一来可以利用AndroidStudio强大的调试能力,便于调试;二来可以调用Apk的JavaVM帮助我们实现部分功能。
[Java]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
public
class
MainActivity
extends
Activity {
static
{
System.loadLibrary(
"native-lib"
);
}
@Override
public
void
onCreate(Bundle saveInstanceState) {
super
.onCreate(saveInstanceState);
setContentView(R.layout.main_activity);
Button lib = (Button)findViewById(R.id.libnative);
Button main = (Button)findViewById(R.id.main);
final
TextView mTextView = (TextView)findViewById(R.id.textView);
mTextView.setText(mTextView.getText(), TextView.BufferType.EDITABLE);
lib.setOnClickListener(
new
View.OnClickListener() {
@Override
public
void
onClick(View view) {
mTextView.append(strFromLib() +
"\n"
);
}
});
main.setOnClickListener(
new
View.OnClickListener() {
@Override
public
void
onClick(View view) {
//mTextView.append(strFromMain() + "\n");
}
});
}
protected
native
String strFromLib();
|
MainActivity类,设置一个按钮,按下后调用native-lib中的strFromLib函数,该函数返回一个jstring,如果正确执行,则返回“jni_onload success”。
[Shell]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
add_library(
# Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
# Associated headers in the same location as their source
# file are automatically included.
src
/main/jni/native
.cpp )
add_library(
# Sets the name of the library.
main-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
# Associated headers in the same location as their source
# file are automatically included.
src
/main/jni/main
.cpp )
|
CMakeLists.txt中,准备两个so库文件
先看main-lib中的代码
[C++]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
|
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm,
void
* reserved) {
void
*env;
if
(vm->GetEnv((
void
**)&env, JNI_VERSION_1_4) == JNI_OK) {
//获取JNI_Env
jstring tmp= ((JNIEnv *)env)->NewStringUTF(
"jni_onload success"
);
//调用NewStringUTF
const
char
*ptr = ((JNIEnv *)env)->GetStringUTFChars(tmp, 0);
//调用GetStringUTFChars
strcpy
(gString, ptr);
((JNIEnv *)env)->ReleaseStringUTFChars(tmp, ptr);
//调用ReleaseStringUTFChars
}
return
JNI_VERSION_1_4;
}
|
gString是一个全局的字符串。如果执行成功,先调用NewStringUTF生成一个jstring,内容为jni_onload success,之后调用GetStringUTFChars转换成一个char *字符串,再拷贝到gString,然后通过ReleaseStringUTFChars将ptr收回。
JNI_OnLoad函数中调用了GetEnv,NewStringUTF,GetStringUTFChars,ReleaseStringUTFChars。这四个函数。
再看native-lib
[C++]
纯文本查看
复制代码
1
2
3
4
5
6
7
|
typedef
struct
NewStruct {
JavaVM *javaVM;
JNIEnv *jniEnv;
JNIEnv *really;
}newStruct;
newStruct *ptr;
|
为了方便之后的调用,准备一个新的结构,包含之后自己创建的JavaVM和JNIEnv,最后一个really变量是Apk中的真正的JNIEnv。ptr是newStruct对象的一个全局指针。
[C++]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
newStruct *newJavaVM() {
newStruct *ptr = (newStruct *)
malloc
(
sizeof
(newStruct));
memset
(ptr, 0,
sizeof
(ptr));
JavaVM *javaVM = (JavaVM *)
malloc
(
sizeof
(javaVM));
memset
(javaVM, 0,
sizeof
(javaVM));
JNIEnv *jniEnv = (JNIEnv *)
malloc
(
sizeof
(JNIEnv));
memset
(jniEnv, 0,
sizeof
(jniEnv));
JNIInvokeInterface *jniInvokeInterface = (JNIInvokeInterface *)
malloc
(
sizeof
(JNIInvokeInterface));
memset
(jniInvokeInterface, 0,
sizeof
(JNIInvokeInterface));
jniInvokeInterface->GetEnv = getEnv;
javaVM->functions = jniInvokeInterface;
JNINativeInterface *jniNativeInterface = (JNINativeInterface *)
malloc
(
sizeof
(JNINativeInterface));
memset
(jniNativeInterface, 0,
sizeof
(JNINativeInterface));
jniNativeInterface->NewStringUTF = newStringUTF;
jniNativeInterface->GetStringUTFChars = getStringUTFChars;
jniNativeInterface->ReleaseStringUTFChars = releaseStringUTFChars;
jniNativeInterface->GetJavaVM = getJavaVM;
jniEnv->functions = jniNativeInterface;
ptr->javaVM = javaVM;
ptr->jniEnv = jniEnv;
return
ptr;
}
|
该函数用来创建我们之前分析提到的结构jniInvokeInterface和jniNativeInterface是JavaVM和JNIEnv中的函数表结构。这当中的函数指针设置为本地的函数,除了之前说到的四个函数,还有一个GetJavaVM
[C++]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
jint getEnv(JavaVM *javaVM,
void
**env, jint version) {
*env = ptr->jniEnv;
return
JNI_OK;
}
//jstring (*NewStringUTF)(JNIEnv *, const char *);
jstring newStringUTF(JNIEnv *jniEnv,
const
char
*bytes) {
return
ptr->really->NewStringUTF(bytes);
}
const
char
*getStringUTFChars(JNIEnv *jniEnv, jstring string, jboolean *isCopy) {
return
ptr->really->GetStringUTFChars(string, isCopy);
}
void
releaseStringUTFChars(JNIEnv *jniEnv, jstring string,
const
char
*utf) {
ptr->really->ReleaseStringUTFChars(string, utf);
}
jint getJavaVM(JNIEnv *jniEnv, JavaVM** vm) {
*vm = ptr->javaVM;
return
0;
}
|
这五个函数完全按照jni.h中的声明实现,为了方便并没有完全自己实现,几个复杂的函数直接调用了really中对应的函数,毕竟我们的目的是看能否顺利执行,如何实现先不管。
[C++]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
/*
* 其实所有函数都位于libdvm.so中(Debug时通过地址可以看到)
* 但是每个函数都会调用一个JNIEnv,并不能保证函数内部没有调用这个JNIEnv对象
* 所以意味着要么自己实现,要么调用一个真正的JNIEnv
*
* */
ptr = newJavaVM();
ptr->really = really_env;
//对于没有办法自己写的函数,调用真正的JNIEnv的函数表中的函数填充
JNIEnv *env = ptr->jniEnv;
//使用自己构造的JNIEnv
void
*hand = (
void
*)dlopen(
"libmain-lib.so"
, RTLD_LAZY);
jint (*jni_onload)(JavaVM *,
void
*);
jni_onload = (jint (*)(JavaVM *,
void
*))dlsym(hand,
"JNI_OnLoad"
);
if
(jni_onload != NULL) {
void
*javaVM;
env->GetJavaVM((JavaVM **)&javaVM);
jni_onload((JavaVM *)javaVM, (
void
*)0);
char
*gString = (
char
*)dlsym(hand,
"gString"
);
if
(gString != NULL) {
return
env->NewStringUTF(gString);
}
}
return
env->NewStringUTF(
"jni_onload fail"
);
|
然后调用JNI_OnLoad,并且获取全局变量gString,如果JNI_OnLoad执行成功,则返回正确的字符串“jni_onload success”
【三二三四】
之后看执行情况
从调试结果看,执行了我们自己定义的本地函数
最终结果成功。
【四二三四】
总结一下。最开始收到问题ELF文件能否调用so文件中的JNI_OnLoad函数。逐步分析:
(1)JavaVM参数必须传入
(2)传入的目的是为了JNIEnv中的functions,也就是函数表中的函数
(3)由(1)和(2)猜想是否只是需要函数表中的函数指针指向正确的地址,其余内容可以为空
(4)猜想(3)的佐证,加载过程和执行过程中没有对JNIEnv做完整性检查
(5)对猜想(3)进行验证,执行过程中成功调用了本地的函数
此外,在正常程序的调试过程中可以看到所有的函数指针指向的位置都在libdvm.so中,第一个参数都是JNIEnv。
在Dalvik虚拟机的源码解读中一直提到一个Java环境和JNI环境,其实所谓的环境最直观的便是能否直接使用相对应的函数指针。
对Apk加固的意义,既然空的JavaVM在完善部分函数后就可以当作参数传入,那么意味着对于多个so文件的Apk,完全可以通过一个so加载其余so,并且通过自定义的JavaVM。在本地的函数中添加各种加密、hook等等手段达到加固的目的。
原文地址:http://bbs.ichunqiu.com/thread-16820-1-1.html