1. 程式人生 > >Android逆向之旅---Hook神器家族的Frida工具使用詳解

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

常見 fin () 文件的 數值 isp extern dex文件 所有

一、前言

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

二、環境安裝配置

因為網上的確有介紹了,而且官網也有文檔說明:https://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類的構造方法

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

技術分享圖片

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

技術分享圖片

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

技術分享圖片

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

技術分享圖片

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

第二、hook類的普通方法

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

技術分享圖片

這個就是把構造方法的固定寫法$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所以直接用系統崩潰日誌就可以查看了。這種方式最簡單粗暴了。對於跟蹤代碼非常有用的。

static struct : public v8::String::ExternalOneByteStringResource {
    // 重寫父類函數
    // 強制進行const unsigned char[] =>www.bomaoyule.cn/ const char*的類型轉換
    const char* data() const override {
        return reinterpret_cast<const char*>(raw_internal_bootstrap_loaders_value);
    }
    // 數組長度
    size_t length() const override { return arraysize(raw_internal_bootstrap_loaders_value); }
    // 默認delete函數
    void Dispose() override {www.huayi1.cn /*

www.huazongyule.net/
www.jyz521.com

 Default calls `delete this`. */ }
    // const char* => Local<String>的類型轉換
    v8::Local<v8::String> ToStringChecked(v8::Isolate* isolate) {
        return v8::String::NewExternalOneByte(isolate,www.078881.cn this).ToLocalChecked();
    }
} internal_bootstrap_loaders_value;

到這裏我們就把所有可能遇到的情形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文件即可:

技術分享圖片

相對地址直接用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類即可。

第二、hook導出函數功能

這部分內容很簡單了,比上面的簡單是因為不需要手動的計算函數地址,因為是導出的,所以直接可以得到導出的函數名即可,因為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,SubstrateCydia這三個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代碼實現插件功能。

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