本教程所用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; }
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時才去掉注釋。
參考:
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