Frida在爆破Windows程序中的應用
*本文原創作者:geek痕,本文屬FreeBuf原創獎勵計劃,未經許可禁止轉載
談到爆破,相信大部分網路安全從業者都並不陌生,爆破爆破,就是暴力破解嘛。通過枚舉嘗試儘可能多的可能解,再進行驗證判斷是否正確。在進行web的爆破時,我們通常會使用brupsuite等工具,那麼,如果是二進位程序中的爆破呢?
本文將介紹一種方法,通過動態插樁(hook)的方式,實現二進位程序中的爆破。最近在學習逆向,刷一些ctf的題目,遇到了一道拖進ida死活分析不出演算法,因為實在是太菜了,目標程序大概長這樣:
有興趣的可以先試試:地址如下:http://ctf5.shiyanbar.com/re/100w.exe
輸入的口令正確則會彈出flag,輸入錯誤則會彈出錯誤提示。
看到提示說是6位數字,而且在逆向的過程中發現有這樣一段文字:
行吧…那就爆破一個試試。之前就聽說過Frida牛逼的不行,跨平台的動態插樁框架,不過之前一直沒親自動手玩過,這次就試試吧。在實踐過程中發現Frida的相關資料本身並不多,而且大多是針對Android移動平台的應用,於是決定寫一篇文章分享一些桌面端Frida應用的技術。先上一段Frida的介紹:
So what is Frida, exactly?
It』sGreasemonkey for
native apps, or, put in more technical terms, it』s a dynamic code
instrumentation toolkit. It lets you inject snippets of JavaScript or your own
library into native apps on Windows, macOS, GNU/Linux, iOS, Android, and QNX.
Frida also provides you with some simple tools built on top of the Frida API.
These can be used as-is, tweaked to your needs, or serve as examples of how to
use the API.
Frida是一個動態插樁的工具包。它可以讓你將js腳本或那你自己的一些庫插入到win、macos、linux、android、ios等平台的應用中。跨平台的實現方案聽起來很牛逼有木有,這意味著熟練掌握這一個工具的性價比是很高的。亂扯了那麼多,先來看下Frida使用的基本代碼框架。以下是python的代碼。首先,用pip安裝一下:
pip install frida
然後下面這段代碼是frida 的基本框架:
import frida
def on_message(message, data):
print("[%s] => %s" % (message, data))
session = frida.attach("100fw.exe")#附加frida到目標進程
script = session.create_script("some js code here")
script.on("message", on_message)
script.load()
sys.stdin.read()
session.deatch()
代碼比較簡單,不多解釋。重點是session.create_script裡面的js代碼。
首先,我們要能夠模擬調用按鈕點擊後執行的函數。
找這個函數地址的思路有兩個。一個,由於這個crackme是用易語言寫的,所以用e-debug可以找到call的地址:
另外一個方法就是拖入od找字元串然後往上找到函數入口,下斷點驗證。不行再往上翻。
最後找到函數入口如下:
然後,我們用frida的js api寫一個模擬調用的函數。
var f=new NativeFunction(ptr("0x0040173a"), "void",[]);
rpc.exports={
once:function(){
f();
}
};
NtiveFunction的後面兩個參數中,第一個是返回值類型,第二個是參數列表的類型,這裡都為空即可。
然後定義once模擬調用一次按鈕點擊事件。
最後,我們在python代碼中調用frida為我們暴露出來的介面:
while(True):
script.exports.once()
以上代碼可以不斷模擬點擊目標程序中按鈕的過程。
再然後,我們需要模擬往輸入中填入各個值。那麼要做的就是hook獲取控制項數值的相關函數。找的方法嘛..我用的是先把斷點下到按鈕事件函數那裡,然後單步走起。看哪個函數返回了輸入值的指針。
ok,找到函數地址為0X00401CE7(最靠近結果的call)
接下來我們hook這個函數的返回結果,讓它依次遍歷每一個可能的值:
var tmp=100000;
var NeedAdd=true;
var f3=ptr("0x00401CE7");
Interceptor.attach(f3,{
onLeave:function(result){
/*
console.log("----------")
console.log(result.toInt32());*/
Memory.writeAnsiString(ptr(result.toInt32()),tmp.toString())//result為輸入值的指針指向
if(NeedAdd){tmp=tmp+1;NeedAdd=false;}
else{NeedAdd=true;}
/*
console.log(Memory.readAnsiString(ptr(result.toInt32())));*///輸出修改後的結果
}
});
上面的代碼有注釋,這裡解釋下為什麼用NeedAdd輔助來讓tmp值每兩次遞增一次.因為…我比較菜hook點不是很合適,每一次調用都會有兩次被hook到,所以..就出此下策了。
接下來,我們要hook掉消息框彈窗函數,獲取提示內容以判斷口令的正確與否。
眼看著這是最後一步了,但我卻在這裡踩了很多坑。
首先,獲取信息框內容嘛,好啊,我hook MessageBox不就好了,於是用OD插件給API下斷一通亂搞,獲取到了彈窗內容美滋滋。跑起來一看,等等!難道要我每一次都點一下確認把消息框弄掉才能進行下一次嘗試嗎?不行!要把這個信息框幹掉。然後想著直接跳過對MessageBox的call,結果程序崩了,調試一番才發現,堆棧不平衡,hook了好幾個都不行。
就在這裡卡了好一會,後來覺得沿著api的調用棧一直往上翻,一定能找到用戶態最初的call,那個call的調用關係應該相對簡單,堆棧平衡問題也比較容易處理,然後就一直找啊找,發現就在搜到的字元串附近有這樣一段代碼,頓時兩眼放光:
這就好辦了,結果跟進去一看,發現這只是一個索引,hook這個會影響很多函數:
那不如..直接把這個call給nop掉吧..把這個嘴給塞住。
測試之後發現經過add esp,0x28後堆棧剛好平衡,很好,perfect!
現在,我們可以寫出最後一段代碼了:
var f4=ptr("0x00401C03");
Interceptor.attach(f4,{
onEnter:function(args){
//console.log(args[0]);
send(Memory.readAnsiString(ptr(args[0])));//讀取相應內容並發送給終端顯示
return;
}
});
這裡解釋一下,為什麼把0x00401c03 nop掉之後仍然可以hook這裡。
根據我的猜測,frida的hook應該是內存斷點,獲取的「參數」就是根據堆棧情況的相對位置確定的,所以我們可以「hook」這個地方,獲取到前面push的內容,至於那個args[0],多試幾個就好了。
其實,成功的時候call的地方不在這裡,而我們沒有處理成功彈窗的相關代碼,成功後自然會彈出來,這裡的顯示有些多餘,當作實驗就好了吧。
下面是完整代碼:
import frida
import sys
def on_message(message, data):
print("[%s] => %s" % (message, data))
session = frida.attach("100fw.exe")
script = session.create_script("""
var tmp=700000;
var NeedAdd=true;
var f1 = ptr("0x00401096");
var f2 = ptr("0x0040169d");
Interceptor.attach(f1, {
onEnter:function(args){
}
});
Interceptor.attach(f2, {
onEnter:function(args){
}
});
var f=new NativeFunction(ptr("0x0040173a"), "void",[]);
rpc.exports={
once:function(){
f();
}
};
var f3=ptr("0x00401CE7");
Interceptor.attach(f3,{
onLeave:function(result){
/*
console.log("----------")
console.log(result.toInt32());*/
Memory.writeAnsiString(ptr(result.toInt32()),tmp.toString())
if(NeedAdd){tmp=tmp+1;NeedAdd=false;}
else{NeedAdd=true;}
//console.log(Memory.readAnsiString(ptr(result.toInt32())));
console.log(tmp);
}
});
/*
var f4=ptr("0x00401C03");
Interceptor.attach(f4,{
onEnter:function(args){
//console.log(args[0]);
send(Memory.readAnsiString(ptr(args[0])));
return;
}
});
*/
""")
script.on("message", on_message)
script.load()
while(True):
script.exports.once()
sys.stdin.read()
session.deatch()
最好運行成功之後是醬紫的:
再說幾點注意吧,首先是運行的時候要先運行程序,再運行py腳本,不然會出現這個:
然後是我們要先在輸入框中輸入一個隨意的六位數,這樣系統才會分配一個儲存的空間。不然會出現這樣:
這個解決方案有個地方不足就是效率還是低了點,完整爆破需要一些時間。
我嘗試過減少調試性的輸出來提升效率,還是有一定效果的。然後因為爆破的時候cpu並沒有跑滿,所以多開幾個實例來分段跑估計也能快不少。看了正解演算法的確比較複雜,orz。
最後,本文旨在探討Frida的使用技巧。總的來說,Frida的可玩性還是很高的,還有很多js api介面沒有介紹,有興趣的可以去官網看看文檔。
寫文章的經驗不多,還請各位dalao拍磚!
*本文原創作者:geek痕,本文屬FreeBuf原創獎勵計劃,未經許可禁止轉載


※Ridrelay:一款用於在內網中快速查找域用戶名的工具
※BlackHat 2018 | 55款大會軟體工具盤點
TAG:FreeBuf |