技術分享 | Frida 實現 Hook 功能的強大能力

語言: CN / TW / HK

技術分享 | Frida 實現 Hook 功能的強大能力

更多技術文章

Frida 通過 C 語言將 QuickJS 注入到目標程序中,獲取完整的記憶體操作許可權,達到在程式執行時實時地插入額外程式碼和資料的目的。官方將呼叫程式碼封裝為 python 庫,當然你也可以直接通過其他的語言呼叫 Frida 中的 C 語言程式碼進行操作。

Frida安裝和啟動

電腦端 Frida 安裝

pip install frida-tools

  • 如果在安裝中卡住,需要在 Frida 的 pypi 頁面下載對應系統的 egg 檔案,對應頁面地址為:https://pypi.org/project/frida/#files ,並將該檔案放置到個人資料夾路徑下,例如 C:\Users\當前使用者名稱,再重新使用命令安裝。

  • 安裝完畢後可以通過命令frida --version來檢視安裝的版本,確認是否安裝成功。

手機端 Frida-server 安裝

  • 本次示例使用 Android App 作為目標程式,所以需要電腦端安裝 SDK 環境,以便能夠連線手機進行除錯操作,還需在手機端準備一個 Frida-server,下載地址為:https://github.com/frida/frida/releases,下載匹配手機 CPU 架構和本地 Frida 版本的包。

  • 下載之後解壓檔案,使用adb push命令將檔案推送到手機端,建議放置在/data/local/tmp資料夾中,並修改該檔案的許可權為 755,以便之後進行啟動。

確認環境執行正常

  • 通過 Frida 提供的一些小工具,對 Frida 的安裝執行環境做簡單的確認。

  • 首先準備一個 Android 模擬器或者真機,將上一步中提到的 Frida-server 推送到手機端中,在本示例中將放置在手機的/data/local/tmp資料夾內,並將檔案命名為frida-server。

  • 通過adb shell命令連線手機,執行/data/local/tmp/frida-server &,將 Frida-server 放在系統後臺自動執行。

  • 在本地電腦終端中執行frida-ps -U,結果如下展示手機中的程序資訊,說明環境已經準備完畢。

``` PID Name


1313 adbd 12621 android.process.acore 18037 android.process.media 14455 com.android.defcontainer 11656 com.android.deskclock ```

示例

目標應用介紹

  • 因為 Hook 需要通過分析原始碼中的邏輯來實現,所以先展示一下目標應用的原始碼部分,方便分析其中的邏輯,找到 Hook 時要修改的方法和變數。

  • 程式碼是簡單的猜黑白遊戲,通過按下黑或白按鈕,與電腦結果進行對比,結果相同加 1 分,結果不同分數清零,當滿 100 分時打出勝利提示語。具體程式碼如下:

``` package com.example.target_frida;

import androidx.appcompat.app.AppCompatActivity;

import android.annotation.SuppressLint; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView;

public class MainActivity extends AppCompatActivity implements View.OnClickListener { TextView winCountView; TextView battleInfoTextView; int winCount = 0;

@SuppressLint("SetTextI18n")
@Override
public void onClick(View view) {
    if (winCount > 100) {
        return;
    }
    if (view.getId() == R.id.tvButtonBlack) {
        if (!getCPUResult()) {
            winCount++;
            winCountView.setText(winCount + "");
            battleInfoTextView.setText("Right!");
        } else {
            winCount = 0;
            winCountView.setText(winCount + "");
            battleInfoTextView.setText("Wrong! Clean All!");
        }
    } else if (view.getId() == R.id.tvButtonWhite) {
        if (getCPUResult()) {
            winCount++;
            winCountView.setText(winCount + "");
            battleInfoTextView.setText("Right!");
        } else {
            winCount = 0;
            winCountView.setText(winCount + "");
            battleInfoTextView.setText("Wrong! Clean All!");
        }
    }
    if (winCount >= 100) {
        battleInfoTextView.setText("Win 100 times!!!");
    }
}

@SuppressLint("SetTextI18n")
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    winCountView = findViewById(R.id.winCount);
    winCountView.setText(winCount + "");
    battleInfoTextView = findViewById(R.id.battleInfo);
    battleInfoTextView.setText("猜黑白!!!");
    Button buttonBlack = (Button) findViewById(R.id.tvButtonBlack);
    Button buttonWhite = (Button) findViewById(R.id.tvButtonWhite);
    buttonBlack.setOnClickListener(this);
    buttonWhite.setOnClickListener(this);
}

public boolean getCPUResult() {
    //true為白,false為黑
    return Math.random() > 0.5;
}

}

```

