Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Check .apk-signature in C/native Code

I have developed an Android application which also contains a native part written in C (which does not depend on the app).

The application itself is useless if the shared library does not do its work.

I would like to let the native-part (a shared library) only do its work, if there exists an unmodified version of the application (.apk) it has been shipped with.

The best method for me would be this way:

  1. Application get installed
  2. Shared Library checks signature/hash of the application/.apk
  3. Only does its work when the signature matches a known one

In this way, I would like to protect my application from modification and piracy.

Are there any tips for doing this? I just found posts with checking own signature in java, but that's jokeless if one could de- & recompile the app.

like image 311
Martin L. Avatar asked Feb 22 '13 13:02

Martin L.


1 Answers

As I have been asked to publish some code how I'm now checking the CRC-code of my Java-application from within C, here are some snippets.

I cannot post a complete working solution as it's spread over multiple lines due to performance-reasons, but I hope this is most complete and working:

In your MyApplication.java:

public class MyApplication extends Application {
    private static Context context;

    public static Context getAppContext() {
        return MyApplication.context;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        MyApplication.context = getApplicationContext();
    }
}

Android.mk:

LOCAL_CFLAGS += -O3 -DDEBUG_MODE=0 -DCLASSES_CRC=2331492378

Inside your C-code:

#define LOG_TAG "Your Log Tag"

#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

#if DEBUG_MODE
#define LOGDH(...) LOGD(__VA_ARGS__)
#define LOGIH(...) LOGI(__VA_ARGS__)
#define LOGEH(...) LOGE(__VA_ARGS__)
#else
#define LOGDH(...) //
#define LOGIH(...) //
#define LOGEH(...) //
#endif

int isSecure = -1;

jclass MyApplication;
jclass Context;
jclass ApplicationInfo;


jclass ZipFile;
jclass ZipEntry;
jclass CheckedInputStream;
jclass Adler32;
jclass Checksum;

jmethodID MyApplication_getAppContextMethodId;
jmethodID Context_getApplicationInfoMethodId;

jmethodID ZipFile_ConstructorMethodId;
jmethodID CheckedInputStream_ConstructorMethodId;
jmethodID Adler32_ConstructorMethodId;

jmethodID ZipFile_getEntryMethodId;
jmethodID ZipFile_getInputStreamMethodId;
jmethodID CheckedInputStream_readMethodId;
jmethodID CheckedInputStream_getChecksumMethodId;
jmethodID Checksum_getValueMethodId;

jfieldID ApplicationInfo_flagsFieldId;
jfieldID ApplicationInfo_FLAG_DEBUGGABLEFieldId;
jfieldID ApplicationInfo_sourceDirFieldId;


static long getClassesCRC(JNIEnv *env) {
    jobject appContextInstance = (*env)->CallStaticObjectMethod(env,
            MyApplication, MyApplication_getAppContextMethodId);
    if (!appContextInstance) {
        LOGEH("Unable to get instance of AppContext");
        return false;
    }

    jobject applicationInfoInstance = (*env)->CallObjectMethod(env,
            appContextInstance, Context_getApplicationInfoMethodId);
    if (!appContextInstance) {
        LOGEH("Unable to get instance of ApplicationInfo");
        return false;
    }

    jobject zipFileInstance = (*env)->NewObject(env, ZipFile,
            ZipFile_ConstructorMethodId,
            (*env)->GetObjectField(env, applicationInfoInstance,
                    ApplicationInfo_sourceDirFieldId));
    if (!zipFileInstance) {
        LOGEH("Unable to get instance of ZipFile");
        return -1;
    }

    jstring classesDexString = (*env)->NewStringUTF(env, "classes.dex");
    jobject zipEntryInstance = (*env)->CallObjectMethod(env, zipFileInstance,
            ZipFile_getEntryMethodId, classesDexString);
    if (!zipFileInstance) {
        LOGEH("Unable to get instance of ZipEntry");
        return -1;
    }
    (*env)->DeleteLocalRef(env, classesDexString);

    jobject adler32Instance = (*env)->NewObject(env, Adler32,
            Adler32_ConstructorMethodId);
    if (!adler32Instance) {
        LOGEH("Unable to get instance of Adler32");
        return -1;
    }

    jobject inputStreamInstance = (*env)->CallObjectMethod(env, zipFileInstance,
            ZipFile_getInputStreamMethodId, zipEntryInstance);
    if (!inputStreamInstance) {
        LOGEH("Unable to get instance of InputStream");
        return -1;
    }

    jobject checkedInputStreamInstance = (*env)->NewObject(env,
            CheckedInputStream, CheckedInputStream_ConstructorMethodId,
            inputStreamInstance, adler32Instance);
    if (!checkedInputStreamInstance) {
        LOGEH("Unable to get instance of CheckedInputStream");
        return -1;
    }

    int bufferSize = 128;
    jbyteArray bufferBytes = (*env)->NewByteArray(env, bufferSize);

    while ((*env)->CallIntMethod(env, checkedInputStreamInstance,
            CheckedInputStream_readMethodId, bufferBytes) > 0) {
    }
    (*env)->DeleteLocalRef(env, bufferBytes);

    jobject checksumInstance = (*env)->CallObjectMethod(env,
            checkedInputStreamInstance, CheckedInputStream_getChecksumMethodId);
    if (!checksumInstance) {
        LOGEH("Unable to get instance of CheckSum");
        return -1;
    }

    return (*env)->CallLongMethod(env, checksumInstance,
            Checksum_getValueMethodId);
}

