Android逆向之旅---Hook神器家族的Frida工具使用詳解

編碼美麗發表於2018-07-02
一、前言

在逆向過程中有一個Hook神器是必不可少的工具,之前已經介紹了Xposed和Substrate了,不瞭解的同學可以看這兩篇文章:Android中Hook神器Xposed工具介紹 和 Android中Hook神器SubstrateCydia工具介紹 這兩篇文章非常重要一個是Hook Java層的時候最常用的Xposed和Hook Native層的SubstrateCydia,可以看我之前的文章比如寫微信外掛等都採用了Xposed工具,因為個人覺得Xposed用起來比較爽,寫程式碼比較方便。而對於SubstrateCydia工具可以Hook Native層的,本文會介紹一下如何使用。那麼有了這兩個神器為啥還要介紹Frida工具呢?而且這個工具網上已經有介紹了,為什麼還有介紹了,因為這個Frida工具對於逆向者操作破解來說非常方便,所謂方便是指他的安裝環境和配置要求都非常簡單相容性也非常好,因為最近在弄一個協議解密,無奈手機上安裝Cydia之後不相容導致當機所以就轉向用了這個工具實現了hook,所以覺得這個工具非常好用就單獨介紹一下。


一、環境安裝配置

因為網上的確有介紹了,而且官網也有文件說明:www.frida.re/docs/javascript-api,但是內容都是片段化就是東一處西一處,沒有歸納性的總結,而且很多常用的功能都沒介紹,所以本文就把常用的hook工具詳細介紹一下,主要從以下幾個方面來介紹:

第一、修改Java層的函式引數和返回值

第二、列印Java層的方法堆疊資訊

第三、攔截native層的函式引數和返回值

對於Java層會注重介紹,因為我們用過Xposed工具之後都知道,比如引數是自定義型別怎麼Hook等。不多說了直接用一個案例作為樣本進行操作,為了能夠覆蓋所有的操作可能性案例需要寫的複雜點:

640?wx_fmt=png

引數和返回值有基本型別,也有自定義型別,接下來我們就開始我們的Frida之旅吧。


這個網上都已經有教程了,因為Frida大致原理是手機端安裝一個server程式,然後把手機端的埠轉到PC端,PC端寫python指令碼進行通訊,而python指令碼中需要hook的程式碼採用javascript語言。所以這麼看來我們首先需要安裝PC端的python環境,這個沒難度直接安裝python即可,然後開始安裝frida了,直接執行命令:pip install frida 

640?wx_fmt=png

前提是你需要配置好python環境變數,不然提示pip命令找不到。安裝完成之後,我們再去官網下載對應版本的手機端程式frida-server:

https://github.com/frida/frida/releases 


注意這裡一定要把frida-server版本和上面PC端安裝的frida版本一致,不然執行報錯的。其實這裡看到真的實現hook功能的是手機端的frida-server,這個也是開源的大家可以研究他的原理。我們也看到這個工具和IDA是不是很類似,也是把手機端的埠轉發到PC端進行通訊而已。有了frida-server之後就好辦了,直接push到手機目錄下,然後修改一下檔案的屬性即可:

adb push /data/local/tmp frida-server

root# chmod 777 /data/local/tmp/frida-server

然後直接執行這個程式:

/data/local/tmp# ./frida-server

640?wx_fmt=png

然後把埠轉發到PC端:

adb forward tcp:27042 tcp:27042

adb forward tcp:27043 tcp:27043

640?wx_fmt=png

到這裡就把通訊的手機端工作做完了,是不是感覺和Xposed相比非常方便,相容性非常好,不需要安裝Xposed等工具考慮系統手機等適配問題了。接下來就開始在PC端開始編寫hook程式進行操作了:

640?wx_fmt=png

這裡程式碼也非常簡單,因為安裝好了frida模組,直接匯入模組,然後呼叫api獲取裝置的session然後hook程式包名,接著就可以執行js指令碼程式碼進行hook操作,然後列印訊息:

