技術分享 | Frida 實現 Hook 功能的強大能力
技術分享 | 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 功能的強大能力,它可以定位到類的例項,並且對例項中的資料進行直接的修改,達到場景構建的目的。