APP安全防護基本方法(混淆/簽名驗證/反調試)


本教程所用Android Studio測試項目已上傳:https://github.com/PrettyUp/SecTest

 

一、混淆

對於很多人而言是因為java才接觸到“混淆”這個詞,由於在前移動互聯網時代在java程序中“混淆”也只是針對java代碼,所以混淆基本就和對java源代碼進行混淆等價。

但說到混淆的本質,不過就是將變量名、函數名由有助於開發維護人員理解其用途的名稱(如my_name,get_key)改用a,b,c,d這種簡短無意義的詞去替換。從這個思路出發,資源其實也是可以混淆的。

在前移動互聯網時代,對於B/S而言,前端頁面資源則是放在服務器上且名稱不適宜隨便修改;對於C/S而言,由於java不擅長寫界面所以java寫的程序要么用到的資源很少要么就直接沒有圖形界面。

也就是說在前移動互聯網時代,資源混淆確實是沒多大用途和意義的。但在移動互聯網時代,或者更直接一點對app而言,資源混淆還是有用武之地的。一是可以減小apk的大小,二是對比電腦客戶端更多將信息直接放在變量中而言app將更多的信息存放於xml文件中,進行混淆有助於提高逆向者理解程序邏輯的難度。

1.1 資源混淆

1.1.1 資源混淆操作步驟

從這里要介紹的資源混淆操作方法看,因為是直接對apk進行操作所以放在最后講才更合適。但順從認識而言,資源混淆這種不是主角的東西就該放最前面講。

資源混淆我們這里使用微信團隊的AndResGuard

1)進入tool_output目錄,下載AndResGuard jar文件和config.xml模板配置文件(注意不要右鍵直接保存那樣下載的是html文件,jar文件點進去下載,config.xml點進去復制內容自己在本地新建個config.xml。或才直接下載整個項目再找出這兩個文件)

2) 修改config.xml

config.xml各配置項具體說明見官方說明,我的大概理解是默認會對res目錄下的各xml文件進行混淆,在config.xml可以配置不進行混淆的白名單(Whitelist項)及是否使用7z對圖片進行壓縮(Compress項)等。其中注意不是寫在config.xml中的項就是啟用的,各項自己的isactive值為"true"時才是啟用的。

我這里只修改最后的sign項,配置簽名信息其他都不做修改(其實出於安全考慮簽名最好用-signature選項而不是配置在config.xml中,但出於教程的統一和簡潔這里我就這么操作)

 

3)進行資源混淆

執行以下命令進行混淆,注意我這里是用到的文件都放在了當前目錄下(C:\Users\ls\Desktop\app),如果不在要注意使用全路徑或寫好相對路徑。

-jar指定----AndResGuard程序

SecTest.apk----是我做好的測試app,改成自己的

-config----指定使用的配置文件

-7zip----指定7z可執行文件的路徑,改成自己的;其實如果不指定命令會報錯,但只是不能生成經過壓縮的apk而已,未壓縮的apk還是成功生成了的。

-zipalign----指定zipalign可執行文件的路徑,這個程序在android sdk中就有,到sdk目錄下找就行了。

官方文檔中說,若7zip或zipalign的路徑已設置環境變量中,這兩項不需要單獨設置。一是這兩個安裝時都不會自己加到環境變量,二是官方文檔7z用的是7za這個名字的可執行程序在我安裝的7z版本中是沒有的。也就是說推薦用直接指定命令位置而不是改環境變量的方法。

mkdir test_dir

java -jar AndResGuard-cli-1.2.12.jar SecTest.apk -config  config.xml -out test_dir -7zip D:\7-Zip\7z.exe -zipalign D:\Language\ASDK\build-tools\28.0.0\zipalign.exe

 

 如果出現“java.io.IOException: the signature file do not exit, raw”等報錯,那多半是文件名等信息寫錯了,重新檢查一遍。

最后test_dir中得到的有以下文件,各文件官方有說明,就我這里想要的是混淆並進行了簽名的SecTest_signed.apk

1.1.2 驗證資源混淆成功【可選】

 以activity_main.xml為例,項目中代碼如下:

使用反編譯工具查看layout,可以看到生成了一堆名為a,b,c,d的xml的文件。我找了半天才找到a2.xml是activity_main.xml,且可以看到其中的控件id和字符串名稱等都已混淆

 

1.2 代碼混淆

1.2.1 代碼混淆操作步驟

