Frida Java Hook 詳解(安卓9):代碼及示例(上)
轉載自:https://www.secpulse.com/archives/132082.html
前言
1.1 FRIDA SCRIPT的"hello world"
1.1.1 "hello world"腳本代碼示例
1.1.2 "hello world"腳本代碼示例詳解
1.2 Java層攔截普通方法
1.2.1 攔截普通方法腳本示例
1.2.2 執行攔截普通方法腳本示例
1.3 Java層攔截構造函數
1.3.1 攔截構造函數腳本代碼示例
1.3.2 攔截構造函數腳本代碼示例解詳解
1.4 Java層攔截方法重載
1.4.1 攔截方法重載腳本代碼示例
1.5 Java層攔截構造對象參數
1.5.1 攔截構造對象參數腳本示例
1.6 Java層修改成員變量的值以及函數的返回值
1.6.1 修改成員變量的值以及函數的返回值腳本代碼示例
1.6.2 修改成員變量的值以及函數的返回值之小實戰 結語
Frida Java Hook 詳解(安卓9):代碼及示例(上)
前言
咱們在這篇來深入學習如何HOOK Java
層函數,應用於與各種不同的Java
層函數,結合實際APK
案例使用FRIDA
框架對其APP
進行附加、hook
、以及FRIDA
腳本的詳細編寫。
1.1 FRIDA SCRIPT的"hello world"
在本章節中,依然會大量使用注入模式附加到一個正在運行進程程序,亦或是在APP
程序啟動的時候對其APP
進程進行劫持,再在目標進程中執行我們的js
文件代碼邏輯。FRIDA
腳本就是利用FRIDA
動態插樁框架,使用FRIDA
導出的API
和方法,對內存空間里的對象方法進行監視、修改或者替換的一段代碼。FRIDA
的API
是使用JavaScript
實現的,所以我們可以充分利用JS
的匿名函數的優勢、以及大量的hook
和回調函數的API
。那么大家跟我一起來操作吧,先打開在vscode
中創建一個js文件:helloworld.js
:
1.1.1 "hello world"腳本代碼示例
setTimeout(function(){ Java.perform(function(){ console.log("hello world!"); }); });
1.1.2 "hello world"腳本代碼示例詳解
這基本上就是一個FRIDA
版本的Hello World!
,我們把一個匿名函數作為參數傳給了setTimeout()
函數,然而函數體中的Java.perform()
這個函數本身又接受了一個匿名函數作為參數,該匿名函數中最終會調用console.log()
函數來打印一個Hello world!
字符串。我們需要調用setTimeout()
方法因為該方法將我們的函數注冊到JavaScript
運行時中去,然后需要調用Java.perform()
方法將函數注冊到Frida
的Java
運行時中去,用來執行函數中的操作,當然這里只是打了一條log
。
然后我們在手機上將frida-server
運行起來,在電腦上進行操作:
roysue@ubuntu:~$ adb shell sailfish:/ $ su sailfish:/ $ ./data/local/tmp/frida-server
這個時候,我們需要再開啟一個終端運行另外一條命令:
frida -U com.roysue.roysueapplication -l helloworld.js
這句代碼是指通過USB
連接對Android
設備中的com.roysue.roysueapplication
進程對其附加並且注入一個helloworld.js
腳本。注入完成之后會立刻執行helloworld.js
腳本所寫的代碼邏輯!
我們可以看到成功注入了腳本以及附加到自己所編寫包名為:com.roysue.roysueapplication
的apk
應用程序中,並且打印了一條 hell world!
。
roysue@ubuntu:~$ frida -U -l helloworld.js com.roysue.roysueapplication ____ / _ | Frida 12.7.24 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https://www.frida.re/docs/home/ Attaching... hello world!
執行了該命令之后,FRIDA
返回一個了CLI
終端工具與我們交互,在上面可見打印出來了一些信息,顯示了我們的FRIDA版本信息還有一個反向R的圖形,往下看,需要退出的時候我們只需要在終端輸入exit
即可完成退出對APP
的附加,其后我們看到的是Attaching...
正在對目標進程附加,當附加成功了打印了一句hello world!
,至此,我們的helloworld.js
通過FRIDA
的-l
命令成功的注入到目標進程並且執行完畢。學會了注入可不要高興的太早喲~~咱們繼續深入學習HOOK Java
代碼中的普通函數。
1.2 Java層攔截普通方法
Java
層的普通方法相當常見,也是我們要學習的基礎中的基礎,我們先來看幾個比較常見的普通的方法,見下圖1-1。
圖1-1 JADX-GUI軟件打開的反編譯代碼
通過圖1-1我們能看三個函數分別是構造函數a()
、普通函數a()
和b()
。諸如這種普通函數會特別多,那我們在本小章節中嘗試hook
普通函數、查看函數中的參數的具體值。
在嘗試寫FRIDA HOOK
腳本之前咱們先來看看需要hook
的代碼吧~,Ordinary_Class
類中有四個函數,都是很普通的函數,add
函數的功能也很簡單,參數a
+b
;sub函數功能是參數a
-b
;而getNumber
只返回100
;getString
方法返回了 getString()
+參數的str
。見下圖1-2。
圖1-2 反編譯的Ordinary_Class類的代碼
然后咱們再看MainActivity
中的編寫的代碼,通過反編譯出來的代碼一共有四個按鈕(Button
),當btn_add
點擊時會運行Ordinary_Class
類中add
方法,計算100+200
的結果,通過String.valueOf
函數把計算結構轉字符串然后通過Toast
彈出信息;點擊btn_sub
按鈕的時候觸發點擊事件會運行Ordinary_Class
類中sub
方法,計算100-100
的結果,通過String.valueOf
函數把計算結構轉字符串然后通過Toast
彈出信息。在MainActivity
類中的onCreate
方法中的四個按鈕分別對應情況是ADD
按鈕對應btn_add
點擊事件,SUB
對應btn_sub
的點擊事件。見下圖1-3。
圖1-3 MainActivity中的編寫的代碼
按照正常流程當我們點擊ADD
的按鈕界面會彈出一條信息顯示,其中的值是300
,因為我們在ADD
的點擊事件中添加了Toast
,將ADD
方法運行的結果放在Toast
參數中,通過它顯示了我們的計算結果;而SUB
函數會顯示0
,見下圖1-4,圖1-5。
圖1-4 點擊ADD按鈕時顯示的結果
圖1-5 點擊SUB按鈕時顯示的結果
我們現在知道已經知道它的運行流程以及函數的執行結果和所填寫的參數,我們現在來正式編寫一個基本的使用Frida
鈎子來攔截圖1-2中add
和sub
函數的調用並且在終端顯示每個函數所傳入的參數、返回的值,開始寫roysue_0.js
:
1.2.1 攔截普通方法腳本示例
setTimeout(function(){ //判斷是否加載了Java VM,如果沒有加載則不運行下面的代碼 if(Java.available) { Java.perform(function(){ //先打印一句提示開始hook console.log("start hook"); //先通過Java.use函數得到Ordinary_Class類 var Ordinary_Class = Java.use("com.roysue.roysueapplication.Ordinary_Class"); //這里我們需要進行一個NULL的判斷,通常這樣做會排除一些不必要的BUG if(Ordinary_Class != undefined) { //格式是:類名.函數名.implementation = function (a,b){ //在這里使用鈎子攔截add方法,注意方法名稱和參數個數要一致,這里的a和b可以自己任意填寫, Ordinary_Class.add.implementation = function (a,b){ //在這里先得到運行的結果,因為我們要輸出這個函數的結果 var res = this.add(a,b); //把計算的結果和參數一起輸出 console.log("執行了add方法 計算result:"+res); console.log("執行了add方法 計算參數a:"+a); console.log("執行了add方法 計算參數b:"+b); //返回結果,因為add函數本身是有返回值的,否則APP運行的時候會報錯 return res; } Ordinary_Class.sub.implementation = function (a,b){ var res = this.sub(a,b); console.log("執行了sub方法 計算result:"+res); console.log("執行了sub方法 計算參數a:"+a); console.log("執行了sub方法 計算參數b:"+b); return res; } Ordinary_Class.getString.implementation = function (str){ var res = this.getString(str); console.log("result:"+res); return res; } } else { console.log("Ordinary_Class: undefined"); } console.log("hook end"); }); } });
1.2.2 執行攔截普通方法腳本示例
寫完腳本后我們執行:frida -U com.roysue.roysueapplication -l roysue_0.js
,當我們執行了腳本后會進入cli
控制台與frida
交互,可以看到已經對該app
附加並且成功注入腳本。立刻打印出了start hook
和hook end
,正是我們剛剛所寫的。再繼續點擊app
應用中的ADD
按鈕和SUB
按鈕會在終端立刻輸出計算的結果和參數,在這里甚至可以看到清晰,參數、返回值一覽無余。
roysue@ubuntu:~$ frida -U com.roysue.roysueapplication -l roysue_0.js ____ / _ | Frida 12.7.24 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https://www.frida.re/docs/home/ Attaching... start hook hook end[ Google Pixel::com.roysue.roysueapplication]-> 執行了add方法 計算result:300 執行了add方法 計算參數a:100 執行了add方法 計算參數b:200 執行了sub方法 計算result:0 執行了sub方法 計算參數a:100 執行了sub方法 計算參數b:100
這樣我們就已經成功的打印了來了我們想要知道的值,每個參數的值和返回值的結構。我們就對普通函數的鈎子完成了一個基本操作,大家可以自己多多嘗試對其他的普通的函數進行hook
,多多練習,那咱們這一章節就愉快的完成了~~繼續深入吧。
1.3 Java層攔截構造函數
那咱們這章來玩如何HOOK
類的構造函數,很多時候在實例化類的瞬間就會把參數傳遞到內部為成員變量賦值,這樣一來就省的類中的成員變量一個個去賦值,在Android
逆向中,也有很多的類似的場景,利用有參構造函數實例化類並且賦值。我建立了一個class
類,類名是User
,其中代碼這樣寫:
我們可以看到User
類中有2個成員變量分別是age
和name
,還有2
個構造方法,分別是無參構造有有參構造。我們現在要做的是在User
類進行有參實例化時查看所填入的參數分別是什么值。在圖1-3中,可以看到btn_init
的點擊事件時會對User
類進行實例化參數分別填寫了roysue
和30
,然后再繼續調用了toString
方法把它們組合到一起並且通過Toast
彈出信息顯示值。TEST_INTI對應btn_init
的點擊事件,點擊時效果見下圖1-6。
setTimeout(function(){ if(Java.available) { Java.perform(function(){ console.log("start hook"); //同樣的在這里先獲取User類對象 var User = Java.use("com.roysue.roysueapplication.User"); if(User != undefined) { //注意在使用鈎子攔截構造函數時需要使用到 $init 也要注意參數的個數,因為該構造函數是2個所以此處填2個參數 User.$init.implementation = function (name,age){ //這里打印成員變量name和age的在運行中被調用填寫的值 console.log("name:"+name); console.log("age:"+age); //最終要執行原本的init方法否則運行時會報異常導致原程序無法正常運行。 this.$init(name, age); } } else { console.log("User: undefined"); } console.log("hook end"); }); } });
1.3.2 攔截構造函數腳本代碼示例解詳解
腳本寫好之后打開終端執行frida -U com.roysue.roysueapplication -l roysue_1.js
,把剛剛寫的腳本注入的目標進程,然后我們在APP
應用中按下TEST_INIT
按鈕,注入的腳本會立即攔截構造函數並且打印2個參數的具體的值,見下圖1-7。
圖1-7 終端顯示
打印的值就是圖1-3中所填的roysue
和30
,這就說明我們使用FRIDA
鈎子攔截到了User
類的有參構造函數並且有效的打印了參數的值。需要注意的是在輸入打印參數的值之后一定要記得執行原本的有參構造函數,這樣程序才可以正常執行。
1.4 Java層攔截方法重載
在學習HOOK
之前,咱們先了解一下什么是方法重載,方法重載是指在同一個類內定義了多個相同的方法名稱,但是每個方法的參數類型和參數的個數都不同。在調用方法重載的函數編譯器會根據所填入的參數的個數以及類型來匹配具體調用對應的函數。總結起來就是方法名稱一樣但是參數不一樣。在逆向JAVA
代碼的時候時常會遇到方法重載函數,見下圖1-8。
圖1-8 反編譯后重載函數樣本代碼
在圖1-8中,我們能看到一共有三個方法重載的函數,有時候實際情況甚至更多。咱們也不要怕,擼起袖子加油干。對於這種重載函數在FRIDA
中,js
會寫.overload
來攔截方法重載函數,當我們使用overload
關鍵字的時候FRIDA
會非常智能把當前所有的重載函數的所需要的類型打印出來。在了解了這個之后我們來開始實戰使用FRIDA
鈎子攔截我們想攔截的重載函數的腳本吧!還是之前的那個app
,還是之前的那個類,原汁原味~~,我新增了一些add
的方法,使add
方法重載,見下圖1-9。
圖1-9 反編譯后的Ordinary_Class
中的重載函數樣本代碼
在圖1-9中add
有三個重名的方法名稱,但是參數的個數不同,在圖1-3中的btn_add
點擊事件會執行擁有2個參數的方法重載的add
函數,當我在腳本中寫Ordinary_Class.add..implementation = function (a,b)
,然后繼續注入所寫好的腳本,FRIDA
在終端提示了紅色的字體,一看嚇一跳!但咱們仔細看,它說add
是一個方法重載的函數,有三個參數不同的add
函數,讓我們寫.overload(xxx)
,以識別hook的到底是哪個add
的方法重載函數。
當我們寫了這樣的js
腳本去運行的時候,frida
提示報錯了,因為有三個重載函數,我用紅色的框圈出了,可以看到frida
十分的智能,三個重載的參數類型完全一致的打印出來了,當它打印出來之后我們就可以復制它的這個智能提示的overloadxxx
重載來修改我們自己的腳本了,進一步完善我們的腳本代碼如下。
1.4.1 攔截方法重載腳本代碼示例
function hook_overload() { if(Java.available) { Java.perform(function () { console.log("start hook"); var Ordinary_Class = Java.use("com.roysue.roysueapplication.Ordinary_Class"); if(Ordinary_Class != undefined) { //要做的僅僅是將frida提示出來的overload復制在此處 Ordinary_Class.add.overload('int', 'int').implementation = function (a,b){ var res = this.add(a,b); console.log("result:"+res); return res; } Ordinary_Class.add.overload('int', 'int', 'int').implementation = function (a,b,d){ var res = this.add(a,b,d); console.log("result:"+res); return res; } Ordinary_Class.add.overload('int', 'int', 'int', 'int').implementation = function (a,b,d,c){ var res = this.add(a,b,d,c); console.log("result:"+res); return res; } } else { console.log("Ordinary_Class: undefined"); } console.log("start end"); }); } } setImmediate(hook_overload);
修改完相應的地方之后我們保存代碼時終端會自動再次運行js中的代碼,不得不說frida
太強大了~,當js
再次運行的時候我們在app
應用中點擊圖1-4中ADD
按鈕時會立刻打印出結果,因為FRIDA
鈎子已經對該類中的所有的add
函數進行了攔截,執行了自己所寫的代碼邏輯。點擊效果如下圖1-11。
圖1-11 終端顯示效果
在這一章節中我們學會了處理方法重載的函數,我們只要依據FRIDA的終端提示,將智能提示出來的代碼銜接到自己的代碼就能夠對方法重載函數進行攔截,執行我們自己想要執行的代碼。
1.5 Java層攔截構造對象參數
很多時候,我們不但要HOOK
使用鈎子攔截函數對函數的參數和返回值進行記錄,而且還要自己主動調用類中的函數使用。FRIDA
中有一個new()
關鍵字,而這個關鍵字就是實例化類的重要方法。
在官方API
中有這樣寫道:“Java.use(ClassName):
動態獲取className
的JavaScript
包裝器,通過對其調用new()
來調用構造函數,可以從中實例化對象。對實例調用Dispose()
以顯式清理它(或等待JavaScript
對象被垃圾收集,或腳本被卸載)。靜態和非靜態方法都是可用的。”,那我們就知道通過Java.use
獲取的class
類可以調用$new()
來調用構造函數,可以從實例化對象。在圖1-9中有6
個函數,了解了API
的調用之后我們來開始入手編寫我們的js文件。(在這里我覺得大家一定要動手做測試,動手嘗試,你會發現其中的妙趣無窮!)。
1.5.1 攔截構造對象參數腳本示例
function hook_overload_1() {
if(Java.available) {
Java.perform(function () {
console.log("start hook");
//還是先獲取類
var Ordinary_Class = Java.use("com.roysue.roysueapplication.Ordinary_Class");
if(Ordinary_Class != undefined) {
//這里因為add是一個靜態方法能夠直接調用方法
var result = Ordinary_Class.add(100,200);
console.log("result : " +result);
//調用方法重載無壓力
result = Ordinary_Class.add(100,200,300);
console.log("result : " +result);
//調用方法重載
result = Ordinary_Class.add(100,200,300,400);
console.log("result : " +result);
//調用方法重載
result = Ordinary_Class.getNumber();
console.log("result : " +result);
//調用方法重載
result = Ordinary_Class.getString(" HOOK");
console.log("result : " +result);
//在這里,使用了官方API的$new()方法來實例化類,實例化之后返回一個實例對象,通過這個實例對象來調用類中方法。
var Ordinary_Class_instance = Ordinary_Class.$new();
result = Ordinary_Class_instance.getString("Test");
console.log("instance --> result : " +result);
result = Ordinary_Class_instance.add(1,2,3,4);
console.log("instance --> result : " +result);
} else {
console.log("Ordinary_Class: undefined");
}
console.log("start end");
});
}
}
setImmediate(hook_overload_1);
當我們執行了上面寫的腳本之后終端會打印調用方法之后的結果,見下圖1-12。
圖1-12 終端顯示調用函數的結果
因為很多時候類中的方法並不一定是靜態的,所以這里提供了2
種調用方法,第一種調用方式十分的方便,不需要實例化一個對象,再通過對象調本身的方法。但是遇到了沒有static
關鍵字的函數時只能使用第二種方式來實現方法調用,在這一章節中我們學會了如何自己主動去調用類中的函數了~~大家也可以嘗試主動調用有參的構造函數玩玩。
1.6 Java層修改成員變量的值以及函數的返回值
我們上章學完了如何自己主動調用JAVA
層的函數了,經過上章的學習我們的功夫又精進了一些~~,現在我們來深入內部修改類的對象的成員變量和返回值,打入敵人內部,提高自己的內功。現在我們來看下圖1-13。
圖1-13 User類
上圖中的User
類是我之前建的一個類,類中寫了2個公開成員變量分別是age
是name
;還有2
個方法分別是User
的有參構造函數和一個toString
函數打印成員變量的函數。我們要做就是在User
類實例化的時候攔截程序並且修改掉age
和name
的值,從而改寫成我們自己需要的值再運行程序,那我們接下開始編寫JS
腳本來修改成員變量的值。
這段代碼主要有的功能是:通過User.$new("roysue",29)
拿到User
類的有參數構造的實例化對象,這個恰好也是使用了上章節中學到的知識自己構建對象,這里我們也學習了如何使用FRIDA框架通過有參構造函數實例化對象,實例化之后先是調用了類本身的toString
方法打印出未修改前的成員變量的值,打印了之后再通過User_instance.age.value = 0;
來修改對象當前的成員變量的值,可以看到修改age
修改為0
,name
修改為roysue_123
,然后再次調用toString
方法查看其成員變量的最新值是否已經被更改。
1.6.1 修改成員變量的值以及函數的返回值腳本代碼示例
function hook_overload_2() {
if(Java.available) {
Java.perform(function () {
console.log("start hook");
//拿到User類
var User = Java.use("com.roysue.roysueapplication.User");
if(User != undefined) {
//這里利用上章學到知識來自己構建一個User的有參構造的實例化對象
var User_instance = User.$new("roysue",29);
//並且調用了類中的toString()方法
var str = User_instance.toString();
//打印成員變量的值
console.log("str:"+str);
//這里獲取了屬性的值以及打印
console.log("User_instance.name:"+User_instance.name);
console.log("User_instance.age:"+User_instance.age);
//這里重新設置了age和name的值
User_instance.age.value = 0;
User_instance.name.value = "roysue_123";
str = User_instance.toString();
//再次打印成員變量的值
console.log("str:"+str);
} else {
console.log("User: undefined");
}
console.log("start end");
});
}
}
可以看到終端顯示了原本有參構造函數的值roysue和30修改為roysue_123和0已經成功了,效果見下圖1-14。
圖1-14 終端顯示修改效果
通過上面的學習,我們學會了如何修改類的成員變量,上個例子中是使用的有參構造函數給與成員變量賦值,通常在寫代碼類似這種實體類會定義相關的get set
方法以及修飾符為私有權限,外部不可調用,這個時候他們可能會通過set方法來設置其值和get
方法獲取成員的變量的值,這個時候我們可以通過鈎子攔截set
和get
方法自己定義值也是可以達到修改和獲取的效果。現在學完了如何修改成員變量了,那我們接下來要學習如何修改函數的返回值,假設在逆向的過程中已知檢測函數A
的結果為B
,正確結果為C
,那我們可以強行修改函數A
的返回值,不論在函數中執行了什么與返回結果無關,我們只要修改結果即可。
1.6.2 修改成員變量的值以及函數的返回值之小實戰
我在rdinary_Class
類建立了2
個函數分別是isCheck
和isCheckResult
,假設isCheck
是一個檢測方法,經過add
運行后必然結果2
,代表被檢測到了,在isCheckResult
方法進行了判斷調用isCheck
函數結果為2
就是錯誤的,那這個時候要把isCheck
函數或者add
函數的結果強行改成不是2
之后isCheckResult
即可打印Successful
,見下圖1-15。
圖1-15 isCheck函數與isCheckResult函數
我們現在要做的是使sCheckResult
函數成功打印出"Successful"
,而不是errer
,那我們現在開始來寫js
腳本吧~~
function hook_overload_7() { if(Java.available) { Java.perform(function () { console.log("start hook"); //先獲取類 var Ordinary_Class = Java.use('com.roysue.roysueapplication.Ordinary_Class'); if(Ordinary_Class != undefined) { //先調用一次肯定會輸出error Ordinary_Class.isCheckResult(); //在這里重寫isCheck方法將返回值強行改為123並且輸出了一句Ordinary_Class: isCheck Ordinary_Class.isCheck.implementation = function (){ console.log("Ordinary_Class: isCheck"); return 123; } //再調用一次isCheckResult()方法 Ordinary_Class.isCheckResult(); } else { console.log("Ordinary_Class: undefined"); } console.log("hook end"); }); } }
上面這段代碼的主要功能是:首先通過Java.use
獲取Ordinary_Class
,因為isCheckResult()
是靜態方法,可以直接調用,在這里先調用一次,因為這樣比較好看效果,第一次調用會在Android LOG
中打印errer
,之后緊接着利用FRIDA
鈎子對isCheck
函數進行攔截,改掉其返回值為123
,這樣每次調用isCheck
函數時返回值都必定會是123
,再調用一次isCheckResult()
方法,isCheckResult()
方法中判斷isCheck
返回值是否等於2
,因為我們已經重寫了isCheck
函數,所以不等於2
,所以程序往下執行,會打印Successful
字符串到Android Log
中,實際運行效果見下圖1-16。
圖1-16 終端顯示以及Android Log信息
可以清晰的看到先是打印了errer
后打印了Successful
了,這說明我們已經成功過掉isCheck
的判斷了。這是一個小小的綜合例子。建議大家多多動手嘗試。
結語
在這章中我們學習了HOOK Java層的一些函數,如攔截普通函數、構造函數、以及修改成員變量以及函數返回值。下一篇中我們來枚舉所有的類、所有的方法重載、所有的子類以及RPC遠程調用Java層函數。