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

yangxi_001發表於2016-12-02

一、前言

在前兩篇破解的文章中,我們介紹瞭如何使用動態除錯來破解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加固的原理了:Android中apk加固原理解析   

破解之道:

但是可惜的是,這種方式也淪陷了,因為我們知道,不管你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檔案,從而做到安全保護,關於Android中so加密可以參考這篇文章:Android中對so加固原理解析  

破解之道:

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

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


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

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

3、加固資原始檔和AndroidManifest.xml檔案

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


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

破解之道:

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


專案下載:http://download.csdn.net/detail/jiangwei0910410003/9534543


三、安全工作流程

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

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

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

4、為了應對與高階破解小白,我們會增加應用的反除錯功能,來防止應用被動態除錯和程式注入問題,增加破解難度

5、為了應對與資深破解小白,我們會採用應用的加固策略,對dex,so,資原始檔進行加固,增加反編譯工作了和除錯難度


四、總結

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

相關文章