Android安全——JNI初探之敏感信息保护

起因

说出来你可能不信,之所以研究JNI是因为爱好学习...
然而,是因为前同事写了一个so库,用来保存客户端敏感信息。结果源码给搞丢了...不得不说,真的是很安全啊...

背景

大家都知道,Android应用大部分是利用java语言开发的,并且我们的APK文件又是可以轻松获取的。那么一些敏感的数据如果放在java层的话,那就很可能被别有用心的人反编译出来,用于一些非法用途,造成无法挽回的损失。
我们一般会采用混淆、加壳等方式对apk进行加固保护,加大反编译的难度。但是,一些常量是不会/不能被混淆的,这种敏感信息就需要我们的额外保护了。

更安全的native代码

相对于java代码容易被反编译,使用NDK开发出来的原生C++代码编译后生成的so库是一个二进制文件,这无疑增加了破解的难度。利用这个特性,可以将客户端敏感信息写在C++代码中,增强应用的安全性。
然而,简单将敏感信息保存在so中就足够安全了吗?
并不是,虽然native方法与java代码存在一个对应关系,如包名、路径、方法名称,但这也是可以轻松实现的。所以,将敏感信息存放在so库中有一个不得不考虑的问题就是,万一别人将你的so库直接copy出来拿去用了呢?因此,我们还需要在native层对应用的包名、签名进行鉴权校验,如果不是自己的应用,不返回相关信息,或者直接退出应用!

创建包含C/C++代码的项目

该项目示例使用Android Studio 3.0.1编译调试。如果需要将生成的so文件用于正式项目中,那么必须保证创建so的项目与你使用so项目的包名、方法路径、方法名相一致,不然将无法正常调用so库中的native代码!
注意,若你的Android Studio不能创建支持C++代码的项目,请先下载NDK、CMake等相关组件。

Android安全——JNI初探之敏感信息保护_第1张图片
创建一个支持C++代码的项目
Android安全——JNI初探之敏感信息保护_第2张图片
使用默认的C++支持

编写代码

  • 在默认生成的cpp目录下添加头文件native-lib.h
#include 
#ifndef JNIDEMO_NATIVE_LIB_H
#define JNIDEMO_NATIVE_LIB_H

#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jstring JNICALL
Java_com_fedming_jnidemo_JniUtils_getSignature(JNIEnv *env);

#ifdef __cplusplus
}
#endif

#endif //JNIDEMO_NATIVE_LIB_H

  • 编写鉴权并返回相关信息的C++方法native-lib.cpp
#include 
#include 
#include 
#include 
#include "native-lib.h"

extern "C"
JNIEXPORT jstring JNICALL
Java_com_fedming_jnidemo_JniUtils_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

extern "C"
const char *AUTH_KEY = "MySecretSignature";
const char *PACKAGE_NAME = "com.fedming.jnidemo";
const char *RELEASE_SIGN_MD5 = "2634EFD64B5CDD13A9CCA6049949D26E";

/**
 * getApplication
 * @param env
 * @return j_object
 */
static jobject getApplication(JNIEnv *env) {
    jobject application = NULL;
    jclass activity_thread_clz = env->FindClass("android/app/ActivityThread");
    if (activity_thread_clz != NULL) {
        jmethodID currentApplication = env->GetStaticMethodID(
                activity_thread_clz, "currentApplication", "()Landroid/app/Application;");
        if (currentApplication != NULL) {
            application = env->CallStaticObjectMethod(activity_thread_clz, currentApplication);
        }
        env->DeleteLocalRef(activity_thread_clz);
    }
    return application;
}

/**
 * HexToString
 * @param source
 * @param dest
 * @param sourceLen
 */
