Android簽名攻與防

原文鏈接:https://cloud.tencent.com/developer/article/1356482

轉自:https://cloud.tencent.com/developer/article/1356482

一. Android簽名背景

  • Android應用使用應用包文件(.apk文件)的形式分發到設備上,由於這個平臺的程序主要是用 Java 編寫的,所以這種格式與 Java 包的格式 -- jar(Java Archive)有很多共同點,它用於將代碼,資源和元數據(來自可選的META-INF目錄 )文件使用 zip 歸檔算法轉換成一個文件。
  • 大多數 Android 應用程序都使用開發人員簽名的證書(注意 Android 的“證書”和“簽名”可以互換使用)。 此證書用於確保原始應用程序的代碼及其更新來自同一位置,並在同一開發人員的應用程序之間建立信任關係。 爲了執行這個檢查,Android 只是比較證書的二進制表示,它用於簽署一個應用程序及其更新(第一種情況)和協作應用程序(第二種情況)。
  • 但由於Android平臺源碼的公開性,安全方面也是一個比較嚴峻的問題。在工作中經常能夠遇到惡意破解或嚴重安全漏洞的情況。Android攻擊手段層出不斷,目前比較流行的方法就是把簽名認證的內容放到動態鏈接庫.so文件中,本文則從JNI簽名驗證淺談下Android的攻防問題。

看點

02

二. 安全目標

通常定義的信息安全主要有三大目標:

  • 保密性(Confidentiality):保護信息內容不會被泄露給未授權的實體,防止被動攻擊;
  • 完整性(Integrity):保證信息不被未授權地修改,或者如果被修改可以檢測出來,防止主動攻擊,比如篡改、插入、重放;
  • 可用性(Availability):保證資源的授權用戶能夠訪問到應得資源或服務,防止拒絕服務攻擊;

除了這三點,有時大家也會加上另外兩點要求:

  • 可控性(Controllability):限制實體的訪問權限,通常是經過認證的合法的實體纔可以訪問,標識與認證是訪問控制的前提;
  • 不可抵賴性(Non-repudiation):防止發送方或者接收方否認傳輸或者接收過某條消息; Android提供給我們了一種驗證方式:數字簽名。但數字簽名放到java層代碼驗證太容易被破解,爲了增加破解難度,把驗證內容需要轉移到native層實現。

看點

03

三. JNI註冊方式

JNI全稱是Java Native Interface(Java本地接口)單詞首字母的縮寫,本地接口就是指用C和C++開發的接口。由於JNI是JVM規範中的一部份,因此可以將我們寫的JNI程序在任何實現了JNI規範的Java虛擬機中運行。同時,這個特性使我們可以複用以前用C/C++寫的大量代碼。JNI目前提供兩種註冊方式,靜態註冊方式實現較爲簡單,但有一些系列的缺陷,動態註冊要複寫JNI_OnLoad函數,過程稍微複雜。

3.1 靜態註冊方法

這種方法我們比較常見,但比較麻煩,大致流程如下:

  1. 實現原理:根據函數名來建立java方法和JNI函數間的一一對應關係。
  2. 實現過程:

1. 先創建Java類,聲明Native方法,編譯成.class文件。 2. 使用Javah命令生成C/C++的頭文件,例如:javah -jni com.jd.jnidemo.MainActivity,則會生成一個以.h爲後綴的文件com_jd_jnidemo_MainActivity.h。 3. 創建.h對應的源文件,然後實現對應的native方法,如下圖所示:

3. 靜態註冊的弊端

1. 書寫很不方便,因爲JNI層函數的名字必須遵循特定的格式,且名字特別長;

2. 會導致程序員的工作量很大,因爲必須爲所有聲明瞭native函數的java類編寫JNI頭文件;

3. 程序運行效率低,因爲初次調用native函數時需要根據根據函數名在JNI層中搜索對應的本地函數,然後建立對應關係,這個過程比較耗時。

3.2 動態註冊

