Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Android敏感数据泄露引发的思考

Android敏感数据泄露引发的思考

作者头像
陈宇明
发布于 2020-12-16 03:47:04
发布于 2020-12-16 03:47:04
1.1K00
代码可运行
举报
文章被收录于专栏:设计模式设计模式
运行总次数:0
代码可运行

作者:Airsj

链接:https://juejin.im/post/6862732328406351879

事件始末

一个平淡的午后,我还悠哉悠哉的敲着代码品着茶。突然服务端同事告诉我,关注接口正在被机械式调用,怀疑是有人在使用脚本刷接口(目的主要是从平台导流)。

纳尼?不会吧,因为据我所知接口请求是做了加密处理的,除非知道加密的密钥和加密方式,不然是不会调用成功的,一定是你感觉错了。然而当服务端同事把接口调用日志发给我看时,彻底否定了我的侥幸心理。

  1. 接口调用频率固定为1s 一次
  2. 被关注者的id每次调用依次加一(目前业务上用户id的生成是按照注册时间依次递增的)
  3. 加密的密钥始终使用固定的一个(正常的是在固定的几个密钥中每次会随机使用一个)

综合以上三点就可以断定,肯定是存在刷接口的行为了。

事件分析

既然上述刷接口的行为成立,也就意味着密钥和加密方式被对方知道了,原因无非是以下两点:

  1. 内部人员泄露
  2. apk被破解

经过确认基本排除了第一点,那就只剩下apk被破解了,可是apk发布出去的包是进行过加固和混淆处理的,难道对方脱壳了?不管三七二十一,自己先来反编译试试。于是乎从最近发布的版本一个一个去反编译,最后在反编译到较早前的一个版本时发现,保存密钥和加密的工具类居然源码完全暴露了。

炸了锅了,排查了一下这个版本居然未加固过就发布出去了,而且这个加密工具类未被混淆。虽然还不太清楚对方是不是按照这种方式获取的密钥和加密算法,但无疑这是客户端存在的一个安全漏洞。

事件处理

既然已经发现了上述问题,那就要想办法解决。首先不考虑加固,如何尽最大可能保证客户端中的敏感数据不泄露?另一方面即使对方想要破解,也要想办法设障,增大破解难度。想到这里基本就大致确定了一个思路:使用NDK,将敏感数据和加密方式放到native层,因为C++代码编译后生成的so库是一个二进制文件,这无疑会增加破解的难度。利用这个特性,可以将客户端的敏感数据写在C++代码中,从而增强应用的安全性。 说干就干吧!!!


1.首先创建了加密工具类:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class HttpKeyUtil {
    static {
        System.loadLibrary("jniSecret");
    }
    //根据随机值去获取密钥
    public static native String getHttpSecretKey(int index);
    //将待加密的数据传入,返回加密后的结果
    public static native String getSecretValue(byte[] bytes);
}

2.生成相应的头文件: com_test_util_HttpKeyUtil.h

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <jni.h>
#ifndef _Included_com_test_util_HttpKeyUtil
#define _Included_com_test_util_HttpKeyUtil
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jstring JNICALL Java_com_esky_common_component_util_HttpKeyUtil_getHttpSecretKey
        (JNIEnv *, jclass, jint);
        
JNIEXPORT jstring JNICALL Java_com_test_util_HttpKeyUtil_getSecretValue
        (JNIEnv *, jclass, jbyteArray);

#ifdef __cplusplus
}
#endif
#endif

3.编写相应的cpp文件:

在相应的Module中创建jni目录,将com_test_util_HttpKeyUtil.h拷贝进来,然后再创建com_test_util_HttpKeyUtil.cpp文件

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <jni.h>
#include <cstring>
#include <malloc.h>
#include "com_test_util_HttpKeyUtil.h"

extern "C"
const char *KEY1 = "密钥1";
const char *KEY2 = "密钥2";
const char *KEY3 = "密钥3";
const char *UNKNOWN = "unknown";

jstring toMd5(JNIEnv *pEnv, jbyteArray pArray);

extern "C" JNIEXPORT jstring JNICALL Java_com_test_util_HttpKeyUtil_getHttpSecretKey
        (JNIEnv *env, jclass cls, jint index) {
    if (随机数条件1) {
        return env->NewStringUTF(KEY1);
    } else if (随机数条件2) {
        return env->NewStringUTF(KEY2);
    } else if (随机数条件3) {
        return env->NewStringUTF(KEY3);
    } else {
        return env->NewStringUTF(UNKNOWN);
    }
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_test_util_HttpKeyUtil_getSecretValue
        (JNIEnv *env, jclass cls, jbyteArray jbyteArray1) {
        //加密算法各有不同,这里我就用md5做个示范
        return toMd5(env, jbyteArray1);
}

//md5
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;
}

//转换为16进制字符串
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;
        }
    }
}

4.事件就此结束?