static void ToHexStr(const char *source, char *dest, int sourceLen) {
    short i;
    char highByte, lowByte;

    for (i = 0; i < sourceLen; i++) {
        highByte = source[i] >> 4;
        lowByte = (char) (source[i] & 0x0f);
        highByte += 0x30;

        if (highByte > 0x39) {
            dest[i * 2] = (char) (highByte + 0x07);
        } else {
            dest[i * 2] = highByte;
        }

        lowByte += 0x30;
        if (lowByte > 0x39) {
            dest[i * 2 + 1] = (char) (lowByte + 0x07);
        } else {
            dest[i * 2 + 1] = lowByte;
        }
    }
}

/**
 *
 * byteArrayToMd5
 * @param env
 * @param source
 * @return j_string
 */
static jstring ToMd5(JNIEnv *env, jbyteArray source) {
    // MessageDigest
    jclass classMessageDigest = env->FindClass("java/security/MessageDigest");
    // MessageDigest.getInstance()
    jmethodID midGetInstance = env->GetStaticMethodID(classMessageDigest, "getInstance",
                                                      "(Ljava/lang/String;)Ljava/security/MessageDigest;");
    // MessageDigest object
    jobject objMessageDigest = env->CallStaticObjectMethod(classMessageDigest, midGetInstance,
                                                           env->NewStringUTF("md5"));

    jmethodID midUpdate = env->GetMethodID(classMessageDigest, "update", "([B)V");
    env->CallVoidMethod(objMessageDigest, midUpdate, source);

    // Digest
    jmethodID midDigest = env->GetMethodID(classMessageDigest, "digest", "()[B");
    jbyteArray objArraySign = (jbyteArray) env->CallObjectMethod(objMessageDigest, midDigest);

    jsize intArrayLength = env->GetArrayLength(objArraySign);
    jbyte *byte_array_elements = env->GetByteArrayElements(objArraySign, NULL);
    size_t length = (size_t) intArrayLength * 2 + 1;
    char *char_result = (char *) malloc(length);
    memset(char_result, 0, length);

    ToHexStr((const char *) byte_array_elements, char_result, intArrayLength);
    // 在末尾补\0
    *(char_result + intArrayLength * 2) = '\0';

    jstring stringResult = env->NewStringUTF(char_result);
    // release
    env->ReleaseByteArrayElements(objArraySign, byte_array_elements, JNI_ABORT);
    // 指针
    free(char_result);

    return stringResult;
}

