当前位置:主页 > 查看内容

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

发布时间:2021-05-12 00:00| 位朋友查看

简介:1.事件始末 一个平淡的午后,我还悠哉悠哉的敲着代码品着茶。突然服务端同事告诉我,关注接口正在被机械式调用,怀疑是有人在使用脚本刷接口(目的主要是从平台导流)。 纳尼?不会吧,因为据我所知接口请求是做了加密处理的,除非知道加密的密钥和加密方式,不……

 1.事件始末

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

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

接口调用频率固定为1s 一次

被关注者的id每次调用依次加一(目前业务上用户id的生成是按照注册时间依次递增的)

加密的密钥始终使用固定的一个(正常的是在固定的几个密钥中每次会随机使用一个)

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

2.事件分析

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

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

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

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

3.事件处理

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

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

  1. public class HttpKeyUtil { 
  2.     static { 
  3.         System.loadLibrary("jniSecret"); 
  4.     } 
  5.     //根据随机值去获取密钥 
  6.     public static native String getHttpSecretKey(int index); 
  7.     //将待加密的数据传入,返回加密后的结果 
  8.     public static native String getSecretValue(byte[] bytes); 

2.生成相应的头文件:

  1. #include <jni.h> 
  2. #ifndef _Included_com_test_util_HttpKeyUtil 
  3. #define _Included_com_test_util_HttpKeyUtil 
  4. #ifdef __cplusplus 
  5. extern "C" { 
  6. #endif 
  7. JNIEXPORT jstring JNICALL Java_com_esky_common_component_util_HttpKeyUtil_getHttpSecretKey 
  8.         (JNIEnv *, jclass, jint); 
  9.          
  10. JNIEXPORT jstring JNICALL Java_com_test_util_HttpKeyUtil_getSecretValue 
  11.         (JNIEnv *, jclass, jbyteArray); 
  12.  
  13. #ifdef __cplusplus 
  14. #endif 
  15. #endif 

3.编写相应的cpp文件:

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

  1. #include <jni.h> 
  2. #include <cstring> 
  3. #include <malloc.h> 
  4. #include "com_test_util_HttpKeyUtil.h" 
  5.  
  6. extern "C" 
  7. const char *KEY1 = "密钥1"
  8. const char *KEY2 = "密钥2"
  9. const char *KEY3 = "密钥3"
  10. const char *UNKNOWN = "unknown"
  11.  
  12. jstring toMd5(JNIEnv *pEnv, jbyteArray pArray); 
  13.  
  14. extern "C" JNIEXPORT jstring JNICALL Java_com_test_util_HttpKeyUtil_getHttpSecretKey 
  15.         (JNIEnv *env, jclass cls, jint index) { 
  16.     if (随机数条件1) { 
  17.         return env->NewStringUTF(KEY1); 
  18.     } else if (随机数条件2) { 
  19.         return env->NewStringUTF(KEY2); 
  20.     } else if (随机数条件3) { 
  21.         return env->NewStringUTF(KEY3); 
  22.     } else { 
  23.         return env->NewStringUTF(UNKNOWN); 
  24.     } 
  25.  
  26. extern "C" JNIEXPORT jstring JNICALL 
  27. Java_com_test_util_HttpKeyUtil_getSecretValue 
  28.         (JNIEnv *env, jclass cls, jbyteArray jbyteArray1) { 
  29.         //加密算法各有不同,这里我就用md5做个示范 
  30.         return toMd5(env, jbyteArray1); 
  31.  
  32. //md5 
  33. jstring toMd5(JNIEnv *env, jbyteArray source) { 
  34.     // MessageDigest 
  35.     jclass classMessageDigest = env->FindClass("java/security/MessageDigest"); 
  36.     // MessageDigest.getInstance() 
  37.     jmethodID midGetInstance = env->GetStaticMethodID(classMessageDigest, "getInstance"
  38.                                                       "(Ljava/lang/String;)Ljava/security/MessageDigest;"); 
  39.     // MessageDigest object 
  40.     jobject objMessageDigest = env->CallStaticObjectMethod(classMessageDigest, midGetInstance, 
  41.                                                            env->NewStringUTF("md5")); 
  42.  
  43.     jmethodID midUpdate = env->GetMethodID(classMessageDigest, "update""([B)V"); 
  44.     env->CallVoidMethod(objMessageDigest, midUpdate, source); 
  45.  
  46.     // Digest 
  47.     jmethodID midDigest = env->GetMethodID(classMessageDigest, "digest""()[B"); 
  48.     jbyteArray objArraySign = (jbyteArray) env->CallObjectMethod(objMessageDigest, midDigest); 
  49.  
  50.     jsize intArrayLength = env->GetArrayLength(objArraySign); 
  51.     jbyte *byte_array_elements = env->GetByteArrayElements(objArraySign, NULL); 
  52.     size_t length = (size_t) intArrayLength * 2 + 1; 
  53.     char *char_result = (char *) malloc(length); 
  54.     memset(char_result, 0, length); 
  55.     toHexStr((const char *) byte_array_elements, char_result, intArrayLength); 
  56.     // 在末尾补\0 
  57.     *(char_result + intArrayLength * 2) = '\0'
  58.     jstring stringResult = env->NewStringUTF(char_result); 
  59.     // release 
  60.     env->ReleaseByteArrayElements(objArraySign, byte_array_elements, JNI_ABORT); 
  61.     // 指针 
  62.     free(char_result); 
  63.     return stringResult; 
  64.  
  65. //转换为16进制字符串 
  66. void toHexStr(const char *source, char *dest, int sourceLen) { 
  67.     short i; 
  68.     char highByte, lowByte; 
  69.     for (i = 0; i < sourceLen; i++) { 
  70.         highByte = source[i] >> 4; 
  71.         lowByte = (char) (source[i] & 0x0f); 
  72.         highByte += 0x30; 
  73.         if (highByte > 0x39) { 
  74.             dest[i * 2] = (char) (highByte + 0x07); 
  75.         } else { 
  76.             dest[i * 2] = highByte; 
  77.         } 
  78.         lowByte += 0x30; 
  79.         if (lowByte > 0x39) { 
  80.             dest[i * 2 + 1] = (char) (lowByte + 0x07); 
  81.         } else { 
  82.             dest[i * 2 + 1] = lowByte; 
  83.         } 
  84.     } 

4.事件就此结束?

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

  1. const char *PACKAGE_NAME = "你的ApplicationId"
  2. //(签名的md5值自己可以写方法获取,或者用签名工具直接获取,一般对接微信sdk的时候也会要应用签名的MD5值) 
  3. const char *SIGN_MD5 = "你的应用签名的MD5值注意是大写"
  4.  
  5. //获取Application实例 
  6. jobject getApplication(JNIEnv *env) { 
  7.     jobject application = NULL
  8.     //这里是你的Application的类路径,混淆时注意不要混淆该类和该类获取实例的方法比如getInstance 
  9.     jclass baseapplication_clz = env->FindClass("com/test/component/BaseApplication"); 
  10.     if (baseapplication_clz != NULL) { 
  11.         jmethodID currentApplication = env->GetStaticMethodID( 
  12.                 baseapplication_clz, "getInstance"
  13.                 "()Lcom/test/component/BaseApplication;"); 
  14.         if (currentApplication != NULL) { 
  15.             application = env->CallStaticObjectMethod(baseapplication_clz, currentApplication); 
  16.         } 
  17.         env->DeleteLocalRef(baseapplication_clz); 
  18.     } 
  19.     return application; 
  20.  
  21.  
  22. bool isRight = false
  23. //获取应用签名的MD5值并判断是否与本应用的一致 
  24. jboolean getSignature(JNIEnv *env) { 
  25.     LOGD("getSignature isRight: %d", isRight ? 1 : 0); 
  26.     if (!isRight) {//避免每次都进行校验浪费资源,只要第一次校验通过后,后边就不在进行校验 
  27.         jobject context = getApplication(env); 
  28.         // 获得Context类 
  29.         jclass cls = env->FindClass("android/content/Context"); 
  30.         // 得到getPackageManager方法的ID 
  31.         jmethodID mid = env->GetMethodID(cls, "getPackageManager"
  32.                                          "()Landroid/content/pm/PackageManager;"); 
  33.  
  34.         // 获得应用包的管理器 
  35.         jobject pm = env->CallObjectMethod(context, mid); 
  36.  
  37.         // 得到getPackageName方法的ID 
  38.         mid = env->GetMethodID(cls, "getPackageName""()Ljava/lang/String;"); 
  39.         // 获得当前应用包名 
  40.         jstring packageName = (jstring) env->CallObjectMethod(context, mid); 
  41.         const char *c_pack_name = env->GetStringUTFChars(packageName, NULL); 
  42.  
  43.         // 比较包名,若不一致,直接return包名 
  44.         if (strcmp(c_pack_name, PACKAGE_NAME) != 0) { 
  45.             return false
  46.         } 
  47.         // 获得PackageManager类 
  48.         cls = env->GetObjectClass(pm); 
  49.         // 得到getPackageInfo方法的ID 
  50.         mid = env->GetMethodID(cls, "getPackageInfo"
  51.                                "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;"); 
  52.         // 获得应用包的信息 
  53.         jobject packageInfo = env->CallObjectMethod(pm, mid, packageName, 
  54.                                                     0x40); //GET_SIGNATURES = 64; 
  55.         // 获得PackageInfo 类 
  56.         cls = env->GetObjectClass(packageInfo); 
  57.         // 获得签名数组属性的ID 
  58.         jfieldID fid = env->GetFieldID(cls, "signatures""[Landroid/content/pm/Signature;"); 
  59.         // 得到签名数组 
  60.         jobjectArray signatures = (jobjectArray) env->GetObjectField(packageInfo, fid); 
  61.         // 得到签名 
  62.         jobject signature = env->GetObjectArrayElement(signatures, 0); 
  63.  
  64.         // 获得Signature类 
  65.         cls = env->GetObjectClass(signature); 
  66.         mid = env->GetMethodID(cls, "toByteArray""()[B"); 
  67.         // 当前应用签名信息 
  68.         jbyteArray signatureByteArray = (jbyteArray) env->CallObjectMethod(signature, mid); 
  69.         //转成jstring 
  70.         jstring str = toMd5(env, signatureByteArray); 
  71.         char *c_msg = (char *) env->GetStringUTFChars(str, 0); 
  72.         LOGD("getSignature release sign md5: %s", c_msg); 
  73.         isRight = strcmp(c_msg, SIGN_MD5) == 0; 
  74.         return isRight; 
  75.     } 
  76.     return isRight; 
  77.  
  78.  
  79. //有了校验的方法,所以我们要对第3步中,获取密钥和加密方法的进行修改,添加校验的逻辑 
  80. extern "C" JNIEXPORT jstring JNICALL Java_com_test_util_HttpKeyUtil_getHttpSecretKey 
  81.         (JNIEnv *env, jclass cls, jint index) { 
  82.     if (getSignature(env)){//校验通过 
  83.       if (随机数条件1) { 
  84.         return env->NewStringUTF(KEY1); 
  85.       } else if (随机数条件2) { 
  86.         return env->NewStringUTF(KEY2); 
  87.       } else if (随机数条件3) { 
  88.         return env->NewStringUTF(KEY3); 
  89.       } else { 
  90.         return env->NewStringUTF(UNKNOWN); 
  91.       } 
  92.     }else { 
  93.         return env->NewStringUTF(UNKNOWN); 
  94.     } 
  95.  
  96. extern "C" JNIEXPORT jstring JNICALL 
  97. Java_com_test_util_HttpKeyUtil_getSecretValue 
  98.         (JNIEnv *env, jclass cls, jbyteArray jbyteArray1) { 
  99.         //加密算法各有不同,这里我就用md5做个示范 
  100.     if (getSignature(env)){//校验通过 
  101.        return toMd5(env, jbyteArray1); 
  102.     }else { 
  103.         return env->NewStringUTF(UNKNOWN); 
  104.     } 
  105.  
  106. 作者:AirSj 
  107. 链接:https://juejin.im/post/6862732328406351879 
  108. 来源:掘金 
  109. 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 

5.总结

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

安全性的认识,安全无小事

发布出去的包必须走加固流程,为了防止疏漏,禁止人工打包加固,全部通过脚本实现

服务端增加相关风险的报警机制

作者:AirSj

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

来源:掘金


本文转载自网络,原文链接:https://juejin.im/post/6862732328406351879
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:快速扩展应用程序的6条经验 下一篇:没有了

推荐图文

  • 周排行
  • 月排行
  • 总排行

随机推荐