到这里就此结束了?too yuang too simple!!!虽然将密钥和加密算法写在了c++中,貌似好像是比较安全了。但是但是万一别人反编译后,拿到c++代码最终生成的so库,然后直接调用so库里的方法去获取密钥并调用加密方法怎么破?看来我们还是要加一步身份校验才行:即在native层对应用的包名、签名进行鉴权校验,校验通过才返回正确结果。下面就是获取apk包名和签名校验的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const char *PACKAGE_NAME = "你的ApplicationId";
//(签名的md5值自己可以写方法获取,或者用签名工具直接获取,一般对接微信sdk的时候也会要应用签名的MD5值)
const char *SIGN_MD5 = "你的应用签名的MD5值注意是大写";

//获取Application实例
jobject getApplication(JNIEnv *env) {
    jobject application = NULL;
    //这里是你的Application的类路径,混淆时注意不要混淆该类和该类获取实例的方法比如getInstance
    jclass baseapplication_clz = env->FindClass("com/test/component/BaseApplication");
    if (baseapplication_clz != NULL) {
        jmethodID currentApplication = env->GetStaticMethodID(
                baseapplication_clz, "getInstance",
                "()Lcom/test/component/BaseApplication;");
        if (currentApplication != NULL) {
            application = env->CallStaticObjectMethod(baseapplication_clz, currentApplication);
        }
        env->DeleteLocalRef(baseapplication_clz);
    }
    return application;
}


bool isRight = false;
//获取应用签名的MD5值并判断是否与本应用的一致
jboolean getSignature(JNIEnv *env) {
    LOGD("getSignature isRight: %d", isRight ? 1 : 0);
    if (!isRight) {//避免每次都进行校验浪费资源,只要第一次校验通过后,后边就不在进行校验
        jobject context = getApplication(env);
        // 获得Context类
        jclass cls = env->FindClass("android/content/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 false;
        }
        // 获得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);
        LOGD("getSignature release sign md5: %s", c_msg);
        isRight = strcmp(c_msg, SIGN_MD5) == 0;
        return isRight;
    }
    return isRight;
}


