Android安全防護之旅---Android應用的安全的攻防之戰

一、前言

在前兩篇破解的文章中,我們介紹瞭如何使用動態調試來破解apk,一個是通過調試smali源碼,一個是通過調試so代碼來進行代碼的跟蹤破解,那麼今天我們就這兩篇文章的破解方法,來看看Android中開發應用的過程中如何對我們的應用做一層安全保護,當然現在市場中大部分的應用已經做了一些防護策略,但是沒有絕對的安全,破解只是時間上的問題。所以攻破和防護是相生相剋,永不停息的戰爭,沒有絕對的安全,也沒有萬能的破解之道。

下面我們就來看看如何做到我們的應用更安全,我們主要從這五個方面來看看怎麼操作:

1、混淆策略

2、應用的簽名

3、修改Native函數名

4、反調試異常檢測

5、應用的加固策略

當然還有其他防護方法,我們今天就介紹這五種,後續還有的話,繼續補充

 


二、技術原理

第一種方式:混淆策略

混淆策略是每個應用必須增加的一種防護策略,同時他不僅是爲了防護,也是爲了減小應用安裝包的大小,所以他是每個應用發版之前必須要添加的一項功能,現在混淆策略一般有兩種:

1、對代碼的混淆

我們在反編譯apk之後,看到的代碼類名,方法名,已經代碼格式看起來不像正常的Android項目代碼,那麼這時候就會增加閱讀難度,增加破解難度,像這樣的代碼混淆:

我們一般現在的破解查看Java層代碼就是兩種方式:

一種是直接先解壓classes.dex文件出來,使用dex2jar工具轉化成jar文件,然後再用jd-gui工具進行查看類結構

一種是使用apktool工具直接反編譯apk,得到smali源碼,閱讀smali源碼

不過這種代碼混淆有時候在一定程度上能夠增加混淆策略,但是有時候也不是很安全,因爲我們知道我們在破解的過程中一般是找程序的入口,那麼這些入口一般都是Application或者是MainActivity之類的,但是這些Android中的組件類是不能進行混淆的,所以我們還是有入口可尋,能夠找到入口代碼,然後進行跟蹤。

2、對工程資源的混淆

我們上面說到了對代碼的混淆能夠增加一定的代碼閱讀難度,有時候我們爲了防止資源的保護也是可以做混淆的,這個資源混淆原理這裏就不多解釋了,微信團隊已經將這個功能開源,不瞭解的同學可以轉戰github查看:

https://github.com/shwenzhang/AndResGuard

當然資源混淆還有一個很大的好處就是減小apk包的大小,當然這個不是本文討論的知識點,這裏我們討論的是混淆資源增加破解查找資源的難度,先來看一下混淆資源之後的結果:

這裏我們可以看到,一個混淆資源的應用,反編譯之後查看他的string.xml內容,發現他的name全是簡單的混淆字母,那麼這個對於我們之前的那種可以通過name的值,來查找對應的字符串內容來獲取消息,這個將是很蛋疼的一件事,因爲你這時候如果全局搜索一個name值的話,比如這裏的name='a',那麼得搜出多少個這樣的name,查找也是很好時間的,其實在沒有混淆之前,一般string中的name都是比較唯一的一種值,查找的話不會有那麼多查找結果,而且查找時間也是很短的。

破解之道:

但是對於這種混淆資源也是絕對的防護安全,因爲我們知道一般在反編譯之後的Java代碼中,看到的獲取資源值的時候,並不是資源的name值了,而是資源對應的int類型的值,比如這樣:

這裏獲取一個字符串的值,那麼,這些int類型的值,我們可以在反編譯之後的res/values/pulblic.xml中找到:

比如這裏的2131230929變成16進制就是:0x0x7f0800d1,我們在public.xml中查找,找到了name='ey‘的一項,然後再去string.xml中進行name查找:

好吧,還是找到了這個字符串的值,反編譯之後的public.xml中記錄了所有資源的id和整型值對應值,混淆之後的代碼中看到的都是資源id的整型值,那麼這麼一看混淆並沒有什麼用途。只能偏偏小白了。

從上面的兩處混淆策略看到,混淆對於破解並沒有什麼太大的阻礙,也是隻是一個障眼法,不過混淆的另外一個功能就是減少apk包的大小,這個也是每個應用添加混淆的最主要原因。

 

