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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容