1. 程式人生 > >深入Android原始碼系列(二) HOOK技術大作戰

深入Android原始碼系列(二) HOOK技術大作戰


   漫天的標題黨的口水文打賞爆表,冷落了一群默默輸出高質量文章的人群。真正的技術文章能否得到認可? 
    本文講解內容有
    hook技術原理探究
    hook本程序方法
    hook跨程序的系統呼叫,方法
    so注入
    GOT完成so方法hook
    ELF檔案頭資訊
00
簡單描述下原理,當我們想去監聽func方法,如果我們按照程式碼去編寫,則會是如下格式:
void hook(){    //這裡加入我們特殊的處理    //呼叫真正的func方法    func();    //這裡加入我們特殊的處理}是這樣子來實現。
而上面的描述,如果用匯編表示,則會變成:將func方法的彙編程式碼的前幾個位元組,進行修改,修改成跳轉到hook方法的位置,然後在hook方法裡面處理,過程中呼叫func,最後退出來。這裡理論便是如此,實際中需要考慮很多問題,類似地址修改,查詢,堆疊平衡之類的內容。
引用網上的一張圖來說明:(原文地址

http://www.tuicool.com/articles/ju2i2ia

我們這裡來講,不會從零開始,我們直接拿到別人寫好的程式碼,進行修改,我們去看下怎麼實現,然後演示下如何使用它,裡面用到的哪些技術函式。下來我們開始準備工作:
所有測試程式碼,下載地址:連結:https://pan.baidu.com/s/1nu6gZKt 密碼:rtbt
我們使用https://github.com/ele7enxxh/Android-Inline-Hook 這個開源庫,來進行inline hook。

01
演示如何hook本程序的某個方法,具體的demo演示為:

這裡用到的方法為(關於編譯,直接給出我這邊的專案,自己去下載編譯吧,需要配置ndk即可)registerInlineHook 引數為:需要hook的原始方法,需要替換的方法,以及儲存下來hook的原始方法,這個是註冊進來,為hook做準備。inlineHook 這個是實行hook,真正的修改位置inlineUnHook 這個是unhook,撤銷動作我們這裡get_val 和 get_val_hook程式碼為:


我們編譯執行下此程式碼,看下效果

我們看到,這裡的第二次執行get_val  的時候,列印的是hook getVal,於是我們的hook方法是有效的。我們現在知道了,此庫可以使用,程式碼沒有什麼問題,那麼我們現在來研究下,這裡的幾個方法registerInlineHook 引數為:需要hook的原始方法,需要替換的方法,以及儲存下來hook的原始方法,這個是註冊進來,為hook做準備。inlineHook 這個是實行hook,真正的修改位置inlineUnHook 這個是unhook,撤銷動作一個個來,先來看下registerInlineHook

這裡主要完成的方法為:isExecutableAddr 是否選擇的hook方法,在可執行區域,因為我們要hook的是方法,程式碼段,是必須可執行的。findInlineHookItem 這個是從當前記錄的列表裡面去找下,我們之前hook過沒有,如果有,直接給結果即可。addInlineHookItem 如果之前沒hook過,加入列表裡面,配置基礎資訊。relocateInstruction 這個完成程式碼的重定位。這裡有個關鍵的地方trampoline_instructions的mmap方法,這個就是要在我們程序的記憶體空間,對映一段記憶體,作為我們的程式碼段,來寫入我們的修改跳轉的邏輯程式碼。我們看下isExecutableAddr 

這個就是從當前程序的maps列表裡面,去找下,我們給出的hook方法的地址,是否落在r-xp ,這裡關鍵的x執行位,如果在,則返回true,否則false。relocateInstruction 裡面完成的內容,我們看下:

這裡依據地址的對齊方式,來檢查是Thumb 還是arm指令,對應的修改彙編指令是不一樣的,我們這裡來看arm的那條case。

這裡簡單講下,就是我們在上面mmap的一段程式碼記憶體中,寫入一堆指令,指令會在hook的原始方法最前面,將後面的執行,轉移到我們的自定義的hook方法上來,然後執行完畢,直接退回到原始方法的返回位置,如此來跳過原始方法,達到我們的hook目的。當然,這裡的註冊,是沒有做真正的hook動作,只是完成了自己構造了一份程式碼,實現了我們的hook跳轉程式碼.
具體實施,是在inlineHook方法裡面。於是我們看下這個方法

這裡的邏輯為:先去看下是否註冊過,如果沒註冊,返回。如果hook了,返回。如果是註冊過,沒有hook,做真正的hook動作。這裡具體為:freezedoInlineHookunFreeze那麼我們一個個來看下:freeze 主要核心的呼叫方法:getAllTids 獲取所有執行緒任務ptrace 跟蹤執行緒 撤銷跟蹤processThreadPC 處理程式碼PTRACE_GETREGS PTRACE_SETREGS 兩個引數,來實現修改暫存器。

doProcessThreadPC 完成暫存器的實際修改,主要完成,當我們當前的pc位置在我們原始函式的執行邊界上,我們即修改到目標的pc位置上。doInlineHook的主要方法:

mprotect 完成將原始方法的程式碼段記憶體,改成可讀可寫。然後修改此段記憶體,讓跳轉到真正的hook方法上去。unFreeze 解除鎖定,繼續執行。
如上,我們講解了整個的inline hook的執行過程,以及實現原理。
02
如果只到這裡,那有什麼意思。上面的內容,主要完成了本程序的hook方案,但是如果我們想hook一個新的程序,不屬於我們自己的程序的話,該如何來做呢?我們參照https://github.com/ManyFace/AndroidInjection專案來講解。
第一個例子,hook systemcall
需要修改的是這裡的printf方法:printf //printf() eventually call write(fd,str,size)


我們看下我們遠端寫入的程式碼:

拿到對應的程序pid //pid_t target_pid = atoi(argv[1]);ptrace(PTRACE_ATTACH 跟蹤程序ptrace(PTRACE_SYSCALL 追蹤直到發生了系統呼叫intercept_syscall 實現修改syscall

這裡具體為:get_syscall_number 從暫存器裡面拿到syscall num  具體是 ptrace(PTRACE_GETREGS 拿到暫存器值,我們的swi在pc上面的地址儲存。如果syscall_number == __NR_write ,拿到這裡的暫存器資訊,拿到對應的字串(peek_data),修改後(reverse),回寫回去(poke_data)如此,則完成了hook syscall
04
第二個例子,hook 具體的某個方法
(這裡它demo演示的比較隨意,並且有些小問題,我們講下原理即可)
我們向遠端程序注入程式碼的時候,根本的實現方法是使用poke_data 向具體的遠端程序寫入一段程式碼,修改對方程序的某段地址彙編指令。相比hook本身的程式碼來說,hook遠端程序麻煩點在於,如果需要呼叫某個方法的時候,是需要找到對方遠端程序裡面的對應方法的位置,然後向遠端地址寫入一段程式碼,讓跳轉到對應的遠端hook方法。(實質就是要解決一個函式在兩個程序裡面的方法具體位置,我們要在遠端程序執行一個方法,那這個方法的地址,不能是我們這個程序的某方法的地址,而需要的是對方遠端程序裡面 某方法的地址)我們直接看這裡的hook方式inject_process 

這裡用了一個get_module_base 這個含義在於,我們要找遠端地址,某個so庫的載入地址poke_data(pid, base_addr+0xcf8,inject_code, strlen(inject_code)); 這裡的0xcf8是我們要hook的方法在so的偏移位置。這裡要說的就是,這裡用了硬編碼0xcf8. 其優雅的方案是怎麼做呢? 我們使用dlopen 開啟so,dlsym找到方法的地址,用找到的地址,減去載入的起始地址,便是偏移地址,這裡便是0xcf8這裡我們看下char inject_code[] = “\x02\x20\xF7\x46”; //mov r0,#2; mov pc,lr.我們真正演示的時候,發現注入進去後,當前遠端程序就退出了。而退出的原因就在這裡的\xF7\x46  //mov pc,lr. 這句話將我們pc指向了lr,直接引向了退出迴圈了,程序則結束了。 我們可以修改下,將\xF7\x46 去掉即可。演示如下

新開一個視窗,執行target
原來的視窗
這裡我們可以看到我們的值改成了1
05
第三種方案,GOT hook關於GOT,參考http://www.cnblogs.com/xingyun/archive/2011/12/10/2283149.html要注入的程序程式碼:

這裡目標為:將libhook.so扔進目標程序,然後讓目標程序呼叫libhook裡面的hook_init,裡面找到我們要hook的方法(GOT),然後hook此方法。show_msg裡面呼叫了strlen strcmp,我們就是要hook這兩個方法。我們看下注入的程式碼實現:

如此看來,我們要關注的就是inject_remote_process 這個方法了。
引數含義:remote_pid 遠端程序pidlibrary_path 需要注入遠端的lib庫remote_hookinit_func 遠端hook init 方法hooking_remote_funcs_name 遠端hook的方法列表funcs_count 方法數目我們先完整的看個程式碼,然後我們慢慢解釋




ptrace_attach 跟蹤遠端程序ptrace_getregs 獲取對應程序的暫存器列表mmap_remote_addr =get_remote_addr(xxx,mmap) 去查詢mmap方法在遠端程序的地址(具體方法,我們本程序的mmap對應方法的地址減去mmap對應so的載入地址,拿到相對地址,然後從遠端程序找到mmap對應的so的載入地址,相加拿到mmap在遠端程序的方法地址)call_mmap_remote 呼叫mmap方法,拿到返回的值,傳入regsdlopen_remote_addr = get_remote_addr (xxx,dlopen)找到dlopen方法在遠端程序的地址ptrace_pokedata  向遠端的mmap地址上面,寫入一段程式碼,為呼叫遠端的dlopen做準備ptrace_call 呼叫遠端方法,這裡是dlopen,開啟的是libhook.sodlsym_remote_addr = get_remote_addr(xxx,dlsym) 去查詢dlsym方法在遠端程序的地址找到”hook_init”方法的地址,後面緊跟著就是呼叫遠端的hook_init 方法hook_init完成查詢需要替換的方法,然後載入libtraget.so,找到這個載入之後,裡面的strlen地址,傳遞回來ptrace_peekdata 讀取返回值 ,拿到hook_strlen 的地址  和 strlen的地址ptrace_pokedata 修改,將遠端程序裡面 strlen 的地址改到hook_strlen地址即可,完成本次修改。ptrace_setregs 恢復現場 ptrace_detach 結束跟蹤,釋放遠端程序,修改完成。具體每一個方法,不再繼續講解了,我們可以自己下去看。核心的技術點在於,自己構造一段程式碼,作為跳轉hook,  然後將之前的程式碼地址進行修改,改到hook的位置。相比較本程序的hook,跨程序主要解決的是我們每個方法的呼叫,都是需要找到遠端程序的具體方法的地址,主要麻煩點就在這裡,同時GOT是為了解決so裡面的地址修正,對於匯入的外部符號,系統預設是未賦值的,我們要做的就是找到未賦值的對應方法的載入位置在這裡塞入我們的hook方法,替換掉系統預設的賦值,完成hook 遠端so的方法
原理講完了,發現測試上面這段程式碼的時候,出異常了。。why??
通過定位,最終找到程式碼查詢GOT的方案,用的是dlopen的返回值,而android4.4之後,將dlopen的返回值,改成了handle,於是沒法強轉成soinfo,也就沒法通過這個途徑開啟符號表了。於是我們要換成ELF解析工具,來處理這裡的邏輯。此demo就此作廢,不過原理已經掌握。
於是又找來一份程式碼https://github.com/shunix/AndroidGotHook這次先驗證了,本身的原理性知識沒有問題,在檢視測試之後,發現仍然異常。。no,why???但我們堅信,此方案是可行的,於是閱讀程式碼,除錯,終於定位到出錯原因,修改後,測試通過。

我們的目標為,修改遠端程序裡面,printf的GOT的資料,改成我們hook的新的my_printf方法(這個方法也在一個so裡面)。遠端程序程式碼為:

my_printf  的so原始碼為:

注入的程式碼為:

我們當前的流程為:
使用GetPid 方法找到程序idInjectLibrary 將我們的libhook.so扔到遠端程序裡面,載入起來CallDlsym 從遠端的程序裡面,找到我們注入的so(libhook.so)裡面的my_printf 地址GetRemoteFuctionAddr 找到遠端程序裡面printf的地址PatchRemoteGot 到遠端程序裡面,去找下當前傳入的要注入的elf檔案,原始要替換的方法(printf),從elf檔案的符號地址裡面,解析出來printf的GOT位置,在此處將hook的地址寫入,完成hook我們具體看下程式碼:InjectLibrary

PtraceAttach我們就不講解了CallDlopen 在遠端開啟一個動態庫,具體的為:GetRemoteFuctionAddr 在遠端程序上,找到dlopen的地址CallMmap 在遠端地址上mmap一段地址PtraceWrite 在mmap的地址上,寫入引數CallRemoteFunction呼叫mmap方法,返回handle控制代碼CallMunmap umap即可InjectLibrary講解完成,主要完成了查詢dlopen,然後傳入so的檔案地址,使用dlopen將其載入起來,返回handle後面再找符號需要。 CallDlsym(pid, so_handle, “my_printf”); 查詢my_printf 在遠端程序的真實地址值。引數pid 遠端程序idso_handle上面dlopen的控制代碼my_printf要找的符號地址完成找到my_printf在遠端程序的方法地址GetRemoteFuctionAddr(pid, LIBC_PATH, (long)printf);找到原始的printf的地址 PatchRemoteGot(pid,     target_library_path,     original_function_addr,     hook_fuction_addr);pid 遠端程序idtarget_library_path 目標庫original_function_addr 原始方法地址hook_fuction_addr hook方法地址

這裡原理為:在pid的程序上面,從map表裡面找到目標庫,拿到基地址,然後使用ELF格式解析,將目標庫的檔案頭拿到,解析裡面的.got節,去匹配裡面的每個地址(.got儲存了所有匯入的符號真實地址),如果跟我們找到的原始地址一致,就說明此處需要hook,替換掉原始的地址記錄下來,然後我們直接修改即可。
看下演示效果:將編譯出來的libhook.so扔進system/lib下面將target和got-hook放在system/bin下面然後執行:

adb shell target

adb shell got-hook target /system/lib/libhook.so /system/bin/target
結果為:

執行變成了hello(我們my_printf方法裡面內容)
這裡引數的 含義為
got-hook執行的程式target注入的程式名字 /system/lib/libhook.so 向target注入的so/system/bin/target 替換這裡面的printf為my_printf我們簡單說下/system/bin/target的執行過程,系統將/system/bin/target載入進入記憶體,對於裡面的printf方法(具體就是匯入符號),會假定是從一個偏移地址去取printf的真實地址,而這個真實地址,就是我們系統載入/system/bin/target的時候,找到printf的地址,放置在這個偏移位置,而這塊偏移地址區域,叫做GOT,於是我們把放置printf的真實地址的這個偏移地址上,放入我們hook的方法,那麼/system/bin/target在執行的時候,找printf方法的時候,從GOT表拿出來的地址,就會是我們的hook方法地址,於是乎,我們就完成了hook
06
最後再來一個做個結束:注入一個

so到遠程序裡面,然後hook住裡面的printf方法.我們看注入的方案:


InjectLibrary實現dlopen,將libinject-hook.so庫在遠端程序開啟.我們看下libinject-hook.so的原始碼:
這裡用到一個屬性attribute((constructor)) ,這個標記的方法,會在so載入之後,立即呼叫。於是這裡就執行了before_main方法,完成hook printf的動作。由於知識點前面都講過,因此不再煩述,讀者自行領悟。

本文結束。

更多精彩,敬請期待。

更多內容,關注微信公眾號:code_gg_home。
加微信 code_gg_boy  進入程式碼GG交流群

參考文件: