安卓動態除錯七種武器之離別鉤 – Hooking(上)

NO IMAGE

0x00 序

隨著移動安全越來越火,各種除錯工具也都層出不窮,但因為環境和需求的不同,並沒有工具是萬能的。另外工具是死的,人是活的,如果能搞懂工具的原理再結合上自身的經驗,你也可以創造出屬於自己的除錯武器。因此,筆者將會在這一系列文章中分享一些自己經常用或原創的除錯工具以及手段,希望能對國內移動安全的研究起到一些催化劑的作用。

目錄如下:

文章中所有提到的程式碼和工具都可以在我的github下載到,地址是:https://github.com/zhengmin1989/TheSevenWeapons

0x01 離別鉤

Hooking翻譯成中文就是鉤子的意思,所以正好配合這一章的名字《離別鉤》。

“我知道鉤是種武器,在十八般兵器中名列第七,離別鉤呢?” “離別鉤也是種武器,也是鉤。”
“既然是鉤,為什麼要叫做離別?”
“因為這柄鉤,無論鉤住什麼都會造成離別。如果它鉤住你的手,你的手就要和腕離別;如果它鉤住你的腳,你的腳就要和腿離別。”
“如果它鉤住我的咽喉,我就和這個世界離別了?”
“是的,”
“因為我不願被人強迫與我所愛的人離別。”
“我明白你的意思了。”
“你真的明白?”
“你用離別鉤,只不過為了要相聚。”
“是的。”

一提到hooking,讓我又回想起了2011年的時候。當時android才出來沒多久,各大安全公司都在忙著研發自己的手機助手。當時手機上最氾濫的病毒就是簡訊扣費類的病毒,但僅僅是靠雲端的病毒庫掃描是遠遠不夠的。而這時候”LBE安全大師”橫空出世,提供了主動防禦的技術,可以在病毒傳送簡訊之前攔截下來,並讓使用者選擇是否傳送。 其實這個主動防禦技術就是hooking。雖然在PC上hooking的技術已經很成熟了,但是在android上的資料卻非常稀少,只有少數人掌握著android上hooking的技術,因此這些人也變成了各大公司爭相搶奪的物件。

但是沒有什麼東西是能夠永久保密的,這些技術早晚會被大家研究出來並對外公開的。因此,到了2015年,android上的hook資料已經遍地都是了,各種開源的hook框架也層出不窮,使用這些hook工具就可以輕鬆的hook native,jni和java層的函式。但這同樣也帶來了一些問題,新手想研究hook的時候因為資料和工具太多往往不知道如何下手,並且就算使用了工具成功的hook,也根本不知道原理是什麼。因此筆者準備從hook的原理開始,配合開源工具循序漸進的介紹native,jni和java層的hook,方便大家對hook進行系統的學習。

0x02 Playing with Ptrace on Android

其實無論是hook還是除錯都離不開ptrace這個system call,利用ptrace,我們可以跟蹤目標程序,並且在目標程序暫停的時候對目標程序的記憶體進行讀寫。在linux上有一篇經典的文章叫《Playing with Ptrace》,簡單介紹瞭如何玩轉ptrace。在這裡我們照貓畫虎,來試一下playing with Ptrace on Android。

PS:這裡的一部分內容借鑑了harry大牛在百度Hi寫的一篇文章,可惜後來百度Hi關了,就失傳了。不過不用擔心,我這篇比他寫的還詳細。:)

首先來看我們要ptrace的目標程式,用來一直迴圈輸出一句話”Hello,LiBieGou!”:

圖片描述

想要編譯它非常簡單,首先建立一個Android.mk檔案,然後填入如下內容,讓ndk將檔案編譯為elf可執行檔案:

圖片描述

接下來我們寫出hook1.c程式來hook target程式的system call,main函式如下:

圖片描述

首先要知道hook的目標的pid,這個用ps命令就能獲取到。然後我們使用ptrace(PTRACE_ATTACH, pid, NULL, NULL)這個函式對目標程序進行載入。載入成功後我們可以使用ptrace(PTRACE_SYSCALL, pid, NULL, NULL)這個函式來對目標程式下斷點,每當目標程式呼叫system call前的時候,就會暫停下載。然後我們就可以讀取暫存器的值來獲取system call的各項資訊。然後我們再一次使用ptrace(PTRACE_SYSCALL, pid, NULL, NULL)這個函式就可以讓system call在呼叫完後再一次暫停下來,並獲取system call的返回值。

