一. Android簽名背景:
- Android應(yīng)用使用應(yīng)用包文件(.apk文件)的形式分發(fā)到設(shè)備上,由于這個平臺的程序主要是用 Java 編寫的,所以這種格式與 Java 包的格式 -- jar(Java Archive)有很多共同點,它用于將代碼,資源和元數(shù)據(jù)(來自可選的META-INF目錄 )文件使用 zip 歸檔算法轉(zhuǎn)換成一個文件。
- 大多數(shù) Android 應(yīng)用程序都使用開發(fā)人員簽名的證書(注意 Android 的“證書”和“簽名”可以互換使用)。 此證書用于確保原始應(yīng)用程序的代碼及其更新來自同一位置,并在同一開發(fā)人員的應(yīng)用程序之間建立信任關(guān)系。 為了執(zhí)行這個檢查,Android 只是比較證書的二進(jìn)制表示,它用于簽署一個應(yīng)用程序及其更新(第一種情況)和協(xié)作應(yīng)用程序(第二種情況)。
- 但由于Android平臺源碼的公開性,安全方面也是一個比較嚴(yán)峻的問題。在工作中經(jīng)常能夠遇到惡意破解或嚴(yán)重安全漏洞的情況。Android攻擊手段層出不斷,目前比較流行的方法就是把簽名認(rèn)證的內(nèi)容放到動態(tài)鏈接庫.so文件中,本文則從JNI簽名驗證淺談下Android的攻防問題。
二. 安全目標(biāo)
通常定義的信息安全主要有三大目標(biāo):
- 保密性(Confidentiality):保護(hù)信息內(nèi)容不會被泄露給未授權(quán)的實體,防止被動攻擊;
- 完整性(Integrity):保證信息不被未授權(quán)地修改,或者如果被修改可以檢測出來,防止主動攻擊,比如篡改、插入、重放;
- 可用性(Availability):保證資源的授權(quán)用戶能夠訪問到應(yīng)得資源或服務(wù),防止拒絕服務(wù)攻擊;
除了這三點,有時大家也會加上另外兩點要求:
- 可控性(Controllability):限制實體的訪問權(quán)限,通常是經(jīng)過認(rèn)證的合法的實體才可以訪問,標(biāo)識與認(rèn)證是訪問控制的前提;
- 不可抵賴性(Non-repudiation):防止發(fā)送方或者接收方否認(rèn)傳輸或者接收過某條消息;
Android提供給我們了一種驗證方式:數(shù)字簽名。但數(shù)字簽名放到j(luò)ava層代碼驗證太容易被破解,為了增加破解難度,把驗證內(nèi)容需要轉(zhuǎn)移到native層實現(xiàn)。
- 不可抵賴性(Non-repudiation):防止發(fā)送方或者接收方否認(rèn)傳輸或者接收過某條消息;
三. JNI注冊方式
JNI全稱是Java Native Interface(Java本地接口)單詞首字母的縮寫,本地接口就是指用C和C++開發(fā)的接口。由于JNI是JVM規(guī)范中的一部份,因此可以將我們寫的JNI程序在任何實現(xiàn)了JNI規(guī)范的Java虛擬機(jī)中運(yùn)行。同時,這個特性使我們可以復(fù)用以前用C/C++寫的大量代碼。JNI目前提供兩種注冊方式,靜態(tài)注冊方式實現(xiàn)較為簡單,但有一些系列的缺陷,動態(tài)注冊要復(fù)寫JNI_OnLoad函數(shù),過程稍微復(fù)雜。
3.1 靜態(tài)注冊方法
這種方法我們比較常見,但比較麻煩,大致流程如下:
- 實現(xiàn)原理:根據(jù)函數(shù)名來建立java方法和JNI函數(shù)間的一一對應(yīng)關(guān)系。
- 實現(xiàn)過程:
-- 先創(chuàng)建Java類,聲明Native方法,編譯成.class文件。
-- 使用Javah命令生成C/C++的頭文件,例如:javah -jni com.jd.jnidemo.MainActivity,則會生成一個以.h為后綴的文件com_jd_jnidemo_MainActivity.h。
-- 創(chuàng)建.h對應(yīng)的源文件,然后實現(xiàn)對應(yīng)的native方法,如下圖所示:
- 實現(xiàn)過程:
- 靜態(tài)注冊的弊端
-- 1. 書寫很不方便,因為JNI層函數(shù)的名字必須遵循特定的格式,且名字特別長;
-- 2. 會導(dǎo)致程序員的工作量很大,因為必須為所有聲明了native函數(shù)的java類編寫JNI頭文件;
-- 3. 程序運(yùn)行效率低,因為初次調(diào)用native函數(shù)時需要根據(jù)根據(jù)函數(shù)名在JNI層中搜索對應(yīng)的本地函數(shù),然后建立對應(yīng)關(guān)系,這個過程比較耗時。
- 靜態(tài)注冊的弊端
3.2 動態(tài)注冊
動態(tài)注冊在JNi層實現(xiàn)的,JAVA層不需要關(guān)心,因為在system.load時就會去掉JNI_OnLoad,有就注冊,沒就不注冊,因為jni.h里有這么一個結(jié)構(gòu)體,分別如下表示
typedef struct {
const char* name; java層函數(shù)名
注:一個簽名信息包含JAVA的參數(shù)和返回,這個貌似有命令生成javap,應(yīng)該是
const char* signature; java層函數(shù)名的簽名信息
void* fnPtr; Jni層對應(yīng)的函數(shù)指針。
} JNINativeMethod;
- 1.實現(xiàn)原理:直接告訴native函數(shù)其在JNI中對應(yīng)函數(shù)的指針;
- 2.實現(xiàn)過程:
-- 1. 利用結(jié)構(gòu)體JNINativeMethod保存Java Native函數(shù)和JNI函數(shù)的對應(yīng)關(guān)系;
-- 2. 在一個JNINativeMethod數(shù)組中保存所有native函數(shù)和JNI函數(shù)的對應(yīng)關(guān)系;
-- 3. 在Java中通過System.loadLibrary加載完JNI動態(tài)庫之后,調(diào)用JNI_OnLoad函數(shù),開始動態(tài)注冊;
-- 4. JNI_OnLoad中會調(diào)用AndroidRuntime::registerNativeMethods函數(shù)進(jìn)行函數(shù)注冊;
-- 5. AndroidRuntime::registerNativeMethods中最終調(diào)用jniRegisterNativeMethods完成注冊。
3.優(yōu)點:克服了靜態(tài)注冊的弊端。
四. 簽名驗證
一般情況下為了防止被反編譯,會把關(guān)鍵代碼寫到so文件中(比如加解密),一般使用到的是在so里加上判斷APk包簽名是否一致的代碼,避免so被二次打包。其實用JNI讀簽名就是用了Java的反射機(jī)制。
4.1 本地校驗
反射代碼如下所示:
PackageManager pm = context.getPackageManager();
String packageName = context.getPackageName();
try {
PackageInfo packageInfo = pm.getPackageInfo(packageName,
PackageManager.GET_SIGNATURES);
Signature sign = info.signatures[0];
Log.i("test", "hashCode : " + sign.hashCode());
} catch (NameNotFoundException e) {
e.printStackTrace();
}
以上我們做了一件事情,獲取 PackageInfo 中的 Signature。當(dāng)然也可以繼續(xù)獲取公鑰SHA1如下
private byte[] getCertificateSHA1(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;
}
計算出 Signature或計算出SHA1 之后,我們就可以進(jìn)行對比了。下面我們看看對應(yīng)的 native 代碼。(由于篇幅原因這里列舉只計算到Signature的過程)
int getSignHashCode(JNIEnv *env, jobject context) {
jclass context_clazz = (*env)->GetObjectClass(env, context);//Context的類
jmethodID methodID_getPackageManager = (*env)->GetMethodID(env, context_clazz,
"getPackageManager", "()Landroid/content/pm/PackageManager;");// 得到 getPackageManager 方法的 ID
jobject packageManager = (*env)->CallObjectMethod(env, context,
methodID_getPackageManager);// 獲得PackageManager對象
jclass pm_clazz = (*env)->GetObjectClass(env, packageManager);// 獲得 PackageManager 類
jmethodID methodID_pm = (*env)->GetMethodID(env, pm_clazz, "getPackageInfo",
"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");// 得到 getPackageInfo 方法的 ID
jmethodID methodID_pack = (*env)->GetMethodID(env, context_clazz,
"getPackageName", "()Ljava/lang/String;");// 得到 getPackageName 方法的 ID
jstring application_package = (*env)->CallObjectMethod(env, context,
methodID_pack);// 獲得當(dāng)前應(yīng)用的包名
const char *str = (*env)->GetStringUTFChars(env, application_package, 0);
__android_log_print(ANDROID_LOG_DEBUG, "JNI", "packageName: %s\n", str);
jobject packageInfo = (*env)->CallObjectMethod(env, packageManager,
methodID_pm, application_package, 64);// 獲得PackageInfo
jclass packageinfo_clazz = (*env)->GetObjectClass(env, packageInfo);
jfieldID fieldID_signatures = (*env)->GetFieldID(env, packageinfo_clazz,
"signatures", "[Landroid/content/pm/Signature;");
jobjectArray signature_arr = (jobjectArray)(*env)->GetObjectField(env,
packageInfo, fieldID_signatures);
jobject signature = (*env)->GetObjectArrayElement(env, signature_arr, 0);//Signature數(shù)組中取出第一個元素
jclass signature_clazz = (*env)->GetObjectClass(env, signature);//讀signature的hashcode
jmethodID methodID_hashcode = (*env)->GetMethodID(env, signature_clazz,
"hashCode", "()I");
jint hashCode = (*env)->CallIntMethod(env, signature, methodID_hashcode);
__android_log_print(ANDROID_LOG_DEBUG, "JNI", "hashcode: %d\n", hashCode);
return hashCode;
}
本示例中這種認(rèn)證方式在 Android Studio 中會有一個 Lint 警告,“android-fake-id-vulnerability”,受影響系統(tǒng)版本:部分Android 4.4及所有4.4以下版本,這個問題屬于系統(tǒng)bug,在獲取cert的方法findCert中判斷有缺陷,但在4.4以后大google已經(jīng)對此修復(fù)。
示例中需要傳入context,其實context也也可以在native層通過反射的方式拿到,本人感受:native的代碼不難就是寫起來比較復(fù)雜,只要有耐心就可以了。
4.2 證書完整性校驗
4.1內(nèi)容是通過context獲取的signature獲取的簽名驗證,我們知道在簽名后apk文件會多出以下文件

