Android NDK AES 加解密

客户端数据进行加密保护还是很有必要的。

对Android来说,一般的方式有:

  • 在 java 代码里进行加密
  • 在 native 代码里进行加密

对于第一种,安全性不高,应用容易被反编译,看到代码逻辑。当然可以进行加固,但是也有脱壳工具,真是道高一尺,魔高一丈。
对于第二种,安全性比第一种高。看不到代码。但是 jni 接口是直接暴露的,别人可以直接拿 so 直接使用。可以做签名验证,防止二次打包等。

加密方式也有很多,如RSA加密,MD5加密,AES加密,DES加密等等
这里我们使用的是 AES CBC Pkcs5Padding。具体代码可参考文章最后的源码。


0x01

首先,我们要创建一个 Android 工程,还有一个 AesUtils.java:

public class AesUtils {

    static {
        System.loadLibrary("aesLib");
    }

    // AES加密, CBC, PKCS5Padding
    public static native String encrypt(String str);

    // AES解密, CBC, PKCS5Padding
    public static native String decrypt(String str);
}

还有对应的 c++ 文件 aes_lib.cpp:

#include <jni.h>
#include <string>
#include "aes_utils.h"
#include "tools.h"


#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jstring JNICALL Java_com_goodl_aes_AesUtils_encrypt(JNIEnv *env, jclass jcls, jstring str_) {
    if (str_ == nullptr) return nullptr;

    const char *str = env->GetStringUTFChars(str_, JNI_FALSE);
    char *result = AES_128_CBC_PKCS5_Encrypt(str);

    env->ReleaseStringUTFChars(str_, str);

    jstring jResult = getJString(env, result);
    free(result);

    return jResult;
}

JNIEXPORT jstring JNICALL Java_com_goodl_aes_AesUtils_decrypt(JNIEnv *env, jclass jcls, jstring str_) {
    if (str_ == nullptr) return nullptr;

    const char *str = env->GetStringUTFChars(str_, JNI_FALSE);
    char *result = AES_128_CBC_PKCS5_Decrypt(str);

    env->ReleaseStringUTFChars(str_, str);

    jstring jResult = getJString(env, result);
    free(result);

    return jResult;
}

#ifdef __cplusplus
}
#endif

其中,getJString 函数在 tools 中,负责将 c 字符串转为 java 字符串,最后记得释放内存。其他文件这里省略,可自行参考源码。

我们看下运行结果:

D/aes: text: abc_-=.,123扫地阿姨发现你的代码有Bug
D/aes: text 加密: 9aba6ccf2b80ca251c1186508e019ca52d7e277dc0b4b4420440ed491fb2aeb8635dce02d1bb174363ad919ae261d10f
D/aes: text 解密: abc_-=.,123扫地阿姨发现你的代码有Bug

然后可以在 http://ctf.ssleye.com/caes.html 验证:

QQ20190917-154909@2x.png

结果一致,万事大吉 ???
一切才刚刚开始,目前为止,我们已经实现了加解密,但是安全呢?
对AES加密来说,最重要的就是密钥 key 和偏移量 iv 了。
我们打开神器 ida,再用神奇的 F5:


ida1.png

然后再看AES_128_CBC_PKCS5_Encrypt:


ida2.png

再看看 off_6008 :


ida3.png

我们的密钥和偏移量就这么暴露了,为什么会这样?因为我们的 key 和 iv 没有任何保护,不管是宏定义还是字符串常量,都很容易被反汇编工具找到 :

#define AES_KEY "goodl-aes-key123"
#define AES_IV  "goodl-aes-iv1234"

static const char *AES_KEY = "goodl-aes-key123";
static const char *AES_IV = "goodl-aes-iv1234";

0x02

那么我们修改一下,对 key 和 iv 进行一些保护:

static const uint8_t *getKey() {
    const int len = 16;
    uint8_t *src = malloc(len + 1);

    for (int i = 0; i < len; ++i) {
        switch (i) {
            case 0:  src[i] = 'g'; break;
            case 1:  src[i] = 'o'; break;
            case 2:  src[i] = 'o'; break;
            case 3:  src[i] = 'd'; break;
            case 4:  src[i] = 'l'; break;
            case 5:  src[i] = '-'; break;
            case 6:  src[i] = 'a'; break;
            case 7:  src[i] = 'e'; break;
            case 8:  src[i] = 's'; break;
            case 9:  src[i] = '-'; break;
            case 10: src[i] = 'k'; break;
            case 11: src[i] = 'e'; break;
            case 12: src[i] = 'y'; break;
            case 13: src[i] = '1'; break;
            case 14: src[i] = '2'; break;
            case 15: src[i] = '3'; break;
        }
    }
    src[len] = '\0';
    return src;
}