第二種方式:應用的簽名

我們知道Android中的每個應用都是有一個唯一的簽名,如果一個應用沒有被簽名是不允許安裝到設備中的,一般我們在運行debug程序的時候也是有默認的簽名文件的,只是IDE幫我們做了簽名工作,一般在應用發版的時候會用唯一的簽名文件進行簽名,那麼我們在以往的破解中可以看到,我們有時候需要在反編譯應用之後,然後從新簽名在打包運行,這個又給了很多二次打包團隊謀取利益的一種手段,就是反編譯市場中的包,然後添加一些廣告代碼,最後使用自家的簽名在此從新打包發佈到市場中,因爲簽名在反編譯之後是獲取不到的,所以只能用自己的簽名文件去簽名,但是在已經安裝了應用設備再去安裝一個簽名不一致的應用也是安裝失敗的,這樣也有一個問題就是有些用戶安裝了這些二次打包的應用之後,無法再安裝正規的應用了,只有卸載重裝。那麼這時候我們可以利用應用的簽名是唯一的特性做一層防護。

我們爲了防止應用被二次打包,或者是需要破解我們的apk的操作,在入口處添加簽名驗證,如果發現應用的簽名不正確就立即退出程序,我們可以在應用啓動的時候獲取應用的簽名值,然後和正規的簽名值作比對,如果不符合就直接退成程序即可,這裏我們做一個簡單的案例測試一下:

這裏定義一個簡單的工具類用於比較應用的簽名,這裏只是簡單處理,正常情況下這裏應該比對簽名的MD5值,這裏爲了簡單就忽略了,然後我們在程序的入口處做一次比對,如果不正確就退出程序:

那麼我們得到上面的apk之後,下面來反編譯,然後從新簽名安裝(關於這裏如何反編譯和簽名,不做解釋了,使用apktool和signapk工具即可,簽名文件是自己的),然後運行:

發現程序根本運行不起來,一點擊就閃退,這裏就做到了防止應用被二次簽名打包的安全問題策略了。

破解之道:

但是這個也不是最安全的,因爲我們知道,既然有簽名比對方法的地方,那麼我只需要反編譯apk之後,修改smali語法,把這個方法調用的地方註釋即可:

只需要使用#把這行代碼註釋,然後回編譯從新打包安裝即可。所以這種方式也是隻能欺騙一下小白,不過這裏需要注意的是,如何找到這個檢測簽名的方法的地方還是最關鍵的,比如有的程序在native層做的,但是不管在哪裏,只要是在代碼中,我們就可以找出來的。

 

第三種方式:修改Naitve函數名

這個方法其實不太常用,因爲他的安全措施不是很強大的,但是也是可以起到一定的障眼法策略,在說這個知識點的時候,我們先來了解一下so加載的流程:

在Android中,當程序在java層運行System.loadLibrary("jnitest");這行代碼後,程序會去載入libjnitest.so文件,與此同時,產生一個"Load"事件,這個事件觸發後,程序默認會在載入的.so文件的函數列表中查找JNI_OnLoad函數並執行,與"Load"事件相對,當載入的.so文件被卸載時,“Unload”事件被觸發,此時,程序默認會去在載入的.so文件的函數列表中查找JNI_OnUnload函數並執行,然後卸載.so文件。需要注意的是,JNI_OnLoad與JNI_OnUnload這兩個函數在.so組件中並不是強制要求的,用戶也可以不去實現,java代碼一樣可以調用到C組件中的函數,之所以在C組件中去實現這兩個函數(特別是JNI_OnLoad函數),往往是做一個初始化工作或“善後”工作。可以這樣認爲,將JNI_ONLoad看成是.so組件的初始化函數,當其第一次被裝載時被執行(window下的dll文件也可類似的機制,在_DLL_Main()函數中,通過一個swith case語句來識別當前是載入還是卸載)。將JNI_OnUnload函數看成是析構函數,當其被卸載時被調用。由此看來,就不難明白爲什麼很多jni C組件中會實現JNI_OnLoad這個函數了。 一般情況下,在C組件中的JNI_OnLoad函數用來實現給VM註冊接口,以方便VM可以快速的找到Java代碼需要調用的C函數。(此外,JNI_OnLoad函數還有另外一個功能,那就是告訴VM此C組件使用那一個JNI版本,如果未實現JNI_OnLoad函數,則默認是JNI 1.1版本)。

