很多时候,为了保护一些核心代码或者增加效率,我们通常会把一些可以在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的方法,这时候方法名红名先不管
然后写出我们的测试类,so包的名字我们暂定为MyTestLib
按照传统方法,我们需要通过javah
来生成头文件,但我们这次就不这么做了,一来麻烦而来实在不太喜欢长长的方法名,因此我们是需要接下来我们直接新建一个c++文件,并且引入常用的几个头文件和定义一些宏
接着写入我们的MK文件,其中MODULE名字就是取我们MyTest.java里面的那个("MyTestLib")
同样新建一个Application.mk,定义需要输出的平台
随后右键我们的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("验证不通过,不返回密码");
}
}
最后我们测试的时候就可以看到验证信息:
尝试绕过验证
虽然现在看起来挺安全的,但是我们有没有想过这个是不是可以绕过验证,直接获取方法呢?
很简单,我们试试就知道了。
接下来切换到我们的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
我们可以看到,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;
}