Android角落 - 淺談jni的簡單使用和身份驗證

很多時候,為了保護一些核心代碼或者增加效率,我們通常會把一些可以在Java層實現的代碼寫到C層,通過Jni來調用。

因為Java代碼很容易被反編譯,但so包的代碼相對而言沒有那么容易被破解(雖然花費一點時間還是可以的,度娘有好多這種資料),所以很多加密解密等代碼放在so里面是個比較不錯的選擇。

然而,我們都知道,so包也是可以隨便通過System.loadLibrary來加載的(so放在lib文件夾下的情況)。

舉個例子,比如我們有一段加密的操作是封裝到so里面,假如我是一個破解者,我通過反編譯java文件得到native方法的調用,然后我直接加載so包,并在我們的包下面創建相同的包名(假設已經破譯出混淆后代碼的包名),這樣一來我們就不需要知道so里面的代碼,直接使用就可以了。

因此,對于so包的使用者,我們就需要做一個身份驗證。


工程搭建

本文需要準備兩個工程(ndk環境等等的配置這里不詳細說明):

  • 原工程,包含兩個module(包名:reazerdp.com.mytestso,native包名:reazerdp.com.mynative)
  • 測試工程(包名:reazerdp.com.myhacktest)
  • Native方法:initLib(),getPassword()

在Native的module里面創建出我們的測試代碼:

首先寫出native的方法,這時候方法名紅名先不管

native方法

然后寫出我們的測試類,so包的名字我們暫定為MyTestLib

測試方法

按照傳統方法,我們需要通過javah來生成頭文件,但我們這次就不這么做了,一來麻煩而來實在不太喜歡長長的方法名,因此我們是需要接下來我們直接新建一個c++文件,并且引入常用的幾個頭文件和定義一些宏

cpp

接著寫入我們的MK文件,其中MODULE名字就是取我們MyTest.java里面的那個("MyTestLib")

mk

同樣新建一個Application.mk,定義需要輸出的平臺

mk2

隨后右鍵我們的cpp,選擇link c++,選擇ndk并選到我們的mk文件,等待as幫我們添加到gradle就好了。


cpp連接

step1:定義包名

既然我們不采用常規的“包名_方法名”的寫法,那么我們就需要做一個方法映射,首先把我們的包名定義好,就是把點換成斜杠

#define PACKATE_PATH "reazerdp/com/mynative/MyNative"

step2:定義方法

然后編寫我們的native方法,至于命名隨意,我這里就統一以native_開頭,其中前面兩個都是固定的,第三個是傳入來的參數,除了基本變量外,基本上都是object(對應到jni就是jobject)

JNICALL jboolean native_initLib(JNIEnv *env, jobject obj, jobject contextObject) {
}

JNICALL jstring native_getPassword(JNIEnv *env, jobject obj) {
    return env->NewStringUTF("返回了一個測試密碼:123");
}

step3:定義映射表

具體格式是:類方法名,函數簽名,cpp里面的函數

static JNINativeMethod nativeMethods[] = {
        {"initLib",     "(Landroid/content/Context;)Z", (void *) native_initLib},
        {"getPassword", "()Ljava/lang/String;",         (void *) native_getPassword}
};

查看方法簽名可以用javap -s xxx.class查看。

具體操作時先build一次工程,然后在對應module的build文件夾下生成的classes找到

如:

查看方法簽名

step4:加載方法

static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *gMethods, int numMethods) {
    jclass clazz;
    clazz = env->FindClass(className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }

    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

static int registerNatives(JNIEnv *env) {
    return registerNativeMethods(env, PACKATE_PATH, nativeMethods, NELEM(nativeMethods));
}


extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    jint result = -1;

    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        LOGE("JNI_ONLOAD:%s", "failed");
        return -1;
    }
    assert(env != NULL);

    if (!registerNatives(env)) {//注冊
        return -1;
    }

/* success -- return valid version number */

    result = JNI_VERSION_1_4;

    return result;
}

