初識Frida--Android逆向之Java層hook (一)

Editor發表於2018-06-11

0x00 文中用到的工具

Frida

jadx-gui 一個強大的android反編譯工具

genymotion模擬器

Python2.7以及frida-python庫

radare2 反彙編器

pycharm

0x01 hook示例的安裝與分析

Frida官網給我們了一個ctf的示例,就以此為例子,開始學習frida在android逆向的使用。
rps.apk下載地址

安裝

使用genymotion等類似android模擬器安裝好開啟,發現這是一個石頭剪刀布的遊戲應用,簡單的玩了一下,沒什麼特別的,直接分析程式碼吧,看看到底想幹什麼。
初識Frida--Android逆向之Java層hook (一)

原始碼分析

使用jadx-gui反編譯,發現app沒有加殼和混淆,當然一來就加殼和混淆的話對我們就太不友好了,接下分析就簡單了,直接看java程式碼。當然也可以使用androidkiller,jeb等其他強大的反編譯工具。
初識Frida--Android逆向之Java層hook (一)

在MainActivity中找到OnCreate()方法,可以看到只是簡單的宣告瞭button控制元件以及對應的監聽器。


protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

this.P=(Button) findViewById(R.id.button);

this.S=(Button) findViewById(R.id.button3);

this.r=(Button) findViewById(R.id.buttonR);

this.P.setOnClickListener(this);

this.r.setOnClickListener(this);

this.S.setOnClickListener(this);

this.flag=0;

}


繼續檢視button的onclick方法,可以看出cpu是通過隨機陣列出的,其判斷輸贏的方法在this.showMessageTask中。


public void onClick(View v) {

if(this.flag !=1) {

this.flag=1;

((TextView) findViewById(R.id.textView3)).setText("");

TextView tv=(TextView) findViewById(R.id.textView);

TextView tv2=(TextView) findViewById(R.id.textView2);

this.m=0;

this.n=new Random().nextInt(3); //隨機數0,1,2

tv2.setText(new String[]{"CPU: Paper","CPU: Rock","CPU: Scissors"}[this.n]);//隨機出石頭,剪刀,布

if(v==this.P) {

tv.setText("YOU: Paper");

this.m=0;

}

if(v==this.r) {

tv.setText("YOU: Rock");

this.m=1;

}

if(v==this.S) {

tv.setText("YOU: Scissors");

this.m=2;

}

this.handler.postDelayed(this.showMessageTask,1000);//輸贏判斷方法

}

}