應用層的Java類別通過VM而調用到native函數。一般是通過VM去尋找*.so裏的native函數。如果需要連續呼叫很多次,每次都需要尋找一遍,會多花許多時間。此時,C組件開發者可以將本地函數向VM進行註冊,以便能加快後續調用native函數的效率.可以這麼想象一下,假設VM內部一個native函數鏈表,初始時是空的,在未顯式註冊之前此native函數鏈表是空的,每次java調用native函數之前會首先在此鏈表中查找需要查找需要調用的native函數,如果找到就直接使用,如果未找到,得再通過載入的.so文件中的函數列表中去查找,且每次java調用native函數都是進行這樣的流程,因此,效率就自然會下降,爲了克服這樣現象,我們可以通過在.so文件載入初始化時,即JNI_OnLoad函數中,先行將native函數註冊到VM的native函數鏈表中去,這樣一來,後續每次java調用native函數時都會在VM中的native函數鏈表中找到對應的函數,從而加快速度

通過上面的分析之後,我們知道原來我們知道so文件加載和卸載的時機,同時我們可以顯示的手動註冊我們自己的native方法,那麼我們知道一般我們在定義native方法的時候,對應的native層的函數名是:Java_類名_方法名  這種樣式:

所以就有兩個問題:

第一個問題就是我們在IDA工具查看so文件的時候,去找到對應的native方法非常容易,以爲我們知道了Java層的native方法名和類型,那麼直接可以定位到這個native函數:

第二問題就是惡意破解人可以得到這個so文件之後,查看這個native方法的參數和返回類型也就是方法簽名,然後自己在Java層寫一個demo程序,然後構造一個和so文件中對應的native方法,然後就可以執行這個native方法,如果我們有一個校驗密碼或者是獲取密碼的方法是個native的,那麼這時候就會很容易的被惡意人執行方法後獲取結果。

說的簡單點就比如上面的這個isEquals例子:

現在有一個人,想執行這個我的應用的isEquals方法,那麼他只需要解壓我的apk,得到so文件,查看so文件中的函數,或者是查看上層的Java代碼,得到這個方法的返回值和簽名,然後他就可以編寫一個簡單的程序,構造一個類:

cn.wjdainkong.encryptdemo.MainActivity

然後在他內部定義一個native方法:

public native boolean isEquals(String str);

然後在使用System.loadLibrary加載我的so文件,然後在適當的地方執行isEquals方法,這樣就等於調用了我的so文件中的isEquals方法了。

所以從上面的兩個爲可以看到,如果我們native層的函數遵從這樣的格式,無疑是給破解者簡單的一種方式,所以我們可以這麼做,就是顯示的註冊我們的JNI方法,只需要在我們native層的代碼中調用這三個函數即可:

第一個函數:(*env)->RegisterNatives(env,clazz, methods, methodsLenght)

這個函數就是手動的註冊一個native方法,這個函數是屬於JNIEnv*的,參數也比較簡單

1》clazz就是,需要註冊native方法的那個類,是jclass類型,這個我們可以使用JNIEnv的FindClass方法,傳遞類的名稱即可獲取這個對象,類似於這樣:

2》methods是一個結構體,定義如下:

typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
第一個變量name是Java中函數的名字。
第二個變量signature,用字符串是描述了函數的參數和返回值
第三個變量fnPtr是函數指針,指向C函數。

類似於這樣的結構:

第二個函數:jint JNI_OnLoad(JavaVM* vm, void* reserved)

這個函數就是上面說到的,so被加載的時候被調用到,同時我們可以看到這裏還可以獲取JVM參數的,一般在這個函數中主要就是執行上面的註冊函數功能,同時這裏還需要獲取一個JNIEnv*變量:

這裏通過JVM來獲取JNIEnv變量,然後調用註冊函數:

實現手動的註冊函數

第三個函數:void JNI_OnUnload(JavaVM* vm, void* reserved)
這個函數和JNI_OnLoad是相對應的,是在so被卸載的時候調用