static const uint8_t *getIV() {
    const int len = 16;
    uint8_t *src = malloc(len + 1);

    for (int i = 0; i < len; ++i) {
        switch (i) {
            case 0:  src[i] = 'g'; break;
            case 1:  src[i] = 'o'; break;
            case 2:  src[i] = 'o'; break;
            case 3:  src[i] = 'd'; break;
            case 4:  src[i] = 'l'; break;
            case 5:  src[i] = '-'; break;
            case 6:  src[i] = 'a'; break;
            case 7:  src[i] = 'e'; break;
            case 8:  src[i] = 's'; break;
            case 9:  src[i] = '-'; break;
            case 10: src[i] = 'i'; break;
            case 11: src[i] = 'v'; break;
            case 12: src[i] = '1'; break;
            case 13: src[i] = '2'; break;
            case 14: src[i] = '3'; break;
            case 15: src[i] = '4'; break;
        }
    }
    src[len] = '\0';
    return src;
}

然后再看下ida:


ida4.png

这样就不能直接看出 key 和 iv 了,起到了一定的保护作用。还可以将 key 和 iv 先 base64 编码,放入数组,再 base64 解码后返回。


0x03

我们还可以进一步增强 so 的安全性,比如代码的混淆和加入花指令,以及 so 的加固。

花指令是由设计者特别构思,希望使反汇编的时候出错,让破解者无法清楚正确地反汇编程序的内容,迷失方向。有兴趣的话可以自行搜索一下花指令。

先说混淆吧,我们可以通过宏定义的方式来混淆。以 aes_utils 为例:

#define AES_128_CBC_PKCS5_Encrypt  ll11l1l1ll
#define AES_128_CBC_PKCS5_Decrypt  ll11lll11l
#define getKey                     ll11lll1l1
#define getIV                      ll11l1l1l1
#define getPaddingInput            ll11l1l11l
#define findPaddingIndex           lll1l1l1l1
#define removePadding              ll11l1llll

0x04

然后是花指令,花指令工具类 junk.h:

#ifndef _JUNK_H_
#define _JUNK_H_

#define JUNK_CODE        //是否插入垃圾代码的开关
#ifdef JUNK_CODE

#define junk_fun0                 li11li1o0
#define junk_fun1                 li11li1o1
#define junk_fun2                 li11li1o2
#define junk_fun3                 li11li1o3

static inline int junk_fun0(void) {
    volatile int i = 138, j = 1949;

    if ((i++) % 2 > 0) j *= i;
    if (j < 0) i *= 2;
    else return 0;

    i = 1;
    while (i++ < 2) {
        j /= i;
        j++;
        i++;
    }
    return i;
}

static inline int junk_fun1(void) {
    volatile int i = 21, j = 75;

    if ((i--) % 3 > 0) j *= i;
    if (j > 1) i *= 3;
    else return 1;

    i = 1;
    while (i++ < 3) {
        j /= i;
        j--;
        i++;
    }
    return j;
}

static inline int junk_fun2(void) {
    volatile int i = 56, j = 17;

    if ((i--) % 5 > 0) j *= i;
    if (j > 2) i *= 5;
    else return 0;

    i = 1;
    while (i++ < 5) {
        j *= i;
        j += 3;
        i += 3;
    }
    return i;
}

static inline int junk_fun3(void) {
    volatile int i = 1909, j = 131;

    if ((i--) % 7 > 0) j *= i;
    if (j > 3) i *= 7;
    else return 1;

    i = 1;
    while (i++ < 7) {
        j /= i;
        j -= 5;
        i += 5;
    }
    return i;
}

#define _JUNK_FUN_0 {if(junk_fun2())junk_fun1();if(junk_fun0()) junk_fun3();if(junk_fun1()) junk_fun2();if(junk_fun3()) junk_fun1(); \
                       if(junk_fun1())junk_fun0();if(junk_fun2()) junk_fun3();if(junk_fun3()) junk_fun1();if(junk_fun1()) junk_fun0();}
#define _JUNK_FUN_1 {if(junk_fun3())junk_fun1();if(junk_fun1()) junk_fun2();if(junk_fun2()) junk_fun0();if(junk_fun0()) junk_fun1(); \
                       if(junk_fun2())junk_fun1();if(junk_fun0()) junk_fun3();if(junk_fun1()) junk_fun2();if(junk_fun3()) junk_fun1();}
#define _JUNK_FUN_2 {if(junk_fun1())junk_fun0();if(junk_fun2()) junk_fun3();if(junk_fun3()) junk_fun1();if(junk_fun1()) junk_fun0(); \
                       if(junk_fun0())junk_fun2();if(junk_fun3()) junk_fun0();if(junk_fun0()) junk_fun3();if(junk_fun2()) junk_fun3();}