//有了校验的方法,所以我们要对第3步中,获取密钥和加密方法的进行修改,添加校验的逻辑
extern "C" JNIEXPORT jstring JNICALL Java_com_test_util_HttpKeyUtil_getHttpSecretKey
        (JNIEnv *env, jclass cls, jint index) {
    if (getSignature(env)){//校验通过
      if (随机数条件1) {
        return env->NewStringUTF(KEY1);
      } else if (随机数条件2) {
        return env->NewStringUTF(KEY2);
      } else if (随机数条件3) {
        return env->NewStringUTF(KEY3);
      } else {
        return env->NewStringUTF(UNKNOWN);
      }
    }else {
        return env->NewStringUTF(UNKNOWN);
    }
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_test_util_HttpKeyUtil_getSecretValue
        (JNIEnv *env, jclass cls, jbyteArray jbyteArray1) {
        //加密算法各有不同,这里我就用md5做个示范
    if (getSignature(env)){//校验通过
       return toMd5(env, jbyteArray1);
    }else {
        return env->NewStringUTF(UNKNOWN);
    }
}

5.总结

以上就是此次事件native的相关代码,至于如何生成so库可以自行百度。从此次事件中需要反思的几点是:

  • 安全性的认识,安全无小事
  • 发布出去的包必须走加固流程,为了防止疏漏,禁止人工打包加固,全部通过脚本实现
  • 服务端增加相关风险的报警机制
代码语言:javascript
代码运行次数:0
运行
复制
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-09-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码个蛋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
JNI 解析以及在 Android 中的实际应用
JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(在Android里面主要是C&C++)。
字节流动
2021/12/28
1.5K0
JNI 解析以及在 Android 中的实际应用
JNI的探索
2、利用javac或者编译器直接编译,生成class文件 3、利用jdk下的javah 生成(.h)的头文件
包子388321
2020/06/16
1K0
Android的JNI【实战教程】3⃣️--Java调用C代码
看了上一篇: http://blog.csdn.net/github_33304260/article/details/62891083 我们应该已经可以构建NDK工程了 , 接下来让我们看看Java调用C代码:
先知先觉
2019/01/21
1K0
JNI开发总结
Android Ndk开发常用网站收集,真正的高手并不是掌握所有的API而是需要的时候可以快速的找到要使用的API。
Anymarvel
2018/10/22
1.7K0
JNI开发总结
使用JNI技术,让程序有飞一般的感觉
如果你对程序的性能要求比较高,或者觉得java的运行速度已经满足不了你,底层也可以采用C++来完成,使用JNI技术直接调用,会让你的程序有飞一般的感觉。前段时间做了调研,踩了几个坑,这里总结下,希望大家少走弯路。
龙哥
2018/10/22
8770
使用JNI技术,让程序有飞一般的感觉
JNI动态注册native方法及JNI数据使用
前言 或许你知道了jni的简单调用,其实不算什么百度谷歌一大把,虽然这些jni绝大多数情况下都不会让我们安卓工程师来弄,毕竟还是有点难,但是我们还是得打破砂锅知道为什么这样干吧,至少也让我们知道调用流
xiangzhihong
2018/02/05
1.4K0
JNI动态注册native方法及JNI数据使用
Android 通过 JNI 访问 Java 字段和方法调用
在前面的两篇文章中,介绍了 Android 通过 JNI 进行基础类型、字符串和数组的相关操作,并描述了 Java 和 Native 在类型和签名之间的转换关系。
音视频开发进阶
2019/07/26
1.2K0
Android学习JNI,使用JNI实现字符串加密
本节学习使用C语言加密字符串,大家都知道使用JAVA实现的加密都可以反编译的,而使用C写的加密是很难被反编译的。所以我们使用JNI学习如何使用C实现对字符串的加密。
DragonKingZhu
2022/05/08
8210
Android学习JNI,使用JNI实现字符串加密
Android NDK 示例-返回字符串,数组,Java对象;兼容性问题
JNI目录,需要mk文件,头文件和源文件。这里头文件和源文件故意不统一文件名,也可实现效果。
AnRFDev
2021/02/01
1.9K0
NDK开发案例 | C/C++调用java层代码
随笔分类 - jni NDK开发(三)——C/C++代码如何调用java层代码 E:\AndroidProject\TheTestPro\NDKDemo\app\build\intermediates\javac\debug\classes>javap -s -p com.lwp.ndkdemo.MainActivity Compiled from "MainActivity.java" public class com.lwp.ndkdemo.MainActivity extends androidx.a
凌川江雪
2020/07/22
5560
Android Studio2.2下NDK开发初试
很久没更博了,这次来记录一下如何在Android Studio2.2中进行NDK开发吧,NDK开发嘛,就是将C/C++的代码编译成so类库,供java调用(当然c调用java也是可以的),还记得以前没
forrestlin
2018/05/24
1K0
【Android 应用开发】Android 开发 之 JNI入门 - NDK从入门到精通
-- 第一个JNI示例程序下载 : GitHub - https://github.com/han1202012/NDKHelloworld.git
韩曙亮
2023/03/27
3.5K0
【Android 应用开发】Android 开发 之 JNI入门 - NDK从入门到精通
Android 通过 JNI 调用 Java 类的构造方法和父类的方法
Android 还可以通过 JNI 来调用 Java 一个类的构造方法,从而创建一个 Java 类。
音视频开发进阶
2019/07/26
3K0
Android JNI 调用时缓存字段和方法 ID
在 JNI 去调用 Java 的方法和访问字段时,最先要做的操作就是获得对应的类以及对应的方法 id。
音视频开发进阶
2019/07/26
1.2K0
【Android NDK 开发】JNI 方法解析 ( C/C++ 调用 Java 方法 | 函数签名 | 调用对象方法 | 调用静态方法 )
调用 jclass GetObjectClass(jobject obj) 或 jclass FindClass(const char* name) 方法 , 获取 jclass 类型变量 ;
韩曙亮
2023/03/27
8.9K0
【Android NDK 开发】JNI 方法解析 ( C/C++ 调用 Java 方法 | 函数签名 | 调用对象方法 | 调用静态方法 )
Android的JNI【实战教程】4⃣️--C调用Java代码
下一篇我们介绍一下AS 2.2 以上配置NDK —-纵享丝滑 http://blog.csdn.net/github_33304260/article/details/71404510
先知先觉
2019/01/21
7040
jni小细节操作
数组的细节处理 #include <jni.h> #include <string> #include <stdlib.h> int compare(const jint *a, const jint *b) { return *a - *b; } extern "C" JNIEXPORT void JNICALL Java_com_zzw_jnidemo_ArrayUtils_sort(JNIEnv *env, jclass type, jintArray array_) { ji
曾大稳
2018/09/11
1.1K0
[干货]再见,Android JNI 封装
最近名名接到友邻团队的“求助”,临时调度帮助其 SDK 封装 JNI SDK,下面就用 SDK 和 JNI SDK 来区分这两个 SDK。以前我也不是搞这个的,但是因为干过一两次,多少有点经验,之前第一次封装后,我觉得这玩意可以总结成通用模板,但是因为本身对它不感兴趣,也就没去弄了,今天又来一次,我觉得有必要了,因为它就是个体力活。今天总结这个模板以及封装思路,可以让我们快速的实现 JNI 封装。有如下这么些数据:
别打名名
2020/10/27
1.6K0
[干货]再见,Android JNI 封装
JNI基础
JNI基础 将java中的字符串转换成C中字符串的工具方法 char* Jstring2CStr(JNIEnv* env, jstring jstr){ char* rtn = NULL; jclass clsstring = (*env)->FindClass(env,"java/lang/String"); jstring strencode = (*env)->NewStringUTF(env
xiangzhihong
2018/01/26
1.4K0
JNI接口操作实践(二)
前面介绍了JNI的基本规范以及JNI的接口的生成过程。本文通过一个jni_test 应用实践操作JNI的接口各种典型应用。
用户4148957
2022/06/14
5550
JNI接口操作实践(二)
推荐阅读
相关推荐
JNI 解析以及在 Android 中的实际应用
更多 >
LV.1
这个人很懒,什么都没有留下~
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验