獲取system call編號的函式如下:

圖片描述

ARM架構上,所有的系統呼叫都是通過SWI來實現的。並且在ARM 架構中有兩個SWI指令,分別針對EABI和OABI:

[EABI] 機器碼:
1110 1111 0000 0000 — SWI 0
具體的呼叫號存放在暫存器r7中.

[OABI] 機器碼:
1101 1111 vvvv vvvv — SWI immed_8
呼叫號進行轉換以後得到指令中的立即數。立即數=呼叫號 | 0x900000

既然需要相容兩種方式的呼叫,我們在程式碼上就要分開處理。首先要獲取SWI指令判斷是EABI還是OABI,如果是EABI,可從r7中獲取呼叫號。如果是OABI,則從SWI指令中獲取立即數,反向計算出呼叫號。

我們接著看hook system call前的函式,和hook system call後的函式:

圖片描述

在獲取了system call的number以後,我們可以進一步獲取個個引數的值,比如說write這個system call。在arm上,如果形參個數少於或等於4,則形參由R0,R1,R2,R3四個暫存器進行傳遞。若形參個數大於4,大於4的部分必須通過堆疊進行傳遞。而執行完函式後,函式的返回值會儲存在R0這個暫存器裡。

下面我們就來實際執行一下看看效果。我們先把target和hook1 push到 /data/local/tmp目錄下,再chmod 777一下。接著執行target。

圖片描述

我們隨後再開一個shell,然後ps獲取target的pid,然後使用hook1程式對target進行hook操作:

圖片描述

我們可以看到第一個SysCallNo是162,也就是sleep函式。第二個SysCallNo是4,也就是write函式,因為printf本質就是呼叫write這個系統呼叫來完成的。關於system call number對應的具體system call可以參考我在github上的reference資料夾中的systemcalllist.txt檔案,裡面有對應的列表。我們的hook1程式還對write的引數做了解析,比如1表示stdout,0xadf020表示字串的地址,19代表字串的長度。而返回值19表示write成功寫入的長度,也就是字串的長度。

整個過程用圖表達如下:

圖片描述

0x03 利用Ptrace動態修改記憶體

僅僅是用ptrace來獲取system call的引數和返回值還不能體現出ptrace的強大,下面我們就來演示用ptrace讀寫記憶體。我們在hook1.c的基礎上繼續進行修改,在write被呼叫之前對要輸出string進行翻轉操作。

我們在hookSysCallBefore()函式中加入modifyString(pid, regs.ARM_r1, regs.ARM_r2)這個函式:

圖片描述

因為write的第二個引數是字串的地址,第三個引數是字串的長度,所以我們把R1和R2的值傳給modifyString()這個函式:

圖片描述

modifyString()首先獲取在記憶體中的字串,然後進行翻轉操作,最後再把翻轉後的字串寫入原來的地址。這些操作用到了getdata()和putdata()函式:

圖片描述

getdata()和putdata()分別使用PTRACE_PEEKDATA和PTRACE_POKEDATA對記憶體進行讀寫操作。因為ptrace的記憶體操作一次只能控制4個位元組,所以如果修改比較長的內容需要進行多次操作。

我們現在執行一下target,並且在執行中用hook2程式進行hook:

圖片描述

哈哈,是不是看到字串都被翻轉了。如果我們退出hook2程式,字串又會回到原來的樣子。

0x04 利用Ptrace動態執行sleep()函式

上一節中我們介紹瞭如何使用ptrace來修改記憶體,現在繼續介紹如何用ptrace來執行libc .so中的sleep()函式。主要邏輯都在inject()這個函式中:

圖片描述

首先我們用ptrace(PTRACE_GETREGS, pid, NULL, &old_regs)來儲存一下暫存器的值,然後獲取sleep()函式在目標程序中的地址,接著利用ptrace執行sleep()函式,最後在執行完sleep()函式後再用ptrace(PTRACE_SETREGS, pid, NULL, &old_regs)恢復暫存器原來值。

下面是獲取sleep()函式在目標程序中地址的程式碼:

圖片描述

因為libc.so在記憶體中的地址是隨機的,所以我們需要先獲取目標程序的libc.so的載入地址,再獲取自己程序的libc.so的載入地址和sleep()在記憶體中的地址。然後我們就能計算出sleep()函式在目標程序中的地址了。要注意的是獲取目標程序和自己程序的libc.so的載入地址是通過解析/proc/[pid]/maps得到的。