代碼混淆這里以Android Studio中使用ProGuard為例,Eclipse看了一下也都是指定一下規則文件而已就不多做介紹。至於其他混淆工具並沒有研究。

將項目切換到Project視圖,找到app文件夾下的build.gradle並打開,鎖定到buildTypes節區,如圖所示其中有minifyEnabled項該項控制編譯時是否啟用混淆,默認為false表示不使用。

minifyEnabled下方的proguardFiles用於指定混淆規則文件,其中的proguard-android.txt是Android Studio自帶的基本的混淆規則(一般在$SDK_PATH\tools\proguard目錄下)這個一般不要去做修改。另一個proguard-rules.pro是專門供寫個性化混淆規則用的,如果有個性化混淆需求將自己的規則寫入其中即可(在下圖中也可看到改文件與build.gradle一樣同處app目錄下)。

proguard-android.txt中已排除了android關鍵組件然后對除此之外的java代碼都進行混淆,已符合我當前需要,所以我這里只將minifyEnabled項由默認的false改為true,其他都不做改動。

如果要寫個性化規則可參考:https://blog.csdn.net/Two_Water/article/details/70233983

1.2.2 驗證代碼成功混淆【可選】

以MainActivity.java為例,項目中OnCreate函數部分代碼如下

 

使用反編譯工具反編譯代碼,對應片段代碼如下,可以看到變量名稱已被i,j,k等代替

另外再查看AESCoder.java,也已被成功混淆

 

二、簽名驗證

簽名驗證,就是在APP中寫入自己私鑰的hash值,和一個獲取當前簽名中的私鑰hash值的函數兩個值相一致,那么就說明APP沒有被改動允許APP運行。如果兩值不一致那么說明APP是被二次打包的,APP就自我銷毀進程。

簽名驗證又可以在兩個地方做,一個是在MainActivity.java的OnCreate函數中做,一個是在原生代碼文件的JNI_OnLoad函數中做。

在OnCreate函數中做,短處是反編譯者只要找到在OnCreate中定位到驗證函數,然后將其注釋,重新打包APP就可以成功運行;好處就是代碼簡單。

在JNI_OnLoad中做,短處是比較復雜(需要創建支持C/C++原生代碼的項目,獲取hash需要繞道java代碼獲取等);好處就是反編譯者需要進一步掌握ida等反匯編工具將驗證函數刪除才能繞過驗證。

為了最大限度地提高安全性,可以考濾兩種驗證都使用。

最后為了避免爭議,在此要做一下統一聲明,以下代碼基本我個人都不是原作者,個人在本節的作用是將幾個方案整合成了一個比較合理的方案,並驗證這些代碼和整合出來的方案是可行的。

2.1 在MainActivity.java的OnCreate函數中進行簽名驗證

 OnCreate函數內、setContentView后加入以下代碼:

       // 獲取當前上下文
        Context context = getApplicationContext();
        // 發布apk時用來簽名的keystore中查看到的sha1值,改成自己的
        String cert_sha1 = "937FF2936CDB81EEF4A776290EA9E076B3BC03A9";
        // 調用isOrgApp()獲取比較結果
        boolean is_org_app = isOrgApp(context,cert_sha1);
        // 如果比較初始從證書里查看到的sha1,與代碼獲取到的當前證書中的sha1不一致,那么就自我銷毀
        if(! is_org_app){
            android.os.Process.killProcess(android.os.Process.myPid());
        }

在MainActivity類內,OnCreate函數外加入以下代碼:

    // 此函數用於返回比較結果
    public static boolean isOrgApp(Context context,String cert_sha1){
        String current_sha1 = getAppSha1(context,cert_sha1);
        // 返回的字符串帶冒號形式,用replace去掉
        current_sha1 = current_sha1.replace(":","");
        return current_sha1.equals(current_sha1);
    }
    // 此函數用於獲取當前APP證書中的sha1值
    public static String getAppSha1(Context context,String cert_sha1) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
            byte[] cert = info.signatures[0].toByteArray();
            MessageDigest md = MessageDigest.getInstance("SHA1");
            byte[] publicKey = md.digest(cert);
            StringBuffer hexString = new StringBuffer();
            for (int i = 0; i < publicKey.length; i++) {
                String appendString = Integer.toHexString(0xFF & publicKey[i]).toUpperCase(Locale.US);
                if (appendString.length() == 1)
                    hexString.append("0");
                hexString.append(appendString);
                hexString.append(":");
            }
            String result = hexString.toString();
            return result.substring(0, result.length()-1);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