640?wx_fmt=png

這裡用了python的print函式列印,其實如果想要列印可以在上面的js指令碼中使用console.log也是可以的,看自己的習慣了。所以這裡我們看到指令碼的大致流程就是最外面用python引用frida庫進行和裝置通訊,然後編寫js指令碼執行hook操作。所以這裡最主要的還是js指令碼也就是需要理解js語法了。不過這個沒啥難度的。好了以上的準備條件都弄完了,下面就開始分部拆解操作看看如何涵蓋我們平常使用的hook案例。


三、Java層Hook操作案例分析

第一個案例:hook類的構造方法

我們有時候想hook一個類的構造方法,在Xposed中直接用findConstructor方法就可以了,因為構造方法可能會有多種過載形式,所以需要用引數作為區分,這裡我們hook我們案例的CoinMoney類的構造方法:

640?wx_fmt=png

首先指令碼中使用Java.use方法通過類名獲取類型別,然後構造方法是固定寫法:$init;這個要記住,然後因為需要過載所以用overload(......)形式即可,引數和引數之間用逗號隔開即可。後面就是攔截之後的操作了,這裡方法引數可以自定義變數名,因為js是弱語言,不對型別做強檢查,當然這裡還有其他獲取引數的方法後面會介紹。這裡CoinMoney類的構造方法:

640?wx_fmt=png

然後我們這裡使用send來傳送列印訊息即可,當然也可以用console.log形式列印日誌,程式碼編寫完了,下面就開始執行看效果,執行也很簡單,直接python frida.py:

640?wx_fmt=png

在這之前一定要先開啟hook的應用,不然會報錯提示找不到這個程式程式:

640?wx_fmt=png

這時候在執行看到了就成功了,我們把構造方法的引數列印出來了,那麼這裡hook就成功了。所以可以看到這個操作是不是比Xposed工具更方便呢。但是他也有弊端後面會總結的。


第二、hook類的普通方法

這裡的普通方法包括了靜態方法,私有方法和公開方法等,這個操作和上面的構造方法其實很類似,程式碼如下:

640?wx_fmt=png

這個就是把構造方法的固定寫法$init改成了需要hook的方法名即可。如果方法有過載形式還是用overload進行區分即可,比如這裡我們hook了Uitls.getPwd(String pwd)方法:

640?wx_fmt=png

然後這裡我們看到可以用一個隱含的變數arguments獲取引數,這個是儲存了方法的引數資訊是系統自帶的。所以我們有兩種方式獲取方法的引數資訊。執行看一下效果:

640?wx_fmt=png

看到列印訊息hook成功了。所以這裡就把hook方法獲取引數的案例都介紹完了,總結一下很簡單,構造方法使用固定寫法$init,其他方法全部用方法名即可。如果方法有過載形式需要用overload形式操作引數用逗號分隔。獲取引數可以自定義引數名或者用系統隱含的arguments變數獲取。當然在這之前都需要用Java.use通過類名獲取型別。


第三、修改方法的引數和返回值

我們在使用Xposed進行hook的時候最常用的可能就是修改引數和返回值來實現外掛和外掛功能了,在Frida中其實也可以做到但是和Xposed不一樣,我們從上面的程式碼可以看到,沒有像Xposed的before方法和after方法,而Frida直接是你可以在function中呼叫原來的方法這樣來進行引數修改,比如這裡我要修改上面的方法引數和返回值:

640?wx_fmt=png

因為Frida中沒有before和after方法,但是可以直接呼叫原來的方法其實Xposed中也可以可以直接呼叫原來的方法的,但是不怎麼常用,只要可以呼叫原來的方法,那麼引數和返回值就可以隨意修改了,這裡我們把引數改成jiangwei212,返回值後面追加yyyy了,看列印的日誌:

640?wx_fmt=png