動態註冊在JNi層實現的,JAVA層不需要關心,因爲在system.load時就會去掉JNI_OnLoad,有就註冊,沒就不註冊,因爲jni.h裏有這麼一個結構體,分別如下表示

typedef struct {
    const char* name;                                java層函數名
    注:一個簽名信息包含JAVA的參數和返回,這個貌似有命令生成javap,應該是
    const char* signature;                         java層函數名的簽名信息
    void*       fnPtr;                                      Jni層對應的函數指針。
} JNINativeMethod;
  • 1.實現原理:直接告訴native函數其在JNI中對應函數的指針;
  • 2.實現過程: 1. 利用結構體JNINativeMethod保存Java Native函數和JNI函數的對應關係; 2. 在一個JNINativeMethod數組中保存所有native函數和JNI函數的對應關係; 3. 在Java中通過System.loadLibrary加載完JNI動態庫之後,調用JNI_OnLoad函數,開始動態註冊; 4. JNI_OnLoad中會調用AndroidRuntime::registerNativeMethods函數進行函數註冊; 5. AndroidRuntime::registerNativeMethods中最終調用jniRegisterNativeMethods完成註冊。
  • 3.優點:克服了靜態註冊的弊端。

看點

04

四. 簽名驗證

一般情況下爲了防止被反編譯,會把關鍵代碼寫到so文件中(比如加解密),一般使用到的是在so里加上判斷APk包簽名是否一致的代碼,避免so被二次打包。其實用JNI讀簽名就是用了Java的反射機制。

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。當然也可以繼續獲取公鑰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 之後,我們就可以進行對比了。下面我們看看對應的 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);// 獲得當前應用的包名

    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數組中取出第一個元素
    
    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;
}

本示例中這種認證方式在 Android Studio 中會有一個 Lint 警告,“android-fake-id-vulnerability”,受影響系統版本:部分Android 4.4及所有4.4以下版本,這個問題屬於系統bug,在獲取cert的方法findCert中判斷有缺陷,但在4.4以後大google已經對此修復。

示例中需要傳入context,其實context也也可以在native層通過反射的方式拿到,本人感受:native的代碼不難就是寫起來比較複雜,只要有耐心就可以了。

4.2 證書完整性校驗

4.1內容是通過context獲取的signature獲取的簽名驗證,我們知道在簽名後apk文件會多出以下文件

其中我們上述過程其實就獲取CERT.RSA中的簽名,但上述過程依賴context進行依賴認證,攻擊者可獲取context進行內容替換修改,截取簽名替換等方式顯示二次打包。

所以,我們可以解析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  

查看證書指紋後會發現,RSA文件和.keystores,證書指紋相同,MD5,SHA1,SHA256三種指紋均相同。

4.2.2 證書格式

1.X.509證書格式如下圖所示:

• 這裏看到證書中並不包含apk簽名流程中生成CERT.RSA時對用私鑰計算出的簽名。所以證書的信息是不會改變的,這也驗證了上面所說的RSA中證書指紋和.keystone中的指紋相同的問題

2.對CERT.RSA進行詳細解析

明確了上面的問題之後,對CERT.RSA 文件進行詳細解析,得到下圖:

說明:

  • (1)首先,我們的通常所說的證書的簽名,是生成證書的時候CA對整個證書的所有域簽名的得到的,而不是對某一部分簽名得到的。整個簽名就是上圖中部分一的最下面的一段十六進制的內容;
  • (2)編程中的獲取到的內容實質上是就是上圖中的部分二,這是一個證書的所有內容;
    openssl pkcs7  -inform DER -in CERT.RSA  -print_certs 
  • (3)部分一中的公鑰等信息就是從部分二中得來的,可以直接在部分二中找到。
  • (4)可以猜測,部分一中的其他信息也是從部分二中得來,只不過編碼方式不一樣,所以顯示不同而已。

4.2.3 RSA加解密實現

由於Android生成的apk文件是以zip文件格式生成的,我們可以查看源碼查看Android簽名校驗機制