static bool isDebuggable(JNIEnv *env) {
    jobject appContextInstance = (*env)->CallStaticObjectMethod(env,
            MyApplication, Application_getAppContextMethodId);
    if (!appContextInstance) {
        LOGEH("Unable to get instance of AppContext");
        return false;
    }

    jobject applicationInfoInstance = (*env)->CallObjectMethod(env,
            appContextInstance, Context_getApplicationInfoMethodId);
    if (!appContextInstance) {
        LOGEH("Unable to get instance of ApplicationInfo");
        return false;
    }

    int FLAG_DEBUGGABLE = (*env)->GetStaticIntField(env, ApplicationInfo,
            ApplicationInfo_FLAG_DEBUGGABLEFieldId);
    int flags = (*env)->GetIntField(env, applicationInfoInstance,
            ApplicationInfo_flagsFieldId);

    return (0 != (flags &= FLAG_DEBUGGABLE));
}

static bool isSecureEnvironment(JNIEnv *env) {

    //isSecure = true; // TODO remove this

    if (isSecure == -1) {
        isSecure = true;

        if (isDebuggable(env)) {
            // someone used the app in debug-mode
#if DEBUG_MODE != 1
            // TODO report

#endif
            LOGEH("App IS DEBUGGABLE!");
            isSecure = false;
        } else {

            // check CRC
            long classesCRC = getClassesCRC(env);
            if (classesCRC != (long) CLASSES_CRC) {
#if DEBUG_MODE != 1
                // TODO report
#endif
                LOGEH("CRC-CHECK FAILED: %lu", classesCRC);

                isSecure = false;
            }
        }
    }
    return isSecure;
}


static bool initJavaClasses(JNIEnv * env) {
    jclass local = (*env)->FindClass(env, "eu/my/MyApplication");
    MyApplication = (*env)->NewGlobalRef(env, local);
    if (!MyApplication) {
    LOGEH("Unable to find the MyApplication class");
    return false;
    }
    local = (*env)->FindClass(env, "android/content/Context");
    Context = (*env)->NewGlobalRef(env, local);
    (*env)->DeleteLocalRef(env, local);
    if (!Context) {
    LOGEH("Unable to find the Context class");
    return false;
    }
    local = (*env)->FindClass(env, "android/content/pm/ApplicationInfo");
    ApplicationInfo = (*env)->NewGlobalRef(env, local);
    (*env)->DeleteLocalRef(env, local);
    if (!ApplicationInfo) {
    LOGEH("Unable to find the ApplicationInfo class");
    return false;
    }

    local = (*env)->FindClass(env, "java/util/zip/ZipFile");
    ZipFile = (*env)->NewGlobalRef(env, local);
    (*env)->DeleteLocalRef(env, local);
    if (!ZipFile) {
    LOGEH("Unable to find the ZipFile class");
    return false;
    }

    local = (*env)->FindClass(env, "java/util/zip/ZipEntry");
    ZipEntry = (*env)->NewGlobalRef(env, local);
    (*env)->DeleteLocalRef(env, local);
    if (!ZipEntry) {
    LOGEH("Unable to find the ZipEntry class");
    return false;
    }

    local = (*env)->FindClass(env, "java/util/zip/CheckedInputStream");
    CheckedInputStream = (*env)->NewGlobalRef(env, local);
    (*env)->DeleteLocalRef(env, local);
    if (!CheckedInputStream) {
    LOGEH("Unable to find the CheckedInputStream class");
    return false;
    }

    local = (*env)->FindClass(env, "java/util/zip/Adler32");
    Adler32 = (*env)->NewGlobalRef(env, local);
    (*env)->DeleteLocalRef(env, local);
    if (!Adler32) {
    LOGEH("Unable to find the Adler32 class");
    return false;
    }

    local = (*env)->FindClass(env, "java/util/zip/Checksum");
    Checksum = (*env)->NewGlobalRef(env, local);
    (*env)->DeleteLocalRef(env, local);
    if (!Checksum) {
    LOGEH("Unable to find the Checksum class");
    return false;
    }

    return true;
}

static bool initJavaMethods(JNIEnv * env) {
    MyApplication_getAppContextMethodId = (*env)->GetStaticMethodID(env,
    MyApplication, "getAppContext", "()Landroid/content/Context;");
    if (!MyApplication_getAppContextMethodId) {
    LOGEH("Unable to find the getAppContext method");
    return false;
    }
    Context_getApplicationInfoMethodId = (*env)->GetMethodID(env, Context,
    "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;");
    if (!Context_getApplicationInfoMethodId) {
    LOGEH("Unable to find the getApplicationInfo method");
    return false;
    }

    ZipFile_ConstructorMethodId = (*env)->GetMethodID(env, ZipFile, "<init>",
    "(Ljava/lang/String;)V");
    if (!ZipFile_ConstructorMethodId) {
    LOGEH("Unable to find the constructor method");
    return false;
    }

    CheckedInputStream_ConstructorMethodId = (*env)->GetMethodID(env,
    CheckedInputStream, "<init>",
    "(Ljava/io/InputStream;Ljava/util/zip/Checksum;)V");
    if (!CheckedInputStream_ConstructorMethodId) {
    LOGEH("Unable to find the constructor method");
    return false;
    }

    Adler32_ConstructorMethodId = (*env)->GetMethodID(env, Adler32, "<init>",
    "()V");
    if (!Adler32_ConstructorMethodId) {
    LOGEH("Unable to find the constructor method");
    return false;
    }

    ZipFile_getEntryMethodId = (*env)->GetMethodID(env, ZipFile, "getEntry",
    "(Ljava/lang/String;)Ljava/util/zip/ZipEntry;");
    if (!ZipFile_getEntryMethodId) {
    LOGEH("Unable to find the getEntry method");
    return false;
    }

    ZipFile_getInputStreamMethodId = (*env)->GetMethodID(env, ZipFile,
    "getInputStream", "(Ljava/util/zip/ZipEntry;)Ljava/io/InputStream;");
    if (!ZipFile_getInputStreamMethodId) {
    LOGEH("Unable to find the getInputStream method");
    return false;
    }

    CheckedInputStream_readMethodId = (*env)->GetMethodID(env, CheckedInputStream,
    "read", "([B)I");
    if (!CheckedInputStream_readMethodId) {
    LOGEH("Unable to find the read method");
    return false;
    }

    CheckedInputStream_getChecksumMethodId = (*env)->GetMethodID(env,
    CheckedInputStream, "getChecksum", "()Ljava/util/zip/Checksum;");
    if (!CheckedInputStream_getChecksumMethodId) {
    LOGEH("Unable to find the getChecksum method");
    return false;
    }

    Checksum_getValueMethodId = (*env)->GetMethodID(env, Checksum, "getValue",
    "()J");
    if (!Checksum_getValueMethodId) {
    LOGEH("Unable to find the getValue method");
    return false;
    }

    return true;
}

static bool initJavaFields(JNIEnv * env) {
    ApplicationInfo_flagsFieldId = (*env)->GetFieldID(env, ApplicationInfo, "flags",
    "I");

    if (!ApplicationInfo_flagsFieldId) {
    LOGEH("Unable to find the flags field");
    return false;
    }

    ApplicationInfo_FLAG_DEBUGGABLEFieldId = (*env)->GetStaticFieldID(env,
    ApplicationInfo, "FLAG_DEBUGGABLE", "I");
    if (!ApplicationInfo_FLAG_DEBUGGABLEFieldId) {
    LOGEH("Unable to get static field FLAG_DEBUGGABLE");
    return false;
    }

    ApplicationInfo_sourceDirFieldId = (*env)->GetFieldID(env, ApplicationInfo,
    "sourceDir", "Ljava/lang/String;");
    if (!ApplicationInfo_sourceDirFieldId) {
    LOGEH("Unable to get static field sourceDir");
    return false;
    }

    return true;
}


jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    (void) reserved; // Suppress the warning.
    JNIEnv * env;

    if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
    return -1;
    }

    if (!initJavaClasses(env)) {
    return -1;
    }

    if (!initJavaMethods(env)) {
    return -1;
    }

    if (!initJavaFields(env)) {
    return -1;
    }

    return JNI_VERSION_1_6;
}

Don't forget to add the methods of the MyApplication into the Proguard-settings to prevent their remove!

Usage:

  1. Create your APK with -DDEBUG_MODE=1
  2. start the APK on your Android
  3. read the value inside "CRC-check failed:xxx"
  4. take that value and insert it into CLASSES_CRC=xxxx
  5. switch to -DDEBUG_MODE=0
  6. rebuild your APK
  7. check if your app is running well
  8. publish

This method is a little more complicated as it requires 2 builds to get a valid APK. But as the CRC is checked inside C and not just the dummy-signature of the APK-file is taken, this check is nearly bullet-proof.

In my situation, I'm for exampling not even doing the setup of the license-mechanisms in case the signature is invalid.

Additionally, as this method only uses numbers and not chars, it's fully compiled.

Hope this helps someone!

like image 71
Martin L. Avatar answered Sep 20 '22 14:09

Martin L.