JNIEXPORT jstring JNICALL
Java_com_fedming_jnidemo_JniUtils_getSignature(JNIEnv *env) {

    jobject context = getApplication(env);
    // 获得Context类
    jclass cls = env->GetObjectClass(context);
    // 得到getPackageManager方法的ID
    jmethodID mid = env->GetMethodID(cls, "getPackageManager",
                                     "()Landroid/content/pm/PackageManager;");

    // 获得应用包的管理器
    jobject pm = env->CallObjectMethod(context, mid);

    // 得到getPackageName方法的ID
    mid = env->GetMethodID(cls, "getPackageName", "()Ljava/lang/String;");
    // 获得当前应用包名
    jstring packageName = (jstring) env->CallObjectMethod(context, mid);
    const char *c_pack_name = env->GetStringUTFChars(packageName, NULL);

    // 比较包名,若不一致,直接return包名
    if (strcmp(c_pack_name, PACKAGE_NAME) != 0) {
        return (env)->NewStringUTF(c_pack_name);
    }
    // 获得PackageManager类
    cls = env->GetObjectClass(pm);
    // 得到getPackageInfo方法的ID
    mid = env->GetMethodID(cls, "getPackageInfo",
                           "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    // 获得应用包的信息
    jobject packageInfo = env->CallObjectMethod(pm, mid, packageName, 0x40); //GET_SIGNATURES = 64;
    // 获得PackageInfo 类
    cls = env->GetObjectClass(packageInfo);
    // 获得签名数组属性的ID
    jfieldID fid = env->GetFieldID(cls, "signatures", "[Landroid/content/pm/Signature;");
    // 得到签名数组
    jobjectArray signatures = (jobjectArray) env->GetObjectField(packageInfo, fid);
    // 得到签名
    jobject signature = env->GetObjectArrayElement(signatures, 0);

    // 获得Signature类
    cls = env->GetObjectClass(signature);
    mid = env->GetMethodID(cls, "toByteArray", "()[B");
    // 当前应用签名信息
    jbyteArray signatureByteArray = (jbyteArray) env->CallObjectMethod(signature, mid);
    //转成jstring
    jstring str = ToMd5(env, signatureByteArray);
    char *c_msg = (char *) env->GetStringUTFChars(str, 0);

    if (strcmp(c_msg, RELEASE_SIGN_MD5) == 0) {
        return (env)->NewStringUTF(AUTH_KEY);
    } else {
        return (env)->NewStringUTF(c_msg);
    }
}

可以看到,代码中的注释已经挺详细的了。我们利用jni在C++中校验了应用的包名以及签名的MD5值,若不符合预期,则不返回敏感信息。
其中获取应用签名的MD5值可以使用以下java代码,与包名等信息提前写到C++代码中用于对比。这里提醒一下,注意debug签名与release签名的区别,在平时的调试中也建议统一使用正式版签名。

    public static String getSignMd5Str(Context context) {
        try {
            @SuppressLint("PackageManagerGetSignatures")
            PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
            Signature[] signs = packageInfo.signatures;
            Signature sign = signs[0];
            return encryptionMD5(sign.toByteArray());
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return "";
    }

    public static String encryptionMD5(byte[] byteStr) {
        MessageDigest messageDigest = null;
        StringBuilder md5StrBuff = new StringBuilder();
        try {
            messageDigest = MessageDigest.getInstance("MD5");
            messageDigest.reset();
            messageDigest.update(byteStr);
            byte[] byteArray = messageDigest.digest();
            for (byte aByteArray : byteArray) {
                if (Integer.toHexString(0xFF & aByteArray).length() == 1) {
                    md5StrBuff.append("0").append(Integer.toHexString(0xFF & aByteArray));
                } else {
                    md5StrBuff.append(Integer.toHexString(0xFF & aByteArray));
                }
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return md5StrBuff.toString().toUpperCase();
    }

编译打包

  • gradle配置
        ndk {
            // 指定输出/支持的abi架构
            abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a'
        }
  • 生成so库
Android安全——JNI初探之敏感信息保护_第3张图片
assembleRelease
Android安全——JNI初探之敏感信息保护_第4张图片
生成的so库
  • 查看效果
Android安全——JNI初探之敏感信息保护_第5张图片
image.png

使用Android Studio的Analyze apk功能可以看到,我们直接在项目中添加C++代码,最终生成的apk文件就包含了我们的so库。当然,直接使用编译出来so文件也是一样的!

客户端获取敏感信息

public class JniUtils {
    //加载so
    static {
        System.loadLibrary("native-lib");
    }
    
    public static native String stringFromJNI();
    public static native String getSignature();
}

总结

在探索JNI敏感信息保护的过程中遇到了不少麻烦,主要是对C++代码不熟悉的缘故。期间参照了一些前人的代码片段,但大部分资料混乱不堪、千奇百怪,多与本人需求出入。而且代码老旧,并非使用最新的构建工具,已不能在新版IDE中正常编译运行。所以,便有了这一解决方案与源码,毕竟坑要自己一个个踩过才深刻。

最后,安全是相对的,混淆、加壳、关键代码native化也好,只能增加破解的难度,相信最终破解只是时间成本而已。但是,攻与防之间的博弈,总不能让对方轻松取胜吧?做好基本安全策略,让应用更加安全可靠也是我们开发者义不容辞的责任!按照惯例,附送源码一份!

参考资料:
破解Android JNI签名校验
向您的项目添加 C 和 C++ 代码

你可能感兴趣的:(Android安全——JNI初探之敏感信息保护)