可參考: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 格式的文件。

獲取證書的方法如下(上面幾張中已經使用openssl獲取相關信息):

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());  
    }  
}  

也可以轉化爲native代碼進行校驗,加固安全性。以上就是目前主流的兩種通過簽名校驗的方式。

看點

05

五. 常見的破解方式及加固方案總結

破解條件

  • 1.Java層通過
   getPackageManager().getPackageInfo.signatures 獲取簽名信息;
  • 2.Native方法 /DLL/Lua腳本等通過獲取Java的context/Activity對象,反射調用getPackageInfo等來獲取簽名;
  • 3.首先獲取apk的路徑,定位到META-INF*.RSA文件,獲取其中的簽名信息;
  • 4.能自動識別apk的加固類型;

破解方式

  • 方式一:substrate框架libhooksig By空道
    • 1.so文件用於hook sign
    • 2.應用於在程序運行時獲取當前進程的簽名信息而進行的驗證;
  • 方式二:重寫繼承類packageInfo和PackageManager By小白
    • 1.適用於Java層packageInfo獲取簽名信息的方式;
    • 2.亦適用於Native/DLL/LUA層反射packageInfo獲取簽名信息的方式;
    • 3.該種方式可能會使PackageInfo中的versionCode和versionName爲NULL,對程序運行有影響的話,需自主填充修復;
  • 方式三:重寫繼承類,重置Sign信息;
    • 1.適用於Java層packageInfo獲取簽名信息的方式;
    • 2.亦適用於Native/DLL/LUA層反射packageInfo獲取簽名信息的方式;
    • 3.該種方式可能會使PackageInfo中的versionCode和versionName爲NULL,對程序運行有影響的話,需自主填充修復;
  • 方式四:針對定位到具體RSA文件路徑獲取簽名的驗證方式;
    • 1.針對定位到具體RSA文件路徑獲取簽名的驗證方式;
    • 2.曾經破解過消消樂_Ver1.27,但是如果程序本身對META-INF簽名文件中的MANIFEST.MF進行了校驗,此方式無效,那就非簽名校驗,而是文件校驗了;
  • 方式五:hook android 解析的packageparse類中的兩個驗證方法
  pp.collectCertificates(pkg, parseFlags);
  pp.collectManifestDigest(pkg);

修改實現方式。

(具體實現請參考LuckyPatchSign實現 新浪微博 @人生無NG 2015.10.10)

應對方案

通過以上信息,我們可以得到的是,證書作爲不變的內容放在PKCS7格式的.RSA文件中,我們在RSA文件上驗證的也只有證書。

  • 方案一:通過PackageManag對象可以獲取APK自身的簽名 這裏得到的sign爲證書的所有數據,對其做摘要算法,例如: MD5可以得到MD5指紋,對比指紋可以進行安全驗證。 Java程序都可以使用jni反射在native實現,Java代碼太容易破解,不建議防止到Java端。方法有很多,最後是都通過
  context.getPackageManager().getPackageInfo(
                    this.getPackageName(), PackageManager.GET_SIGNATURES).signatures)
  • 方案二:調用隱藏類PackageParser.java中的collectCertificates方法,從源頭獲取cert證書
  • 方案三:使用openssl使用JNI做RSA解析破解難度是相當的大,同樣的解析出x.509證書,java解析轉換爲native解析so文件,但得到的文件比較大1.3M。。。。顯然不可取(太大)
  • 方案四:通過源碼解析我們可以知道,apk文件驗證是按照zip文件目錄形式查找到.RSA文件結尾,我們可以直接去取文件的絕對路徑,拿到證書的公鑰信息進行驗證(但需要引入PKCS7的庫)
  • 方案五:由棒棒加固和愛加密做的思路可以知道,自己重新定製摘要算法,在asset 裏面重新搞一套驗證流程。思路就是生成一個定製的CERT,另外開闢一套驗證流程,不使用Android固有的簽名認證流程。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章