Frida之app逆向hook,動態插裝
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("helloworld!");
});
});
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:~$adbshell
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-lhelloworld.jscom.roysue.roysueapplication
____
/_|Frida12.7.24-Aworld-classdynamicinstrumentationtoolkit
|(_||>_|Commands:
/_/|_|help->Displaysthehelpsystem
....object?->Displayinformationabout'object'
....exit/quit->Exit
....
....Moreinfoathttps://www.frida.re/docs/home/
Attaching...
helloworld!
執行了該命令之後,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(){
//判斷是否載入了JavaVM,如果沒有載入則不執行下面的程式碼
if(Java.available){Java.perform(function(){
//先列印一句提示開始hookconsole.log("starthook");
//先通過Java.use函式得到Ordinary_Class類
varOrdinary_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){
//在這裡先得到執行的結果,因為我們要輸出這個函式的結果
varres=this.add(a,b);
//把計算的結果和引數一起輸出
console.log("執行了add方法計算result:"+res);
console.log("執行了add方法計算引數a:"+a);
console.log("執行了add方法計算引數b:"+b);
//返回結果,因為add函式本身是有返回值的,否則APP執行的時候會報錯
returnres;
}
Ordinary_Class.sub.implementation=function(a,b){
varres=this.sub(a,b);
console.log("執行了sub方法計算result:"+res);
console.log("執行了sub方法計算引數a:"+a);
console.log("執行了sub方法計算引數b:"+b);
returnres;
}
Ordinary_Class.getString.implementation=function(str){
varres=this.getString(str);
console.log("result:"+res);
returnres;
}
}else{
console.log("Ordinary_Class:undefined");
}
console.log("hookend");
});
}
});
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-Ucom.roysue.roysueapplication-lroysue_0.js
____
/_|Frida12.7.24-Aworld-classdynamicinstrumentationtoolkit
|(_||
>_|Commands:
/_/|_|help->Displaysthehelpsystem
....object?->Displayinformationabout'object'
....exit/quit->Exit
....
....Moreinfoathttps://www.frida.re/docs/home/
Attaching...
starthook
hookend[
GooglePixel::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("starthook");
//同樣的在這裡先獲取User類物件
varUser=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("hookend");
});
}
});
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 攔截方法過載指令碼程式碼示例
functionhook_overload(){
if(Java.available){
Java.perform(function(){
console.log("starthook");
varOrdinary_Class=Java.use("com.roysue.roysueapplication.Ordinary_Class");
if(Ordinary_Class!=undefined){
//要做的僅僅是將frida提示出來的overload複製在此處
Ordinary_Class.add.overload('int','int').implementation=function(a,b){
varres=this.add(a,b);
console.log("result:"+res);
returnres;
}
Ordinary_Class.add.overload('int','int','int').implementation=function(a,b,d){
varres=this.add(a,b,d);
console.log("result:"+res);
returnres;
}
Ordinary_Class.add.overload('int','int','int','int').implementation=function(a,b,d,c){
varres=this.add(a,b,d,c);
console.log("result:"+res);
returnres;
}
}else{
console.log("Ordinary_Class:undefined");
}
console.log("startend");
});
}
}
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
指令碼吧~~
functionhook_overload_7(){
if(Java.available){
Java.perform(function(){
console.log("starthook");
//先獲取類
varOrdinary_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");
return123;
}
//再呼叫一次isCheckResult()方法
Ordinary_Class.isCheckResult();
}else{
console.log("Ordinary_Class:undefined");
}
console.log("hookend");
});
}
}
上面這段程式碼的主要功能是:首先通過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
的判斷了。