0.一个so的加载流程
首先在java层肯定是要去调用so层文件,本篇重点讲解native层的加载流程,此处不再赘述。
1.了解dlopen函数/android_dlopen_ext()
“Dynamic Link” 动态装载库,我们会想到windows系统中,存在加载dll文件的动态加载类型,在linux中,类似的文件就是.so
文件了,而加载文件的重要函数就是dlopen
。
dlopen系列函数
dlopen:该函数将打开一个新库,并把它装入内存。该函数主要用来加载库中的符号,这些符号在编译的时候是不知道的。这种机制使得在系统中添加或者删除一个模块时,都不需要重新进行编译。
函数原型
1 |
|
第一个参数是头文件所在的文件名,也就是so文件,第二个标志参数有很多,例如有RTLD_NOW
和RTLD_LAZY
立即计算和需要时计算,以及RTLD_GLOBAL
使得那些在以后才加载的库可以获得其中的符号。方式就是dlopen返回的句柄作为dlsym()的第一个参数,获取符号在库中的地址。
android_dlopen_ext()函数
此函数作为安卓平台上特有的函数 是dlopen函数的拓展版本,进一步增强了dlopen函数的功能
函数原型
1 |
|
可以看到在原函数基础上增加了ext_data参数
参照源码可以大致了解增强的功能,其中大部分功能与第二个参数flag中的一些功能标志位相关联
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
dlopen函数后的流程
2.do_dlopen 函数
此版本为安卓4源码分析,最新版本的安卓号的加载流程在代码量上有进一步的提升,但是基础原理类似。
上面的dlopen函数只是一个引子,真正的核心功能代码实现在do_dlopen函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
此函数的类型为soinfo
指针,soinfo
代表的含义是“进程加载的so链”,其中包含了已经被加载的so 的信息。这里所返回的值也是si——进程加载的so链。
其中涉及的主要两步函数为find_library
和CallConstructor
下面会继续介绍。
3.find_library函数
find_library函数传入参数后也会进行进一步的函数调用,流程见注释
1 2 3 4 5 6 7 8 9 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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
|
load_library()函数加载【待补充】
涉及知识点:elf文件格式,文件分区的加载
4.call_constructors 函数
此函数根据上面的soinfo链接映像函数分析的section动态节区中的信息,获取共享库依赖的所有的so文件名,所有的依赖库初始化完成后,执行init_func、init_array方法初始化该动态库。
1 2 3 4 5 6 7 8 9 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 35 36 37 38 39 40 41 |
|
5.init 和 init_array函数
函数简介
这两个函数是so文件在被加载或者卸载时自动执行的函数,用于初始化的操作,其中init函数优先于init_array函数。作为so层加载很早的函数,可以通过实现hook他来绕过一些关键的检测点。
也正是因为加载过早且初始化后只加载一次,我们如果直接去hook是无法get到的,通过上面的流程我们知道了这两个函数是在call_constructors中进行加载,我们就可以通过逆向hook相关的native函数进行加载hook。通过在android_dlopen_ext
加载过程中进行hook操作。
关键突破口
这里有一步很关键的操作,关于call_constructors 函数,call_constructors
是在共享库加载时被调用的函数。意思就是他是存储在安卓本机中的本地链接库函数,而他的位置文件具体就在/system/bin/linker64
中,我们将他从手机上pull下来进行反编译并查找相关函数可以看到。
反编译的结果与我们上文看到的函数功能接近。
1 2 3 4 5 6 7 8 9 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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
|
hook init/init_array思路
通过hookandroid_dlopen_ext
定位我们要hook的so文件,通过hook linker64
中的 call_constructors
函数可以修改init和init_array的流程。
这里需要注意的是关于偏移地址的查找,可以通过pie,ida等工具 或者在linux环境中执行命令
1 2 3 4 |
|
1 2 3 4 5 6 7 8 9 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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
|
6.JNI注册方法流程
理解静态注册与动态注册
静态注册
通过 JNIEXPORT 和 JNICALL 两个宏定义声明,在虚拟机加载 so 时发现上面两个宏定义的函数时就会链接到对应的 native 方法。
动态注册
通过 RegisterNatives 方法手动完成 native 方法和 so 中的方法的绑定,这样虚拟机就可以通过这个函数映射表直接找到相应的方法了。
流程如下。
通过RegisterNatives(JNI *env ,jclass clazz ,const JNINativeMethord *methord ,jint nmethods)
函数,参数分别代表:java环境,java类的描述符,待注册的方法集合,待注册方法数量。
其中“待注册方法集合”是名为JNINativeMethord
的结构体,内容如下
1 2 3 4 5 |
|
这里的第二个参数为方法签名需要注意,类型为字符串,由一对小括号和若干签名符号组成,其中括号内写传入参数的签名符号,没有参数则不写,括号外写返回参数的签名符号。
签名符号 | C/C++ | java |
---|---|---|
V | void | void |
Z | jboolean | boolean |
I | jint | int |
J | jlong | long |
D | jdouble | double |
F | jfloat | float |
B | jbyte | byte |
C | jchar | char |
S | jshort | short |
[Z | jbooleanArray | boolean[] |
[I | jintArray | int[] |
[J | jlongArray | long[] |
[D | jdoubleArray | double[] |
[F | jfloatArray | float[] |
[B | jbyteArray | byte[] |
[C | jcharArray | char[] |
[S | jshortArray | short[] |
L+完整包名+类名 | jobject | class |
实例:Java层函数String getText(int a,byte[] b)
就是(I[B)Ljava/lang/String;
1 2 3 4 5 6 7 8 9 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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
可以看出,在动态加载的过程中,很关键的方法就是流程图中的RegisterNatives
在这其中的参数有很多设置方法名的敏感信息。
通过hook查看动态注册
1 2 3 4 5 6 7 8 9 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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
|
通过hook查看静态注册
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|