安卓動態調試七種武器之離別鈎 – Hooking(上)
作者:蒸米@阿里聚安全
0x00 序
隨着移動安全越來越火,各種調試工具也都層出不窮,但因為環境和需求的不同,並沒有工具是萬能的。另外工具是死的,人是活的,如果能搞懂工具的原理再結合上自身的經驗,你也可以創造出屬於自己的調試武器。因此,筆者將會在這一系列文章中分享一些自己經常用或原創的調試工具以及手段,希望能對國內移動安全的研究起到一些催化劑的作用。
目錄如下:
安卓動態調試七種武器之長生劍 - Smali Instrumentation
安卓動態調試七種武器之離別鈎 – Hooking (上)
安卓動態調試七種武器之離別鈎 – Hooking (下)
安卓動態調試七種武器之碧玉刀- Customized DVM
安卓動態調試七種武器之多情環- Customized Kernel
安卓動態調試七種武器之霸王槍 - Anti Anti-debugging
安卓動態調試七種武器之拳頭 - Tricks & Summary
文章中所有提到的代碼和工具都可以在我的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, ®s)調用完mmap()函數之后,要記得使用ptrace(PTRACE_GETREGS, pid, NULL, ®s); 用來獲取保存返回值的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 參考資料
- Playing with Ptrace http://www.linuxjournal.com/article/6100
- System Call Tracing using ptrace
- 古河的Libinject http://bbs.pediy.com/showthread.php?t=141355
作者:蒸米@阿里聚安全,更多技術文章,請訪問阿里聚安全博客