前言:
二次打包作为移动app安全风险的一部分,通常由逆向破解者进行破解,然后插入广告、植入恶意代码、修改内购逻辑逃避支付等等。这些恶意行为严重危害移动产品和用户利益,同时也影响企业口碑。
签名校验:
防止二次打包最普遍的方式之一,便是进行签名校验。校验又分为很多层次,有针对package信息,有的针对文件hash,有的甚至针对代码段等等。这里只列举最简单的几种方式供参考。
1、普通校验
系统将应用的签名信息封装在 PackageInfo 中,调用 PackageManager 的 getPackageInfo(String packageName, int flags) 即可获取指定包名的签名信息。
/**
* 做普通的签名校验 - Java层
*/
private byte[] getCertificateSHA1Fingerprint(Context context) {
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();
X509Certificate x509 = X509Certificate.getInstance(cert);
MessageDigest md = MessageDigest.getInstance("SHA1");
return md.digest(x509.getEncoded());
} catch (PackageManager.NameNotFoundException | CertificateException |
NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
当然如果java层不够安全的话,可以放在native层去做。 native 代码调用 Java 函数的套路:先找到 jclass
和 jmethodID
,再 CallXXXMethod
。
#include <jni.h>
#include <stddef.h>
extern "C" {
JNIEXPORT jbyteArray JNICALL
Java_com_github_piasy_MainActivity_nativeGetSig(
JNIEnv *env, jclass type, jobject context) {
// context.getPackageManager()
jclass context_clazz = env->GetObjectClass(context);
jmethodID getPackageManager = env->GetMethodID(context_clazz,
"getPackageManager", "()Landroid/content/pm/PackageManager;");
jobject packageManager = env->CallObjectMethod(context,
getPackageManager);
// context.getPackageName()
jmethodID getPackageName = env->GetMethodID(context_clazz,
"getPackageName", "()Ljava/lang/String;");
jstring packageName = (jstring) env->CallObjectMethod(context,
getPackageName);
// packageManager->getPackageInfo(packageName, GET_SIGNATURES);
jclass package_manager_clazz = env->GetObjectClass(packageManager);
jmethodID getPackageInfo = env->GetMethodID(package_manager_clazz,
"getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
jint flags = 0x00000040;
jobject packageInfo = env->CallObjectMethod(packageManager,
getPackageInfo, packageName, flags);
jthrowable exception = env->ExceptionOccurred();
env->ExceptionClear();
if (exception) {
return NULL;
}
// packageInfo.signatures[0]
jclass package_info_clazz = env->GetObjectClass(packageInfo);
jfieldID fid = env->GetFieldID(package_info_clazz, "signatures",
"[Landroid/content/pm/Signature;");
jobjectArray signatures = (jobjectArray) env->GetObjectField(
packageInfo, fid);
jobject signature = env->GetObjectArrayElement(signatures, 0);
// signature.toByteArray()
jclass signature_clazz = env->GetObjectClass(signature);
jmethodID signature_toByteArray = env->GetMethodID(signature_clazz,
"toByteArray", "()[B");
jbyteArray sig_bytes = (jbyteArray) env->CallObjectMethod(
signature, signature_toByteArray);
// X509Certificate appCertificate = X509Certificate.getInstance(sig_bytes);
jclass x509_clazz = env->FindClass("javax/security/cert/X509Certificate");
jmethodID x509_getInstance = env->GetStaticMethodID(x509_clazz,
"getInstance", "([B)Ljavax/security/cert/X509Certificate;");
jobject x509 = (jstring) env->CallStaticObjectMethod(x509_clazz,
x509_getInstance, sig_bytes);
exception = env->ExceptionOccurred();
env->ExceptionClear();
if (exception) {
return NULL;
}
// x509.getEncoded()
jmethodID getEncoded = env->GetMethodID(x509_clazz,
"getEncoded", "()[B");
jbyteArray public_key = (jbyteArray) env->CallObjectMethod(x509, getEncoded);
exception = env->ExceptionOccurred();
env->ExceptionClear();
if (exception) {
return NULL;
}
// MessageDigest.getInstance("SHA1")
jclass message_digest_clazz = env->FindClass("java/security/MessageDigest");
jmethodID message_digest_getInstance = env->GetStaticMethodID(
message_digest_clazz, "getInstance",
"(Ljava/lang/String;)Ljava/security/MessageDigest;");
jstring sha1_name = env->NewStringUTF("SHA1");
jobject sha1 = env->CallStaticObjectMethod(message_digest_clazz,
message_digest_getInstance, sha1_name);
exception = env->ExceptionOccurred();
env->ExceptionClear();
if (exception) {
return NULL;
}
// sha1.digest(public_key)
jmethodID digest = env->GetMethodID(message_digest_clazz,
"digest", "([B)[B");
jbyteArray sha1_bytes = (jbyteArray) env->CallObjectMethod(
sha1, digest, public_key);
return sha1_bytes;
}
}
相应破解方式,动态代理IPackageManager。
2、动态代理检测
动态代理的原理是系统动态的为我们创建了一个代理类,所以检测 IPackageManager 的类名即可发现端倪
/**
* 检测 PM 代理
*/
@SuppressLint("PrivateApi")
private boolean checkPMProxy(){
String truePMName = "android.content.pm.IPackageManager$Stub$Proxy";
String nowPMName = "";
try {
// 被代理的对象是 PackageManager.mPM
PackageManager packageManager = getPackageManager();
Field mPMField = packageManager.getClass().getDeclaredField("mPM");
mPMField.setAccessible(true);
Object mPM = mPMField.get(packageManager);
// 取得类名
nowPMName = mPM.getClass().getName();
} catch (Exception e) {
e.printStackTrace();
}
// 类名改变说明被代理了
return truePMName.equals(nowPMName);
}
3、使用新的API
api28以上,可以使用新的api进行检测。
/**
* 使用较新的 API 检测
*/
@SuppressLint("PackageManagerGetSignatures")
private boolean useNewAPICheck(){
String trueSignMD5 = "d0add9987c7c84aeb7198c3ff26ca152";
String nowSignMD5 = "";
Signature[] signs = null;
try {
// 得到签名 MD5
if (VERSION.SDK_INT >= 28) {
PackageInfo packageInfo = getPackageManager().getPackageInfo(
getPackageName(),
PackageManager.GET_SIGNING_CERTIFICATES);
signs = packageInfo.signingInfo.getApkContentsSigners();
} else {
PackageInfo packageInfo = getPackageManager().getPackageInfo(
getPackageName(),
PackageManager.GET_SIGNATURES);
signs = packageInfo.signatures;
}
String signBase64 = Base64Util.encodeToString(signs[0].toByteArray());
nowSignMD5 = MD5Utils.MD5(signBase64);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return trueSignMD5.equals(nowSignMD5);
}
总结:
签名校验还有一些其他的骚操作,比如提早检测、校验application等。虽然使用校验不能防止应用被破解、二次打包,但是可以极大的提高破解者的破解成本。虽然目前Android项目已经采取了加固措施,但仍然无法防止应用被二次打包。