frida的用法--Hook Java代碼篇


frida是一款方便並且易用的跨平台Hook工具,使用它不僅可以Hook Java寫的應用程序,而且還可以Hook原生的應用程序。

1. 准備

frida分客戶端環境和服務端環境。在客戶端我們可以編寫Python代碼,用於連接遠程設備,提交要注入的代碼到遠程,接受服務端的發來的消息等。在服務端,我們需要用Javascript代碼注入到目標進程,操作內存數據,給客戶端發送消息等操作。我們也可以把客戶端理解成控制端,服務端理解成被控端。
假如我們要用PC來對Android設備上的某個進程進行操作,那么PC就是客戶端,而Android設備就是服務端。

1.1 准備frida服務端環境

本文,服務端在Android平台測試。服務端環境准備步驟如下:

  1. 根據自己的平台下載frida服務端並解壓
    https://github.com/frida/frida/releases
    frida_server

  2. 執行以下命令將服務端推到手機的/data/local/tmp目錄
    adb push frida-server /data/local/tmp/frida-server

  3. 執行以下命令修改frida-server文件權限
    adb shell chmod 777 /data/local/tmp/frida-server

注:Windows系統執行命令可以在CMD中進行;Linux和MacOS執行命令可以在終端中進行。adb是Android一個調試工具,具體安裝方法不是本文的重點。

1.2 准備客戶端環境

在PC上安裝Python的運行環境,安裝完成后執行下面的命令安裝frida

pip install frida-tools

1.3 客戶端命令參數

下面是frida客戶端命令行的參數幫助

Usage: frida [options] target

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -D ID, --device=ID    connect to device with the given ID
  -U, --usb             connect to USB device
  -R, --remote          connect to remote frida-server
  -H HOST, --host=HOST  connect to remote frida-server on HOST
  -f FILE, --file=FILE  spawn FILE
  -n NAME, --attach-name=NAME
                        attach to NAME
  -p PID, --attach-pid=PID
                        attach to PID
  --debug               enable the Node.js compatible script debugger
  --enable-jit          enable JIT
  -l SCRIPT, --load=SCRIPT
                        load SCRIPT
  -c CODESHARE_URI, --codeshare=CODESHARE_URI
                        load CODESHARE_URI
  -e CODE, --eval=CODE  evaluate CODE
  -q                    quiet mode (no prompt) and quit after -l and -e
  --no-pause            automatically start main thread after startup
  -o LOGFILE, --output=LOGFILE
                        output to log file

1.3.1 將一個腳本注入到Android目標進程

frida -U -l myhook.js com.xxx.xxxx

參數解釋:

  • -U 指定對USB設備操作
  • -l 指定加載一個Javascript腳本
  • 最后指定一個進程名,如果想指定進程pid,用-p選項。正在運行的進程可以用frida-ps -U命令查看

1.3.2 重啟一個Android進程並注入腳本

frida -U -l myhook.js -f com.xxx.xxxx --no-pause

參數解釋:

  • -f 指定一個進程,重啟它並注入腳本
  • --no-pause 自動運行程序

這種注入腳本的方法,常用於hook在App就啟動期就執行的函數。

frida運行過程中,執行%resume重新注入,執行%reload來重新加載腳本;執行exit結束腳本注入

2. Hook Java方法

2.1 載入類

Java.use方法用於加載一個Java類,相當於Java中的Class.forName()。比如要加載一個String類:

var StringClass = Java.use("java.lang.String");

加載內部類:

var MyClass_InnerClass = Java.use("com.luoyesiqiu.MyClass$InnerClass");

其中InnerClass是MyClass的內部類

2.2 修改函數的實現

修改一個函數的實現是逆向調試中相當有用的。修改一個函數的實現后,如果這個函數被調用,我們的Javascript代碼里的函數實現也會被調用。

2.2.1 函數參數類型表示

不同的參數類型都有自己的表示方法

  1. 對於基本類型,直接用它在Java中的表示方法就可以了,不用改變,例如:
  • int
  • short
  • char
  • byte
  • boolean
  • float
  • double
  • long
  1. 基本類型數組,用左中括號接上基本類型的縮寫

基本類型縮寫表示表:

基本類型 縮寫
boolean Z
byte B
char C
double D
float F
int I
long J
short S

