編輯:關於android開發
安卓動態調試七種武器之離別鉤 – Hooking(上)
作者:蒸米@阿裡聚安全
隨著移動安全越來越火,各種調試工具也都層出不窮,但因為環境和需求的不同,並沒有工具是萬能的。另外工具是死的,人是活的,如果能搞懂工具的原理再結合上自身的經驗,你也可以創造出屬於自己的調試武器。因此,筆者將會在這一系列文章中分享一些自己經常用或原創的調試工具以及手段,希望能對國內移動安全的研究起到一些催化劑的作用。
目錄如下:
安卓動態調試七種武器之長生劍 - Smali Instrumentation
安卓動態調試七種武器之孔雀翎 – Ida Pro
安卓動態調試七種武器之離別鉤 – Hooking (上)
安卓動態調試七種武器之離別鉤 – Hooking (下)
安卓動態調試七種武器之碧玉刀- Customized DVM
安卓動態調試七種武器之多情環- Customized Kernel
安卓動態調試七種武器之霸王槍 - Anti Anti-debugging
安卓動態調試七種武器之拳頭 - Tricks & Summary
文章中所有提到的代碼和工具都可以在我的github下載到,地址是:https://github.com/zhengmin1989/TheSevenWeapons
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進行系統的學習。
其實無論是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成功寫入的長度,也就是字符串的長度。
整個過程用圖表達如下:
僅僅是用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程序,字符串又會回到原來的樣子。
上一節中我們介紹了如何使用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)在目標程序中。
僅僅是執行現有的libc函數是不能滿足我們的需求的,接下來我們繼續介紹如何動態的加載自定義so文件並且運行so文件中的函數。邏輯大概如下:
保存當前寄存器的狀態 -> 獲取目標程序的mmap, dlopen, dlsym, dlclose 地址 -> 調用mmap分配一段內存空間用來保存參數信息 –> 調用dlopen加載so文件 -> 調用dlsym找到目標函數地址 -> 使用ptrace_call執行目標函數 -> 調用 dlclose 卸載so文件 -> 恢復寄存器的狀態。
實現整個邏輯的函數 injectSo()的代碼如下:
mmap()可以用來將一個文件或者其它對象映射進內存,如果我們把flag設置為MAP_ANONYMOUS並且把參數fd設置為0的話就相當於直接映射一段內容為空的內存。mmap()的函數聲明和參數如下:
在我們使用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文件並執行任意代碼了。
現在我們已經可以做到hook system call以及動態的加載自定義so文件並且運行so文件中的函數了,但離執行以及hook java層的函數還有一定距離。因為篇幅原因,我們的hook之旅就先進行到這裡,敬請期待一下篇《離別鉤 - Hooking》
文章中所有提到的代碼和工具都可以在我的github下載到,地址是:https://github.com/zhengmin1989/TheSevenWeapons
作者:蒸米@阿裡聚安全,更多技術文章,請訪問阿裡聚安全博客
android 簡單地設置Activity界面的跳轉動畫 動畫這一知識點算是水比較深了,主要在自定義動畫中可是大有文章,並且技術都會了後就需要看設計能力了。 當然這些不是
在Linux上分析死鎖問題的簡單方法死鎖 (deallocks): 是指兩個或兩個以上的進程(線程)在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們
Android安全攻防戰,反編譯與混淆技術完全解析(下) 在上一篇文章當中,我們學習了Android程序反編譯方面的知識,包括反編譯代碼、反編譯資源、以及重新打包等內
Android特效專輯(十二)——如何仿支付寶咻一咻功能實現波紋擴散特效 Android特效專輯(十二)——仿支付寶咻一咻功能實現波紋擴散特效