其中我們上述過程其實就獲取CERT.RSA中的簽名,但上述過程依賴context進(jìn)行依賴認(rèn)證,攻擊者可獲取context進(jìn)行內(nèi)容替換修改,截取簽名替換等方式顯示二次打包。
所以,我們可以解析RSA文件,通過本地驗證的方式來完成 '證書完整性校驗' 。
4.2.1 了解證書
查看證書指紋.keystores命令
keytool –list –v –keystore debug.keystore
查看證書指紋.RSA文件命令
keytool –printcert –file CERT.RSA
使用openssl查看.RSA文件
openssl pkcs7 -inform DER -in CERT.RSA -noout -print_certs –text
查看證書指紋后會發(fā)現(xiàn),RSA文件和.keystores,證書指紋相同,MD5,SHA1,SHA256三種指紋均相同。
4.2.2 證書格式
1.X.509證書格式如下圖所示:

? 這里看到證書中并不包含apk簽名流程中生成CERT.RSA時對用私鑰計算出的簽名。所以證書的信息是不會改變的,這也驗證了上面所說的RSA中證書指紋和.keystone中的指紋相同的問題
2.對CERT.RSA進(jìn)行詳細(xì)解析
明確了上面的問題之后,對CERT.RSA 文件進(jìn)行詳細(xì)解析,得到下圖:

說明:
- (1)首先,我們的通常所說的證書的簽名,是生成證書的時候CA對整個證書的所有域簽名的得到的,而不是對某一部分簽名得到的。整個簽名就是上圖中部分一的最下面的一段十六進(jìn)制的內(nèi)容;
- (2)編程中的獲取到的內(nèi)容實質(zhì)上是就是上圖中的部分二,這是一個證書的所有內(nèi)容;
openssl pkcs7 -inform DER -in CERT.RSA -print_certs
- (3)部分一中的公鑰等信息就是從部分二中得來的,可以直接在部分二中找到。
- (4)可以猜測,部分一中的其他信息也是從部分二中得來,只不過編碼方式不一樣,所以顯示不同而已。
4.2.3 RSA加解密實現(xiàn)
由于Android生成的apk文件是以zip文件格式生成的,我們可以查看源碼查看Android簽名校驗機(jī)制
可參考:Apk在安裝的過程中核心類:
frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java
private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
……
}
Apk 包中的META-INF目錄下,CERT.RSA,它是一個PKCS7 格式的文件。
獲取證書的方法如下(上面幾張中已經(jīng)使用openssl獲取相關(guān)信息):
import sun.security.pkcs.PKCS7; //注意:需要引入jar包android-support-v4
import java.io.FileInputStream;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class Test {
public static void main(String[] args) throws CertificateException, IOException {
FileInputStream fis = new FileInputStream("/home/AnyMarvel/CERT.RSA");
PKCS7 pkcs7 = new PKCS7(fis);
X509Certificate publicKey = pkcs7.getCertificates()[0];
System.out.println("issuer1:" + publicKey.getIssuerDN());
System.out.println("subject2:" + publicKey.getSubjectDN());
System.out.println(publicKey.getPublicKey());
}
}
也可以轉(zhuǎn)化為native代碼進(jìn)行校驗,加固安全性。以上就是目前主流的兩種通過簽名校驗的方式。
五. 常見的破解方式及加固方案總結(jié)
破解條件
- 1.Java層通過
getPackageManager().getPackageInfo.signatures 獲取簽名信息;
- 2.Native方法
/DLL/Lua腳本等通過獲取Java的context/Activity對象,反射調(diào)用getPackageInfo等來獲取簽名; - 3.首先獲取apk的路徑,定位到META-INF*.RSA文件,獲取其中的簽名信息;
- 4.能自動識別apk的加固類型;
破解方式
luckypathsign作者提供
-
方式一:substrate框架libhooksig By空道
1.so文件用于hook sign
2.應(yīng)用于在程序運(yùn)行時獲取當(dāng)前進(jìn)程的簽名信息而進(jìn)行的驗證;
-
方式二:重寫繼承類packageInfo和PackageManager By小白
1.適用于Java層packageInfo獲取簽名信息的方式;
2.亦適用于Native/DLL/LUA層反射packageInfo獲取簽名信息的方式;
3.該種方式可能會使PackageInfo中的versionCode和versionName為NULL,對程序運(yùn)行有影響的話,需自主填充修復(fù);
-
方式三:重寫繼承類,重置Sign信息;
1.適用于Java層packageInfo獲取簽名信息的方式;
2.亦適用于Native/DLL/LUA層反射packageInfo獲取簽名信息的方式;
3.該種方式可能會使PackageInfo中的versionCode和versionName為NULL,對程序運(yùn)行有影響的話,需自主填充修復(fù);
-
方式四:針對定位到具體RSA文件路徑獲取簽名的驗證方式;
1.針對定位到具體RSA文件路徑獲取簽名的驗證方式;
2.曾經(jīng)破解過消消樂_Ver1.27,但是如果程序本身對META-INF簽名文件中的MANIFEST.MF進(jìn)行了校驗,此方式無效,那就非簽名校驗,而是文件校驗了;
方式五:hook android 解析的packageparse類中的兩個驗證方法
pp.collectCertificates(pkg, parseFlags);
pp.collectManifestDigest(pkg);
修改實現(xiàn)方式。
應(yīng)對方案
通過以上信息,我們可以得到的是,證書作為不變的內(nèi)容放在PKCS7格式的.RSA文件中,我們在RSA文件上驗證的也只有證書。
- 方案一:通過PackageManag對象可以獲取APK自身的簽名 這里得到的sign為證書的所有數(shù)據(jù),對其做摘要算法,例如:
MD5可以得到MD5指紋,對比指紋可以進(jìn)行安全驗證。
Java程序都可以使用jni反射在native實現(xiàn),Java代碼太容易破解,不建議防止到Java端。方法有很多,最后是都通過
context.getPackageManager().getPackageInfo(
this.getPackageName(), PackageManager.GET_SIGNATURES).signatures)
方案二:調(diào)用隱藏類PackageParser.java中的collectCertificates方法,從源頭獲取cert證書
方案三:使用openssl使用JNI做RSA解析破解難度是相當(dāng)?shù)拇?,同樣的解析出x.509證書,java解析轉(zhuǎn)換為native解析so文件,但得到的文件比較大1.3M。。。。顯然不可?。ㄌ螅?/p>
方案四:通過源碼解析我們可以知道,apk文件驗證是按照zip文件目錄形式查找到.RSA文件結(jié)尾,我們可以直接去取文件的絕對路徑,拿到證書的公鑰信息進(jìn)行驗證(但需要引入PKCS7的庫)
方案五:由棒棒加固和愛加密做的思路可以知道,自己重新定制摘要算法,在asset
里面重新搞一套驗證流程。思路就是生成一個定制的CERT,另外開辟一套驗證流程,不使用Android固有的簽名認(rèn)證流程。
文章有涉及侵權(quán)行為請及時提醒,謝謝