我自己調試結果如下,確實可以成功獲取sha1值(獲取sha1函數代碼原文鏈接

 

2.2 原生代碼文件的JNI_OnLoad函數中進行簽名驗證

開始看到這位小哥哥的文章,就喜歡這種有圖有真相的文章,說明其代碼應該是真的可以獲取到APP當前的sha1值的。

但后來理清他的做法是:從java中把context傳過去,在c++中完成比較返回true或false;也就是說決定程序退不退出的if語句還是在java中的,這種做法和2.1中全在java中做除了顯示技術比較強之外安全效果完全一樣並沒有提升啊。if應當在c++中實現,context也需要c++自己獲取。

后來找到另一位小哥哥的文章,其指出判斷需要在c++中做而且是在JNI_OnLoad函數中做並給出了方法,但是他獲取context時實現的NoProGuard我沒搞清楚在哪導入。

最后找到了又一位小哥哥的文章,其給出了JNI獲取context的方案,驗證也確實是可行的。

所以整合的方案就是:第二位小哥哥在JNI_OnLoad函數中做的思想+第三位小哥哥獲取context的方法+第一位小哥哥獲取sha1的方法。

(其實第二位小哥哥還有一個思想就是debug時不需要驗證release才要驗證,這也是可取的,我這里也采用了。但debug時要做驗證也不是不可以的,只是要注意debug時運行在avd中的app使用的是Android Studio自己生成的keystore而不是我們發布apk時自己的keystore,所以此時填的sha1的值應當是Android Studio自己生成的keystore的sha1,

當然第二位小哥哥獲取md5的方法改一下好像也是能獲取正確的sha1值的)

2.2.1 C++中驗證簽名代碼

最終C++中驗證簽名的代碼如下,自己使用時要注意將其中的app_sha1賦值成自己keystore中的sha1值

#include <jni.h>
#include <string>

// const char *app_sha1="FAAB30C11EEF7333C81D48FECA25D21A18E2C789";
// 這里是keystore中的sha1值,改成自己的
const char *app_sha1 = "937FF2936CDB81EEF4A776290EA9E076B3BC03A9";
const char hexcode[] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};

jobject getGlobalContext(JNIEnv *env)
{
    //獲取Activity Thread的實例對象
    jclass activityThread = env->FindClass("android/app/ActivityThread");
    jmethodID currentActivityThread = env->GetStaticMethodID(activityThread, "currentActivityThread", "()Landroid/app/ActivityThread;");
    jobject at = env->CallStaticObjectMethod(activityThread, currentActivityThread);
    //獲取Application,也就是全局的Context
    jmethodID getApplication = env->GetMethodID(activityThread, "getApplication", "()Landroid/app/Application;");
    jobject context = env->CallObjectMethod(at, getApplication);
    return context;
}