#define _JUNK_FUN_3 {if(junk_fun0())junk_fun2();if(junk_fun3()) junk_fun0();if(junk_fun0()) junk_fun3();if(junk_fun2()) junk_fun3(); \
                       if(junk_fun3())junk_fun1();if(junk_fun1()) junk_fun2();if(junk_fun2()) junk_fun0();if(junk_fun0()) junk_fun1();}

#else

#define _JUNK_FUN_0 {}
#define _JUNK_FUN_1 {}
#define _JUNK_FUN_2 {}
#define _JUNK_FUN_3 {}

#endif
#endif

然后在需要插入花指令的地方加入 _JUNK_FUN_0,_JUNK_FUN_1 等。
做完这些我们再来看一下效果:


ida5.png

之前可以清晰看到的函数名,现在全变成了 o000OO0O,qqppqp,ll11l1llll,bbbddbdbb 这些没有含义又相似度极高的名称了。总之看起来废眼,头疼。长时间看还容易引起头晕,脑胀,恶心反胃等不良反应。


0x05

目前为止,安全性有了一定的提升,但感觉还不够,因为我们的 jni 入口还是可以一眼就看出来的。Java_com_goodl_aes_AesUtils_encryptJava_com_goodl_aes_AesUtils_decrypt 实在是鹤立鸡群,太扎眼。

解决方法:

  • java 层的类名和方法名就不要那么规范了,人肉混淆
  • 改为动态注册(有兴趣的可以搜下 jni 动态注册)
  • so 名字也换掉,带个 aes,谁都知道是干什么的

AesUtils.java 改为 FooTools.java

public class FooTools {

    static {
        System.loadLibrary("fooLib");
    }

    // AES加密, CBC, PKCS5Padding
    public static native String method01(String str);

    // AES解密, CBC, PKCS5Padding
    public static native String method02(String str);
}

aes_lib.cpp 改为 foo_tools.cpp,使用动态注册:

#include <jni.h>
#include <string>
#include "aes_utils.h"
#include "tools.h"
#include "junk.h"

#define JNIREG_CLASS "com/goodl/aes/FooTools"
#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))

#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jstring JNICALL method01(JNIEnv *env, jclass jcls, jstring str_) {
    if (str_ == nullptr) return nullptr;

    const char *str = env->GetStringUTFChars(str_, JNI_FALSE);
    char *result = AES_128_CBC_PKCS5_Encrypt(str);

    env->ReleaseStringUTFChars(str_, str);

    jstring jResult = getJString(env, result);
    free(result);

    return jResult;
}

JNIEXPORT jstring JNICALL method02(JNIEnv *env, jclass jcls, jstring str_) {
    if (str_ == nullptr) return nullptr;

    const char *str = env->GetStringUTFChars(str_, JNI_FALSE);
    char *result = AES_128_CBC_PKCS5_Decrypt(str);

    env->ReleaseStringUTFChars(str_, str);

    jstring jResult = getJString(env, result);
    free(result);

    return jResult;
}

static JNINativeMethod method_table[] = {
        {"method01", "(Ljava/lang/String;)Ljava/lang/String;", (void *) method01},
        {"method02", "(Ljava/lang/String;)Ljava/lang/String;", (void *) method02},
};

static int registerMethods(JNIEnv *env, const char *className,
                           JNINativeMethod *gMethods, int numMethods) {
    jclass clazz = env->FindClass(className);
    if (clazz == nullptr) {
        return JNI_FALSE;
    }
    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    _JUNK_FUN_0

    JNIEnv *env = nullptr;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    assert(env != nullptr);

    // 注册native方法
    if (!registerMethods(env, JNIREG_CLASS, method_table, NELEM(method_table))) {
        return JNI_ERR;
    }

    return JNI_VERSION_1_6;
}

#ifdef __cplusplus
}
#endif

现在 so 的安全性又有提高。我们还可以做什么?

  • 验证签名,签名不一致就报错或是返回空
  • 防调试
  • 防 Xposed
  • 对 so 加固

其实对于 key 和 iv 的保护,本地怎么做也会有暴露的风险。也有人说,干嘛不服务器下发呢?那怎么保证网络传输的安全呢?如果大家有什么好的想法,可以留言给我

最后附上源码: https://github.com/luck-apple/aesTool

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容

  • 目录一、对称加密 1、对称加密是什么 2、对称加密的优点 3、对称加密的问题 4、对称加密的应用场景 5、对称加密...
    意一ineyee阅读 61,857评论 8 110
  • // com.adobe.flash.listen settings.gradle 定义项目包含那些模块app.i...
    zeromemcpy阅读 1,609评论 0 1
  • 的鬼地方个地方跟得上过水电费热管让哥高高个4托管人的发生过太太3痛痛痛2痛2他发的的风格
    Lunroid阅读 177评论 0 1
  • 以前没觉得迟到有多严重,但当我是被迟到者时,才明白,你的迟到于对方而言,有多煎熬。以后,绝不能再迟到,如果真的来不...
    定点阅读 381评论 0 0