zoukankan      html  css  js  c++  java
  • jni 文件加密

      开发过程中常常涉及加密,一般直接在java层对参数进行加密,当app被反编译时,对方可以拿到我们的代码,可以看到我们加密的方式从而让对方找到破解密文的方法,很不安全;

      那么是否可以防止这种反编译的破解呢,所以便有了在c层处理加密的方法,通过jni将加密方法打包到so库中,可以防止对方反编译看到我们的加密条件,但是这样也不安全,对方只需要反编译apk后得到 应用的包名 你的so库 你的native方法,就可以创建包名相同方法名相同的一个应用,把so放进去,然后就可以绕过密钥检查去调用你的接口,所以我们还需要在so库中加入签名验证,当调用加密方法对操作参数的时候,验证此时应用签名是否是我们本应用的,如果不是,则表示当前应用是伪应用,签名和包名必须得要一致,就算遇到逆向工程师,要破解我们的app也是有一定难度了

      作为一个初学jni的猿类,注释一般比较多,也没使用第三方c库,当自己练手

      androidstudio编写c还是挺方便的,jni使用 CMakeLists 构建,CMake是一种跨平台编译工具,比make更为高级,使用起来要方便得多。CMake主要是编写CMakeLists.txt文件,然后用cmake命令将CMakeLists.txt文件转化为make所需要的makefile文件,最后用make命令编译源码生成可执行程序或共享库

    新建一个C++project,你会发现自动给你配置好了,可以直接运行,里面有个默认的方法,我们可以依照此为基础,省去一些小麻烦

    此处项目为Kotlin,不是Java,流程大致相似

    可以看到在project中有个cpp文件夹,里面就是编写c层代码的,在MainActivity里面加载了这个库文件,调用了c库中的方法,这只是官方自动生成的一个例子,这里就可以直接去创建文件编写加密方法了

    1.新建一个Encrypt.cpp文件,编写加密解密方法

    2.在 CMakeLists.txt 文件中加入cpp文件

    3.在应用层定义好加密解密方法,调用方法

    首先要在 CMakeLists.txt 里加入cpp构建好才能开始编写,不然会编译报错,识别不了,所以一般先创建文件,然后同步在CMakeLists.txt里加入我们新建的cpp,运行的适合会生成so

    为了方便查看,通过查看日志缺认程序状态,要打印日志,还需要配置设置log库到我们的动态库中,不然会抛异常

    如果不想直接加载在MainActivity里面也可以新建一个Utils类去实现,在Utils里定义好我们的加密解密方法,之前看过一些资料,我也跟他们一样,用三个测试方法去测试,创建一个原始文件,然后通过运算加密,生成一个加密文件,然后在解密,生成一个解密文件,三个文件对比差异,所以需要三个方法,createFile,encryption,decryption

    对应的,我们需要在cpp文件里也定义三个名称一样的方法,注意包名别错了

    我这里看着麻烦,直接写了一个LogUtils的头文件,避免重复代码

    这样后面需要引用就行了,虽然也就一行代码,不过有强迫症,不喜欢重复去写这样的代码

    定义好了方法可以开始具体实现,首先需要先创建文件,创建文件需要传入地址,此处传入根目录,调用  fputs 方法写入测试文字

    /** createFile */
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_kotlinstrong_stronglib_cutil_EncryptUtils_createFile(JNIEnv *env, jobject type,
                                                                  jstring path_) {
        Logger("createFile path = %s", path_);
        //得到一个UTF-8编码的字符串(java使用 UTF-16 编码的,中文英文都是2字节,jni内部使用UTF-8编码,ascii字符是1字节,中文是3字节)
        const char *normalPath = env->GetStringUTFChars(path_, nullptr);
        if (normalPath == NULL) {
            return;
        }
        //wb:打开或新建一个二进制文件;只允许写数据
        FILE *fp = fopen(normalPath, "wb");
    
        //把字符串写入到指定的流 stream 中,但不包括空字符。
        fputs("账号:123
    密码:123;
    账号:456
    密码:456;
    ", fp);
    
        //关闭流 fp。刷新所有的缓冲区
        fclose(fp);
        //释放JVM保存的字符串的内存
        env->ReleaseStringUTFChars(path_, normalPath);//ReleaseStringUTFChars : 表示此内存不在使用,通知JVM回收,用了GetXXX就必须调用ReleaseXXX
    }

    因为是初学,C 已经忘的差不多了,所以注释也比较详细,看着注释大致的意思也就懂了

    下面是加密解密方法

    /** encryption */
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_kotlinstrong_stronglib_cutil_EncryptUtils_encryption(JNIEnv *env, jclass type,
                                                                  jstring normalPath_,
                                                                  jstring encryptPath_) {
        //获取字符串保存在JVM中内存中
        const char *normalPath = env->GetStringUTFChars(normalPath_, nullptr);
        const char *encryptPath = env->GetStringUTFChars(encryptPath_, nullptr);
    
        Logger("normalPath = %s, encryptPath = %s", normalPath, encryptPath);
    
        //rb:只读打开一个二进制文件,允许读数据。
        //wb:只写打开或新建一个二进制文件;只允许写数据
        FILE *normal_fp = fopen(normalPath, "rb");
        FILE *encrypt_fp = fopen(encryptPath, "wb");
    
        if (normal_fp == nullptr) {
            Logger("%s", "文件打开失败");
            return;
        }
        if(encrypt_fp == NULL) {
            Logger("%s","没有写权限") ;
        }
        //一次读取一个字符
        int ch = 0;
        int i = 0;
        size_t pwd_length = strlen(password);//计数器
        while ((ch = fgetc(normal_fp)) != EOF) {//读取文件中的字符
            //写入(异或运算)
            /**  ^(相同为0,不同为1)
                 int a=3=011
                 int b=6=110
                 result : a^b=101=5
              */
            fputc(ch ^ password[i % pwd_length], encrypt_fp);
            i++;
        }
    
        //关闭流 normal_fp和encrypt_fp。刷新所有的缓冲区
        fclose(normal_fp);
        fclose(encrypt_fp);
    
        //释放JVM保存的字符串的内存
        env->ReleaseStringUTFChars(normalPath_, normalPath);
        env->ReleaseStringUTFChars(encryptPath_, encryptPath);
    }
    View Code
    /** decryption */
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_kotlinstrong_stronglib_cutil_EncryptUtils_decryption(JNIEnv *env, jclass type,
                                                                  jstring encryptPath_,
                                                                  jstring decryptPath_) {
        //获取字符串保存在JVM中内存中
        const char *encryptPath = env->GetStringUTFChars(encryptPath_, nullptr);
        const char *decryptPath = env->GetStringUTFChars(decryptPath_, nullptr);
    
        Logger("encryptPath = %s, decryptPath = %s", encryptPath, decryptPath);
    
        //rb:只读打开一个二进制文件,允许读数据。
        //wb:只写打开或新建一个二进制文件;只允许写数据
        FILE *encrypt_fp = fopen(encryptPath, "rb");
        FILE *decrypt_fp = fopen(decryptPath, "wb");
    
        if (encrypt_fp == nullptr) {
            Logger("%s", "加密文件打开失败");
            return;
        }
    
        int ch;
        int i = 0;
        size_t pwd_length = strlen(password);
        while ((ch = fgetc(encrypt_fp)) != EOF) {
            fputc(ch ^ password[i % pwd_length], decrypt_fp);
            i++;
        }
    
        //关闭流 encrypt_fp 和 decrypt_fp。刷新所有的缓冲区
        fclose(encrypt_fp);
        fclose(decrypt_fp);
    
        //释放JVM保存的字符串的内存
        env->ReleaseStringUTFChars(encryptPath_, encryptPath);
        env->ReleaseStringUTFChars(decryptPath_, decryptPath);
    }
    View Code

    C层代码编写完毕后,直接在应用层调用测试

    首先在Utils里面编写一个测试方法

    写入文件别忘记了权限申请

    测试方法可以看到,连续调用了三个方法,首先先创建文件,创建好测试文件调用加密解密方法生成结果文件,或者查看日志

    文件生成,里面的内容便是默认写入的测试数据,以及加密解密的结果,下面分别是打开后默认的文件,加密后的文件以及解密后的文件

      

    此时加密完成,但是还缺少上面说的包名签名验证,不然还是很容易就能破解,先定义两个变量,一个包名一个签名,用作判断,签名方法需要用到一些获取安卓系统的Context等方法,此处可以分离出一个系统Utils,方面复用

     然后根据加密的方法,新建一个签名验证的cpp文件,然后构建好,跟应用层对应

    值得一说的就是通过C代码,获取到Java层的代码调用方法,并且通过应用层的方法获取签名,对比包名和签名,是否一致,以此加固安全

    jstring getSignature(JNIEnv *env, jobject obj)
    {
        jclass native_class = env->GetObjectClass(obj);
        jmethodID pm_id = env->GetMethodID(native_class, "getPackageManager", "()Landroid/content/pm/PackageManager;");
        jobject pm_obj = env->CallObjectMethod(obj, pm_id);
        jclass pm_clazz = env->GetObjectClass(pm_obj);
    // 得到 getPackageInfo 方法的 ID
        jmethodID package_info_id = env->GetMethodID(pm_clazz, "getPackageInfo","(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
        jstring pkg_str = getPackname(env, obj);
        Logger("getPackname: %d", pkg_str);
    // 获得应用包的信息
        jobject pi_obj = env->CallObjectMethod(pm_obj, package_info_id, pkg_str, 64);
    // 获得 PackageInfo 类
        jclass pi_clazz = env->GetObjectClass(pi_obj);
    // 获得签名数组属性的 ID
        jfieldID signatures_fieldId = env->GetFieldID(pi_clazz, "signatures", "[Landroid/content/pm/Signature;");
        jobject signatures_obj = env->GetObjectField(pi_obj, signatures_fieldId);
        jobjectArray signaturesArray = (jobjectArray)signatures_obj;
    //    jsize size = env->GetArrayLength(signaturesArray);
        jobject signature_obj = env->GetObjectArrayElement(signaturesArray, 0);
        jclass signature_clazz = env->GetObjectClass(signature_obj);
        jmethodID string_id = env->GetMethodID(signature_clazz, "toCharsString", "()Ljava/lang/String;");
        jstring str = static_cast<jstring>(env->CallObjectMethod(signature_obj, string_id));
    //    char *c_msg = (char*)env->GetStringUTFChars(str,0);
    //    Logger("signsture: %s", c_msg);
        return str;
    }
    /** 验证程序包和签名 */
    jboolean checkSignature(JNIEnv *env, jobject context){
    
        //根据传入的context对象getPackageName
        jstring pkg_str = getPackname(env, context);
        const char *pkg = env->GetStringUTFChars(pkg_str, NULL);
        //对比
        if (strcmp(package_name, pkg) != 0) {
            Logger("程序包验证失败:%s",pkg);
            return false;
        }
        Logger("程序包验证成功:%s",pkg);
        //调用String的toCharsString
        jstring signature_string = getSignature(env,context);
        //转换为char*
        const char *signature_char = env->GetStringUTFChars(signature_string, NULL);
        Logger("app signature:%s
    ", signature_char);
        Logger("cpp signature:%s
    ", app_signature);
        //对比签名
        if (strcmp(signature_char, app_signature) == 0) {
            Logger("程序签名验证通过");
            return true;
        } else {
            Logger("程序签名验证失败");
            return false;
        }
    }
    View Code

    此时写好的LogUtils又在签名此处可以复用

    接下来就是跟加密方法一样的套用,此处在点击事件中触发,然后查看打印结果是否为一致

    已经Success了,上图日志首先是验证的包名,包名验证需要获取application中context,还能防止java层传入恶意的context对象,如果是恶意的context,获取时会为null。否则容易被利用修改

    Github:https://github.com/1024477951/KotlinStrong

  • 相关阅读:
    CVPR-2021-Papers
    caffe中使用多个GPU的方法
    大道至简!深度解读CVPR2021论文RepVGG
    195上的rknn
    RKNN1.6下载路径
    基于深度学习的2D和3D仿射变换配准
    涵盖18+ SOTA GAN实现,开源工程StudioGAN火了
    基于opencv实战眼睛控制鼠标
    S7-200SMART PLC与变频器MODBUS RTU通讯与SMART LINE系列屏控制(案例三)
    Java异常处理学习
  • 原文地址:https://www.cnblogs.com/LiuZhen/p/12024257.html
Copyright © 2011-2022 走看看