例如:int[]類型,在重載時要寫成[I

  1. 任意類,直接寫完整類名即可

例如:java.lang.String

  1. 對象數組,用左中括號接上完整類名再接上分號

例如:[java.lang.String;

2.2.2 帶參數的構造函數

修改參數為byte[]類型的構造函數的實現

ClassName.$init.overload('[B').implementation=function(param){
    //do something
}

注:ClassName是使用Java.use定義的類;param是可以在函數體中訪問的參數

修改多參數的構造函數的實現

ClassName.$init.overload('[B','int','int').implementation=function(param1,param2,param3){
    //do something
}

2.2.3 無參數構造函數

ClassName.$init.overload().implementation=function(){
    //do something
}

調用原構造函數

ClassName.$init.overload().implementation=function(){
    //do something
    this.$init();
    //do something
}

注意:當構造函數(函數)有多種重載形式,比如一個類中有兩個形式的func:void func()void func(int),要加上overload來對函數進行重載,否則可以省略overload

2.2.4 一般函數

修改函數名為func,參數為byte[]類型的函數的實現

ClassName.func.overload('[B').implementation=function(param){
    //do something
    //return ...
}

2.2.5 無參數的函數

ClassName.func.overload().implementation=function(){
    //do something
}

注: 在修改函數實現時,如果原函數有返回值,那么我們在實現時也要返回合適的值

ClassName.func.overload().implementation=function(){
    //do something
    return this.func();
}

3. 調用函數

和Java一樣,創建類實例就是調用構造函數,而在這里用$new表示一個構造函數。

var ClassName=Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();

實例化以后調用其他函數

var ClassName=Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();
instance.func();

4. 字段操作

字段賦值和讀取要在字段名后加.value,假設有這樣的一個類:

package com.luoyesiqiu.app;
public class Person{
    private String name;
    private int age;
}

寫個腳本操作Person類的name字段和age字段:

var person_class = Java.use("com.luoyesiqiu.app.Person");
//實例化Person類
var person_class_instance = person_class.$new();
//給name字段賦值
person_class_instance.name.value = "luoyesiqiu";
//給age字段賦值
person_class_instance.age.value = 18;
//輸出name字段和age字段的值
console.log("name = ",person_class_instance.name.value, "," ,"age = " ,person_class_instance.age.value);

輸出:

name =  luoyesiqiu , age =  18

5. 類型轉換

Java.cast方法來對一個對象進行類型轉換,如將variable轉換成java.lang.String

var StringClass=Java.use("java.lang.String");
var NewTypeClass=Java.cast(variable,StringClass);

6. Java.available字段

這個字段標記Java虛擬機(例如: Dalvik 或者 ART)是否已加載, 操作Java任何東西之前,要確認這個值是否為true

7. Java.perform方法

Java.perform(fn)在Javascript代碼成功被附加到目標進程時調用,我們核心的代碼要在里面寫。格式:

Java.perform(function(){
//do something...
});

8. 實例講解

有了以上的基礎知識,我們就可以進行編寫代碼了

8.1 修改返回值

8.1.1 場景

假設有以下的程序,給isExcellent方法傳入兩個值,通過計算,返回一個布爾值,表示是否優秀。默認情況下,它是只會顯示是否優秀:false的,因為我們默認傳入的數很小:

exp1_before

public class MainActivity extends AppCompatActivity {
    private  String TAG="Crackme";
    private TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView =findViewById(R.id.tv);
        textView.setText("是否優秀:"+isExcellent(46,54));
    }

    private  boolean isExcellent(int chinese, int math){
        if( chinese + math >=180){
            return true;
        }
        else{
            return false;
        }
    }

}

我們編寫一個腳本來Hook isExcellent函數,使它返回true,顯示為是否優秀:true

對於這種簡單的場景,直接修改返回值就可以了,因為只有結果是重要的。

8.1.2 代碼

想直接返回結果很簡單,直接在匿名方法里return即可。

if(Java.available){
    Java.perform(function(){
        var MainActivity = Java.use("com.luoyesiqiu.crackme.MainActivity");
        MainActivity.isExcellent.implementation=function(){
            return true;        
        }
    });

}
  • 將上面的代碼保存為:exp1.js

  • 執行adb shell 'su -c /data/local/tmp/frida-server'啟動服務端

  • 運行目標App

  • 執行frida -U -l exp1.js com.luoyesiqiu.crackme注入代碼

  • 按返回鍵返回桌面,再重新打開App,發現達到預期

  • 在命令行輸入exit,回車,停止注入代碼

exp1_after

注:這里為什么要打開兩次App?第一打開是為了讓frida能夠找到進程,第二次打開是為了驗證結果,即使Hook成功了,界面是有緩存的,並不能實時顯示Hook結果,所以需要重新打開App

8.2 修改參數

8.2.1 場景

假設有以下場景,isExcellent除了返回是否優秀以外,方法的內部還把分數打印出來。

exp2_before

public class MainActivity extends AppCompatActivity {
    private  String TAG="Crackme";
    private TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView =findViewById(R.id.tv);
        textView.append("是否優秀:"+isExcellent(46,54)+"\n");
    }

    private  boolean isExcellent(int chinese, int math){
        textView.append("語文+數學總分:"+(chinese+math)+"\n");
        if( chinese + math >=180){
            return true;
        }
        else{
            return false;
        }
    }
}

