Android Apk 安全之校验签名

校验签名

一般绝大多数的 app 在上线前都会做一层安全防护,比如代码混淆、加固等。

今天就来讲讲其中的一项:校验签名。

校验签名可以有效的防止二次打包,避免你的 app 被植入广告甚至破解等。而今天就从两个角度来讲签名的具体校验:

  • Java 层
  • C/C++ 层

那么就先开始讲 java 层好了。

Java 层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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++ 层中也加入签名校验。

C/C++ 层

在 so 文件加载的时候,会去调用 JNI_OnLoad 函数,所以我们可以在这里做签名校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 方法中调用 :

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Security {

private static Application application;

public static Context getContext() {
return application;
}

private static void setContext(Application context) {
application = context;
}

}

获取到 Context 之后,就可以来比较签名了 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 才能获得。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 的那段代码实现了一遍,并没有什么差别。