[IOV安全入门] 九. 手动注册native快速定位关键Smali代码方法

[IOV安全入门] 九. 手动注册native快速定位关键Smali代码方法,第1张

[IOV安全入门] 九. 手动注册native/快速定位关键Smali代码方法

欢迎新同学的光临
… …
人若无名,便可专心练剑


我不是一条咸鱼,而是一条死鱼啊!


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基本数据类型

Java TypeNative TypeDescriptionbooleanjbooleanunsigned 8bitsbytejbyteunsigned 8bitscharjcharsigned 16bitsshortjshortsigned 16bitsintjintsigned 32bitslongjlongsigned 64bitsfloatjfloat32bitsdoublejdouble64bitsvoidvoidN/A

PS:JNI_TRUE,JNI_FALSE是JNI中定义的宏,用于表示true/false

  • JNI引用类型
Java TypeNative TypeDescriptionObjectjobjectJava对象Classjclass类Stringjstring字符串xxxxjxxxxArray数组
Note:Java中的Array(如byte[], char[],int[],long[],object[])类型在JNI中对应jxxxxArray,xxxx是java中数组的类型,如jbyteArray,jcharArray,jintArray,jlongArray,jobjectArray
  • JNI引用类型的继承关系如下

  • 签名的基本数据类型以及数组和Array
类型标示Java类型ZbooleanBbyteCcharSshortIintJlongFfloatDdoubleLclass完全限定类[type数组类型type[](arg-type)ret-type函数类型

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种定位关键代码的方法:
  1. 信息反馈法( 资源id / 字符串 ):根据程序正常运行的提示信息进行定位,例如:错误提示,运行提示等等。可以在程序中直接搜索字符串,当提示信息在 String.xml 资源文件中的时候,可以根据 R.java 的映射文件,查找对应的资源id,然后在 smali 或者在 ida 窗口中进行搜索
  2. 特征函数法( api 函数 ):根据程序提示信息的方法,在对应的 Android 的 API 下断点,通过 API 来检索相关的代码
  3. 顺序查看法(分析程序执行流程 / 病毒分析):从 AndroidManifest.xml 中找到 主 Activity 界面,然后顺序分析代码,通常在分析病毒软件时使用。( 通过 UI 进行分析, adb shell dumpsys activity top )
  4. 代码注入法:( 动态调试 / 插入log / 查看logcat / 分析加解密 )。动态调试,又叫插桩,即在关键反汇编代码处插入可以输出 logcat 调试信息的代码
  5. 栈跟踪法( 动态调试 / 函数调用流程 ):栈跟踪法属于动态调试的方法,原理是输出运行时栈调用跟踪信息,然后查看函数调用序列来理解方法的执行流程
  6. 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


我自横刀向天笑,去留肝胆两昆仑


欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5686389.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存