其實這麼做會比before和after形式更為方便,而且可以在原始方法呼叫前做一些事情和後面做一些事情。


第四、構造和修改自定義型別物件和屬性

我們在Xposed寫外掛的時候也會遇到這種比較常見的問題,就是方法的引數不是基本型別是自定義型別,然後也想修改他的屬性值或者呼叫他的一個方法我們會使用反射來進行操作,而在返回值的時候,想構造一個自定義型別的物件也是直接用反射例項化一個物件進行操作的。其實在這裡因為js中也是支援反射操作的,所以就很簡單了:

640?wx_fmt=png

這裡構造一個物件其實很簡單直接固定寫法$new即可,然後有了物件也可以直接呼叫其對應的方法即可,然後就是如何修改一個物件型別的欄位值呢?這個就要用反射了:

640?wx_fmt=png

這裡我們攔截了getCoinMoney方法,引數是CoinMoney型別,我們想修改他的money欄位值,這時候我們直接呼叫他的方法沒什麼問題,但是如果直接呼叫欄位值或者修改就會出現失敗了,所以只能通過反射去修改欄位值,不過要先獲取這個物件對應的class型別,用Java.cast介面就可以,然後獲取反射欄位直接修改即可,這裡要注意不管欄位是private還是public的寫法都是一樣的,都是這段程式碼大家要注意把這段程式碼記住即可。我們看看hook之後的結果:

640?wx_fmt=png

如果沒有用反射去操作直接獲取欄位值列印就是object了。


第五、列印方法的堆疊資訊

我們在破解過程中有時候通過丟擲異常來列印堆疊資訊跟蹤程式碼效率會更高,Xposed中操作很方便直接Java程式碼用Log.xxx方法列印堆疊資訊即可,但是在Frida中有點麻煩了,因為他是js程式碼不好操作,第一次想到的辦法就是自己寫一個列印堆疊資訊的類然後弄成一個dex之後,把這個dex注入到程式中,因為Frida支援把一個dex檔案注入到原始程式中執行的,注入之後在需要列印堆疊資訊的方法中呼叫這個dex中的那個方法就可以了。具體怎麼注入本文不多介紹了。當時覺得這種方案太麻煩了,那麼還有其他方案嗎?其實還是有的,因為我們既然可以構造一個物件那麼為什麼不直接構造一個Exception物件呢?其實操作很簡單,首先我們用Java.use方法獲取型別變數:

var Exception = Java.use("java.lang.Exception");

然後是js中支援throw語法的,直接在需要列印堆疊資訊的方法中呼叫即可:

640?wx_fmt=png

不過這個是真的丟擲異常了沒有捕獲住,所以程式崩潰,我們在開發Android應用的時候如果程式崩潰了最快的檢視異常資訊的方法就是用日誌過濾方式:adb logcat -s AndroidRuntime

640?wx_fmt=png

這樣我們就把堆疊資訊列印出來了,其實這裡可以看到這個是真的一個崩潰異常了,因為沒有catch所以直接用系統崩潰日誌就可以檢視了。這種方式最簡單粗暴了。對於跟蹤程式碼非常有用的。


到這裡我們就把所有可能遇到的情形Java層hook操作都介紹完了,主要包括以下幾種常見情形:

第一、Hook類的構造方法和普通方法,注意構造方法是固定寫法$init即可,獲取引數可以通過自定義引數名也可以直接用系統隱含的arguments變數獲取即可。

第二、修改方法的引數和返回值,直接呼叫原始方法傳入需要修改的引數值和直接修改返回值即可。

第三、構造物件使用固定寫法$new即可。

第四、如果需要修改物件的欄位值需要用反射去進行操作。

第五、堆疊資訊列印直接呼叫Java的Exception類即可,通過adb logcat -s AndroidRuntime來過濾日誌資訊檢視崩潰堆疊資訊。

總結:記得用Java.use方法獲取類的型別,如果遇到過載的方法用overload實現即可。


四、Native層Hook操作案例分析

下面繼續來看Frida更強大的地方就是hook native程式碼,說的強大不是因為功能,而是便捷程度,我們之前hook native可能用Cydia比較多,但是都知道Cydia和Xposed一樣都有相容問題,環境安裝配置太麻煩了,而Frida還是隻需要幾行js程式碼即可搞定,這裡hook native還是用兩個案例介紹:一個是hook匯出的函式,一個是hook未匯出的函式,通過獲取引數和修改返回值來演示,這裡不自己寫native程式碼了,直接用之前破解快手的資料請求的so檔案,他有一個函式在底層獲取字串資訊,還有一個是最近正在研究的資訊類app的加密演算法so,我們修改他的函式返回值。


第一、hook未匯出函式功能

未匯出的函式我們需要手動的計算出函式地址,然後將其轉化成一個NativePointer的物件然後進行hook操作,那麼如何計算一個函式地址呢?這個很簡單隻要得到so的記憶體基地址加上函式的相對地址就可以了。基地址獲取直接檢視程式對應的maps檔案即可:

640?wx_fmt=png

相對地址直接用IDA開啟so檔案就可以檢視,比如這裡我們通過靜態分析之後想hook這個sub_5070函式:

640?wx_fmt=png

然後我們F5檢視函式對應的C語言程式碼檢視引數資訊:

640?wx_fmt=png

這裡看到是三個引數,那麼計算了後的實際地址就是0x7816A000+5070=0x7816F070,不過這個地址不是最後的地址,因為thumb和arm指令的區分,地址最後一位的奇偶性來進行標誌,所以這裡還需加1也就是最終的0x7816F071,這一點很重要不管使用Cydia還是Frida都要注意最後計算的絕對地址要+1,不然會報錯的:

640?wx_fmt=png

這裡hook之後有兩個回撥方法一個是進入函式之前,一個是執行完之後,這個和Xposed非常類似了,我們列印引數,不過這個和之前Hook Java層就不一樣了,因為在C中大部分都是和地址指標相關,特別是常見的字串資訊,我們如果要正確的列印字串值就需要藉助Memory系統類來通過指標獲取字串資訊了,這個類是非常重要,在後面修改返回值也是用它寫記憶體值的。我們先看看這個函式原始返回值是什麼:

640?wx_fmt=png

這個是加密之後的值了,然後我們獲取到引數了,而通過IDA分析之後發現這個函式最終的結果不是通過return來返回的,而是通過第三個指標引數返回的,因為C中有一個引數傳值功能,就是直接操作指標就可以傳回結果,這個在C中經常用到,因為一個函式返回值只有一處要是一個函式有多個返回值就沒辦法了,所以可以通過引數指標來傳遞。所以如果想修改函式的最終結果,需要修改引數指標的記憶體段資料,先把那個記憶體段資料獲取到列印出來,這裡因為通過靜態分析知道最終的結果是16個位元組資料,所以這裡不能在用讀取記憶體字串方法了,而是讀取純的位元組資料:

640?wx_fmt=png

然後在把返回值修改了,返回值修改也很簡單,直接重寫那段記憶體值就可以了,比如這裡修改成1111:

640?wx_fmt=png

所以看到了C語言中很多地方都在直接操作記憶體也就是地址,特別需要藉助Memory類,他有很多方法,包括記憶體拷貝等。具體用到的可以去官網查詢:https://www.frida.re/docs/javascript-api/#memory;然後我們看hook結果:

640?wx_fmt=png

我們hook到了他的引數資訊,第一個引數是需要加密的字串資訊我們是通過Memory方法獲取字串的,因為本身這個引數是一個字串指標,第二個引數應該是字串長度,第三個引數是操作結果值的指標,然後看到我們獲取到的結果值就是原始加密的資訊。說明我們獲取成功了,然後再看看我們修改之後的1111值,通過日誌檢視:

640?wx_fmt=png

看到了在Java成通過native訪問得到的簽名資訊已經被修改成了1111了,說明我們成功了。到這裡我們就成功的,在hook native的時候一定要注意函式的絕對地址要計算對,最後一定要記住+1,函式的返回值有可能不是通過return而是引數指標傳遞的,操作記憶體的時候用Memory類即可。


第二、hook匯出函式功能

這部分內容很簡單了,比上面的簡單是因為不需要手動的計算函式地址,因為是匯出的,所以直接可以得到匯出的函式名,因為C語言中沒有過載的形式,而C++中有,所以有時候發現匯出的函式名和正常的函式名前面加上了一串資料作為區分那應該是C++程式碼寫的。有了so檔案和匯出的函式名就不需要構造NativePoniter了:

640?wx_fmt=png

這個看到比上面自己手動找函式地址方便多了吧,列印引數都一樣的程式碼了。這裡通過函式名可以知道就是一個native函式了,那麼他第一個引數肯定是JNIEnv指標,第二個引數是jclass型別,這個是標準的如果是靜態方法第二個引數沒啥用,後面的引數就是真的傳遞到native層的值了,比如這裡Java層的方法:

640?wx_fmt=png

那麼按照上面的說明native層的函式就是4個引數了:

640?wx_fmt=png

的確是這樣的,後面兩個引數才是我們想要的值,我們通過IDA檢視這個函式:

640?wx_fmt=png

然後我們用F5檢視虛擬碼他的返回值:

640?wx_fmt=png

用env指標呼叫了NewStringUTF返回一個jstring物件了,好了到這裡我們先不說返回值修改的問題,先看看hook引數資訊:

640?wx_fmt=png

但是我們看到我們列印的返回值是個空也就是空指標,而如果這裡我們想hook他的返回值怎麼辦呢?如果是一個正常的返回字串資訊,我們可以直接用Memory的方法構造出來Memory.allocUtf8String("XXXXX")一個記憶體字串資訊,然後直接返回一個指標地址即可,但是現在這裡是返回一個jstring物件,其實這個我們通過檢視jni.h檔案可以知道jstring是C++中定義的物件:

640?wx_fmt=png

而基本型別就是基本資料型別:

640?wx_fmt=png

這個修改沒有任何問題的,那麼現在問題是修改非基本型別,比如這裡的如何返回jstring物件呢?這裡我能想到的一個辦法就是通過獲取NewStringUTF函式指標,通過NativeFunction方法獲取函式,然後呼叫

640?wx_fmt=png

這裡看到程式碼邏輯沒什麼問題,現在卻的就是NewStringUTF的函式地址了,這個因為在so中沒法檢視,所以怎麼辦呢?不著急我們在看看JNIEnv的定義:

640?wx_fmt=png

他是一個結構體,再看看那個函式地址:

640?wx_fmt=png

我們已經有了JNIEnv結構體指標了,每個函式指標都是int型別也就是四個位元組,所以從JNIEnv指標開始依次計算就可以得到NewStringUTF函式對應的地址了。不過都說了找不到方法的時候就去官網找,JNIEnv變數其實有對應的方法,這裡構造jstring方法其實很簡單:

640?wx_fmt=png

這個比找函式指正方便多了,其實env有很多方法在這裡都有對應的api。


所以到這裡我們發現了Frida在Hook底層函式返回jni中的型別的時候有點麻煩了,但是Cydia就不會了,因為他是Android工程,可以引用jni.h標頭檔案的,比如我們用Cydia來修改這個函式的返回值:

640?wx_fmt=png

看到了吧,這樣就很方便了因為是Android工程,所以可以直接應用jni.h標頭檔案,然後直接呼叫NewStringUTF方法返回了,看看hook的結果:

640?wx_fmt=png

也修改成功了。所以這裡看到Frida也不是萬能的,要看什麼問題怎麼去分析了。