接下來是執行sleep()函式的程式碼:

圖片描述

首先是將引數賦值給R0-R3,如果引數大於四個的話,再使用putdata()將引數存放在棧上。然後我們將PC的值設定為函式地址。接著再根據是否是thumb指令設定ARM_cpsr暫存器的值。隨後我們使用ptrace_setregs()將目標程序暫存器的值進行修改。最後使用waitpid()等待函式被執行。

編譯完後,我們使用hook3對target程式進行hook:

圖片描述

正常的情況是target程式每秒輸出一句話,但是用hook3程式hook後,就會暫停10秒鐘的時間,因為我們利用ptrace執行了sleep(10)在目標程式中。

0x05 利用Ptrace動態載入so並執行自定義函式

僅僅是執行現有的libc函式是不能滿足我們的需求的,接下來我們繼續介紹如何動態的載入自定義so檔案並且執行so檔案中的函式。邏輯大概如下:

儲存當前暫存器的狀態 -> 獲取目標程式的mmap, dlopen, dlsym, dlclose 地址 -> 呼叫mmap分配一段記憶體空間用來儲存引數資訊 –> 呼叫dlopen載入so檔案 -> 呼叫dlsym找到目標函式地址 -> 使用ptrace_call執行目標函式 -> 呼叫 dlclose 解除安裝so檔案 -> 恢復暫存器的狀態。

實現整個邏輯的函式 injectSo()的程式碼如下:

圖片描述
圖片描述

mmap()可以用來將一個檔案或者其它物件對映進記憶體,如果我們把flag設定為MAP_ANONYMOUS並且把引數fd設定為0的話就相當於直接對映一段內容為空的記憶體。mmap()的函式宣告和引數如下:

圖片描述

  • start:對映區的開始地址,設定為0時表示由系統決定對映區的起始地址。 length:對映區的長度。

  • prot:期望的記憶體保護標誌,不能與檔案的開啟模式衝突。我們這裡設定為RWX。

  • flags:指定對映物件的型別,對映選項和對映頁是否可以共享。我們這裡設定為:MAP_ANONYMOUS(匿名對映,對映區不與任何檔案關聯),MAP_PRIVATE(建立一個寫入時拷貝的私有對映。記憶體區域的寫入不會影響到原檔案)。

  • fd:有效的檔案描述詞。匿名對映設定為0。

  • off_toffset:被對映物件內容的起點。設定為0。

在我們使用ptrace_call(pid, mmap_addr, parameters, 6, &regs)呼叫完mmap()函式之後,要記得使用ptrace(PTRACE_GETREGS, pid, NULL, &regs); 用來獲取儲存返回值的regs.ARM_r0,這個返回值也就是對映的記憶體的起始地址。

mmap()對映的記憶體主要用來儲存我們傳給其他函式的引數。比如接下來我們需要用dlopen()去載入”/data/local/tmp/libinject.so”這個檔案,所以我們需要先用putdata()將”/data/local/tmp/libinject.so”這個字串放置在mmap()所對映的記憶體中,然後就可以將這個對映的地址作為引數傳遞給dlopen()了。接下來的dlsym(),so中的目標函式,dlclose()都是相同呼叫的方式,這裡就不一一贅述了。

我們再來看一下被載入的so檔案,裡面的內容為:

圖片描述

這裡我們不光使用printf()還使用了android debug的函式LOGD()用來輸出除錯結果。所以在編譯時我們需要加上LOCAL_LDLIBS := -llog。

編譯完後,我們使用hook4對target程式進行hook:

圖片描述

可以看到無論是stdout還是logcat都成功的輸出了我們的除錯資訊。這意味著我們可以通過注入讓目標程序載入so檔案並執行任意程式碼了。

0x06 小節

現在我們已經可以做到hook system call以及動態的載入自定義so檔案並且執行so檔案中的函式了,但離執行以及hook java層的函式還有一定距離。因為篇幅原因,我們的hook之旅就先進行到這裡,敬請期待一下篇《離別鉤 – Hooking》

文章中所有提到的程式碼和工具都可以在我的github下載到,地址是:https://github.com/zhengmin1989/TheSevenWeapons

0x07 參考資料

作者:蒸米@阿里聚安全,更多技術文章,請訪問阿里聚安全部落格