一般绝大多数的 app 在上线前都会做一层安全防护,比如代码混淆、加固等。
今天就来讲讲其中的一项:校验签名。
校验签名可以有效的防止二次打包,避免你的 app 被植入广告甚至破解等。而今天就从两个角度来讲签名的具体校验:
- Java 层
- C/C++ 层
那么就先开始讲 java 层好了。
private static boolean validateAppSignature(Context context, String apkSignature) {
try {
PackageManager packageManager = context.getApplicationContext().getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
for (Signature signature : packageInfo.signatures) {
String lowerCaseSignature = signature.toCharsString().toLowerCase();
if (lowerCaseSignature.equals(apkSignature)) {
return true;
}
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return false;
}
Java 层的签名校验核心代码就这些,传入的两个参数 :
- Context context : 一般都是 Application
- String apkSignature : 你的 apk 的正式签名
Java 层的签名校验比较容易被攻破,因为别人可以反编译一下,然后在 smali 中把 validateAppSignature 方法的返回值改成 true 就大功告成了。
也正因为如此,所以需要在 C/C++ 层中也加入签名校验。
在 so 文件加载的时候,会去调用 JNI_OnLoad 函数,所以我们可以在这里做签名校验。
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
jint result = -1;
if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
LOGE("no jni version 1.6");
return result;
}
if (checkAppSignature(env) != JNI_TRUE) {
LOGE("the signature of apk is invalid");
return result;
}
return JNI_VERSION_1_6;
}
签名校验的代码主要在 checkAppSignature 函数中:
static jboolean checkAppSignature(JNIEnv *env) {
jclass classNativeContext = (*env)->FindClass(env, "me/yuqirong/security/Security");
jmethodID midGetAppContext = (*env)->GetStaticMethodID(env, classNativeContext,
"getContext",
"()Landroid/content/Context;");
jobject appContext = (*env)->CallStaticObjectMethod(env, classNativeContext, midGetAppContext);
if (appContext != NULL) {
jboolean signatureValid = Android_checkSignature(env, NULL, appContext);
return signatureValid;
} else {
LOGE("app context is null, please check the context");
}
return JNI_FALSE;
}
可以看出来,checkAppSignature 主要是通过 C 的代码反射来获取 Context 。
对应的 Java 层代码如下,一般来说, Security.setContext(application)
会在 Application.onCreate 方法中调用 :
public class Security {
private static Application application;
public static Context getContext() {
return application;
}
private static void setContext(Application context) {
application = context;
}
}
获取到 Context 之后,就可以来比较签名了 :
const char* APP_SIGNATURE = "input your signature of apk";
static jboolean Android_checkSignature(
JNIEnv *env, jclass clazz, jobject context) {
jstring appSignature = loadSignature(env, context);
jstring releaseSignature = (*env)->NewStringUTF(env, APP_SIGNATURE);
const char *charAppSignature = (*env)->GetStringUTFChars(env, appSignature, NULL);
const char *charReleaseSignature = (*env)->GetStringUTFChars(env, releaseSignature, NULL);
jboolean result = JNI_FALSE;
if (charAppSignature != NULL && charReleaseSignature != NULL) {
if (strcmp(charAppSignature, charReleaseSignature) == 0) {
LOGI("the signature of apk is valid, so pass it");
result = JNI_TRUE;
}
}
(*env)->ReleaseStringUTFChars(env, appSignature, charAppSignature);
(*env)->ReleaseStringUTFChars(env, releaseSignature, charReleaseSignature);
return result;
}
这里的 APP_SIGNATURE 就是正式版的签名字符串,而 loadSignature 函数需要反射安卓系统的 API 才能获得。
static jstring loadSignature(JNIEnv *env, jobject context) {
// 获得Context类
jclass cls = (*env)->GetObjectClass(env, context);
// 得到getPackageManager方法的ID
jmethodID mid = (*env)->GetMethodID(env, cls, "getPackageManager", "()Landroid/content/pm/PackageManager;");
// 获得应用包的管理器
jobject pm = (*env)->CallObjectMethod(env, context, mid);
// 得到getPackageName方法的ID
mid = (*env)->GetMethodID(env, cls, "getPackageName", "()Ljava/lang/String;");
// 获得当前应用包名
jstring packageName = (jstring) (*env)->CallObjectMethod(env, context, mid);
// 获得PackageManager类
cls = (*env)->GetObjectClass(env, pm);
// 得到getPackageInfo方法的ID
mid = (*env)->GetMethodID(env, cls, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
// 获得应用包的信息
jobject packageInfo = (*env)->CallObjectMethod(env, pm, mid, packageName, 0x40); //GET_SIGNATURES = 64;
// 获得PackageInfo 类
cls = (*env)->GetObjectClass(env, packageInfo);
// 获得签名数组属性的ID
jfieldID fid = (*env)->GetFieldID(env, cls, "signatures", "[Landroid/content/pm/Signature;");
// 得到签名数组
jobjectArray signatures = (jobjectArray) (*env)->GetObjectField(env, packageInfo, fid);
// 得到签名
jobject signature = (*env)->GetObjectArrayElement(env, signatures, 0);
// 获得Signature类
cls = (*env)->GetObjectClass(env, signature);
// 得到toCharsString方法的ID
mid = (*env)->GetMethodID(env, cls, "toCharsString", "()Ljava/lang/String;");
// 返回当前应用签名信息
jstring signatureString = (jstring) (*env)->CallObjectMethod(env, signature, mid);
// toLowerCase
cls = (*env)->GetObjectClass(env, signatureString);
mid = (*env)->GetMethodID(env, cls, "toLowerCase", "()Ljava/lang/String;");
jstring lowerCaseSignatureString = (jstring) (*env)->CallObjectMethod(env, signatureString, mid);
return lowerCaseSignatureString;
}
loadSignature 函数可以说就是用 C 语言把上面 Java 的那段代码实现了一遍,并没有什么差别。
至此,有了 Java 和 C/C++ 的双重保护,app 的安全性又提升了一级。