Android so库防客户端破解的解决方案

栏目: IOS · Android · 发布时间: 5年前

内容简介:随着移动互联网的发展,移动应用的安全问题越来越突显,特别是涉及到钱相关的产品,前段一段时间,我们的Android客户端产品被人破解了,修改了一些代码,重新打包签名后,就可以免费获得资源,对我们的收入造成了一些影响,移动产品安全性就不得不提重视,安全性这个话题很大,包括客户端、服务端、数据存储、协议等很多方面,这里只是从客户端的角度来讨论一上如何保证客户端产品的安全性,抛砖引玉,也希望大家多提意见和建议。下面主要从以下几个方面来展开讨论:在不使用https的前提下,要保证C/S协议的安全,一般都会进行参数的校

背景

随着移动互联网的发展,移动应用的安全问题越来越突显,特别是涉及到钱相关的产品,前段一段时间,我们的Android客户端产品被人破解了,修改了一些代码,重新打包签名后,就可以免费获得资源,对我们的收入造成了一些影响,移动产品安全性就不得不提重视,安全性这个话题很大,包括客户端、服务端、数据存储、协议等很多方面,这里只是从客户端的角度来讨论一上如何保证客户端产品的安全性,抛砖引玉,也希望大家多提意见和建议。

下面主要从以下几个方面来展开讨论:

C/S协议安全

在不使用https的前提下,要保证C/S协议的安全,一般都会进行参数的校验,以及参数加密,客户端和服务端会约定一个固定的字符串作为key,对于客户端来说,这个key应该放到哪里?最早之前,我们是直接放到 Java 代码中,这样可以说没有什么安全性,后来为了稍微更加安全一点,把这些key都统一放到so库中实现,虽然也不能保证绝对安全,但起码可以增加破解的难度。

你肯定要说,如果这样做,就会有一个问题,如果别人把so拿出来,在直接调用这些native接口,也同样可以获得key,所以也同样不安全,怎么办呢?

能不能让 so 库只能在我们自己的app运行,别人调用就是砖头呢?

各位看官,接着往下看。

so库校验签名

一般的情况下,都会在 Application.onCreate() 方法里面检查当前应用的签名是否合法,如果不合法就直接退出,这种情况其实无法正在防止破解,因为破解的可以找到调用入口,把相应的代码删除,所以这样方法也就失效了,那有没有更好的方案呢?

想到的一种思路就是,so库本身就具体签名校验的机制,当so库被加载时 ( JNI_OnLoad() 方法),如果签名不合法,直接失败,so库根本加载不起来。