五、技術總結

到這裡我們就把Frida常用的功能和hook常見的用法都說明完了,下面就來總結一下:

第一、Java層程式碼Hook操作

1、hook方法包括構造方法和物件方法,構造方法固定寫法是$init,普通方法直接是方法名,引數可以自己定義也可以使用系統隱含的變數arguments獲取。

2、修改方法的引數和返回值,直接呼叫原始方法通過傳入想要修改的引數來做到修改引數的目的,以及修改返回值即可。

3、構造物件和修改物件的屬性值,直接用反射進行操作,構造物件用固定寫法的$new即可。

4、直接用Java的Exception物件列印堆疊資訊,然後通過adb logcat -s AndroidRuntime來檢視異常資訊跟蹤程式碼。

總結:獲取物件的類型別是Java.use方法,方法有過載的話用overload(.......)解決。


第二、Native層程式碼Hook操作

1、hook匯出的函式直接用so檔名和函式名即可。

2、hook未匯出的函式需要計算出函式在記憶體中的絕對地址,通過檢視maps檔案獲取so的基地址+函式的相對地址即可,最後不要忘了+1操作。

總結:Native中最常用的就是記憶體地址指標了,所以如果要正確的獲取值一定要用Memory類作為輔助,特別是字串資訊。


六、Hook家族神器對比

下面繼續來看Frida,Xposed,Substrate Cydia這三個Hook神器的區別和優缺點:

第一、Xposed的優缺點

優點:在編寫Java層hook外掛的時候非常好用,這一點完全優越於Frida和SubstrateCydia,因為他也是Android專案,可以直接編寫Java程式碼呼叫各類api進行操作。而且可以安裝到手機上直接使用了。

缺點:配置安裝環境繁瑣,相容性差,在Hook底層的時候就很無助了。

第二、Frida的優缺點

優點:在上面我們可以看到他的優點在於配置環境很簡單,操作也很便捷,對於破解者開發階段非常好用。支援Java層和Native層hook操作,在Native層hook如果是非基本型別的話操作有點麻煩。

缺點:因為他只適用於破解者在開發階段操作,也就是他沒法像Xposed用於實踐生產中,比如我寫一個微信外掛用Frida寫肯定不行的,因為他無法在手機端執行。也就是破解者用的比較多。

第三、SubstrateCydia的優缺點

優點:可以執行在手機端,和Xposed類似可以用於實踐生產中。支援Java層和Native層的hook操作,但是Java層hook不怎麼常用,用的比較多的是Native層hook操作,因為他也是Android工程可以引用系統api,操作更為方便。

缺點:和Xposed一樣安裝配置環境繁瑣相容性差。


以上這三個工具可以說是現在用的最多的hook工具了,總結一句話就是寫Java層Hook還是Xposed方便,寫Native層Hook還是Cydia了,而對於破解者開發那還是Frida最靠譜了。但是不管怎麼樣,寫外掛最難的也是最重要的不是寫程式碼而是尋找hook點,也就是逆向分析app找到那個地方,然後寫hook程式碼實現外掛功能。


本文的目的只有一個就是學習逆向分析技巧,如果有人利用本文技術進行非法操作帶來的後果都是操作者自己承擔,和本文以及本文作者沒有任何關係,本文涉及到的程式碼專案可以去編碼美麗小密圈自取,長按下方二維碼加入小密圈一起學習探討技術

640?wx_fmt=png


總結

本文主要介紹了Frida工具,其實原來不想介紹的,因為最近在弄一個app的協議加密,就用這個工具hook了底層函式,發現的確很好用,就整理了最常見用法的案例了,方便日後查閱也給大家提供資料,喜歡的點贊分享。


手機檢視文章不方便,可以網頁看

http://www.520monkey.com


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

 點選檢視圖書詳情

640?wx_fmt=jpeg

長按下面

相關文章