欢迎新同学的光临
… …
人若无名,便可专心练剑
我不是一条咸鱼,而是一条死鱼啊!
0x01 前言
手动注册native的方法在实际情况中不太常见到,因为它的安全措施不是很强大,但也可以起到一定的防护作用。
-
so加载流程
-
加载so,主要分为两个步骤find和load,一个是来自classLoader构造函数传进的libraryPath,一个是来自java.library.path这个环境变量的值
-
环境变量的值大部分情况下是在/vendor/lib , /system/lib 下:
libraryPath 主要来自两个方向:一个是 data 目录下 app-lib 中安装包目录,比如:/data/app-lib/com.test-1,另一个方向就是来自于 apkpath+”!/lib/”+primaryCpuAbi 的地址了,比如:/data/app/com.test-1.apk!/lib/arm64-v8a -
System.loadLibrary()加载so文件流程
- 先读取so文件的.init_array段
- 再执行JNI_OnLoad函数
- JNI_ONLoad是.so文件的初始函数
- 然后调用具体的native方法
void *dlopen(const char * pathname,int mode); //打开动态库 void *dlsym(void *handle,const char *name); //获取动态库对象地址 char *dlerror(vid); //错误检测 int dlclose(void * handle); //关闭动态库
- android上层Java代码分析,将以上方法都做好封装,只需要一行代码就即可完成动态库的加载过程
System.load("/data/local/tmp/libtest_jni.so"); System.loadLibrary("test_jni");
-
以上两个方法都用于加载动态库,区别如下:
- 加载的路径不同:System.load(String filename)是指定动态库的完整路径名;而System.loadLibrary(String libname)则只会从指定lib目录下查找,并加上lib前缀和.so后缀
- 自动加载库的依赖库的不同:System.load(String filename)不会自动加载依赖库;而System.loadLibrary(String libname)会自动加载依赖库
-
java如何调用so库里的方法
JNI:Java Native Interface的缩写,用Java调用so库就叫着JNI- 调用方法:
//在Java中申明一个Native方法 public static native String securityCheck(Context context, String str); //用System.loadLibrary()加载so库 全称是 libnative-lib.so static { System.loadLibrary("native-lib"); } //native-lib注意看, `在lib文件夹下面找到的对应的文件是:libnative-lib.so`
- JIN和SO关系
函数名称和so文件里面对应
Java_com_yaotong_crackme_MainActivity_securityCheck()
-
格式
Java_类名_方法名() -
不正常,函数名称对应不一致
-
.so文件里对应的函数名称不一致,是手动注册native方法,函数对应的名称是在JNI_onLoad()函数里注册
Android中当程序在Java层运行System.loadLibrary("jnitest")、System.load(),System.loadLibrary() 是我们在使用Java的JNI机制时,通常都会放在static {}中执行,会用到的一个非常重要的函数,它的作用即是把实现了我们在Java code中声明的native方法的那个libraryload进来,或者load其他什么动态连接库这行代码后,在这里是代表程序会去载入libjnitest.so文件
此时Load事件触发后,程序默认在载入的.so文件的函数列表中查找JNI_OnLoad函数并执行
当载入的.so文件被卸载时,Unload事件被触发,此时,程序默认会去在载入的.so文件的函数列表中查找JNI_OnUnload函数并执行,然后卸载.so文件
注:JNI_OnLoad与JNI_OnUnload函数在.so组件中并不是强制要求的,用户也可不去实现,Java代码一样可调用到C组件中的函数,之所以在C组件中去实现这两个函数(特别是JNI_OnLoad函数),将JNI_ONLoad看成是.so组件的初始化函数,当其第一次被装载时被执行(Window下的dll文件也可类似的机制,在_DLL_Main()函数中,通过一个swith case语句来识别当前是载入或卸载)
将JNI_OnUnload函数看成是析构函数,当其第一次被装载时被执行(Window下的dll文件也可类似的机制,在_DLL_Main()函数中,通过一个swith case语句来识别当前是载入或卸载)
将JNI_OnUnload函数看成是析构函数,当其被卸载时被调用,就不难明白为什么很多jni C组件中会实现JNI_OnLoad这个函数了
一般情况下,在C组件中的JNI_OnLoad函数用来实现给VM注册接口,以方便VM可以快速地找到Java代码需要调用的C函数(JNI_OnLoad函数另外一个功能,就是告诉VM此C组件使用那一个JNI版本,如果未实现JNI_OnLoad函数,则默认是JNI 1.1版本)
应用层的Java类通过VM而调用到native函数,一般情况下是VM去寻找.so里的native函数,如果调用多次,那么每次都需要寻找一遍,会浪费查询时间,影响体验以及消耗服务器性能,那么C语言组件开发人员可将本地函数向VM进行注册,来加快后续调用native函数的效率,例如如果找到就直接使用,如果未找到,就再通过载入的.so文件中的函数列表中去查找,且每次Java调用native函数重复相同的流程。所以,才通过在.so文件载入初始化时,即JNI_OnLoad函数中,先将native函数注册到VM的native函数链表中,方便每次Java调用native函数时都会在VM中的native函数链表中找到对应的函数,加快查询效率
ida动态调试时,常常需要将断点下在init,initArray或者JNI_OnLoad函数
注册native方法的实现方法:
- 静态注册
- 动态注册
JNIEXPORT void JNICALL Java_com_example_plasma_PlasmaView_bug(JNIEnv * env, jobject obj, jobject bitmap, jlong time_ms)
1)一种是默认的,如JNIEXPORT void JNICALL Java_com_example_plasma_PlasmaView_bug(JNIEnv * env, jobject obj, jobject bitmap, jlong time_ms) ,JNI_onLoad 会自动识别该方法,从而进行注册,但这种方式虽然方便,但同时也让破坏者容易找到进攻的路口(IDA工具查看so文件的时候,在知道了Java层的native方法名和类型,去找到对应的native方法,定位到这个native函数;得到so文件之后,查看这个native方法的参数和返回类型(方法签名),然后在Java层写一个demo程序,然后构造一个和so文件中对应的native方法,就可执行这个native方法,如果有一个校验密码或者是获取密码的方法是个native的,那么这时候就会很容易被攻击者执行方法后获取结果)
手动注册比较方便的就是可以任意命令的注册JNI方法,只需要在native层的代码中调用如下三个函数:
- 第一个函数(JNINativeMethod):
JNI允许我们提供一个函数映射表,注册给Java虚拟机,这样JVM就可以用函数映射表来调用相应的函数。这样就可以不必通过函数名来查找需要调用的函数了。Java与JNI通过JNINativeMethod的结构来建立联系,它被定义在jni.h中,其结构内容如下:
typedef struct { const char* name; //name,代表的是Java中的函数名 const char* signature; //signature,代表的是Java中的参数和返回值 void* fnPtr; //fnPtr,代表的是的指向C函数的函数指针 } JNINativeMethod;
jniRegisterNativeMethods函数内部的实现:
//首先通过clazz = (env)->FindClass( className);找到声明native方法的类 //然后通过调用RegisterNatives函数将注册函数的Java类,以及注册函数的数组,以及个数注册在一起,这样就实现了绑定 static int jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { jclass clazz; LOGI("JNI","Registering %s nativesn", className); clazz = (env)->FindClass( className); if (clazz == NULL) { LOGE("JNI","Native registration unable to find class '%s'n", className); return -1; } int result = 0; if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) { LOGE("JNI","RegisterNatives failed for '%s'n", className); result = -1; } (env)->DeleteLocalRef(clazz); return result; }
- 第二个函数是jint JNI_onLoad(JavaVM* vm, void* reserved)
在使用System.loadLibarary()方法加载so库的时候,Java虚拟机会开始寻找JNI_OnLoad函数并调用该函数,该函数的作用是让Dalvik虚拟机知道,该C库使用的是哪一个JNI版本,如果开发人员自己的库里没声明JNI_onLoad()函数,VM会默认该库使用最老的JNI 1.1版本。但由于最新版本的JNI做了很多扩充,也优化了一些内容,如果使用JNI新版本的功能的话,就必须在JNI_onLoad()函数声明JNI的版本,同时也可在该函数中做一些初始化的动作,这个函数有点类似于Android中的Activity中的onCreate()方法,该函数前面也有三个关键字分别是JNIEXPORT,JNICALL ,jint,其中JNIEXPORT和JNICALL是两个宏定义,用于指定该函数时JNI函数,jint是JNI定义的数据类型,因为Java层和C/C++的数据类型或者对象不能直接相互的引用或者使用,JNI层定义了自己的数据类型,用于衔接Java层和JNI层
该函数在加载so的时候被调用,同时也可在这里获取JVM参数的,一般在这个函数中主要就是执行上面的注册函数功能,这里还需要获取一个JNIEnv*变量
PS:与JNI_onLoad()函数相对应的有JNI_onUnload()函数,当虚拟机释放的该C库的时候,则会调用JNI_onUnload()函数来进行善后清除工作
JNI_onLoad()函数会有两个参数,其中*jvm为Java虚拟机实例,JavaVM结构体定义一下函数,如下:
DestroyJavaVM AttachCurrentThread DetachCurrentThread GetEnv
JNI_onLoad()函数,举例:
# 加载so库 public class JniDemo1{ static { System.loadLibrary("samplelib_jni"); } } # 在jni中的实现 jint JNI_onLoad(JavaVM* vm, void* reserved)
并且在这个函数里面去动态的注册native方法代码如下:
#include#include "Log4Android.h" #include #include //主要模块是两个代码块,一个是if语句,一个是jniRegisterNativeMethods函数的实现 using namespace std; #ifdef __cplusplus extern "C" { #endif static const char *className = "com/gebilaolitou/jnidemo/JNIDemo2"; static void sayHello(JNIEnv *env, jobject, jlong handle) { LOGI("JNI", "native: say hello ###"); } static JNINativeMethod gJni_Methods_table[] = { {"sayHello", "(J)V", (void*)sayHello}, }; static int jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { jclass clazz; LOGI("JNI","Registering %s nativesn", className); clazz = (env)->FindClass( className); if (clazz == NULL) { LOGE("JNI","Native registration unable to find class '%s'n", className); return -1; } int result = 0; if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) { LOGE("JNI","RegisterNatives failed for '%s'n", className); result = -1; } (env)->DeleteLocalRef(clazz); return result; } jint JNI_OnLoad(JavaVM* vm, void* reserved){ LOGI("JNI", "enter jni_onload"); JNIEnv* env = NULL; jint result = -1; // 获取JNI环境对象 if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {// 调用了GetEnv函数时为了获取JNIEnv结构体指针,其实JNIEnv结构体指向了一个函数表,该函数表指向了对应的JNI函数,我们通过这些JNI函数实现JNI编程 return result; } // 调用了jniRegisterNativeMethods函数来实现注册,这里面注意一个静态变量gJni_Methods_table。它其实代表了一个native方法的数组,如果你在一个Java类中有一个native方法,这里它的size就是1,如果是两个native方法,size就是2 jniRegisterNativeMethods(env, className, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod)); return JNI_VERSION_1_4; } #ifdef __cplusplus } #endif
JVM来获取JNIEnv变量,然后调用注册函数,如:
methodsLenght = sizeof (methods) / sizeof(methods[0]); if ((*env)->RegisterNatives(env, clazz, methods, methodsLenght) < 0) { LOGD("RegisterNatives failed for '% s'", className); return JNI_ERR;
- 第三个函数是void JNI_OnUnload(JavaVM* vm, void* reserved),跟JNI_OnLoad对应的,是在so被卸载的时候调用
当GC回收了加载这个库的ClassLoader时,该函数被调用,该函数可用于执行清理 *** 作。由于这个函数是在未知的上下文中调用的,所以程序员在使用Java VM服务时应该保持谨慎,并且避免任意的Java回调
JNIEXPORT void JNI_onUnload(JavaVM* vm, void* reserved){ __android_log_print(ANDROID_LOG_INFO, "native", "JNI_OnUnload"); }
手动注册native三个函数就可以修改native层的函数名,不要写默认包名的那种格式了,增加逆向APK寻找关键native层函数的难度,增加安全性。
但一般打开so文件的时候,找不到对应的native方法(so ELF格式被破坏掉了),就会去找JNI_OnLoad函数,然后通过分析ARM汇编代码,找到register函数,分析注册方法结构体,找到对应的native方法,但有时候去JNI_OnLoad或init_array里面找不到,一般来讲这种情况是被加密了然后加载的时候再去init_array解密出来,难点去如何定位到对应init_array的位置。
PS:init_array段是在so加载的时候 执行的 执行顺序要优于 JNI_OnLoad
0x02 JNI规范定义的函数签名信息- 格式如下:
(参数1类型标示;参数2类型标示;参数3类型标示…)返回值类型标示
当参数为引用类型的时候,参数类型的标示的根式为"L包名",其中包名的.(点)要换成"/",看我上面的例子就差不多,比如String就是Ljava/lang/String,Menu为Landroid/view/Menu
-
JNI Type Signatures 类型签名如下:
JNI类型签名在很多地方需要用到,例如使用RegisterNatives函数注册函数时、使用GetFiledID/GetMethodID时。类型签名是JNI数据类型在JVM中的唯一标识符,使用类型签名可以区分函数形参、返回值,确定变量类型 -
JNI基本数据类型
PS:JNI_TRUE,JNI_FALSE是JNI中定义的宏,用于表示true/false
- JNI引用类型
Note:Java中的Array(如byte[], char[],int[],long[],object[])类型在JNI中对应jxxxxArray,xxxx是java中数组的类型,如jbyteArray,jcharArray,jintArray,jlongArray,jobjectArray
- JNI引用类型的继承关系如下
- 签名的基本数据类型以及数组和Array
PS:完全限定类如String,Integer的签名为Ljava/lang/String,Ljava/lang/Integer。函数的签名规则为圆括号内按形参顺序依次列出形参的签名列表,返回值签名紧跟圆括号后面
0x03 native代码反调用Java层代码- jclass FindClass(const char* clsName):
通过类的名称(类的全名,这时候包名不是用’".“点号而是用”/"来区分的)来获取jclass。比如:
jclass park_string=env->FindClass("java/lang/String");
来获取Java中的String对象的class对象
jclass GetObjectClass(jobject obj): 通过对象实例来获取jclass,相当于Java中的getClass()函数 jclass getSuperClass(jclass obj): 通过jclass可以获取其父类的jclass对象3.1 获取属性方法
Native访问Java层的代码,一个常用的常见的场景就是获取Java类的属性和方法。所以为了在C/C++获取Java层的属性和方法,JNI在jni.h头文件中定义了jfieldID和jmethodID这两种类型来分别代表Java端的属性和方法。在访问或者设置Java某个属性的时候,首先就要现在本地代码中取得代表该Java类的属性的jfieldID,然后才能在本地代码中进行Java属性的 *** 作,同样,在需要调用Java类的某个方法时,也是需要取得代表该方法的jmethodID才能进行Java方法 *** 作
-
常见的调用Java层的方法(一般是使用JNIEnv来进行 *** 作)如下:
- GetFieldID/GetMethodID:获取某个属性/某个方法
- GetStaticFieldID/GetStaticMethodID:获取某个静态属性/静态方法
-
方法的实现如下:
// JNIEnv代表一个JNI环境接口,jclass上面也说了代表Java层中的"类",name则代表方法名或者属性名。那最后一个char *sig代表什么?它其实代表了JNI中的一个特殊字段——签名 jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig); jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig); jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
- 常用的JNI中创建对象的方法如下:
jobject NewObject(jclass clazz, jmethodID methodID, ...)
比如,知道Java类中可能有多个构造函数,要指定调用某个构造函数的时候,会调用如下这个方法:
jmethodID mid = (*env)->GetMethodID(env, cls, "", "()V"); obj = (*env)->NewObject(env, cls, mid);
即把指定的构造函数传入进去即可,来看下上面代码的两个主要参数:
clazz:是需要创建的Java对象的Class对象 methodID:是传递一个方法ID,Java对象创建的时候,执行构造函数
上面说到,参数是个数组,如果参数不是数组怎么处理,jni.h同样也提供了一个方法,如下:
jobject NewObjectV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
PS:上面代码的方法和上面不同在于,这里将构造函数的所有参数放到在va_list类型的参数args中,该参数紧跟着放在methodID参数的后面
0x04 快速定位关键Smali代码- 6种定位关键代码的方法:
- 信息反馈法( 资源id / 字符串 ):根据程序正常运行的提示信息进行定位,例如:错误提示,运行提示等等。可以在程序中直接搜索字符串,当提示信息在 String.xml 资源文件中的时候,可以根据 R.java 的映射文件,查找对应的资源id,然后在 smali 或者在 ida 窗口中进行搜索
- 特征函数法( api 函数 ):根据程序提示信息的方法,在对应的 Android 的 API 下断点,通过 API 来检索相关的代码
- 顺序查看法(分析程序执行流程 / 病毒分析):从 AndroidManifest.xml 中找到 主 Activity 界面,然后顺序分析代码,通常在分析病毒软件时使用。( 通过 UI 进行分析, adb shell dumpsys activity top )
- 代码注入法:( 动态调试 / 插入log / 查看logcat / 分析加解密 )。动态调试,又叫插桩,即在关键反汇编代码处插入可以输出 logcat 调试信息的代码
- 栈跟踪法( 动态调试 / 函数调用流程 ):栈跟踪法属于动态调试的方法,原理是输出运行时栈调用跟踪信息,然后查看函数调用序列来理解方法的执行流程
- Method Profiling( 动态调试 / 热点分析 / 函数调用流程 ):动态调试方法。主要用于热点分析和性能优化。除了可以记录每个函数赵勇的CPU时间外,还可以跟踪所有的函数调用关系,并提供比栈跟踪法更详细的函数调用序列报告
参考链接:
https://www.codeleading.com/article/64015992699/
https://www.bilibili.com/video/BV1UE411A7rW?p=34
https://blog.csdn.net/m0_37344790/article/details/78658115
https://blog.csdn.net/u014635374/article/details/118529129
https://lonzoc.gitbooks.io/jni-devp-notes/content/JNIDataTypes.html
我自横刀向天笑,去留肝胆两昆仑
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)