1. The whole story
One cool afternoon, I saw a news that the interface was being mechanically called. I suspected that someone was using script to brush the interface (mainly to divert water from the platform).
what? No, the general interface request is encrypted. Unless you know the encryption key and encryption method, the call will not succeed. You must feel wrong. When the colleagues on the server side log out the interface call, they completely negate the fluke mentality.
- The frequency of interface call is fixed as once per 1s
- The id of the followed person is incremented by one for each call (at present, the generation of user id is incremented according to the registration time in business)
- The encryption key always uses a fixed one (normally, one of the fixed keys will be used randomly at a time)
Based on the above three points, it can be concluded that there must be the behavior of brushing the interface.
2. Event analysis
Since the above behavior of brushing the interface is established, it means that the key and encryption method are known by the other party. The reasons are nothing more than the following two points:
- Internal personnel leakage
- apk cracked
After confirmation, the first point is basically excluded, and only apk is cracked. However, the package released by apk has been reinforced and confused. Has the other party shelled? No matter 37 or 21, try decompiling yourself first.
So I decompile one by one from the recently released version. Finally, when decompiling to an earlier version, I found that the source code of the tool class that saved the key and encrypted was completely exposed.
After frying the pot, I checked that this version was released without reinforcement, and this encryption tool class was not confused. Although it is not clear whether the other party obtains the key and encryption algorithm in this way, there is no doubt that this is a security vulnerability on the client.
3. Event handling
Now that the above problems have been found, we must find a way to solve them.
First of all, without considering reinforcement, how to ensure that the sensitive data in the client is not leaked as much as possible? On the other hand, even if the other party wants to crack, they should also find ways to set up obstacles and increase the difficulty of cracking.
Thinking of this, I basically determined an idea: use NDK to put sensitive data and encryption methods into the native layer, because the so library generated after C + + code compilation is a binary file, which will undoubtedly increase the difficulty of cracking. Using this feature, the sensitive data of the client can be written in C + + code, so as to enhance the security of the application. Just do it!!!
1. First, create the encryption tool class:
public class HttpKeyUtil { static { System.loadLibrary("jniSecret"); } //Obtain the key according to the random value public static native String getHttpSecretKey(int index); //Pass in the data to be encrypted and return the encrypted result public static native String getSecretValue(byte[] bytes); }
2. Generate corresponding header file:
com_test_util_HttpKeyUtil.h
#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. Prepare corresponding cpp documents:
Create a jni directory in the corresponding Module and send com_test_util_HttpKeyUtil.h copy in, and then create com_test_util_HttpKeyUtil.cpp file
#include <jni.h> #include <cstring> #include <malloc.h> #include "com_test_util_HttpKeyUtil.h" extern "C" const char *KEY1 = "Key 1"; const char *KEY2 = "Key 2"; const char *KEY3 = "Key 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 (Random number condition 1) { return env->NewStringUTF(KEY1); } else if (Random number condition 2) { return env->NewStringUTF(KEY2); } else if (Random number condition 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) { //Encryption algorithms are different. Here I'll use md5 as a demonstration 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); // Fill in at the end \ 0 *(char_result + intArrayLength * 2) = '\0'; jstring stringResult = env->NewStringUTF(char_result); // release env->ReleaseByteArrayElements(objArraySign, byte_array_elements, JNI_ABORT); // Pointer free(char_result); return stringResult; } //Convert to hexadecimal string 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. Is this the end of the incident?
Is this the end? too yuang too simple!!! Although the key and encryption algorithm are written in c + +, it seems to be more secure.
But what if someone gets the so library finally generated by c + + code after decompiling, and then directly calls the method in the so library to obtain the key and call the encryption method?
It seems that we still need to add one step of identity verification: that is, authenticate and verify the package name and signature of the application in the native layer, and return the correct result only after passing the verification. The following is the code for obtaining apk package name and signature verification:
const char *PACKAGE_NAME = "Yours ApplicationId"; //(the MD5 value of the signature can be obtained by writing or directly using the signature tool. Generally, the MD5 value of the signature will also be applied when connecting to the wechat sdk) const char *SIGN_MD5 = "Your app is signed MD5 Note that the value is capitalized"; //Get Application instance jobject getApplication(JNIEnv *env) { jobject application = NULL; //Here is the class path of your Application. When confusing, be careful not to confuse this class with the method of obtaining instances of this class, such as 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; //Obtain the MD5 value of the application signature and judge whether it is consistent with that of the application jboolean getSignature(JNIEnv *env) { LOGD("getSignature isRight: %d", isRight ? 1 : 0); if (!isRight) {//Avoid wasting resources by checking every time. As long as the first verification is passed, the latter will not be checked jobject context = getApplication(env); // Get Context class jclass cls = env->FindClass("android/content/Context"); // Get the ID of the getPackageManager method jmethodID mid = env->GetMethodID(cls, "getPackageManager", "()Landroid/content/pm/PackageManager;"); // Get the manager of the application package jobject pm = env->CallObjectMethod(context, mid); // Get the ID of the getPackageName method mid = env->GetMethodID(cls, "getPackageName", "()Ljava/lang/String;"); // Get the current app package name jstring packageName = (jstring) env->CallObjectMethod(context, mid); const char *c_pack_name = env->GetStringUTFChars(packageName, NULL); // Compare the package names. If they are inconsistent, return the package name directly if (strcmp(c_pack_name, PACKAGE_NAME) != 0) { return false; } // Get PackageManager class cls = env->GetObjectClass(pm); // Get the ID of the getPackageInfo method mid = env->GetMethodID(cls, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;"); // Get application package information jobject packageInfo = env->CallObjectMethod(pm, mid, packageName, 0x40); //GET_SIGNATURES = 64; // Get PackageInfo class cls = env->GetObjectClass(packageInfo); // Gets the ID of the signature array property jfieldID fid = env->GetFieldID(cls, "signatures", "[Landroid/content/pm/Signature;"); // Get signature array jobjectArray signatures = (jobjectArray) env->GetObjectField(packageInfo, fid); // Get signature jobject signature = env->GetObjectArrayElement(signatures, 0); // Get Signature class cls = env->GetObjectClass(signature); mid = env->GetMethodID(cls, "toByteArray", "()[B"); // Current application signature information jbyteArray signatureByteArray = (jbyteArray) env->CallObjectMethod(signature, mid); //Convert to 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; } //With the verification method, we need to modify the key acquisition and encryption method in step 3 and add the verification logic extern "C" JNIEXPORT jstring JNICALL Java_com_test_util_HttpKeyUtil_getHttpSecretKey (JNIEnv *env, jclass cls, jint index) { if (getSignature(env)){//Verification passed if (Random number condition 1) { return env->NewStringUTF(KEY1); } else if (Random number condition 2) { return env->NewStringUTF(KEY2); } else if (Random number condition 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) { //Encryption algorithms are different. Here I'll use md5 as a demonstration if (getSignature(env)){//Verification passed return toMd5(env, jbyteArray1); }else { return env->NewStringUTF(UNKNOWN); } }
5. Summary
The above is the relevant code of the native event. As for how to generate the so library, you can baidu by yourself. From this incident, we need to reflect on the following points:
- Safety awareness, safety is no small matter
- The released package must go through the reinforcement process to prevent omissions
Finally, thank you for reading. Each of your likes, comments and sharing is our greatest encouragement. Refill ~
If you have any questions, please discuss them in the comment area!