脑洞型so加载过程实验

1.文章难易度【★★
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

### 回答1: Java类的加载过程可以分为以下几个步骤: 1. 通过类的全限定名在类路径中查找该类的class文件。 2. 如果class文件存在,Java虚拟机将其读入内存,并对其进行校验。校验的主要目的是验证class文件的内容是否符合Java语言的语法规则。 3. 如果class文件通过了校验,Java虚拟机将其转换为方法区中的数据结构,供程序员在运行时使用。 4. 如果class文件不存在,或者校验失败,Java虚拟机将抛出一个ClassNotFoundException异常。 在Java类加载过程中,Java虚拟机默认使用的是双亲委派模来管理类的加载。在这种模中,父类加载器会优先加载类,如果父类加载器无法加载该类,那么子类加载器再加载。这样的好处是可以防止类的重复加载,保证了Java类的单例性。 这个加载过程对于程序员透明,所以无需关心具体的加载过程,只需要关注Java类的使用即可。 ### 回答2: Java的类加载过程包括了加载、链接和初始化三个阶段。 1. 加载(Loading):将字节码文件读取到内存中,并创建一个对应的Class对象。这个过程是通过类加载器来实现的。 2. 链接(Linking):链接阶段主要包括了验证、准备和解析三个步骤。 - 验证(Verification):验证阶段会对加载的字节码进行一系列验证,包括语法验证、语义验证和字节码验证等,确保字节码的正确性和安全性。 - 准备(Preparation):准备阶段会为类的静态变量分配内存,并设置默认初始值。同时也会在方法区中创建常量池,并进行一些必要的静态符号的引用。 - 解析(Resolution):解析阶段会将符号引用替换为直接引用,即将类、方法、字段的引用都解析为实际内存地址的引用。 3. 初始化(Initialization):在初始化阶段,会执行类的初始化器<clinit>()方法,该方法由编译器自动生成,包含了类中静态变量的赋值和静态代码块的执行等。在这个阶段,会按照父类-子类的顺序来初始化类。 总结来说,Java的类加载过程就是通过类加载器将字节码文件加载到内存中,然后经过验证、准备和解析等链接阶段,最后进行初始化,执行类中的静态代码块和静态变量的赋值等操作。 ### 回答3: Java的类加载过程主要分为加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)和初始化(Initialization)五个步骤。 加载阶段是指将类的字节码文件读入内存,并将其转换成相应的数据结构。对于由类加载器所加载的每个类,Java虚拟机都会生成一个唯一的Class对象,用来表示这个类。 验证阶段是为了确保类的字节码符合Java虚拟机的规范,包括检查文件格式、语义一致性、字节码验证等。 准备阶段是为类的静态字段分配内存,并为它们设置初始值。在这个阶段,Java虚拟机会为每个字段分配内存空间,并根据字段的类设置默认值。注意,这里只分配了内存空间,并没有实际初始化字段的值。 解析阶段是将常量池中的符号引用转换为直接引用,即找到对应的内存地址。这个阶段通常在初始化之前执行,它的目的是为了准备执行初始化阶段所需要的信息。 初始化阶段是类加载的最后一步,主要是对静态变量进行初始化,执行静态代码块中的代码。在这个阶段,Java虚拟机会按照顺序执行静态变量的赋值操作和静态代码块中的代码。 需要注意的是,类的加载过程是按需进行的,即在第一次使用这个类之前才会触发其加载过程。同时,类加载是按照委托机制进行的,即先将请求委派给父类加载器,只有在父类加载器找不到对应的类时才会由当前类加载器进行加载。 总的来说,Java的类加载过程包括加载、验证、准备、解析和初始化五个阶段,它们按需触发,有严格的顺序,是Java虚拟机保证类加载的正确性和安全性的重要机制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值