char* getSha1(JNIEnv *env){
    // 調用getGlobalContext,獲取上下文
    jobject context_object = getGlobalContext(env);
    if (context_object == NULL){
        printf("context is NULL");
        return NULL;
    }
    jclass context_class = env->GetObjectClass(context_object);

    //反射獲取PackageManager
    jmethodID methodId = env->GetMethodID(context_class, "getPackageManager", "()Landroid/content/pm/PackageManager;");
    jobject package_manager = env->CallObjectMethod(context_object, methodId);
    if (package_manager == NULL) {
        printf("package_manager is NULL!!!");
        return NULL;
    }

    //反射獲取包名
    methodId = env->GetMethodID(context_class, "getPackageName", "()Ljava/lang/String;");
    jstring package_name = (jstring)env->CallObjectMethod(context_object, methodId);
    if (package_name == NULL) {
        printf("package_name is NULL!!!");
        return NULL;
    }
    env->DeleteLocalRef(context_class);

    //獲取PackageInfo對象
    jclass pack_manager_class = env->GetObjectClass(package_manager);
    methodId = env->GetMethodID(pack_manager_class, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    env->DeleteLocalRef(pack_manager_class);
    jobject package_info = env->CallObjectMethod(package_manager, methodId, package_name, 0x40);
    if (package_info == NULL) {
        printf("getPackageInfo() is NULL!!!");
        return NULL;
    }
    env->DeleteLocalRef(package_manager);

    //獲取簽名信息
    jclass package_info_class = env->GetObjectClass(package_info);
    jfieldID fieldId = env->GetFieldID(package_info_class, "signatures", "[Landroid/content/pm/Signature;");
    env->DeleteLocalRef(package_info_class);
    jobjectArray signature_object_array = (jobjectArray)env->GetObjectField(package_info, fieldId);
    if (signature_object_array == NULL) {
        printf("signature is NULL!!!");
        return NULL;
    }
    jobject signature_object = env->GetObjectArrayElement(signature_object_array, 0);
    env->DeleteLocalRef(package_info);

    //簽名信息轉換成sha1值
    jclass signature_class = env->GetObjectClass(signature_object);
    methodId = env->GetMethodID(signature_class, "toByteArray", "()[B");
    env->DeleteLocalRef(signature_class);
    jbyteArray signature_byte = (jbyteArray) env->CallObjectMethod(signature_object, methodId);
    jclass byte_array_input_class=env->FindClass("java/io/ByteArrayInputStream");
    methodId=env->GetMethodID(byte_array_input_class,"<init>","([B)V");
    jobject byte_array_input=env->NewObject(byte_array_input_class,methodId,signature_byte);
    jclass certificate_factory_class=env->FindClass("java/security/cert/CertificateFactory");
    methodId=env->GetStaticMethodID(certificate_factory_class,"getInstance","(Ljava/lang/String;)Ljava/security/cert/CertificateFactory;");
    jstring x_509_jstring=env->NewStringUTF("X.509");
    jobject cert_factory=env->CallStaticObjectMethod(certificate_factory_class,methodId,x_509_jstring);
    methodId=env->GetMethodID(certificate_factory_class,"generateCertificate",("(Ljava/io/InputStream;)Ljava/security/cert/Certificate;"));
    jobject x509_cert=env->CallObjectMethod(cert_factory,methodId,byte_array_input);
    env->DeleteLocalRef(certificate_factory_class);
    jclass x509_cert_class=env->GetObjectClass(x509_cert);
    methodId=env->GetMethodID(x509_cert_class,"getEncoded","()[B");
    jbyteArray cert_byte=(jbyteArray)env->CallObjectMethod(x509_cert,methodId);
    env->DeleteLocalRef(x509_cert_class);
    jclass message_digest_class=env->FindClass("java/security/MessageDigest");
    methodId=env->GetStaticMethodID(message_digest_class,"getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;");
    jstring sha1_jstring=env->NewStringUTF("SHA1");
    jobject sha1_digest=env->CallStaticObjectMethod(message_digest_class,methodId,sha1_jstring);
    methodId=env->GetMethodID(message_digest_class,"digest","([B)[B");
    jbyteArray sha1_byte=(jbyteArray)env->CallObjectMethod(sha1_digest,methodId,cert_byte);
    env->DeleteLocalRef(message_digest_class);

    //轉換成char
    jsize array_size=env->GetArrayLength(sha1_byte);
    jbyte* sha1 =env->GetByteArrayElements(sha1_byte,NULL);
    char *hex_sha=new char[array_size*2+1];
    for (int i = 0; i <array_size ; ++i) {
        hex_sha[2*i]=hexcode[((unsigned char)sha1[i])/16];
        hex_sha[2*i+1]=hexcode[((unsigned char)sha1[i])%16];
    }
    hex_sha[array_size*2]='\0';

    printf("hex_sha %s ",hex_sha);
    return hex_sha;
}

static jboolean checkSignature(JNIEnv *env) {
    // 調用getSha1獲取app當前證書中的sha1
    char *sha1 = getSha1(env);
    // 調用checkValidity獲取比較結果並直接返回
    // jboolean signatureValid = checkValidity(env,sha1);
    if (strcmp(sha1,app_sha1)==0)
    {
        return JNI_TRUE;
    }
    else{
        return JNI_FALSE;
    }
}
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    if (vm->GetEnv((void **) (&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    // RELEASE_MODE這個宏是通過編譯腳本設定的,如果是release模式,
    // 則RELEASE_MODE=1,否則為0或者未定義
    // 如果想不管release還是debug都進行簽名驗證,注釋掉下方ifdef和endif兩條預編譯語句即可
#ifdef RELEASE_MODE
    // 如果是release版本,檢查當前應用的簽名是否一致;如果不簽名不一致的話則返回-1,-1會引發app異常自動退出
    if (RELEASE_MODE == 1) {
        if (checkSignature(env) != JNI_TRUE) {
            return -1;
        }
    }
#endif

    return JNI_VERSION_1_6;
}
View Code

2.2.2 配置build_gradle

由於代碼中使用了以下預編譯語句,所以如果只是使用上邊的代碼,驗證是沒有生效的。說明如下:

build_gradle中未配置RELEASE_MODE=1----release/debug都不進行簽名驗證
build_gradle中配置RELEASE_MODE=1----release模式驗證/debug模式不驗證
注釋掉ifdef和endif兩條預編譯語句----release/debug都進行簽名驗證

#ifdef RELEASE_MODE
    // 檢查當前應用的簽名是否一致,如果不簽名不一致的話則返回-1,-1會引發app異常自動退出
    if (checkSignature(env) != JNI_TRUE) {
        return -1;
    }
#endif

所以為了啟用驗證,還需要打開app目錄下的build_gradle文件,在如下圖所示位置加入以下代碼:

ndk {
    // release包定義RELEASE_MODE=1宏,供so庫中的ifdef語句使用
    cFlags "-DRELEASE_MODE=1"
}

 

2.2.3 MainActivity.java中加載so文件

當然最還得要在java文件中,載入so文件才能起來作用。netive-test是我這里so的庫名改成自己的

// Used to load the 'native-test' library on application startup.
static {
    System.loadLibrary("native-test");
}

 

三、反調試

反調試,這位小哥哥說可以有兩個思路。

第一個是一個進程同時最多只能被一個進程所調試,所以可以自己使用ptrace()函數假裝自己在調試自己,占住調試的位置以此來拒絕別的進程的調試請求。

第二個是查看/proc/{pid}/status文件如果發現TracerPid的值不等於0(TracerPid是調試進程的pid,如果不為0則表示有進程在調試),則kill掉自己。

第一個思路由於我在復現時沒有起到反調試效果,未排查到原因暫且就先不管了。

第二個思路中作者給的具體做法是只是在JNI_Onload中只檢測一次。在姜維的《Android 應用安全防護和逆向分析》中也提到了第二種思路,但他給出的具體做法是去啟動一個線程不斷地檢測/proc/{pid}/status的TracerPid值,如果檢測到不為0則使用exit退出。

不是很清楚有沒有可能/proc/{pid}/status的TracerPid值開始為0,后來不為0的情況。但其實我是先看到姜維寫的,所以這里就采用姜維的做法。

3.1 加入檢測函數

在要防護的c++文件中加入以下兩個函數

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
void* thread_function(void *arg){
    int pid = getpid();
    char file_name[20] = {'\0'};
    sprintf(file_name,"proc/%d/status",pid);
    char line_str[256];
    int i = 0,traceid;
    FILE *fp;
    while(1){
        i = 0;
        fp = fopen(file_name,"r");
        if(fp == NULL){
            break;
        }
        while(!feof(fp)){
            fgets(line_str,256,fp);
            if(i == 5){
                // traceid = getnumberfor_str(line_str);
                traceid = atoi(&line_str[10]);
                if(traceid > 0){
                    exit(0);
                }
                break;
            }
            i++;
        }
        fclose(fp);
        sleep(5);
    }
}


void create_thread_check_traceid(){
    pthread_t thread_id;
    int err = pthread_create(&thread_id,NULL,thread_function,NULL);
    if(err != 0){

    }
}

3.2 在JNI_OnLoad函數開頭調用檢測函數

要強調兩點,一個是這里是反調試只有ida進行動態分析時才能起到防護效果,ida靜態打開還是不能阻止的。第二個是這里是反調試,自己開發過程中使用IDE debug也是調試,如果加了以下代碼那IDE debug時進程也會自我銷毀的(實際發現IDE中 run也是不行的)。

也就是說,在開發時要注意先注釋該函數調用,在打包生成apk時才去掉注釋。

 

 

參考:

《Android 應用安全防護和逆向分析》

https://blog.csdn.net/Two_Water/article/details/70233983

https://blog.csdn.net/liyi0930/article/details/77413525

http://leehong2005.com/2016/08/08/android-so-signature-check/

https://blog.csdn.net/lb377463323/article/details/75315167

https://blog.csdn.net/leifengpeng/article/details/52681196

https://blog.csdn.net/feibabeibei_beibei/article/details/60956307

https://www.cnblogs.com/biggerman/p/6940888.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM