當前位置:
首頁 > 新聞 > Frida在爆破Windows程序中的應用

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原創獎勵計劃,未經許可禁止轉載


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 FreeBuf 的精彩文章:

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

TAG:FreeBuf |