大概的思路如下代码所示:

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    if (vm->GetEnv((void **) (&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    LOGI("Library JNI_OnLoad begin =========");

  if (checkSignature(env) != JNI_TRUE) {
            LOGE("    The app signature is NOT correct, please check the apk signture. ");
            LOGI("Library JNI_OnLoad end ===========");
            return -1;
    } else {
            LOGI("    The app signature is correct.");
    }

    LOGI("Library JNI_OnLoad end ===========");

    return JNI_VERSION_1_6;
}
复制代码

说明:这里有一个问题,需要注意,在开发过程中,我们不需要检查签名的合法性,只有在release版本才检查,所以上述逻辑还再完善,需要添加上DEBUG和RELASE的判断。

要怎么判断呢?我目前的思路是通过宏来判断,如果定义了宏并且为 JNI_TRUE 的话,就认为是release版本。

以下是完整的实现:

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    if (vm->GetEnv((void **) (&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    LOGI("Library JNI_OnLoad begin =========");

    // RELEASE_MODE这个宏是通过编译脚本设定的,如果是release模式,
    // 则RELEASE_MODE=1,否则为0或者未定义
#ifdef RELEASE_MODE
    if (RELEASE_MODE == 1) {
        // 检查当前应用的签名是否一致,如果不签名不一致的话,则直接退出
        if (checkSignature(env) != JNI_TRUE) {
            LOGE("    The app signature is NOT correct, please check the apk signture. ");
            LOGI("Library JNI_OnLoad end ===========");
            return -1;
        } else {
            LOGI("    The app signature is correct.");
        }
    } else {
        // Do nothing
    }
#endif

    LOGI("Library JNI_OnLoad end ===========");

    return JNI_VERSION_1_6;
}
复制代码

RELEASE_MODE 在哪里定义的?

那问题又来了? RELEASE_MODE 宏要在哪里定义?不能改代码吧?

很容易想到通过编译中的 buildTypes 来控制,如果当前打release包,那么就定义这个宏。请看Android Stuido的build.gradle中的buildTypes

buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

            ndk {
                // release包定义RELEASE_MODE=1宏,so库中会使用
                cFlags "-DRELEASE_MODE=1"
            }
        }
        debug {
               // do nothing
        }
    }
复制代码

我们在buildTypes中添加了一个ndk块,里面定义了 RELEASE_MODE 宏,这里使用了 cFlags。

注意: -DRELEASE_MODE=1 中前面的 -D 不能省略。

checkSignature()如何实现?

最主要的就是解决如何得到当前app的context,我的做法是反射Java层的一个特定的方法 ,返回 getAppContext() 方法得到context。其中 setAppContext() 方法在 Application.onCreate()中调用。

public class NativeContext implements NoProGuard {
    /**
     * App context
     */
    private static Context sAppContext;

    /**
     * 得到 app context
     */
    public static Context getAppContext() {
        return sAppContext;
    }

    /**
     * Set the app context
     */
    static void setAppContext(Context appContext) {
        sAppContext = appContext;
    }
}
复制代码

Native这一层的实现如下:

/**
 * 检查加载该so的应用的签名,与预置的签名是否一致
 */
static jboolean checkSignature(JNIEnv *env) {
    // 得到当前app的NativeContext类
    jclass classNativeContext = env->FindClass(CLASS_NAME_NATIVECONTEXT);
    // 得到getAppContext静态方法
    jmethodID midGetAppContext = env->GetStaticMethodID(classNativeContext,
                                                        METHOD_NAME_GETAPPCONTEXT,
                                                        METHOD_SIGNATURE_GETAPPCONTEXT);
    // 调用getAppContext方法得到context对象
    jobject appContext = env->CallStaticObjectMethod(classNativeContext, midGetAppContext);

    if (appContext != NULL) {
        jboolean signatureValid = Java_com_xxxx_android_AppRuntime_checkSignature(env, NULL, appContext);
        if (signatureValid == JNI_TRUE) {
            LOGI("    checkSignature() return true");
        } else {
            LOGI("    checkSignature() return false");
        }
        return signatureValid;
    }

    return JNI_FALSE;
}
复制代码

这里调用了 Java_com_xxxx_android_AppRuntime_checkSignature 方法,它的实现如下所示,核心的思路是将从 Context 里面得到当前app的签名MD5字符串,然后再与预置的常量作比较,调用了 strcmp C函数。

extern "C" JNIEXPORT jboolean JNICALL
Java_com_xxxx_android_AppRuntime_checkSignature(
        JNIEnv *env, jclass clazz, jobject context) {

    jstring appSignature = loadSignature(env, context);
    jstring releaseSignature = env->NewStringUTF(APP_SIGNATURE);
    const char *charAppSignature = env->GetStringUTFChars(appSignature, NULL);
    const char *charReleaseSignature = env->GetStringUTFChars(releaseSignature, NULL);

    jboolean result = JNI_FALSE;
    if (charAppSignature != NULL && charReleaseSignature != NULL) {
        if (strcmp(charAppSignature, charReleaseSignature) == 0) {
            result = JNI_TRUE;
        }
    }

    env->ReleaseStringUTFChars(appSignature, charAppSignature);
    env->ReleaseStringUTFChars(releaseSignature, charReleaseSignature);

    return result;
}
复制代码

APP_SIGNATURE 是const char*的常量,是release版本的签名字符串。

完整的代码如下:

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"));

    // update方法,这个函数的返回值是void,写V
    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);

    // 将byte数组转换成16进制字符串,发现这里不用强转,jbyte和unsigned char应该字节数是一样的
    ByteToHexStr((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
    free(char_result);

    return stringResult;
}

jstring loadSignature(JNIEnv *env, jobject context) {
    // 获得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);

    // 获得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);
    // 得到toCharsString方法的ID
    mid = env->GetMethodID(cls, "toByteArray", "()[B");
    // 返回当前应用签名信息
    jbyteArray signatureByteArray = (jbyteArray) env->CallObjectMethod(signature, mid);

    return ToMd5(env, signatureByteArray);
}

void ByteToHexStr(const char *source, char *dest, int sourceLen) {
    short i;
    char highByte, lowByte;

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

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

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

以下就是在native层实现签名检验的逻辑,下面接下来说一下如何编译。

如何编译,mk vs gradle

创建jni的过程,这里就不多说,网上有很多相关的文章,简单地说,就是 src/main/ 路径下,创建jni文件夹,然后把native的代码放在这个目录下,在根目录下在,创建对应的Android.mk和Application.mk文件,关于 Android.mkApplication.mk 的说明,请参考Android开发的官方文档:

如果是使用gradle构建的话,需要作一点配置,添加一个 ndk block,gradle里面的配置会覆盖 mk 中设置的。

defaultConfig {
        ...

        ndk {
            // so库的名字
            moduleName 'libAppRuntime_V1_0'
            // 支持armeabi和armeabi-v7a
            abiFilters("armeabi", "armeabi-v7a")
            // 依赖的类库
            ldLibs("log")
        }
    }
复制代码
  • moduleName:so库的名字
  • abiFilters:so库的平台
  • ldLibs:依赖的类库,这里需要输出Log到logcat中,所以依赖log这个Android提供的库

Android Gradle插件如何编译出library module的debug包

为什么需要debug模式的library呢?因为我是想主工程打debug模式的包,library也是走debug的配置,如果主工程是release配置模式,那么library也是release的配置,这样做的目的是为了定义 RELEASE_MODE=1 这样的宏,是为了控制在不同的版本的so库执行不同的业务逻辑。

由于上述的功能逻辑是放到一个library module中的,那么就面临一个问题,library module如何打出debug包,对于library Android默认是打出release模式的,这一点可以从编译出来的 BuildConfog.DEBUG 恒为false可以看出。

那么到底要如何才能打出debug的aar呢?为了实现这个功能,再真费了点劲。直接上结论!

参考文档: Gradle插件不能编译出library模块的DEBUG模式

文中也有人说到了不能打debug模式的包:

Well, Gradle Android plugin simply can't build the debug version of dependent library modules. This is a well-known, old issue and this is not resolved yet. You can try to use some workarounds from the discussion I mentioned, specifically take a look at posts #35 and #38.

解决方案也大概如文中所说的,也进行了多次尝试:

主工程依赖方式需要改:

通常是这样引用libraray module

compile project(':AppLibrary')

需要改成这样:

debugCompile project(path: ':AppLibrary', configuration: 'debug') releaseCompile project(path: ':AppLibrary', configuration: 'release')

再看看library modlue的gradle配置:

  • 首先 buildTypes 增加 debugrelease 配置块
  • android 块里面增加 publishNonDefault true

这样改后,我们编译后就可以发现生成的文件中会有debug和release两个文件夹了,如下图所示:

Android so库防客户端破解的解决方案

JNI开发的一些tips

官方的tips请点击这里:JNI Tips

在开发过程中,遇到了一些比较蛋疼的问题,给大家说说,避免踩坑。

1、生成 .h 头文件

如果native方法中引用了android的类,例如Context之类的,需要显示指定--classpath

参考链接: android - javah doesn't find my class

If you are on Linux or MAC-OS, use ":" to separate the directories for classpath rather than ";" character: Example:

javah -cp /Users/Android/android-sdk/platforms/android-xy/android.jar:. com.test.JniTest  

2、JNI so库未找到方法实现

如果实现是C++(后续是cpp),没有头文件(.h)的话 ,需要在接口实现处添加上 extern "C" ,简单地说,C++的实现需要向前兼容C的实现,关于 extern "C"的作用,这里不多讲,

可以参考:extern "C"用法解析

extern "C" JNIEXPORT jboolean JNICALL
Java_com_xxxx_android_AppRuntime_checkSignature(
复制代码

总结

1、上述的东西,可以再进一步封装,独立成为一个libaray module,提供一个sdk,输出的就是aar,Java层面上就是一个NativeContext类,这个类的 setAppContext() 接口必须由业务方来调用。

2、上述提到的 so 最大的一个好处是自己具备识别签名的能力,只能在我们自己的app里面使用,别人是无法用的


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

认知盈余

认知盈余

[美] 克莱·舍基 / 胡泳、哈丽丝 / 中国人民大学出版社 / 2011-12 / 49.80元

“互联网革命最伟大的思考者”克莱•舍基 继《未来是湿的》之后最新力作 看自由时间如何变革世界的未来 如果说《未来是湿的》揭示的是“无组织的组织力量”, 那么《认知盈余》揭示的就是 “无组织的时间力量”。 腾讯董事会主席兼首席执行官马化腾首度亲笔作序倾情推荐 克莱•舍基说,美国人一年花在看电视上的时间大约2 000亿个小时,而这几乎是2 000个维基百科项目一年所需要的......一起来看看 《认知盈余》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具