• Android安全专题(三)JNI混淆


    如果你不了解逆向,你都不知道别人回怎么破解你的应用,越是有价值的应用越需要增加一些防破解手段,否则能赚钱的事情都会变得不赚钱,jadx能破解大部分未加壳的应用,使用dexdump能实现脱壳。所以如果希望你的应用安全,那就不能只是一些简单的处理,而应该是组合拳。

    应用安全的演变

    安卓的应用安全是一个渐进的过程,最开始只是写一些简单的业务代码,这个时候不会过分考虑应用安全。过了一段时间,到了产品能挣钱的时候,就有人来破解。这个时候就会考虑做一些代码混淆,混淆分几个等级,最开始当然是用ide自带的proguard开启混淆模式。后面就会发现这个方法还是弱,于是就开始考虑增加一些垃圾代码来混淆视听,增加破解难度。结果发现破解的人是根据安卓代码的运行逻辑来索引的,通过application逐步定位就不会跑错。于是这种方法也不太安全了,这个时候就想着能不能增大混淆难度,于是吧java代码混淆成了00oo00或者特殊字符的样式。但是这样只是混淆了类和方法,破解的人通过关键字搜索还是能快速定位到核心代码所在的地方。

    这个时候如果把一些核心算法放在java层就不太安全了,于是就开始考虑放到C里面。很多公司做android开发并不一定有C语言开发的专业人才,因为多数情况下并不需要一个专门做C开发的,于是就学着写一些简单的jni代码,把核心代码写进去封装成动态库。这回想着一般的做逆向是不会去逆向so文件了吧,毕竟逆向C笔逆向java难多了。但是如果你的产品足够吸引力,也会有人去逆向so。特别是知道了ida这种工具后,逆向so变得容易很多。如果不做处理,代码看起来也是很容易。

    做JNI混淆的好处

    所以就需要知道怎么做JNI混淆了。那么,做了JNI混淆有什么好处呢?我们知道ida有一个方法搜索功能,通过搜索native方法名能快速所引到对应的jni方法,再配合f5就能看到代码执行逻辑。我放个图大家感受一下,是不是很直白?

    在这里插入图片描述

    怎么做JNI混淆

    JNI混淆的原理是通过JNI方法的动态注册,从而使native方法和jni方法不能一一匹配上,从而增加破解的难度。
    那这种动态是怎么实现的呢?其实就是在jni加载的时候判断是否是动态注册的jni方法,是的话做简单处理。具体到代码就是这样的:

    JNI_OnLoad:

    extern "C"
    jint JNI_OnLoad(JavaVM *vm, void *reserved) {
        JNIEnv *env;
        if (vm->GetEnv((void **) (&env), JNI_VERSION_1_6) != JNI_OK) {
            return -1;
        }
        if (!registerNatives(env)) {
            return -1;
        }
        return JNI_VERSION_1_6;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    然后实现registerNatives方法,在里面判断是不是动态注册的方法:

    registerNatives:

    // 指定要注册的类
    #define JNIREG_CLASS "com/android/studio/secure/DexSecure"
    
    extern "C"
    int registerNatives(JNIEnv *env) {
        if (!registerNativeMethods(env, JNIREG_CLASS, getMethods,
                                   sizeof(getMethods) / sizeof(getMethods[0]))) {
            return JNI_FALSE;
        }
        return JNI_TRUE;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    然后是具体的判断逻辑:

    extern "C"
    int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *getMethods,
                              int numMethods) {
        jclass clazz;
        clazz = env->FindClass(className);
        if (clazz == NULL) {
            return JNI_FALSE;
        }
        if (env->RegisterNatives(clazz, getMethods, numMethods) < 0) {
            return JNI_FALSE;
        }
        return JNI_TRUE;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这个getMethods里面保存的是动态注册方法的映射逻辑。这个是很多新手最头疼的地方,因为jni的数据类型不同于java也不同于C,他是android定义的规范,我这里是这样写的:

    // 第一个参数:Java层的方法名
    // 第二个参数:方法的签名,括号内为参数类型,后面为返回类型
    // 第三个参数:需要重新注册的方法名
    static JNINativeMethod getMethods[] = {
            {"decode", "([B)[B", (void *) encriptHttps}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    jni的数据类型

    这个里面难的是第二个参数,android官方给了一个符号表,大家可以参考一下:

    • 常用类型
    字符c/c++类型Java类型
    Vvoidvoid
    Zjbooleanboolean
    Ijintint
    Jjlonglong
    Djdoubledouble
    Fjfloatfloat
    Bjbytebyte
    Cjcharchar
    Sjshortshort
    • 复杂类型(数组)
      数组则以"["开始,用两个字符表示
    字符c/c++类型Java类型
    [IjintArrayint[]
    [FjfloatArrayfloat[]
    [BjbyteArraybyte[]
    [CjcharArraychar[]
    [SjshortArrayshort[]
    [DjdoubleArraydouble[]
    [JjlongArraylong[]
    [ZjbooleanArrayboolean[]

    上面的都是基本类型。如果Java函数的参数是class,则以"L"开头,以";" 结尾中间是用"/" 隔开的包及类名。而其对应的C函数名的参数则为jobject.

    举一个例子:Java是String类,其对应的类为jstring:

    Ljava/lang/String; String jstring
    // 其他的膜是一样的,都是L包名/类
    Ljava/net/Socket; Socket jobject
    
    • 1
    • 2
    • 3

    如果JAVA函数位于一个嵌入类,则用$作为类名间的分隔符。

    例如 "(Ljava/lang/String;Landroid/os/Utils$UtilsStatus;)Z"

    温馨提示:查表这种方法是比较笨的,像我通常就是通过IDE的自动补全功能实现,首先在设置关闭Clang Completion。然后在java native方法自动创建jni方法,在里面通过env->GetStaticMethodID的方式实现自动补全。这个时候就知道这第二个参数是什么了,复制过去,再把这个自动创建的方法删掉就行了。

    在这里插入图片描述

    在这里插入图片描述

    指定代码所在段

    由于在java层没有定义该函数,因此需要写到一个自定义的section里。,指定代码所在的段。在编译时,把该函数编译到自定义的section里。

    extern "C"
    __attribute__((section(".mysection")))
    JNICALL jbyteArray
    encriptHttps(JNIEnv *env, jclass clazz,
                 jbyteArray decrypt_string) {
        jbyteArray decodedBytes = decryptHttpBytes(env, decrypt_string);
        return decodedBytes;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    到这里,剩下来的就是具体的核心代码实现了。当然,这种知识增加一道关卡,其实防护力度还是比较弱的,但是比起不做保护有好了很多。

  • 相关阅读:
    源代码防泄密
    ARM64-内嵌汇编
    Redis(七)【持久化文件rdb&aof】
    2022-06-17 网工进阶(九)IS-IS-原理、NSAP、NET、区域划分、网络类型、开销值
    扬帆际海—为什么要做shopee跨境本土店
    香港第一金:美元指数昨晚拉高,不确定性加深金价下跌
    创建个人github.io主页(基础版)//吐槽:很多国内教程已经失效了
    windows MNN 的使用流程(Python版)
    GBASE 8A v953报错集锦23--多 sql 任务并发操作报错 get cluster task id fail
    魏副业而战:做闲鱼比打工强
  • 原文地址:https://blog.csdn.net/zhonglunshun/article/details/126020197