通過上面的三個函數我們就可以手動的顯示註冊我們的native函數方法了,那麼我們同時就可以修改native層的函數名,不要按照之前的那種格式了,增加破解者尋找關鍵的native層函數的難度:

這裏我們把isEquals函數名變成了jiangwei:

然後在修改註冊方法的結構體:

編譯運行,在使用IDA查看:

這時候破解者不能按照常規的套路,找到了native層的函數了,那麼上面的兩個問題就可以避免了。增加安全性

破解之道:

但是問題來了,現在的破解者,一般打開SO文件的時候,如果找不到對應的native方法之後,就會去找JNI_OnLoad函數,然後在通過分析arm彙編代碼,找到register函數,分析註冊方法結構體,找到對應的native方法,那麼這種方式還是不靠譜,也是隻能糊弄一下小白破解者。不過我們通過這個例子也可以得知,在JNI_OnLoad中可以做很多事的,比如上面說到的簽名機制校驗,我們也可以在JNI_OnLoad中做一次,增加安全性:

看看equal_sign函數功能:

在這個方法中,其實用我們用JNIEnv變量調用了Java層的方法,來獲取應用的簽名,然後進行比對的

所以我們用這種簽名校驗方式來做安全性保證也是一個思路至少native層的代碼分析比smali代碼分析難度大點,而且這種簽名校驗機制必須用靜態方式去破解apk,也就是通過分析代碼來破解,因爲程序沒有運行起來無法通過動態方式破解的。那麼應對與靜態方式破解的話,我們只能增加代碼的閱讀難度了。

 

第四種方式:反調試異常檢測

這種方式其實是爲了應對現在很多破解者使用IDA進行動態方式調試so文件,從而獲取重要的信息,如果還不知道如何使用IDA進行動態調試so文件的同學可以查看這篇文章:Android中使用IDA進行動態調試so文件 ,看完這篇文章之後,我們可以知道IDA進行so動態調試是基於進程的注入技術,然後使用Linux中的ptrace機制,進行調試目標進程的,那麼ptrace機制有一個特點,就是如果一個進程被調試了,在他進程的status文件中有一個字段TracerPid會記錄調試者的進程id值,比如:

查看文件:/proc/[myPid]/status

在第六行,有一個TracerPid字段,就是記錄了調試者的進程id

那麼我們就可以這麼做來達到反調試的功效了,就是我們可以輪訓的遍歷自己進程的status文件,然後讀取TracerPid字段值,如果發現他大於0,那麼就代表着自己的應用在被人調試,所以就立馬退出程序。原理知道了,代碼實現也很簡單,這裏用pthread創建一個線程,然後進行輪訓操作:

使用pthread_create創建一個線程,線程啓動之後執行thread_function函數

看看thread_funcation函數:

開始輪訓,讀取TracerPid字段的值,發現大於0,就立馬退出程序,我們運行結果看看:

看到了,當我們使用IDA工具進行調試的時候,程序立馬退出,同時IDA的調試頁面也退出了。

所以這裏我們看到這種輪訓機制來實現反調試策略,可以應對與一般的破解小白了。

破解之道:

但是還是有問題,因爲現在破解者們,他們已經免疫了,知道會有這種檢測,所以就會用IDA工具給JNI_OnLoad函數下斷點,然後進行調試,找到檢測輪訓代碼,使用nop指令,替換檢測指令,就相當於把檢測代碼給註釋了,功能的夭折,所以這種反調試方法還是不好使,知道的人多了,也沒什麼意義了,但是有總比沒有的好。

 

第五種方式:應用的加固策略

關於這種方式,那就是現在很多應用都用的一種方式了,也是安全性最高的一種防護了,他加固主要有三方面:

1、對dex文件進行加密

這樣我們用dex2jar工具,或者apktools等工具反編譯失敗,關於這個dex加密這裏,也不做太多的介紹了

破解之道:

但是可惜的是,這種方式也淪陷了,因爲我們知道,不管你dex怎麼加密,最後都是需要用DVM加載dex文件到內存中的,我們知道Android中所有關於DVM的函數都是在libdvm.so文件中的,而且這個文件是存在設備的/system/lib目錄中的,加載dex有一個重要的函數:

int dvmDexFileOpenPartial(const void* addr, int len, DvmDex** ppDvmDex)
第一個參數就是dex內存起始地址

第二個參數就是dex大小。所以在這個函數下斷點可以直接dump出明文dex

所以我們使用IDA調試程序,找到模塊libdvm.so的內存地址,找到其中的函數dvmdexfileopenpartialPKviPP6DvmDex,在這個函數下斷點即可dump處dex文件了。

關於這個案例,後續還會寫一篇文章來介紹如何破解現在加固dex的應用。

2、對so文件進行加密

現在很多應用把中號的功能都放到了native層中,那麼如果我們隊so文件進行加密的話,那麼IDA工具就無法打開so文件,從而做到安全保護

破解之道:

但是可惜的是,這種方式也被淪陷了,看完這篇文章之後,我們知道加固so的有一個特點就是你必須在so在被調用的時候需要進行解密,不然會影響正常的native層調用,那麼這個時機很重要,一般都是在so文件的入口處,也就是用:

__attribute__((constructor)) 這個屬性來標註一個解密函數,這樣就能保證解密函數執行的時機比任何一個函數時機都早,或者可以理解爲是一個類的構造函數的執行時機即可,但是一般有這種屬性的函數都在so的.inin_array段中的:

那麼現在的問題就變成了,如果我知道了.init_array端的位置,然後在查看這個段中的arm指令代碼,就可以得到解密函數了,然後在解讀這個解密函數的邏輯即可。那麼最後就要看這個加密函數的難度程度怎麼樣了。

關於這個案例,後續還會寫一篇文章來介紹如何破解現在加固so的應用。

3、加固資源文件和AndroidManifest.xml文件

這個加固一般是應對與現在最流行的反編譯工具apktool了,他是開源的,比如下面的這個應用:

所以看到了,這種加固就是利用apktool工具的漏洞來進行加固的,不過這個apktool工具也是實時在更新的,也是爲了解決現在的apk這種資源文件的加固導致反編譯失敗的問題。

破解之道:

所以對於這種反編譯失敗的問題,我們應該自己編譯apktool的源碼,找到指定的保存位置,然後修改異常即可,不過這個可不是一個簡單的工作,是需要耐心和經驗的。

 

三、安全工作流程

1、爲了應對與低級破解小白,同時也是爲了減小apk包的大小,我們會對代碼和資源的一個混淆,增加破解難度

2、爲了應對與初級破解小白,我們將手動的註冊我們的native方法,讓破解者找不到對應的native方法,同時解決一些重要的native方法的被調用問題,增加破解難度

3、爲了應對與中級破解小白,我們會利用應用的簽名,來防止應用的二次簽名打包,同時防止動態調試問題,增加破解難度

4、爲了應對與高級破解小白,我們會增加應用的反調試功能,來防止應用被動態調試和進程注入問題,增加破解難度

5、爲了應對與資深破解小白,我們會採用應用的加固策略,對dex,so,資源文件進行加固,增加反編譯工作了和調試難度

 

本文的目的只有一個就是學習更多的逆向技巧和思路,如果有人利用本文技術去進行非法商業獲取利益帶來的法律責任都是操作者自己承擔,和本文以及作者沒關係,本文涉及到的代碼項目可以去編碼美麗小密圈自取,歡迎加入小密圈一起學習探討技術

 

四、總結

通過這篇文章我們看到,介紹了幾種安全防護應用的方法,但是我們在每個方法後面也都介紹了破解者如何應對與這種方法,所以說這裏說的這些安全防護都是可以被破解的,只是時間問題,隨着時間的推移,我們看到沒有絕對的安全,也沒有統一的破解之道,只有一個安全策略出來了,破解之道也就相對應出來的,這樣相生相剋,彼此進步。但是個人感覺破解還是要大於防護的,因爲破解是逆向思維,這種要求會更高點,特別是對於那種變態的加密算法的破解和逆向,尤其蛋疼。最後也希望通過這篇文章能夠讓你們瞭解到Android中的破解不是那麼容易的,安全也不是那麼容易的。兩者都在進步,我們也要進步!

 

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

點擊立即購買:京東  天貓  

更多內容:點擊這裏

關注微信公衆號,最新技術乾貨實時推送

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章