so編譯并測試

復制我們的cpp所在文件目錄,在命令行下編譯,具體命令只有一個:ndk-build(需要配置環境)

編譯

編譯出的so包會在對應module下的libs文件夾下,編譯完之后我們就可以將so包復制到我們的工程里面了

接下來我們測試一下so包是否可用

首先去gradle加一下我們的jnilib:

sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }

然后log一下我們的pass:

測試

可以看到現在已經返回了我們的密碼了。

so的驗證方式

身份驗證的方式有很多種,聯網的話方法更是多樣,這里我們不談聯網驗證,只談談單機存在的app如何進行so身份驗證。

就目前而言,比較流行的是包簽名驗證,至于簽名驗證的方法這里就簡單說下,具體網上也有很多。

我們這里在initLib里面先簡單的填寫一下當前流行的身份驗證:

JNICALL jboolean native_initLib(JNIEnv *env, jobject obj, jobject contextObject) {
    jclass contextClass = env->FindClass("android/content/Context");
    jclass signatureClass = env->FindClass("android/content/pm/Signature");
    jclass packageNameClass = env->FindClass("android/content/pm/PackageManager");
    jclass packageInfoClass = env->FindClass("android/content/pm/PackageInfo");

    jmethodID getPackageManagerId = env->GetMethodID(contextClass, "getPackageManager", "()Landroid/content/pm/PackageManager;");
    jmethodID getPackageNameId = env->GetMethodID(contextClass, "getPackageName", "()Ljava/lang/String;");
    jmethodID signToStringId = env->GetMethodID(signatureClass, "toCharsString", "()Ljava/lang/String;");
    jmethodID getPackageInfoId = env->GetMethodID(packageNameClass, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");


    jobject packageManagerObject = env->CallObjectMethod(contextObject, getPackageManagerId);
    jstring packNameString = (jstring) env->CallObjectMethod(contextObject, getPackageNameId);
    jobject packageInfoObject = env->CallObjectMethod(packageManagerObject, getPackageInfoId, packNameString, 64);
    jfieldID signaturefieldID = env->GetFieldID(packageInfoClass, "signatures", "[Landroid/content/pm/Signature;");
    jobjectArray signatureArray = (jobjectArray) env->GetObjectField(packageInfoObject, signaturefieldID);
    jobject signatureObject = env->GetObjectArrayElement(signatureArray, 0);

    jstring signatureStr = (jstring) env->CallObjectMethod(signatureObject, signToStringId);
    const char *signStrng = env->GetStringUTFChars(signatureStr, 0);

    env->DeleteLocalRef(contextClass);
    env->DeleteLocalRef(signatureClass);
    env->DeleteLocalRef(packageNameClass);
    env->DeleteLocalRef(packageInfoClass);


    if (strcmp(signStrng, RELEASE_SIGN) == 0) {
        env->ReleaseStringUTFChars(signatureStr, signStrng);
        auth = JNI_TRUE;
        return JNI_TRUE;
    } else {
        auth = JNI_FALSE;
        return JNI_FALSE;
    }
}

代碼有點長,其實主要都是把java的獲取簽名方法翻譯一遍而已。

其中RELEASE_SIGN這個數據是事先打包獲取的簽名,在java層獲取簽名代碼如下:

  public static String getSignature(Context context) {
        try {
          
            PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
        
            Signature[] signatures = packageInfo.signatures;
      
            return signatures[0].toCharsString();
      
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

最后修改一下我們獲取密碼的代碼:

JNICALL jstring native_getPassword(JNIEnv *env, jobject obj) {
    if (auth) {
        return env->NewStringUTF("返回了一個測試密碼:123");
    } else{
        return env->NewStringUTF("驗證不通過,不返回密碼");
    }
}

最后我們測試的時候就可以看到驗證信息:

so驗證

嘗試繞過驗證

雖然現在看起來挺安全的,但是我們有沒有想過這個是不是可以繞過驗證,直接獲取方法呢?

很簡單,我們試試就知道了。

接下來切換到我們的hack工程,把so包復制過去并配置gradle。

接下啦我們需要新建一個跟原工程一樣路徑的包,并把java代碼復制過去:(reazerdp.com.mynative)

偽造一個包

做到這里,我們就相當于偽造了一個包。。。

接下來我們開始盜用我們測試應用的context,至于怎么盜用,非常簡單。。。

代碼如下:

 Context context=this.createPackageContext("reazerdp.com.mytestso",CONTEXT_INCLUDE_CODE|CONTEXT_IGNORE_SECURITY);

因為我們是在activity下寫的這句代碼,所以直接用this代替context即可。

接著我們用創建出的這個context來進行盜用。

繞過驗證

通過圖片上的代碼我們不難看出,通過createPackageContext創建出來的context毫無疑問通過了驗證,而傳入我們hack工程的context則是無法通過驗證的。


分析

從上面的測試不難看出,簡單的身份驗證這個方法似乎不能有效的防止盜用,從根本上來說,是因為我們可以使用到別的項目的context,只要調用類的包名對得上,就可以繞過這種驗證方式了。

我們都知道,相同包名的兩個應用是不能共存的,而通過一個context創建出來的context也必然會有一些自己的消息。

所以我們debug一下我們創建出來的context

debug context

我們可以看到,packageinfo里面的包名是我們創建出的context的包名,但同時有個mBasePackageName是屬于我們這個工程的包名。

那么我們是否可以在這方面入手呢?比如我們在驗證簽名的同時還要驗證傳進來的context的basepackagename

————答案是:不可取- -

原因有二:

  • 首先,在api17或者以下,contextWrapper沒辦法獲取到這個字段
  • 其次,既然我們在java層創建出了context,也就意味著我們可以直接反射改掉這個字段,從而繞過驗證。

雖然從驗證字段這方面不能入手,但是從這個方向上來說,我們如果可以知道當前運行代碼的程序的包名,然后再通過它來驗證的話,似乎就可以解決這個問題。

事實上,很幸運,我們確實可以做得到。


包名驗證

我們都知道,Binder是安卓系統里IPC的方式之一,既然如此,我們必定可以通過Binder獲取一些運行著的應用的信息。

比如pid,uid等。

而如果平時有碰到過packageManager的同學,應該知道packageManager可以通過uid獲取到包的名字。

而我們正是通過這個方式來解決上面說的問題。

在java層,獲取包名的方式是這樣的:

this.getPackageManager().getNameForUid(Binder.getCallingUid());

而翻譯到C,我們則需要下面幾步:

  • 獲取到packageManager對象
  • 獲取到Binder對象
  • 調用getCallingUid()方法

在我們上面的cpp中,我們其實已經獲取過packageManager了,所以這里我們只需要補充一下Binder的即可

JNICALL jboolean native_initLib(JNIEnv *env, jobject obj, jobject contextObject) {

    //binder
    jclass binderClass = env->FindClass("android/os/Binder");
    
    ...獲取packageManager等,跟上面代碼一樣,略過
    
    //反射packageManager的getNameForUid方法
    jmethodID getRunningPackageName = env->GetMethodID(packageNameClass, "getNameForUid", "(I)Ljava/lang/String;");
    
    //反射Binder的getCallingUid方法(該方法是靜態方法))
    jmethodID getUid = env->GetStaticMethodID(binderClass, "getCallingUid", "()I");

    //得到uid
    jint uid = env->CallStaticIntMethod(binderClass, getUid);
    
    ...獲取簽名等方法,跟上面一樣,略過

    //獲取uid對應的app的包名
    jstring mRunningPackageName = (jstring) env->CallObjectMethod(packageManagerObject, getRunningPackageName, uid);
    
    //跟我們的包對應并判斷
    if (mRunningPackageName) {
        const char *runPackageName = env->GetStringUTFChars(mRunningPackageName, 0);
        LOGI("rPackageName:%s", runPackageName);
        if (strcmp(runPackageName, "reazerdp.com.mytestso") != 0) {
            return JNI_FALSE;
        }
        env->ReleaseStringUTFChars(mRunningPackageName, runPackageName);
    } else {
        LOGE("rPackageName:%s", "is null");
        return JNI_FALSE;
    }

    ...返回值,跟上面的一樣,忽略
}

最后打包我們在測試一次,

驗證

這一次,即使是創建出來的context,我們也沒法繞過驗證了。

寫在最后

這個方法我不清楚是否很好,但目前來說可以解決我的需求,如果您有更好的方法,歡迎一起探討-V-

附錄:CPP完整代碼:

//
// Created by 大燈泡 on 2017/1/16.
//

#include <jni.h>
#include <string.h>
#include <assert.h>
#include <android/log.h>

#define   NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
#define   LOG_TAG    "MyNativeLib"
#define   LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define   LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

#define PACKATE_PATH "reazerdp/com/mynative/MyNative"
const char *RELEASE_SIGN = "3082033130820219a0030201020204596e28f5300d06092a864886f70d01010b05003049310b3009060355040613023836310a30080603550408130161310a30080603550407130161310a3008060355040a130161310a3008060355040b130161310a30080603550403130161301e170d3137303131373039303231365a170d3432303131313039303231365a3049310b3009060355040613023836310a30080603550408130161310a30080603550407130161310a3008060355040a130161310a3008060355040b130161310a3008060355040313016130820122300d06092a864886f70d01010105000382010f003082010a02820101009f1f3731ef4c65ccd6c4a7589eaffe813117d2112cc92279f41a22f210398baa2ddae52fd61c736b51b21c01d4a3233fd34b2b29365723bdb285bf0eddd043b7a9dd2829366974a690aa885b859a2d3fb272baf8c3ab94024f97117b6d6a68b74f2ed35daca41ef601a48c9f3393d92a4c3bb6f26152142e03290ef1d607361b0a2759479a7f0b94425bd885db49bcbb777f7dc10e7d3eff1fa4cc3080b4c8524ca6b761732100347b80d56a9bd5f6e7d503debe5c25c60194bd1c34c54f40172f2add9cf7e934aa7e64467c362d87fc91069fd29afc5e3445f609daf4fb99905c6ec17bea73252f6b264fdbb6963f5822997b36af9caccb2869a8b87a942df50203010001a321301f301d0603551d0e041604144aaa523ada5947919a2f7dbbe8cd3711b8dbb08e300d06092a864886f70d01010b050003820101008e6153b54104503b04a04d2746c35ce094688c2f05cd6f8c7edbcabb0d801a57c55f75930081294e63bbe27af5705511d8b7e5e263f0c6a9af58fd8c87fa43e22358c92ec4378ced89aa164f9770ebde94f865572bb846ce2cdf48ec5f6ddd1e4a733a5faca96244cd8e250cec6c0a16740e5bb7907db19d1db260806b4efd890c264ec59d46135b4f82077d3f233f5b349601b217f28d8392d90ae1fd5f462ec7e5889677bbd6c0054ea680b6dc9746077d8d536d7bc5a39dbb3074658c986a8ca14b6110599808d6f4532e32e179af558df1305880d97599d23eda5f25b0b82f091cfd702d187cfbdffc3f5bbbb9f17ae660683b07c566df5622d6e19462f8";
static jboolean auth = JNI_FALSE;

JNICALL jboolean native_initLib(JNIEnv *env, jobject obj, jobject contextObject) {

    jclass binderClass = env->FindClass("android/os/Binder");

    jclass contextClass = env->FindClass("android/content/Context");
    jclass signatureClass = env->FindClass("android/content/pm/Signature");
    jclass packageNameClass = env->FindClass("android/content/pm/PackageManager");
    jclass packageInfoClass = env->FindClass("android/content/pm/PackageInfo");

    jmethodID getPackageManagerId = env->GetMethodID(contextClass, "getPackageManager", "()Landroid/content/pm/PackageManager;");
    jmethodID getPackageNameId = env->GetMethodID(contextClass, "getPackageName", "()Ljava/lang/String;");
    jmethodID signToStringId = env->GetMethodID(signatureClass, "toCharsString", "()Ljava/lang/String;");
    jmethodID getPackageInfoId = env->GetMethodID(packageNameClass, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    jmethodID getRunningPackageName = env->GetMethodID(packageNameClass, "getNameForUid", "(I)Ljava/lang/String;");
    jmethodID getUid = env->GetStaticMethodID(binderClass, "getCallingUid", "()I");

    jint uid = env->CallStaticIntMethod(binderClass, getUid);


    jobject packageManagerObject = env->CallObjectMethod(contextObject, getPackageManagerId);
    jstring packNameString = (jstring) env->CallObjectMethod(contextObject, getPackageNameId);
    jobject packageInfoObject = env->CallObjectMethod(packageManagerObject, getPackageInfoId, packNameString, 64);
    jfieldID signaturefieldID = env->GetFieldID(packageInfoClass, "signatures", "[Landroid/content/pm/Signature;");
    jobjectArray signatureArray = (jobjectArray) env->GetObjectField(packageInfoObject, signaturefieldID);
    jobject signatureObject = env->GetObjectArrayElement(signatureArray, 0);

    jstring mRunningPackageName = (jstring) env->CallObjectMethod(packageManagerObject, getRunningPackageName, uid);

    if (mRunningPackageName) {
        const char *runPackageName = env->GetStringUTFChars(mRunningPackageName, 0);
        LOGI("rPackageName:%s", runPackageName);
        if (strcmp(runPackageName, "reazerdp.com.mytestso") != 0) {
            return JNI_FALSE;
        }
        env->ReleaseStringUTFChars(mRunningPackageName, runPackageName);
    } else {
        LOGE("rPackageName:%s", "is null");
        return JNI_FALSE;
    }

    jstring signatureStr = (jstring) env->CallObjectMethod(signatureObject, signToStringId);
    const char *signStrng = env->GetStringUTFChars(signatureStr, 0);

    env->DeleteLocalRef(binderClass);
    env->DeleteLocalRef(contextClass);
    env->DeleteLocalRef(signatureClass);
    env->DeleteLocalRef(packageNameClass);
    env->DeleteLocalRef(packageInfoClass);


    if (strcmp(signStrng, RELEASE_SIGN) == 0) {
        env->ReleaseStringUTFChars(signatureStr, signStrng);
        auth = JNI_TRUE;
        return JNI_TRUE;
    } else {
        auth = JNI_FALSE;
        return JNI_FALSE;
    }
}

JNICALL jstring native_getPassword(JNIEnv *env, jobject obj) {
    if (auth) {
        return env->NewStringUTF("返回了一個測試密碼:123");
    } else{
        return env->NewStringUTF("驗證不通過,不返回密碼");
    }
}

static JNINativeMethod nativeMethods[] = {
        {"initLib",     "(Landroid/content/Context;)Z", (void *) native_initLib},
        {"getPassword", "()Ljava/lang/String;",         (void *) native_getPassword}
};

static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *gMethods, int numMethods) {
    jclass clazz;
    clazz = env->FindClass(className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }

    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

static int registerNatives(JNIEnv *env) {
    return registerNativeMethods(env, PACKATE_PATH, nativeMethods, NELEM(nativeMethods));
}


extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    jint result = -1;

    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        LOGE("JNI_ONLOAD:%s", "failed");
        return -1;
    }
    assert(env != NULL);

    if (!registerNatives(env)) {//注冊
        return -1;
    }

/* success -- return valid version number */

    result = JNI_VERSION_1_4;

    return result;
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容