1.活动期间是一个沉闷的下午,我还在悠闲地敲着代码,喝着茶。突然服务端同事告诉我注意力接口被生搬硬套了,我怀疑有人用脚本刷接口(目的主要是为了平台引流)。纳尼?没办法,因为据我所知,接口请求是加密的。除非您知道加密密钥和加密方法,否则调用不会成功。你一定觉得不对。然而,当服务器同事把接口调用日志发给我的时候,却完全否定了我的运气。接口调用频率固定为1s,每次调用follower的id加1(目前业务中userid的生成是根据注册时间递增)。加密的密钥总是使用固定的(一般在固定的几个密钥中每次都会随机使用其中一个)根据以上三点可以断定一定存在刷界面的行为。2、事件分析由于上述刷界面行为成立,说明密钥和加密方式被对方知晓。原因不外乎以下两点:第一点在确认内部人士泄露的apk已经被破解后基本排除。只是破解了apk,但是apk发布的包都经过了加固和混淆。难道对方已经拆包了?不管发生什么,先尝试自己反编译。于是我从最近发布的版本开始一个个反编译,最后在反编译到一个更早的版本时发现,存储密钥和加密的工具源码完全暴露了。大惊小怪,经查,这个版本没有加固就发布了,加密工具类也没有混淆。虽然不清楚对方是否通过这种方式获得了密钥和加密算法,但毫无疑问,这是客户端的安全漏洞。3、事件处理既然发现了上面的问题,那就想办法解决。首先,不管加固,如何尽可能保证客户端中的敏感数据不被泄露?另一方面,就算对方想要破解,也必须想方设法设置障碍,增加破解的难度。想到这里,我基本确定了一个思路:使用NDK将敏感数据和加密方式放在native层,因为编译C++代码后生成的so库是一个二进制文件,这无疑会增加破解的难度。利用这个特性,可以将客户端的敏感数据写入C++代码中,从而增强应用程序的安全性。我们开始做吧!!!1.首先创建一个加密工具类:publicclassHttpKeyUtil{static{System.loadLibrary("jniSecret");}//根据随机值获取密钥publicstatinativeStringgetHttpSecretKey(intindex);//将被加密的数据传入,返回加密后的结果publicstaticnativeStringgetSecretValue(byte[]bytes);}2.生成相应的头文件:#include#ifndef_Included_com_test_util_HttpKeyUtil#define_Included_com_test_util_HttpKeyUtil#ifdef__cplusplusextern"C"{#endifJNIEXPORTjstringJNICALLJava_com_esky_common_component_util_HttpKeyUtil_getHttpSecretKey(JNIEnv*,jclass,jint);JNIEXPORTjstringJNICALLJava_com_test_util_HttpKeyUtil_getSecretValue(JNIEnv*,jclass,jbyteArray);#ifdef__cplusplus}#endif#endif3.编写对应的cpp文件:在对应的Module中创建jni目录,复制com_test_util_HttpKeyUtil.h,然后创建com_ttest.putcpp文件#include#include#include#include"com_test_util_HttpKeyUtil.h"extern"C"constchar*KEY1="key1";constchar*KEY2="key2";constchar*KEY3="Key3";构造har*UNKNOWN="unknown";jstringtoMd5(JNIEnv*pEnv,jbyteArraypArray);extern"C"JNIEXPORTjstringJNICALLJava_com_test_util_HttpKeyUtil_getHttpSecretKey(JNIEnv*env,jclasscls,jintindex){if(随机数条件1){returnenvKelse->EYString(1RUTFnumber)}(条件2){returnenv->NewStringUTF(KEY2);}elseif(随机数条件3){returnenv->NewStringUTF(KEY3);}else{returnenv->NewStringUTF(UNKNOWN);}}extern"C"JNIEXPORTjstringJNICALLJava_com_test_util_HttpKeyUtil_getSecretValue(JNIEnv*env,jclasscls,jbyteArrayjbyteArray1){//加密算法不一样,这里我用md5来做示范("java/security/MessageDigest");//MessageDigest.getInstance()jmethodIDmidGetInstance=env->GetStaticMethodID(classMessageDigest,"getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;");//MessageDigestobjectjobjectobjMessageDigest=env->CallStaticObjectMethod(classMessageDigest,midGetInstance,env->NewStringUTF("md5"));jmethodIDmidUpdate=env->GetMethodID(classMessageDigest,"更新","([B)V");env->CallVoidMethod(objMessageDigest,midUpdate,source);//DigestjmethodIDmidDigest=env->GetMethodID(classMessageDigest,"digest","()[B");jbyteArrayobjArraySign=(jbyteArray)env->CallObjectMethod(objMessageDigest,midDigest);jsizeintArrayLength=env->GetArrayLength(objArraySign);jbyte*byte_array_elements=env->GetByteArrayElements(objArraySign,NULL);size_tlength=(size_t)intArrayLength*2+1;char*char_result=(char*)malloc(length);memset(char_result,0,length);toHexStr((constchar*)byte_array_elements,char_result,intArrayLength);//在末尾补\0*(char_result+intArrayLength*2)='\0';jstringstringResult=env->NewStringUTF(char_result);//releaseenv->ReleaseByteArrayElements(objArraySign,byte_array_elements),JNI_ABORT);//指针free(char_result);returnstringResult;}//转为十六进制字符串voidtoHexStr(constchar*source,char*dest,intsourceLen){shorti;charhighByte,lowByte;for(i=0;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。事件就此结束?到这里就结束了吗?太元太简单了!!!虽然密钥和加密算法都是用c++写的,看起来比较安全,但是万一有人反编译,获取c++代码最终生成的so库,然后直接调用so库中的方法获取密钥并调用加密方法。怎么破?看来还需要增加一个身份验证的步骤:即在native层对应用的包名和签名进行身份验证验证,验证通过后才返回正确的结果。下面是获取apk包名和签名校验的代码:constchar*PACKAGE_NAME="yourApplicationId";//(签名的md5值可以写方法获取,也可以直接用签名工具获取,一般在连接微信SDK时使用应用签名的MD5值)constchar*SIGN_MD5="你的应用签名的MD5值大写";//获取Application实例jobjectgetApplication(JNIEnv*env){jobjectapplication=NULL;//这里是你的Application的类路径,混淆的时候注意不要把这个类和获取这个类实例的方法搞混了,比如getInstancejclassbaseapplication_clz=env->FindClass("com/test/component/BaseApplication");如果(baseapplication_clz!=NULL){jmethodIDcurrentApplication=env->GetStaticMethodID(baseapplication_clz,"getInstance","()Lcom/test/component/BaseApplication;");if(currentApplication!=NULL){application=env->CallStaticObjectMethod(baseapplication_clz,currentApplication);}env->DeleteLocalRef(baseapplication_clz);}returnapplication;}boolisRight=false;//获取应用签名的MD5值,判断是否与本应用一致jbooleangetSignature(JNIEnv*env){LOGD("getSignatureisRight:%d",isRight?1:0);if(!isRight){//每次都检查,避免浪费资源,只要第一次检查通过,以后就不再检查了jobjectcontext=getApplicationtion(env);//获取Context类jclasscls=env->FindClass("android/content/Context");//获取getPackageManager方法的IDjmethodIDmid=env->GetMethodID(cls,"getPackageManager","()Landroid/content/pm/PackageManager;");//获取应用包管理器jobjectpm=env->CallObjectMethod(context,mid);//获取getPackageName方法的IDmid=env->GetMethodID(cls,"getPackageName","()Ljava/lang/String;");//获取当前应用包名jstringpackageName=(jstring)env->CallObjectMethod(context,mid);constchar*c_pack_name=env->GetStringUTFChars(packageName,NULL);//比较包名,如果不一致,直接返回包名if(strcmp(c_pack_name,PACKAGE_NAME)!=0){returnfalse;}//获取PackageManager类cls=env->GetObjectClass(pm);//获取getPackageInfo方法的IDmid=env->GetMethodID(cls,"getPackageInfo","(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");//获取应用包信息jobjectpackageInfo=env->CallObjectMethod(pm,mid,packageName,0x40);//GET_SIGNATURES=64;//获取PackageInfo类cls=env->GetObjectClass(packageInfo);//获取签名数组属性的IDjfieldIDfid=env->GetFieldID(cls,"signatures","[Landroid/content/pm/Signature;");//获取签名数组jobjectArraysignatures=(jobjectArray)env->GetObjectField(packageInfo,fid);//获取签名jobjectsignature=env->GetObjectArrayElement(signatures,0);//获取签名类cls=env->GetObjectClass(signature);mid=env->GetMethodID(cls,"toByteArray","()[B");//当前应用签名信息jbyteArraysignatureByteArray=(jbyteArray)env->CallObjectMethod(signature,mid);//转换为jstringjstringstr=toMd5(env,signatureByteArray);char*c_msg=(char*)env->GetStringUTFChars(str,0);LOGD("getSignaturereleasesignmd5:%s",c_msg);isRight=strcmp(c_msg,SIGN_MD5)==0;returnisRight;}returnisRight;}//有一种验证方法,所以我们需要修改步骤3中的访问密钥和加密方法,并添加验证逻辑extern"C"JNIEXPORTjstringJNICALLJava_com_test_util_HttpKeyUtil_getHttpSecretKey(JNIEnv*env,jclasscls,jintindex){if(getSignature(env)){//验证通过if(随机数条件1){returnenv->NewStringUTF(KEY1);}elseif(随机数Item2){returnenv->NewStringUTF(KEY2);}elseif(随机数条件3){returnenv->NewStringUTF(KEY3);}else{returnenv->NewStringUTF(UNKNOWN);}}else{returnenv->NewStringUTF(UNKNOWN);}}extern"C"JNIEXPORTjstringJNICALLJava_com_test_util_HttpKeyUtil_getSecretValue(JNIEnv*env,jclasscls,jbyteArrayjbyteArray1){//加密算法不同,这里我用md5来演示if(getSignature(env)){//验证通过returnMd5(env,jbyteArray1);}else{returnenv->NewStringUTF(UNKNOWN);}}作者:AirSj链接:https://juejin.im/post/6862732328406351879来源:掘金版权所有归作者所有。如需商业转载,请联系作者获得授权。非商业转载请注明出处。5.总结以上就是本次事件native的相关代码。至于如何生成so库,可以自行百度。此次事件需要反思的几点是:安全意识,安全无小事,发布的包必须经过加固过程,为防止遗漏,禁止人工打包加固,所有服务器告警机制-端增加相关风险通过脚本实现作者:AirSj链接:https://juejin.im/post/6862732328406351879来源:掘金