這種情況下我們不可能只返回是否優秀吧,顯示的總分很低,但是卻返回優秀,是很尷尬的...所以我們要修改isExcellent方法的參數,使其通過計算打印和返回合理的值。

8.2.2 代碼

if(Java.available){
    Java.perform(function(){
        var MainActivity = Java.use("com.luoyesiqiu.crackme.MainActivity");
        MainActivity.isExcellent.overload("int","int").implementation=function(chinese,math){
            return this.isExcellent(95,96);      
        }
    });

}

上面的代碼,通過overload方法重載參數,修改isExcellent方法實現,並在實現函數里調用原來的方法,得到新的返回值

  • 將上面的代碼保存為:exp2.js

  • 執行adb shell 'su -c /data/local/tmp/frida-server'啟動服務端(如果上面啟動的服務端還開着可省略這一步)

  • 運行目標App

  • 執行frida -U -l exp2.js com.luoyesiqiu.crackme注入代碼

  • 按返回鍵,再重新打開App,發現達到預期

  • 在命令行輸入exit,回車,停止注入代碼

exp2_after

9. 配合Python腳本注入

在本文剛開始的時候說到,我們可以編寫Python代碼來配合Javascript代碼注入。下面我們來看看,怎么使用,先看一段代碼:

# -*- coding: UTF-8 -*-

import frida, sys

jscode = """
if(Java.available){
    Java.perform(function(){
        var MainActivity = Java.use("com.luoyesiqiu.crackme.MainActivity");
        MainActivity.isExcellent.overload("int","int").implementation=function(chinese,math){
            console.log("[javascript] isExcellent be called.");
            send("isExcellent be called.");
            return this.isExcellent(95,96);      
        }
    });

}
"""

def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)
pass

# 查找USB設備並附加到目標進程
session = frida.get_usb_device().attach('com.luoyesiqiu.crackme')
# 在目標進程里創建腳本
script = session.create_script(jscode)
# 注冊消息回調
script.on('message', on_message)
print('[*] Start attach')
# 加載創建好的javascript腳本
script.load()
# 讀取系統輸入
sys.stdin.read()
  • 將上面的代碼,保存為exp3.py

  • 執行adb shell 'su -c /data/local/tmp/frida-server'啟動服務端(如果上面啟動的服務端還開着可省略這一步)

  • 運行目標App

  • 執行python exp3.py注入代碼

  • 按返回鍵,再重新打開App,發現達到預期

  • Ctrl+C停止腳本和停止注入代碼

上面是一段Python代碼,我們來分析它的步驟:

  1. 通過調用frida.get_usb_device()方法來得到一個連接中的USB設備(Device類)實例
  2. 調用Device類的attach()方法來附加到目標進程並得到一個會話(Session類)實例,該方法有一個參數,參數是需要注入的進程名或者進程pid。如果需要Hook的代碼在App的啟動期執行,那么在調用attach方法前需要先調用Device類的spawn()方法,這個方法也有一個參數,參數是進程名,該方法調用后會重啟對應的進程,並返回新的進程pid。得到新的進程pid后,我們可以將這個進程pid傳遞給attach()方法來實現附加。
  3. 接着調用Session類的create_script()方法創建一個腳本,傳入需要注入的javascript代碼並得到Script類實例
  4. 調用Script類的on()方法添加一個消息回調,第一個參數是信號名,乖乖傳入message就行,第二個是回調函數
  5. 最后調用Script類的load()方法來加載剛才創建的腳本。

注:如果想在javascript輸出日志,可以調用console.log()方法。如果想給客戶端發送消息,可以在javascript代碼里調用send()方法,並在客戶端Python代碼里注冊一個消息回調來接收服務端發來的消息。

可以看到,結合python代碼,使注入更加的靈活了。如果想看Python端frida模塊的代碼,可以訪問:https://github.com/frida/frida-python/blob/master/frida/core.py

10. 參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM