0%

Android 游戏启动时检查签名

因为 Android 的 apk 很容易被下载,再被人反编译修改并重新打包,不过由于证书的签名是唯一的,并且只有自己才能知晓,所以我们可以在游戏启动时检查签名,以此来确认 apk 是否被修改过。

如何获取签名证书的指纹,通常有几种方法。

获取文件的签名证书指纹

获取 keystore 文件的证书指纹

使用 keytool 工具来获取签名证书的指纹,keytool 通常位于 JDK安装路径/bin 下面。

debug.keystore 为例,输入命令:

1
keytool -list -v -keystore ~/.android/debug.keystore -storepass android

其中 -keystore 后的参数 ~/.android/debug.keystore 是 keystore 文件的路径,-storepass 后的参数 android 是 keystore 的口令。回车后会显示如下内容:

其中红框部分就是签名证书的指纹。

获取 apk 文件的证书指纹

解压 apk 文件,其中的 META-INF\CERT.RSA 文件保存了证书的公钥、采用的加密算法等信息。可以用来验证签名证书的指纹。

仍然使用 keytool 工具,输入如下命令:

1
keytool -print cert -file META_INF/CERT.RSA

可以得到如下内容:

可见与上面 keystore 文件的证书指纹是一样的,说明这个 apk 文件是用 debug.keystore 签名的。

通过代码获取签名证书指纹

Java 代码

例如:

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
44
45
46
47
48
49
50
51
/**
* 获取证书指纹
* @param context 上下文
* @param algorithm 算法
* @return
*/
private static String getCertificateFingerprint(Context context, String algorithm) {
// 获取包管理器
PackageManager pm = context.getPackageManager();
// 获取包名
String packageName = context.getPackageName();

try {
PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
// 获取签名信息
Signature[] signatures = packageInfo.signatures;
byte[] cert = signatures[0].toByteArray();
// 获取X.509证书
X509Certificate x509Certificate = X509Certificate.getInstance(cert);
// 选择讯息摘要算法,例如:MD5,SHA1,SHA256
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] bytes = md.digest(x509Certificate.getEncoded());
// 转换为 16 进制格式字符串
return bytesToHexString(bytes);
} catch (PackageManager.NameNotFoundException | CertificateException |
NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}

/**
* 转换为 16 进制格式
* @param bytes
* @return
*/
public static String bytesToHexString(byte[] bytes) {
int length = bytes.length;
Formatter formatter = new Formatter();

for (int i = 0; i < length; ++i) {
formatter.format("%02X", bytes[i]);
if (i < length - 1) {
formatter.format("%s", ":");
}
}
String result = formatter.toString();
formatter.close();

return result;
}

通过 native 代码

