當前位置:
首頁 > 最新 > Android逆向之旅-Hook神器家族的Frida工具使用詳解

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

一、前言

在逆向過程中有一個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等。不多說了直接用一個案例作為樣本進行操作,為了能夠覆蓋所有的操作可能性案例需要寫的複雜點:

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

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

前提是你需要配置好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

然後把埠轉發到PC端:

adb forward tcp:27042 tcp:27042

adb forward tcp:27043 tcp:27043

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

這裡代碼也非常簡單,因為安裝好了frida模塊,直接導入模塊,然後調用api獲取設備的session然後hook程序包名,接著就可以執行js腳本代碼進行hook操作,然後列印消息:

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

三、Java層Hook操作案例分析

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

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


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

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

然後是js中支持throw語法的,直接在需要列印堆棧信息的方法中調用即可:

不過這個是真的拋出異常了沒有捕獲住,所以程序崩潰,我們在開發Android應用的時候如果程序崩潰了最快的查看異常信息的方法就是用日誌過濾方式:adb logcat -s AndroidRuntime

這樣我們就把堆棧信息列印出來了,其實這裡可以看到這個是真的一個崩潰異常了,因為沒有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,我們修改他的函數返回值。


未導出的函數我們需要手動的計算出函數地址,然後將其轉化成一個NativePointer的對象然後進行hook操作,那麼如何計算一個函數地址呢?這個很簡單只要得到so的內存基地址加上函數的相對地址就可以了。基地址獲取直接查看程序對應的maps文件即可:

相對地址直接用IDA打開so文件就可以查看,比如這裡我們通過靜態分析之後想hook這個sub_5070函數:

然後我們F5查看函數對應的C語言代碼查看參數信息:

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

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

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

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

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

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

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


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

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

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

的確是這樣的,後面兩個參數才是我們想要的值,我們通過IDA查看這個函數:

然後我們用F5查看偽代碼他的返回值:

用env指針調用了NewStringUTF返回一個jstring對象了,好了到這裡我們先不說返回值修改的問題,先看看hook參數信息:

但是我們看到我們列印的返回值是個空也就是空指針,而如果這裡我們想hook他的返回值怎麼辦呢?如果是一個正常的返回字元串信息,我們可以直接用Memory的方法構造出來Memory.allocUtf8String("XXXXX")一個內存字元串信息,然後直接返回一個指針地址即可,但是現在這裡是返回一個jstring對象,其實這個我們通過查看jni.h文件可以知道jstring是C++中定義的對象:

而基本類型就是基本數據類型:

這個修改沒有任何問題的,那麼現在問題是修改非基本類型,比如這裡的如何返回jstring對象呢?這裡我能想到的一個辦法就是通過獲取NewStringUTF函數指針,通過NativeFunction方法獲取函數,然後調用

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

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

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

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

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

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

也修改成功了。所以這裡看到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如果是非基本類型的話操作有點麻煩。

第三、SubstrateCydia的優缺點

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

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

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

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

總結

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

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

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

長按下面二維碼,關注編碼美麗


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 編碼美麗 的精彩文章:

TAG:編碼美麗 |