Thoughts caused by Android sensitive data disclosure

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.

  1. The frequency of interface call is fixed as once per 1s
  2. 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)
  3. 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!

Keywords: Python Database Back-end

Added by iHack on Sun, 30 Jan 2022 23:01:34 +0200