有时为了安全考虑,会写在 native 代码(C/C++ 语言)中,例如:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
JNIEXPORT jstring JNICALL
Java_com_xie_jun_unitytest_MainActivity_nativeGetCertificateFingerprint(JNIEnv *env, jobject context) {
// 获取 context 的类
jclass context_clazz = (*env)->GetObjectClass(env, context);
// 获取 getPackageManager 方法ID
jmethodID methodID_getPackageManager = (*env)->GetMethodID(env,
context_clazz, "getPackageManager", "()Landroid/content/pm/PackageManager;");
// 获取包管理器,对比 Java 代码为: PackageManager pm = context.getPackageManager();
jobject packageManager = (*env)->CallObjectMethod(env, context, methodID_getPackageManager);

jmethodID methodID_getPackageName = (*env)->GetMethodID(env,
context_clazz, "getPackageName", "()Ljava/lang/String;");
// 获取包名, 对比 Java 代码为(下同):String packageName = context.getPackageName();
jstring packageName = (*env)->CallObjectMethod(env, context, methodID_getPackageName);
// 打印包名
const char *str = (*env)->GetStringUTFChars(env, packageName, 0);
// Log.d("JNI", "packageName: " + packageName);
__android_log_print(ANDROID_LOG_DEBUG, "JNI", "packageName: %s\n", str);

jclass pm_clazz = (*env)->GetObjectClass(env, packageManager);
jmethodID methodID_getPackageInfo = (*env)->GetMethodID(env,
pm_clazz, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
int flags = 0x00000040;
// PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
jobject packageInfo = (*env)->CallObjectMethod(env,
packageManager, methodID_getPackageInfo, packageName, flags);

jclass packageInfo_clazz = (*env)->GetObjectClass(env, packageInfo);
jfieldID fieldID_signatures = (*env)->GetFieldID(env, packageInfo_clazz,
"signatures", "[Landroid/content/pm/Signature;");
// 获取签名信息,Signature[] signatures = packageInfo.signatures;
jobjectArray signatures = (*env)->GetObjectField(env, packageInfo, fieldID_signatures);
// Signature signature = signatures[0];
jobject signature = (*env)->GetObjectArrayElement(env, signatures, 0);

jclass signature_clazz = (*env)->GetObjectClass(env, signature);
jmethodID methodID_toByteArray = (*env)->GetMethodID(env, signature_clazz,
"toByteArray", "()[B");
// byte[] cert = signature.toByteArray();
jbyteArray cert = (*env)->CallObjectMethod(env, signature, methodID_toByteArray);

jclass x509_clazz = (*env)->FindClass(env, "javax/security/cert/X509Certificate");
jmethodID methodID_getInstance = (*env)->GetStaticMethodID(env,
x509_clazz, "getInstance", "([B)Ljavax/security/cert/X509Certificate;");
// X509Certificate x509Certificate = X509Certificate.getInstance(cert);
jobject x509 = (*env)->CallStaticObjectMethod(env, x509_clazz, methodID_getInstance, cert);

jclass md_clazz = (*env)->FindClass(env, "java/security/MessageDigest");
jmethodID methodID_md_getInstance = (*env)->GetStaticMethodID(env,
md_clazz, "getInstance", "(Ljava/lang/String;)Ljava/security/MessageDigest;");
jstring algorithm = (*env)->NewStringUTF(env, "SHA1");
// MessageDigest md = MessageDigest.getInstance(algorithm);
jobject md = (*env)->CallStaticObjectMethod(env,
md_clazz, methodID_md_getInstance, algorithm);

jclass certificate_clazz = (*env)->GetSuperclass(env, x509_clazz);
jmethodID methodID_getEncoded = (*env)->GetMethodID(env,
certificate_clazz, "getEncoded", "()[B");
// byte[] x509Encoded = x509.getEncoded();
jbyteArray x509Encoded = (*env)->CallObjectMethod(env, x509, methodID_getEncoded);

jmethodID methodID_digest = (*env)->GetMethodID(env, md_clazz, "digest", "([B)[B");
// byte[] mdDigestBytes = md.digest(x509Encoded);
jbyteArray mdDigestBytes = (*env)->CallObjectMethod(env, md, methodID_digest, x509Encoded);

// 获取 MainActivity 类
jclass mainActivity_clazz = (*env)->FindClass(env, "com/xie/jun/checksignaturedemo/MainActivity");
jmethodID methodID_bytesToHexString = (*env)->GetStaticMethodID(env,
mainActivity_clazz, "bytesToHexString", "([B)Ljava/lang/String;");
// 这里调用了 Java 中的方法:String hexString = bytesToHexString(mdDigestBytes)
jstring hexString = (*env)->CallStaticObjectMethod(env,
mainActivity_clazz, methodID_bytesToHexString, mdDigestBytes);
const char *c_hexString = (*env)->GetStringUTFChars(env, hexString, 0);
__android_log_print(ANDROID_LOG_DEBUG, "JNI", "fingerprint: %s\n", c_hexString);

return hexString;
}

最终结果都是一样的:

检查签名

启动就可以对比获取到的签名指纹与我们签名文件的指纹来验证安装的 apk 是否被修改过。

参考代码已上传至 GitHub

参考链接