Hook 需求分析

  • 由於正常情況下,連贏 100 次的概率幾乎為零,如果想要達到勝利條件,Hook 就是一個比較好的方式。

  • 首先分析一下原始碼,想要達到連贏 100 次的情況,可以有兩種解決辦法:一種是通過修改用來記錄連贏次數的變數winCount,將連贏記錄改成 99,這樣只需要再贏一次就可以獲得勝利;還有一種是通過修改getCPUResult方法的返回值,讓其固定返回一種可能性,這樣只需要選擇對應的顏色就可以連續獲勝。接下來通過實現第一個方案,看看使用 Frida 如何達到想要的效果。

第一種實現:修改結果變數中儲存的值

  • 首先展示修改程式碼,然後再進行逐步講解:

``` import time import frida, sys

date_str = time.strftime('%m-%d %H:%M:%S')

def on_message(message, data): if message['type'] == 'send': print(f"[{date_str}] {message['payload']}") else: print(f"[{date_str}] {message}")

def run_all(): # Java.perform方法:當 js 附加到目標的程序中時被執行,執行其中定義的函式 # Java.choose方法:通過完整類名,獲取它的例項,從而對例項中的資料進行修改 # 通過 key:value 結構定義了兩個函式: # onMatch 對應的函式在命中一個例項的時候被呼叫,傳入函式中的引數 instance 就是被命中的例項 # onComplete 函式會在所有例項遍歷完畢之後被呼叫,可以做一些後續處理操作 jscode = """ Java.perform(function () { Java.choose("com.example.target_frida.MainActivity",{ onMatch:function(instance){ console.log("winCount value is "+instance.winCount.value); instance.winCount.value=99; console.log("winCount value is "+instance.winCount.value); }, onComplete:function(){ console.log("Complete!!!") } }); }); """ # attach目標App程序 target_app = 'com.example.target_frida' process = frida.get_usb_device().attach(target_app) # 將JS程式碼注入程序,並附加監聽方法,用來獲取返回的日誌資訊 script = process.create_script(jscode) script.on('message', on_message) # 列印起始日誌 print(f'[{date_str}] Start Frida on {target_app}') # 載入注入的JS程式碼邏輯 script.load() # 使用系統輸入語句阻止函式執行完畢自動退出 sys.stdin.read()

if name == 'main': run_all() ```

  • 程式碼中的 python 語句已經添加了註釋,Hook 的核心邏輯,JS 語句作為字串儲存在 jscode 變數中。

  • 梳理一下整個 JS 語句的流程:通過Java.choose函式獲取com.example.target_frida.MainActivity類的例項。在獲取到例項時,首先使用console.log語句將當前例項中的 winCount 變數值(使用 winCount.value)列印到日誌中,之後直接通過賦值語句把變數值改為 99,再次輸出日誌確認修改無誤,修改邏輯就完成了。

  • 在手機端啟動 Frida-server 和被測 App,電腦端執行指令碼,可以看到在命令列中輸出如下內容:

[04-15 18:19:57] Start Frida on com.example.target_frida winCount value is 0 winCount value is 99 Complete!!!

這時在 App 中選擇一個顏色點選,只要選中正確的顏色,就可以成功達到預期的 100 次連勝目標,如果沒能選中正確的顏色導致清零,可以再重複執行指令碼修改一次數值,在這種情況下要達到預期的場景就很容易。

總結

第二個方案以及其他更多的可能性,就留給讀者自行探索,在這裡送上 Frida 官方 JavaScript API 連結:https://frida.re/docs/javascript-api/ ,可以通過這個連結找到你所需要的 JS 函式。

通過示例可以看到 Frida 實現 Hook 功能的強大能力,它可以定位到類的例項,並且對例項中的資料進行直接的修改,達到場景構建的目的。

更多技術文章