跟進分析showMessageTask,可以看到如果贏了mainActivity.cnt會+1,但是一旦輸了cnt就會置0,而獲取flag的要求是我們得獲勝1000次,...... :(


private final Runnable showMessageTask=new Runnable() {

public void run() {

TextView tv3=(TextView) MainActivity.this.findViewById(R.id.textView3);

MainActivity mainActivity;

//我方:布 CPU:石頭or我方:石頭 CUP:剪刀 ,則為贏

if(MainActivity.this.n-MainActivity.this.m==1) {

mainActivity=MainActivity.this;

mainActivity.cnt++;

tv3.setText("WIN! +"+String.valueOf(MainActivity.this.cnt));

//反過來當然是輸咯

}elseif(MainActivity.this.m-MainActivity.this.n==1) {

MainActivity.this.cnt=0;

tv3.setText("LOSE +0");

//一樣則打平

}elseif(MainActivity.this.m==MainActivity.this.n) {

tv3.setText("DRAW +"+String.valueOf(MainActivity.this.cnt));

//我布  cup:剪刀

}elseif(MainActivity.this.m < MainActivity.this.n) {

MainActivity.this.cnt=0;

tv3.setText("LOSE +0");

}else{

mainActivity=MainActivity.this;

mainActivity.cnt++;

tv3.setText("WIN! +"+String.valueOf(MainActivity.this.cnt));

}

//獲勝1000次則能夠獲取flag

if(1000==MainActivity.this.cnt) {

tv3.setText("SECCON{"+String.valueOf((MainActivity.this.cnt+MainActivity.this.calc())*107)+"}");

}

MainActivity.this.flag=0;

}

};

簡單分析一下獲取flag需要的條件,總結有3個辦法:

分析calc()方法能算出答案,但這個方法在so中,得分析彙編程式碼才行,當然可以嘗試使用ida pro,F5檢視C程式碼分析,前提是演算法不難。

獲取calc函式的返回值,從而計算答案。

還有一個方法就是,直接將MainActivity.this.cnt的值構造成1000。

接下來就用frida,使用後兩種思路來解這個簡單的示例。但在這之前得先了解Frida自帶的Messages機制,瞭解frida怎麼從通過一個python指令碼傳送和接收message訊息是一個提升理解frida的好方法。


0x02 frida自帶的Messages機制與程式互動


先來看看一個Messages的模板,這裡用到的語言分別是python和javascript,他們之間的關係是python作為載體,javascript作為在android中真正執行程式碼。


importfrida, sys

//hook程式碼,採用javascript編寫

jscode="""

//javascript程式碼,重點

"""

//自定義回撥函式

defon_message(message, data):

ifmessage['type']=='send':

print("[*] {0}".format(message['payload']))

else:

print(message)

#重點的4行程式碼

process=frida.get_usb_device().attach('應用完整包名')

script=process.create_script(jscode)

script.on('message', on_message)

script.load()

sys.stdin.read()


當然如果是對此簡單的使用,只需要編寫jscode,以及填寫你要hook的應用完整包名就行了,不過如果單純只會用可能在以後會被模板限制,所以一探究竟還是很有必要。


可以在cmd中,使用python終端的help()函式找到frida庫的原始碼的絕對路徑。


初識Frida--Android逆向之Java層hook (一)

接下來就來具體看看這幾句程式碼做了什麼事情。


process=frida.get_usb_device().attach('應用完整包名')

script=process.create_script(jscode)

script.on('message', on_message)

script.load()

sys.stdin.read()


首先使用了frida.get_usb_device(),返回了一個_get_device函式,跟進_get_device方法。


defget_usb_device(timeout=0):

return_get_device(lambdadevice: device.type=='tether', timeout)


在_get_device中,通過get_device_manager()例項化DeviceManager類,並呼叫該類中的enumerate_devices()方法。


def_get_device(predicate, timeout):

mgr=get_device_manager()               //獲取裝置管理

deffind_matching_device():              //尋找匹配裝置

usb_devices=[devicefordeviceinmgr.enumerate_devices()ifpredicate(device)]

iflen(usb_devices) >0:

returnusb_devices[0]

else:

returnNone

device=find_matching_device()

...省略

get_device_manager()程式碼


defget_device_manager():

global_device_manager

if_device_managerisNone:

from.importcore

_device_manager=core.DeviceManager(_frida.DeviceManager())

return_device_manager

DeviceManager中enumerate_devices()方法,可以看到enumerate_devices()方法實際上是返回了一個Device()類的例項化物件List。


classDeviceManager(object):

def__init__(self, impl):

self._impl=impl

def__repr__(self):

returnrepr(self._impl)

//返回了一個Device()類的例項化。

defenumerate_devices(self):

return[Device(device)fordeviceinself._impl.enumerate_devices()]

defadd_remote_device(self, host):

returnDevice(self._impl.add_remote_device(host))

defremove_remote_device(self, host):

self._impl.remove_remote_device(host)

defget_device(self, device_id):

devices=self._impl.enumerate_devices()

ifdevice_idisNone:

returnDevice(devices[0])

fordeviceindevices:

ifdevice.id==device_id:

returnDevice(device)

raise_frida.InvalidArgumentError("unable to find device with id %s"%device_id)

defon(self, signal, callback):

self._impl.on(signal, callback)

defoff(self, signal, callback):

self._impl.off(signal, callback)


繼續跟進Device類中的,就找到了attach()方法。在attach方法這是設定斷點,看看傳入的資料。


初識Frida--Android逆向之Java層hook (一)


接下來提供的“應用完整名”是通過self._pid_of()函式去找到對應的程式號pid,然後將pid後通過Session類初始化。到此第一句程式碼過程就算是明白了,最終得到的是一個對應程式號pid的Session例項化物件process。


classDevice(object):

def__init__(self, device):

self.id=device.id

self.name=device.name

self.icon=device.icon

self.type=device.type

self._impl=device

def__repr__(self):

returnrepr(self._impl)

...節省空間刪除部分方法,詳細內容可自行檢視原始碼

defkill(self, target):

self._impl.kill(self._pid_of(target))

//返回了一個Session的例項化物件

defattach(self, target):

returnSession(self._impl.attach(self._pid_of(target)))

definject_library_file(self, target, path, entrypoint, data):

returnself._impl.inject_library_file(self._pid_of(target), path, entrypoint, data)

definject_library_blob(self, target, blob, entrypoint, data):

returnself._impl.inject_library_blob(self._pid_of(target), blob, entrypoint, data)

defon(self, signal, callback):

self._impl.on(signal, callback)

defoff(self, signal, callback):

self._impl.off(signal, callback)

def_pid_of(self, target):

ifisinstance(target, numbers.Number):

returntarget

else:

returnself.get_process(target).pid


第二句,緊接著process.create_script(jscode),可以看到它返回一個Script類的例項化,引數不確定。


defcreate_script(self,*args,**kwargs):

returnScript(self._impl.create_script(*args,**kwargs))


跟進Script類,可以找到on()方法,在on方法中可以設定自定義回撥函式。


classScript(object):

def__init__(self, impl):

self.exports=ScriptExports(self)

self._impl=impl

self._on_message_callbacks=[]

self._log_handler=self._on_log

self._pending={}

self._next_request_id=1

self._cond=threading.Condition()

impl.on('destroyed',self._on_destroyed)

impl.on('message',self._on_message)

...節省空間刪除部分類方法,詳細內容可自行檢視原始碼

defload(self):

self._impl.load()

//設定自定義回撥函式

defon(self, signal, callback):

ifsignal=='message':

self._on_message_callbacks.append(callback)

else:

self._impl.on(signal, callback)


在IDE中可以看到_on_message_callbacks中存放的on_message函式地址。


初識Frida--Android逆向之Java層hook (一)

接下來呼叫load()方法,在服務端就啟動javascript指令碼了,至於在frida-server服務端怎麼執行的,可逆向研究一下frida-server,它才是真正的核心。


0x03 Javascript程式碼構造與執行


現在就來使用frida實現剛剛試想的方法。


方法一:獲取calc()返回值


第一種思路就是直接獲取calc的返回值,從native函式定義上知道它的返回值是int型別,當然直接獲取calc函式的返回值是解出問題最簡單的方法。

public nativeintcalc();


那怎麼獲取calc()函式的返回值呢,這個函式在MainActivity類中,直接引用該類下的calc()方法,不就ok了嗎,原理是這樣,下面就來構造一下Javascript程式碼。


//Java.Perform 開始執行JavaScript指令碼。

Java.perform(function () {

//定義變數MainActivity,Java.use指定要使用的類

var MainActivity=Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity');

//hook該類下的onCreate方法,重新實現它

MainActivity.onCreate.implementation=function () {

send("Hook Start...");

//呼叫calc()方法,獲取返回值

var returnValue=this.calc();

send("Return:"+returnValue);

var result=(1000+returnValue)*107;

//解出答案

send("Flag:"+"SECCON{"+result.toString()+"}");

}

});


JavaScript程式碼就是這樣,如果不是很理解,學習一下JavaScript基礎即可,下面看看完整的python指令碼。


importfrida, sys

defon_message(message, data):

ifmessage['type']=='send':

print("[*] {0}".format(message['payload']))

else:

print(message)

jscode="""

Java.perform(function () {

var MainActivity = Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity');

MainActivity.onCreate.implementation = function () {

send("Hook Start...");

var returnValue = this.calc();

send("Return:"+returnValue);

var result = (1000+returnValue)*107;

send("Flag:"+"SECCON{"+result.toString()+"}");

}

});

"""

process=frida.get_usb_device().attach('com.example.seccon2015.rock_paper_scissors')

script=process.create_script(jscode)

script.on('message', on_message)

script.load()

sys.stdin.read()

接下來執行一下,看看能否成功。

步驟如下:

啟動模擬器,使用adb push將對應架構的frida-server檔案push到模擬器中
/data/local/tmp目錄下。

adb shell 進入/data/local/tmp目錄,啟動frida-server。

開啟埠轉發
adb forward tcp:27043 tcp:27043
adb forward tcp:27042 tcp:27042

啟動應用後,在命令列等執行python指令碼。

因為hook的是應用的onCreate方法,執行python指令碼的前提是應用首先啟動,這樣才能attach到該應用,所以還得返回模擬器桌面重新啟動應用,這樣它才會執行hook的onCreate()方法,結果如下。
初識Frida--Android逆向之Java層hook (一)


方法二:修改cnt的值為1000


第二種思路也比較簡單,我們需要修改cnt的值,但如果直接修改cnt的初始值為1000的話,在遊戲中可能存在不確定因素,比如輸了會置0,贏了cnt值就變成1001了,所以還得控制一下輸贏,而輸贏的條件是電腦出什麼,所以最終hook的方法就在onClick中。
從onClick()中可以知道,控制輸贏的在於修改this.n 和 this.m的值,再來看看原始碼。


public void onClick(View v) {

if(this.flag !=1) {

this.flag=1;

((TextView) findViewById(R.id.textView3)).setText("");

TextView tv=(TextView) findViewById(R.id.textView);

TextView tv2=(TextView) findViewById(R.id.textView2);

this.m=0;

//控制電腦出拳

this.n=new Random().nextInt(3);

tv2.setText(new String[]{"CPU: Paper","CPU: Rock","CPU: Scissors"}[this.n]);

if(v==this.P) {

tv.setText("YOU: Paper");

this.m=0;

}

if(v==this.r) {

tv.setText("YOU: Rock");

this.m=1;

}

if(v==this.S) {

tv.setText("YOU: Scissors");

this.m=2;

}

this.handler.postDelayed(this.showMessageTask,1000);

}


來看JavaScript程式碼怎麼寫吧


Java.perform(function () {

var MainActivity=Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity');

//hook onClick方法,此處要注意的是onClick方法是傳遞了一個View引數v

MainActivity.onClick.implementation=function (v) {

send("Hook Start...");

//呼叫onClick,模擬點選事件

this.onClick(v);

//修改引數

this.n.value=0;

this.m.value=2;

this.cnt.value=999;

send("Success!")

}

});


完整python程式碼


importfrida, sys

defon_message(message, data):

ifmessage['type']=='send':

print("[*] {0}".format(message['payload']))

else:

print(message)

jscode="""

Java.perform(function () {

var MainActivity = Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity');

MainActivity.onClick.implementation = function (v) {

send("Hook Start...");

this.onClick(v);

this.n.value = 0;

this.m.value = 2;

this.cnt.value = 999;

send("Success!")

}

});

"""

process=frida.get_usb_device().attach('com.example.seccon2015.rock_paper_scissors')

script=process.create_script(jscode)

script.on('message', on_message)

script.load()

sys.stdin.read()

執行python指令碼,任意點選按鈕,答案就出來了。
初識Frida--Android逆向之Java層hook (一)


當然,如果so中的calc()函式演算法不難的前提,直接使用ida pro或者radare2分析彙編程式碼也是可以的。這裡給出用radare2反彙編出來的程式碼。可以看到,calc()函式就單純的返回了int值7。


初識Frida--Android逆向之Java層hook (一)


0x04 總結


一般分析流程
1.反編譯apk,分析程式碼尋找hook點。
2.編寫js程式碼,呼叫類的方法或者替換。
3.在python中執行即可。
下面一篇會更詳細介紹frida的使用。



本文由看雪論壇 ghostmazeW 原創

轉載請註明來自看雪社群


相關文章