frida是一款方便而且易用的跨平臺Hook工具,使用它不只能夠Hook Java寫的應用程序,並且還能夠Hook原生的應用程序。javascript
frida分客戶端環境和服務端環境。在客戶端咱們能夠編寫Python代碼,用於鏈接遠程設備,提交要注入的代碼到遠程,接受服務端的發來的消息等。在服務端,咱們須要用Javascript代碼注入到目標進程,操做內存數據,給客戶端發送消息等操做。咱們也能夠把客戶端理解成控制端,服務端理解成被控端。
假如咱們要用PC來對Android設備上的某個進程進行操做,那麼PC就是客戶端,而Android設備就是服務端。java
本文,服務端在Android平臺測試。服務端環境準備步驟以下:python
根據本身的平臺下載frida服務端並解壓
https://github.com/frida/frida/releases
git
執行如下命令將服務端推到手機的/data/local/tmp目錄
adb push frida-server /data/local/tmp/frida-server
github
執行如下命令修改frida-server文件權限
adb shell chmod 777 /data/local/tmp/frida-server
shell
注:Windows系統執行命令能夠在CMD中進行;Linux和MacOS執行命令能夠在終端中進行。adb是Android一個調試工具,具體安裝方法不是本文的重點。數組
在PC上安裝Python的運行環境,安裝完成後執行下面的命令安裝frida緩存
pip install frida
session
下面是frida客戶端命令行的參數解釋,看一下就好app
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目標進程
frida -U -l myhook.js com.xxx.xxxx
參數解釋:
-p
選項。正在運行的進程能夠用frida-ps -U
命令查看frida運行過程當中,執行%resume
從新注入,執行%reload
來從新加載腳本;執行exit
結束腳本注入
Java.use方法用於聲明一個Java類,在用一個Java類以前首先得聲明。好比聲明一個String類,要指定完整的類名:
var StringClass=Java.use("java.lang.String");
修改一個函數的實現是逆向調試中至關有用的。修改一個函數的實現後,若是這個函數被調用,咱們的Javascript代碼裏的函數實現也會被調用。
不一樣的參數類型都有本身的表示方法
基本類型縮寫表示表:
基本類型 | 縮寫 |
---|---|
boolean | Z |
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
例如:int[]
類型,在重載時要寫成[I
例如:java.lang.String
例如:[java.lang.String;
修改參數爲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 }
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
修改函數名爲func,參數爲byte[]類型的函數的實現
ClassName.func.overload('[B').implementation=function(param){ //do something //return ... }
ClassName.func.overload().implementation=function(){ //do something }
注: 在修改函數實現時,若是原函數有返回值,那麼咱們在實現時也要返回合適的值
ClassName.func.overload().implementation=function(){ //do something return this.func(); }
和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();
用Java.cast
方法來對一個對象進行類型轉換,如將variable
轉換成java.lang.String
:
var StringClass=Java.use("java.lang.String"); var NewTypeClass=Java.cast(variable,StringClass);
這個字段標記Java虛擬機(例如: Dalvik 或者 ART)是否已加載, 操做Java任何東西以前,要確認這個值是否爲true
Java.perform(fn)在Javascript代碼成功被附加到目標進程時調用,咱們核心的代碼要在裏面寫。格式:
Java.perform(function(){ //do something... });
有了以上的基礎知識,咱們就能夠進行編寫代碼了
假設有如下的程序,給isExcellent方法傳入兩個值,經過計算,返回一個布爾值,表示是否優秀。默認狀況下,它是隻會顯示是否優秀:false
的,由於咱們默認傳入的數很小:
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
對於這種簡單的場景,直接修改返回值就能夠了,由於只有結果是重要的。
想直接返回結果很簡單,直接在匿名方法裏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
,回車,中止注入代碼
注:這裏爲何要打開兩次App?第一打開是爲了讓frida可以找到進程,第二次打開是爲了驗證結果,即便Hook成功了,界面是有緩存的,並不能實時顯示Hook結果,因此須要從新打開App
假設有如下場景,isExcellent除了返回是否優秀之外,方法的內部還把分數打印出來。
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方法的參數,使其經過計算打印和返回合理的值。
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
,回車,中止注入代碼
在本文剛開始的時候說到,咱們能夠編寫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代碼,咱們來分析它的步驟:
frida.get_usb_device()
方法來獲得一個鏈接中的USB設備(Device類)實例attach()
方法來附加到目標進程並獲得一個會話(Session類)實例,該方法有一個參數,參數是須要注入的進程名或者進程pid。若是須要Hook的代碼在App的啓動期執行,那麼在調用attach方法前須要先調用Device類的spawn()
方法,這個方法也有一個參數,參數是進程名,該方法調用後會重啓對應的進程,並返回新的進程pid。獲得新的進程pid後,咱們能夠將這個進程pid傳遞給attach()
方法來實現附加。create_script()
方法建立一個腳本,傳入須要注入的javascript代碼並獲得Script類實例on()
方法添加一個消息回調,第一個參數是信號名,乖乖傳入message
就行,第二個是回調函數load()
方法來加載剛纔建立的腳本。注:若是想在javascript輸出日誌,能夠調用
console.log()
方法。若是想給客戶端發送消息,能夠在javascript代碼裏調用send()
方法,並在客戶端Python代碼裏註冊一個消息回調來接收服務端發來的消息。
能夠看到,結合python代碼,使注入更加的靈活了。若是想看Python端frida模塊的代碼,能夠訪問:https://github.com/frida/frida-python/blob/master/frida/core.py