frida是一款方便并且易用的跨平臺Hook工具弱卡,使用它不僅可以Hook Java寫的應(yīng)用程序,而且還可以Hook原生的應(yīng)用程序。
1. 準(zhǔn)備
frida分客戶端環(huán)境和服務(wù)端環(huán)境咕晋。在客戶端我們可以編寫Python代碼雹拄,用于連接遠(yuǎn)程設(shè)備,提交要注入的代碼到遠(yuǎn)程掌呜,接受服務(wù)端的發(fā)來的消息等滓玖。在服務(wù)端,我們需要用Javascript代碼注入到目標(biāo)進(jìn)程质蕉,操作內(nèi)存數(shù)據(jù)势篡,給客戶端發(fā)送消息等操作。我們也可以把客戶端理解成控制端饰剥,服務(wù)端理解成被控端殊霞。
假如我們要用PC來對Android設(shè)備上的某個進(jìn)程進(jìn)行操作,那么PC就是客戶端汰蓉,而Android設(shè)備就是服務(wù)端绷蹲。
1.1 準(zhǔn)備frida服務(wù)端環(huán)境
本文,服務(wù)端在Android平臺測試顾孽。服務(wù)端環(huán)境準(zhǔn)備步驟如下:
-
根據(jù)自己的平臺下載frida服務(wù)端并解壓
https://github.com/frida/frida/releases
執(zhí)行以下命令將服務(wù)端推到手機(jī)的/data/local/tmp目錄
adb push frida-server /data/local/tmp/frida-server
執(zhí)行以下命令修改frida-server文件權(quán)限
adb shell chmod 777 /data/local/tmp/frida-server
注:Windows系統(tǒng)執(zhí)行命令可以在CMD中進(jìn)行祝钢;Linux和MacOS執(zhí)行命令可以在終端中進(jìn)行。adb是Android一個調(diào)試工具若厚,具體安裝方法不是本文的重點(diǎn)拦英。
1.2 準(zhǔn)備客戶端環(huán)境
在PC上安裝Python的運(yùn)行環(huán)境,安裝完成后執(zhí)行下面的命令安裝frida
pip install frida
1.3 客戶端命令參數(shù)
下面是frida客戶端命令行的參數(shù)解釋测秸,看一下就好
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
如果將一個腳本注入到Android目標(biāo)進(jìn)程
frida -U -l myhook.js com.xxx.xxxx
參數(shù)解釋:
- -U 指定對USB設(shè)備操作
- -l 指定加載一個Javascript腳本
- 最后指定一個進(jìn)程名疤估,如果想指定進(jìn)程pid,用
-p
選項。正在運(yùn)行的進(jìn)程可以用frida-ps -U
命令查看
frida運(yùn)行過程中霎冯,執(zhí)行%resume
重新注入铃拇,執(zhí)行%reload
來重新加載腳本;執(zhí)行exit
結(jié)束腳本注入
2. Hook Java方法
2.1 載入類
Java.use方法用于聲明一個Java類沈撞,在用一個Java類之前首先得聲明慷荔。比如聲明一個String類,要指定完整的類名:
var StringClass=Java.use("java.lang.String");
2.2 修改函數(shù)的實現(xiàn)
修改一個函數(shù)的實現(xiàn)是逆向調(diào)試中相當(dāng)有用的缠俺。修改一個函數(shù)的實現(xiàn)后显晶,如果這個函數(shù)被調(diào)用,我們的Javascript代碼里的函數(shù)實現(xiàn)也會被調(diào)用壹士。
2.2.1 函數(shù)參數(shù)類型表示
不同的參數(shù)類型都有自己的表示方法
- 對于基本類型磷雇,直接用它在Java中的表示方法就可以了,不用改變墓卦,例如:
- int
- short
- char
- byte
- boolean
- float
- double
- long
- 基本類型數(shù)組倦春,用左中括號接上基本類型的縮寫
基本類型縮寫表示表:
基本類型 | 縮寫 |
---|---|
boolean | Z |
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
例如:int[]
類型,在重載時要寫成[I
- 任意類落剪,直接寫完整類名即可
例如:java.lang.String
- 對象數(shù)組睁本,用左中括號接上完整類名再接上分號
例如:[java.lang.String;
2.2.2 帶參數(shù)的構(gòu)造函數(shù)
修改參數(shù)為byte[]類型的構(gòu)造函數(shù)的實現(xiàn)
ClassName.$init.overload('[B').implementation=function(param){
//do something
}
注:ClassName是使用Java.use定義的類;param是可以在函數(shù)體中訪問的參數(shù)
修改多參數(shù)的構(gòu)造函數(shù)的實現(xiàn)
ClassName.$init.overload('[B','int','int').implementation=function(param1,param2,param3){
//do something
}
2.2.3 無參數(shù)構(gòu)造函數(shù)
ClassName.$init.overload().implementation=function(){
//do something
}
調(diào)用原構(gòu)造函數(shù)
ClassName.$init.overload().implementation=function(){
//do something
this.$init();
//do something
}
注意:當(dāng)構(gòu)造函數(shù)(函數(shù))有多種重載形式,比如一個類中有兩個形式的func:
void func()
和void func(int)
忠怖,要加上overload來對函數(shù)進(jìn)行重載呢堰,否則可以省略overload
2.2.4 一般函數(shù)
修改函數(shù)名為func,參數(shù)為byte[]類型的函數(shù)的實現(xiàn)
ClassName.func.overload('[B').implementation=function(param){
//do something
//return ...
}
2.2.5 無參數(shù)的函數(shù)
ClassName.func.overload().implementation=function(){
//do something
}
注: 在修改函數(shù)實現(xiàn)時凡泣,如果原函數(shù)有返回值枉疼,那么我們在實現(xiàn)時也要返回合適的值
ClassName.func.overload().implementation=function(){
//do something
return this.func();
}
3. 調(diào)用函數(shù)
和Java一樣,創(chuàng)建類實例就是調(diào)用構(gòu)造函數(shù)鞋拟,而在這里用$new
表示一個構(gòu)造函數(shù)骂维。
var ClassName=Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();
實例化以后調(diào)用其他函數(shù)
var ClassName=Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();
instance.func();
4. 類型轉(zhuǎn)換
用Java.cast
方法來對一個對象進(jìn)行類型轉(zhuǎn)換,如將variable
轉(zhuǎn)換成java.lang.String
:
var StringClass=Java.use("java.lang.String");
var NewTypeClass=Java.cast(variable,StringClass);
5. Java.available字段
這個字段標(biāo)記Java虛擬機(jī)(例如: Dalvik 或者 ART)是否已加載, 操作Java任何東西之前贺纲,要確認(rèn)這個值是否為true
6. Java.perform方法
Java.perform(fn)在Javascript代碼成功被附加到目標(biāo)進(jìn)程時調(diào)用航闺,我們核心的代碼要在里面寫。格式:
Java.perform(function(){
//do something...
});
7. 實例講解
有了以上的基礎(chǔ)知識猴誊,我們就可以進(jìn)行編寫代碼了
7.1 修改返回值
7.1.1 場景
假設(shè)有以下的程序潦刃,給isExcellent方法傳入兩個值,通過計算懈叹,返回一個布爾值乖杠,表示是否優(yōu)秀。默認(rèn)情況下澄成,它是只會顯示是否優(yōu)秀:false
的胧洒,因為我們默認(rèn)傳入的數(shù)很小:
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("是否優(yōu)秀:"+isExcellent(46,54));
}
private boolean isExcellent(int chinese, int math){
if( chinese + math >=180){
return true;
}
else{
return false;
}
}
}
我們編寫一個腳本來Hook isExcellent函數(shù),使它返回true墨状,顯示為是否優(yōu)秀:true
對于這種簡單的場景卫漫,直接修改返回值就可以了,因為只有結(jié)果是重要的歉胶。
7.1.2 代碼
想直接返回結(jié)果很簡單汛兜,直接在匿名方法里return即可。
if(Java.available){
Java.perform(function(){
var MainActivity = Java.use("com.luoyesiqiu.crackme.MainActivity");
MainActivity.isExcellent.implementation=function(){
return true;
}
});
}
將上面的代碼保存為:
exp1.js
執(zhí)行
adb shell 'su -c /data/local/tmp/frida-server'
啟動服務(wù)端運(yùn)行目標(biāo)App
執(zhí)行
frida -U -l exp1.js com.luoyesiqiu.crackme
注入代碼按返回鍵返回桌面通今,再重新打開App,發(fā)現(xiàn)達(dá)到預(yù)期
在命令行輸入
exit
粥谬,回車,停止注入代碼
注:這里為什么要打開兩次App辫塌?第一打開是為了讓frida能夠找到進(jìn)程漏策,第二次打開是為了驗證結(jié)果,即使Hook成功了臼氨,界面是有緩存的掺喻,并不能實時顯示Hook結(jié)果,所以需要重新打開App
7.2 修改參數(shù)
7.2.1 場景
假設(shè)有以下場景,isExcellent除了返回是否優(yōu)秀以外感耙,方法的內(nèi)部還把分?jǐn)?shù)打印出來褂乍。
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("是否優(yōu)秀:"+isExcellent(46,54)+"\n");
}
private boolean isExcellent(int chinese, int math){
textView.append("語文+數(shù)學(xué)總分:"+(chinese+math)+"\n");
if( chinese + math >=180){
return true;
}
else{
return false;
}
}
}
這種情況下我們不可能只返回是否優(yōu)秀吧,顯示的總分很低即硼,但是卻返回優(yōu)秀逃片,是很尷尬的...所以我們要修改isExcellent方法的參數(shù),使其通過計算打印和返回合理的值只酥。
7.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方法重載參數(shù),修改isExcellent方法實現(xiàn)裂允,并在實現(xiàn)函數(shù)里調(diào)用原來的方法损离,得到新的返回值
將上面的代碼保存為:
exp2.js
執(zhí)行
adb shell 'su -c /data/local/tmp/frida-server'
啟動服務(wù)端(如果上面啟動的服務(wù)端還開著可省略這一步)運(yùn)行目標(biāo)App
執(zhí)行
frida -U -l exp2.js com.luoyesiqiu.crackme
注入代碼按返回鍵,再重新打開App,發(fā)現(xiàn)達(dá)到預(yù)期
在命令行輸入
exit
绝编,回車僻澎,停止注入代碼
8. 配合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設(shè)備并附加到目標(biāo)進(jìn)程
session = frida.get_usb_device().attach('com.luoyesiqiu.crackme')
# 在目標(biāo)進(jìn)程里創(chuàng)建腳本
script = session.create_script(jscode)
# 注冊消息回調(diào)
script.on('message', on_message)
print('[*] Start attach')
# 加載創(chuàng)建好的javascript腳本
script.load()
# 讀取系統(tǒng)輸入
sys.stdin.read()
將上面的代碼绷跑,保存為
exp3.py
執(zhí)行
adb shell 'su -c /data/local/tmp/frida-server'
啟動服務(wù)端(如果上面啟動的服務(wù)端還開著可省略這一步)運(yùn)行目標(biāo)App
執(zhí)行
python exp3.py
注入代碼按返回鍵拳恋,再重新打開App,發(fā)現(xiàn)達(dá)到預(yù)期
按
Ctrl+C
停止腳本和停止注入代碼
上面是一段Python代碼,我們來分析它的步驟:
- 通過調(diào)用
frida.get_usb_device()
方法來得到一個連接中的USB設(shè)備(Device類)實例 - 調(diào)用Device類的
attach()
方法來附加到目標(biāo)進(jìn)程并得到一個會話(Session類)實例砸捏,該方法有一個參數(shù)谬运,參數(shù)是需要注入的進(jìn)程名或者進(jìn)程pid。如果需要Hook的代碼在App的啟動期執(zhí)行垦藏,那么在調(diào)用attach方法前需要先調(diào)用Device類的spawn()
方法梆暖,這個方法也有一個參數(shù),參數(shù)是進(jìn)程名掂骏,該方法調(diào)用后會重啟對應(yīng)的進(jìn)程轰驳,并返回新的進(jìn)程pid。得到新的進(jìn)程pid后弟灼,我們可以將這個進(jìn)程pid傳遞給attach()
方法來實現(xiàn)附加级解。 - 接著調(diào)用Session類的
create_script()
方法創(chuàng)建一個腳本,傳入需要注入的javascript代碼并得到Script類實例 - 調(diào)用Script類的
on()
方法添加一個消息回調(diào)田绑,第一個參數(shù)是信號名勤哗,乖乖傳入message
就行,第二個是回調(diào)函數(shù) - 最后調(diào)用Script類的
load()
方法來加載剛才創(chuàng)建的腳本掩驱。
注:如果想在javascript輸出日志芒划,可以調(diào)用
console.log()
方法冬竟。如果想給客戶端發(fā)送消息,可以在javascript代碼里調(diào)用send()
方法民逼,并在客戶端Python代碼里注冊一個消息回調(diào)來接收服務(wù)端發(fā)來的消息泵殴。
可以看到,結(jié)合python代碼缴挖,使注入更加的靈活了袋狞。如果想看Python端frida模塊的代碼焚辅,可以訪問:https://github.com/frida/frida-python/blob/master/frida/core.py