前言:
二次打包作為移動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項目已經采取了加固措施,但仍然無法防止應用被二次打包。
掃描二維碼獲取更多內容