Android Hook技術學習——常見的Hook技術方案總結
前言
最近一段時間在研究Android加殼和脫殼技術,其中涉及到了一些hook技術,于是將自己學習的一些hook技術進行了一下梳理,以便后面回顧和大家學習。
本文第二節主要講述編譯原理,了解編譯原理可以幫助進一步理解hook技術。
本文第三節主要講述NDK開發的一些基礎知識。
本文第四節主要講述各類hook技術的實現原理。
本文第五節主要講述各hook技術的實現步驟和案例演示。
編譯原理
1.編譯過程

我們可以借助gcc來實現上面的過程:
預處理階段:預處理器(cpp)根據以字符#開頭的命令修給原始的C程序,結果得到另一個C程序,通常以.i作為文件擴展名。主要是進行文本替換、宏展開、刪除注釋這類簡單工作。
命令行:gcc -E hello.c hello.i
編譯階段:將文本文件hello.i翻譯成hello.s,包含相應的匯編語言程序。
匯編階段:將.S文件翻譯成機器指令,然后把這些指令打包成一種可重定位目標程序的格式,并把結果保存在目標文件.o中(匯編——>機器)。
命令行:gcc -c hello.c hello.o
鏈接階段:hello程序調用了printf函數,鏈接器(Id)就把printf.o文件并入hello.o文件中,得到hello可執行文件,然后加載到存儲器中由系統執行。
函數庫包括靜態庫和動態庫
靜態庫:編譯鏈接時,把庫文件代碼全部加入可執行文件中,運行時不需要庫文件,后綴為.a。
動態庫:編譯鏈接時,不加入,在程序執行時,由運行時鏈接文件加載庫,這樣節省開銷,后綴為.so。(gcc編譯時默認使用動態庫)
再經過匯編器和連接器的作用后輸出一個目標文件,這個目標文件為可執行文件。
這里我們對編譯過程做了一個初步的講解,詳細大家可以去看《程序員的自我修養——鏈接、裝載與庫》一書,下面我們主要介紹鏈接方式、鏈接庫、可執行目標文件幾個基本概念。
(1)鏈接方式
靜態鏈接:
對于靜態庫,程序在編譯鏈接時,將庫的代碼鏈接到可執行文件中,程序運行時不再需要靜態庫。在使用過程中只需要將庫和我們的程序編譯后的文件鏈接在一起就可形成一個可執行文件。
缺點:
1、內存和磁盤空間浪費:靜態鏈接方式對于計算機內存和磁盤的空間浪費十分嚴重。假如一個c語言的靜態庫大小為1MB,系統中有100個需要使用到該庫文件,采用靜態鏈接的話,就要浪費進100M的內存,若數量再大,那浪費的也就更多。
2.更新麻煩:比如一個程序20個模塊,每個模塊只有1MB,那么每次更新任何一個模塊,用戶都得重新下載20M的程序。
動態鏈接:
由于靜態鏈接具有浪費內存和模塊更新困難等問題,提出了動態鏈接。基本實現思想是把程序按照模塊拆分成各個相對獨立部分,在程序運行時才將他們鏈接在一起形成一個完整的程序,而不是像靜態鏈接那樣把所有的程序模塊都鏈接成一個單獨的可執行文件。所以動態鏈接是將鏈接過程推遲到了運行時才進行。
例子:
同樣,假如有程序1,程序2,和Lib.o三個文件,程序1和程序2在執行時都需要用到Lib.o文件,當運行程序1時,系統首先加載程序1,當發現需要Lib.o文件時,也同樣加載到內存,再去加載程序2當發現也同樣需要用到Lib.o文件時,則不需要重新加載Lib.o,只需要將程序2和Lib.o文件鏈接起來即可,內存中始終只存在一份Lib.o文件。

優點:
① 毋庸置疑的就是節省內存;
② 減少物理頁面的換入換出;
③ 在升級某個模塊時,理論上只需要將對應舊的目標文件覆蓋掉即可。新版本的目標文件會被自動裝載到內存中并且鏈接起來;
④ 程序在運行時可以動態的選擇加載各種程序模塊,實現程序的擴展。
(2)鏈接庫
我們在鏈接的過程中,一般會鏈接一些庫文件,主要分為靜態鏈接庫和動態鏈接庫。靜態鏈接庫一般為Windows下的.lib和Linux下的.a,動態鏈接庫一般為Windows下的.dll和Linux下的.so,這里考慮到我們主要是對so文件hook講解,下面我們主要介紹linux系統下的情況。
靜態庫:
命名規范為libXXX.a庫函數會被連接進可執行程序,可執行文件體積較大可執行文件運行時,不需要從磁盤載入庫函數,執行效率較高庫函數更新后,需要重新編譯可執行程序
動態庫:
命名規范為libXXX.so庫函數不被連接進可執行程序,可執行文件體積較小可執行文件運行時,庫函數動態載入使用靈活,庫函數更新后,不需要重新編譯可執行程序
2.可執行文件(ELF)
目前PC平臺比較流行的可執行文件格式主要是Windows下的PE和Linux下的ELF,它們都是COFF格式的變種。在Windows平臺下就是我們比較熟悉的.exe文件,而Linux平臺下現在便是統稱的ELF文件。這里我們主要介紹一下Linux下的ELF文件。
ELF文件的類型:
可重定位目標文件:包含二進制代碼和數據,其形式可以和其他目標文件進行合并,創建一個可執行目標文件。比如linux下的.o文件。
可執行目標文件:包含二進制代碼和數據,可直接被加載器加載執行。比如/bin/sh文件。
共享目標文件:可被動態的加載和鏈接。比如.so文件。
ELF文件的結構:
elf文件在不同的平臺上有不同的格式,在Unix和x86-64 Linux上稱ELF:
(1)ELF文件結構
目標文件既要參與程序鏈接,又要參與程序執行:

(1)文件開始處:是一個ELF頭部(ELF Header),用來描述整個文件的組織。節區部分包含鏈接視圖的大量信息:指令、數據、符號表、重定位信息等。
(2)程序頭部表(Program Header Table):如果存在的話,會告訴系統如何創建進程映像。用來構造進程映像的目標文件必須具有程序頭部表,可重定位文件不需要這個表。
(3)節區頭部表(Section Header Table):包含了描述文件節區的信息,每個節區在表中都有一項,每一項給出諸如節區名稱、節區大小這類信息。用于鏈接的目標文件必須包含節區頭部表,其他目標文件可以有,也可以沒有這個表。
下面我們來從分別從連接視角和程序執行的視角來看ELF文件:

ELF Header:描述了描述了體系結構和操作系統等基本信息并指出Section Header Table和Program Header Table在文件中的什么位置。
Program Header Table: 保存了所有Segment的描述信息;在匯編和鏈接過程中沒有用到,所以是可有可無的。
Section Header Table:保存了所有Section的描述信息;Section Header Table在加載過程中沒有用到,所以是可有可無的。
下面我們來看一張更加詳細的ELF結構圖:

從中我們可以詳細的知道ELF文件各個字段的含義,其他字段的含義如下圖:

(2)GOT和PLT
上面我們簡單的分析了ELF的文件結構,而這里我們介紹一下其中兩個重要的節表GOT(全局偏移表)和PLT(程序鏈接表)。
首先,我們需要理解為什么需要GOT表和PLT表。
經過上面的分析,我們知道程序在經歷了編譯流程后,就來到了鏈接過程,鏈接過程就是將一個或者多個中間文件(.o文件)通過鏈接器將它們鏈接成一個可執行文件,主要要完成以下事情:
① 各個中間文之間的同名section合并。
② 對代碼段,數據段以及各符號進行地址分配。
③ 鏈接時重定位修正。
但是當我們程序運行起來,glibc動態庫也裝載了,函數地址也確定了,那我們程序如何去調用動態庫中的函數呢,這個時候就需要理解一下重定位的概念:
重定位:
1.鏈接重定位:將一個或多個中間文件(.o文件)通過鏈接器將它們鏈接成一個可執行文件,一般分為兩種情況:
(1)如果是在其他中間文件中已經定義了的函數,鏈接階段可以直接重定位到函數地址,比如我們從頭文件訪問另一個函數。
(2)如果是在動態庫中定義了的函數,鏈接階段無法直接重定位到函數地址,只能生成額外的小片段代碼,也就是PLT表,然后重定位到該代碼片段。
2.運行重定位:運行后加載動態庫,把動態庫中的相應函數地址填入GOT表,由于PLT表是跳轉到GOT表的,這就構成了運行時重定位。
3.延遲重定位:只有動態庫函數在被調用時,才會進行地址解析和重定位工作,這時候動態庫函數的地址才會被寫入到GOT表項中。
這里我們就可以明白流程,程序在加載動態庫中函數時,需要兩部分:
需要存放外部函數的代碼段表(PLT表);
存放函數地址的數據表(GOT表)。
這里我用一個實例加深大家的理解,例如程序在鏈接時發現scanf定義在動態庫時,鏈接器生成一小段代碼scanf_stub,這就是我們的PLT表,然后scanf_stub地址取代原來的scanf,因此程序此時就轉換為鏈接scanf_stub,這個過程叫鏈接重定位,然后在運行時動態庫glibc中的scanf_libc地址填入GOT表,然后程序通過scanf_stub訪問到scanf_libc,這個過程叫運行時重定位。
講到這里,其實我們對PLT和GOT表的作用已經了解了,PLT(程序鏈接表)就是鏈接時需要存放外部函數的數據段,GOT(全局偏移表)是存放函數地址的代碼
PLT和GOT的結構:
PLT表中的第一項為公共表項,剩下的是每個動態庫函數為一項,每項PLT都從對應的GOT表項中讀取目標函數地址。
GOT表中前3個為特殊項,分別用于保存 .dynamic段地址、本鏡像的link_map數據結構地址和_dl_runtime_resolve函數地址。
dynamic段:提供動態鏈接的信息,例如動態鏈接中各個表的位置。
link_map:已加載庫的鏈表,由動態庫函數的地址構成的鏈表。
_dl_runtime_resolve:在第一次運行時進行地址解析和重定位工作。
根據操作系統規定不允許修改代碼段,只能修改數據段,所以PLT表是不變的,GOT表是可以改變的。

因此我們可以看一下程序調用PLT表和GOT表的邏輯。

最后我們來詳細看一下程序調用函數的變化流程:
程序第一次調用函數時:

此時第一步由函數調用跳入到PLT表中,然后第二步PLT表跳到GOT表中,可以看到第三步由GOT表回跳到PLT表中,這時候進行壓棧,把代表函數的ID壓棧,接著第四步跳轉到公共的PLT表項中,第5步進入到GOT表中,然后_dl_runtime_resolve對動態函數進行地址解析和重定位,第七步把動態函數真實的地址寫入到GOT表項中,然后執行函數并返回,此時GOT表中就存放了函數的真實地址。
之后函數被調用時:

第一步還是由函數調用跳入到PLT表,但是第二步跳入到GOT表中時,由于這個時候該表項已經是動態函數的真實地址了,所以可以直接執行然后返回。
NDK基礎知識
這里我們主要介紹Android中的so文件加載的原理,為后面hook技術講解做鋪墊:
1.Android so文件的類型
NDK開發的so不再具備跨平臺特性,需要編譯提供不同平臺支持。

我們從官網可以得知so文件在不同架構下也不同,這里依次對應arm32位和64位,x86_32位和64位。
我們可以使用指令查看我們手機的架構:
adb shellcat /proc/cpuinfo

2.so文件加載
Android中我們通常使用系統提供的兩種API:System.loadLibrary或者System.load來加載so文件:
//加載的是libnative-lib.so,注意的是這邊只需要傳入"native-lib"System.loadLibrary("native-lib");//傳入的是so文件完整的絕對路徑System.load("/data/data/應用包名/lib/libnative-lib.so")
System.loadLibrary()和System.load()的區別:
(1)loadLibray傳入的是編譯腳本指定生成的so文件名稱,一般不需要包含開頭的lib和結尾的.so,而load傳入的是so文件所在的絕對路徑。
(2)loadLibrary傳入的不能是路徑,查找so時會優先從應用本地路徑下(/data/data/${package-name}/lib/arm/)進行查找,不存在的話才會從系統lib路徑下(/system/lib、/vendor/lib等)進行查找;而load則沒有路徑查找的過程。
(3)load傳入的不能是sdcard路徑,會導致加載失敗,一般只支持應用本地存儲路徑/data/data/${package-name}/,或者是系統lib路徑system/lib等這2類路徑。
(4)loadLibrary加載的都是一開始就已經打包進apk或系統的so文件了,而load可以是一開始就打包進來的so文件,也可以是后續從網絡下載,外部導入的so文件。
(5)重復調用loadLibrar,load并不會重復加載so,會優先從已加載的緩存中讀取,所以只會加載一次。
(6)加載成功后會去搜索so是否有"JNI_OnLoad",有的話則進行調用,所以"JNI_OnLoad"只會在加載成功后被主動回調一次,一般可以用來做一些初始化的操作,比如動態注冊jni相關方法等。
源碼分析:
Android 6.0:
[System.java] java.lang.System:
public static void load(String pathName) { Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader()); } public static void loadLibrary(String libName) { Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());}
[Runtime.java] java.lang.Runtime:
void load(String absolutePath, ClassLoader loader) { if (absolutePath == null) { throw new NullPointerException("absolutePath == null"); } String error = doLoad(absolutePath, loader); if (error != null) { throw new UnsatisfiedLinkError(error); } }public void loadLibrary(String nickname) { loadLibrary(nickname, VMStack.getCallingClassLoader()); } void loadLibrary(String libraryName, ClassLoader loader) { if (loader != null) { String filename = loader.findLibrary(libraryName); if (filename == null) {...
我們對比了Android6.0下的System.load和System.loadLibrary:
我們可以發現System.loadLibrary()中會修改類加載器,這個在我們后面hook過程可能會報錯,而Runtime.loadLibray()中有重寫的方法,則可以正確實現。
Android 7.0:
[System.java] java.lang.System:
public static void load(String filename) { Runtime.getRuntime().load0(VMStack.getStackClass1(), filename); } public static void loadLibrary(String libname) { Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname); }
[Runtime.java] java.lang.Runtime:
synchronized void load0(Class fromClass, String filename) { if (!(new File(filename).isAbsolute())) { throw new UnsatisfiedLinkError( "Expecting an absolute path of the library: " + filename); } if (filename == null) { throw new NullPointerException("filename == null"); } String error = doLoad(filename, fromClass.getClassLoader()); if (error != null) { throw new UnsatisfiedLinkError(error); } } public void loadLibrary(String libname, ClassLoader classLoader) { java.lang.System.logE("java.lang.Runtime#loadLibrary(String, ClassLoader)" + " is private and will be removed in a future Android release"); loadLibrary0(classLoader, libname); }
我們可以發現不同版本的區別:
Android 6.0采用的是loadLibrary,6.0之后都采用的是loadLibrary0; 同理 load函數也一樣,6.0之后采用的是load0。
同時我們分析了loadLibrary0:
① classLoader存在時,通過classLoader.findLibrary(libraryName)來獲取存放指定so文件的路徑。
② classLoader不存在時,則通過getLibPaths()接口來獲取。
③ 最終調用nativeLoad加載指定路徑的so文件。
各類hook技術原理分析
hook技術就是指截獲進程對某個API函數的調用,使得API的執行流程轉向我們實現的代碼片段,從而實現我們要的功能,在Android中使用hook的方法有很多,常用的Xposed和frida hook技術、inlinehook技術、基于inline
hook的開源框架Sandhook、PLT/Got hook技術、以及當下模擬cpu的Unicorn的hook技術,下面我們將逐一介紹其原理。
1.Xposed hook技術
Xposed的基本原理,我在源碼編譯(3)——Xposed框架定制(https://bbs.pediy.com/thread-269627.htm)中已經給大家做了詳細的講解,其主要就是Android應用進程都是由 zygote 進程孵化而來,zygote對應的可執行程序就是app_process,posed 框架通過替換系統的 app_process 可執行文件以及虛擬機動態鏈接庫,讓 zygote 在啟動應用程序進程時注入框架代碼,進而實現對應用程序進程的劫持。
具體怎么實現hook技術,Xposed就是通過修改了Art虛擬機,將需要hook的函數注冊為Native函數,當執行這一函數時,虛擬機會優先執行Native函數,然后執行java函數,這樣就成功完成了函數的hook。

具體實現流程:
在 Android 系統啟動的時候, zygote 進程加載 XposedBridge 將所有需要替換的 Method 通過 JNI 方法 hookMethodNative 指向 Native 方法 xposedCallHandler , xposedCallHandler 在轉入 handleHookedMethod 這個 Java 方法執行用戶規定的 Hook Func。

dvmCallMethodV會根據accessFlags決定調用native還是java函數,因此修改accessFlags后,Dalvik會認為這個函數是一個native函數,便走向了native分支也就是說Xposed在對java方法進行hook時,先將虛擬機里面這個方法的Method的accessFlag改為native對應的值,然后將該方法的nativeFunc指向自己實現的一個native方法,這樣方法在調用時,就會調用到這個native方法,接管了控制權。
其他的就詳細參考上篇文章了。
2.Frida hook技術
frida 也是一種動態插樁工具,原理和Xposed hook一樣,也是把java method轉為native method,但是Art下的實現與Dalivk有所不同,這里就需要了解ART的運行機制,這里主要參考博客:Frida源碼分析。
ART 是一種代替 Dalivk 的新的運行時,它具有更高的執行效率。ART虛擬機執行 Java 方法主要有兩種模式:quick code 模式和 Interpreter 模式。
quick code 模式:執行 arm 匯編指令
Interpreter 模式:由解釋器解釋執行 Dalvik 字節碼
即使是在quick code模式中,也有類方法可能需要以Interpreter模式執行。反之亦然。解釋執行的類方法通過函數artInterpreterToCompiledCodeBridge的返回值調用本地機器指令執行的類方法;本地機器指令執行的類方法通過函數GetQuickToInterpreterBridge的返回值調用解釋執行的類方法。
這里引用博客中的一張圖:

如圖,對于一個native方法,ART虛擬機會先嘗試使用quickcode的模式去執行,并檢查ARTMethod結構中的entry_point_from_quick_compiledcode成員,這里分3種情況:
① 如果函數已經存在quick code, 則指向這個函數對應的 quick code的起始地址,而當quick code不存在時,它的值則會代表其他的意義;
② 當一個 java 函數不存在 quick code時,它的值是函數 artQuickToInterpreterBridge 的地址,用以從 quick 模式切換到 Interpreter 模式來解釋執行 java 函數代碼;
③ 當一個 java native(JNI)函數不存在 quick code時,它的值是函數 art_quick_generic_jni_trampoline 的地址,用以執行沒有quick code的 jni 函數。
因此,frida將一個java method修改jni mthod 顯然是不存在quick code,這時需要將entry_point_from_quick_compiledcode值修改為art_quick_generic_jni_trampoline 的地址。
總結,frida把java method改為jni method,需要修改ARTMethod結構體中的這幾個值:
accessflags = nativeentry_point_fromjni = 自定義代碼的入口entry_point_from_quick_compiledcode = art_quick_generic_jni_trampoline函數的地址entry_point_frominterpreter = artInterpreterToCompiledCodeBridge函數地址
3.inlinehook 技術
(1)基本原理
首先,我們先介紹一下什么是inline Hook:
inline Hook是一種攔截目標函數調用的方法,主要用于殺毒軟件、沙箱和惡意軟件。一般的想法是將一個函數重定向到我們自己的函數,以便我們可以在函數執行它之前和/或之后執行處理;這可能包括:檢查參數、填充、記錄、欺騙返回的數據和過濾調用。
hook是通過直接修改目標函數內的代碼來放置,通常是用跳轉覆蓋的前幾個字節,允許在函數進行任何處理之前重定向執行。
(2)inlineHook組成
hook:一個5字節的相對跳轉,在被寫入目標函數以鉤住它,跳轉將從被鉤住的函數跳轉到我們的代碼。
proxy:這是我們指定的函數(或代碼),放置在目標函數上的鉤子將跳轉到該函數(或代碼)。
Trampoline:用于繞過鉤子,以便我們可以正常調用鉤子函數。
(3)inlineHook實現

從示意圖上,我們可以這樣理解:
我們將目標函數MessgeBoxA()中的地址拿出來,然后我們用重寫的hook函數替換,然后我們執行完成之后,再回調到函數的執行地址出,保證程序的正常運行。

我們也可以通過上述示意圖去理解inlinehook的基本原理。
(4)Android-Inline-Hook和SandHook 技術
Android-lnline-Hook和SandHook都是基于inlinehook的兩種開源框架,在Android中對native層hook,使用的比較常見,前者主要針對32位進行hook,后者即可以用于32位也可以用于64位,但是官方表示32位并未進行測試,所以應用在64位上仍然更多。
4.PLT/GOT hook技術
前面我們已經很詳細的講述了全局偏移表(GOT)和動態鏈接表(PLT),Inline Hook能Hook幾乎所有函數,但是兼容性較差,不能達到上線標準,相比于inlineHook,GOT Hook兼容性比較好,可以達到上線標準,但是只能Hook基于GOT表的一些函數。
GOT/PLT Hook 主要是通過解析SO文件,將待hook函數在got表的地址替換為自己函數的入口地址,這樣目標進程每次調用待hook函數時,實際上是執行了我們自己的函數。
這里我們還要理解GOT表中含包含了導入表和導出表:
導出表指將當前動態庫的一些函數符號保留,供外部調用;
導入表中的函數實際是在該動態庫中調用外部的導出函數。
例如導入表存放的是一些其他so的函數,例如libc的open,而導出表存放的是一些共其他so調用的函數,比如自己so中編寫的函數,而無論導入表還是導出表基本都是針對導出函數,針對非導出函用inlinehook更常用一些。
5.Unicorn hook技術
Unicore是一款常優秀的跨平臺模擬執行框架,該框架可以跨平臺執行Arm, Arm64 (Armv8), M68K, Mips, Sparc, & X86 (include X86_64)等指令集的原生程序,通過模擬CPU,可以實現很多強大的功能,也可以實現函數級別的Hook。
參考資料:無名大佬文章Unicorn 在 Android 的應用(https://bbs.pediy.com/thread-253868.htm#msg_header_h1_7)
nicorn 內部并沒有函數的概念,它只是一個單純的CPU, 沒有HOOK_FUNCTION的callback,AndroidNativeEmu 中的函數級Hook 并不是真正意義上的Hook,它不僅能Hook存在的函數,還能Hook不存在的函數。AndroidNativeEmu 使用這種技術實現了JNI函數Hook、庫函數Hook。Jni函數是不存的,Hook它只是為了能夠用Python 實現 Jni Functions。有一些庫函數是存在的,Hook只是為了重新實現它。五
各類hook技術實操
1.Xposed hook實操
(1)環境安裝
Xposed環境安裝詳細可以參考我寫的Xposed系列文章,這里只是簡單的總結一下:
(1) 4.4以下Android版本安裝比較簡單,只需要兩步即可 1.對需要安裝Xposed的手機進行root 2.下載并安裝xposedInstaller,之后授權其root權限,進入app點擊安裝即可 但是由于官網不在維護,導致無法直接通過xposedinstaller下載補丁包(2)Android 5.0-8.0 由于5.0后出現ART,所以安裝步驟分成兩個部分:xposed.zip 和 XposedInstaller.apk,zip文件是框架主體,需要進入Recovery后刷入,apk文件用于Xposed管理 1.完成對手機的root,并刷入reconvery(比如twrp),使用Superroot 2.下載你對應的zip補丁包,并進入recovery刷入 3.重啟手機,安裝xposedInstaller并授予root權限即可 官網地址:https://dl-xda.xposed.info/framework/(3)由于Android 8.0后,Xposed官方作者沒有再對其更新,我們一般就使用國內大佬riyu的Edxposed框架 Magisk + riyu + Edxposed
這里我們用的是nexus5進行操作,簡單演示一下android6.0的Xposed安裝。
資源準備:
asop鏡像:https://developers.google.com/android/ota#hammerheadtwrp: https://twrp.me/xposed: https://dl-xda.xposed.info/framework/xposed installer https://repo.xposed.info/module/de.robv.android.xposed.installer
首先我們先下載n5鏡像,然后刷機,這里我們已經安裝就不再安裝了。
然后我們刷入 twrp-3.4.0-0-hammerhead.img
fastboot flash recovery twrp-3.4.0-0-hammerhead.img
然后我們就可以進入recovery模式了。
然后我們將Supersu拷貝進去,然后將Xposed-v89-sdk.zip拷貝進去。


然后我們進入recovery模式,將兩個文件依次刷入即可。
接下來我們安裝XposedInstall.apk,來管理Xposed。

如果我們開機后發現xposed框架沒有激活,嘗試再重啟一下,我們可以看見:

這樣我們的Xposed框架就成功安裝了。
(2)Xposed插件編寫
Xposed插件編寫的流程網上已經有很多了,這里我就簡單的講解一下。
基本流程:
(1)拷貝XposedBridgeApi.jar到新建工程的libs目錄;
(2)修改app目錄下的build.gradle文件,在AndroidManifest.xml中增加Xposed相關內容;
(3)新建hook類,編寫hook代碼;
(4)新建assets文件夾,然后在assets目錄下新建文件xposed_init,在里面寫上hook類的完整路徑。
首先,我們查找XposedBridgeApi.jar到新建工程的libs目錄:

然后,修改AndroidManifest.xml文件,在Application標簽下增加內容如下:
android:name="xposedmodule" //是否配置為Xposed插件,設置為true android:value="true"/> android:name="xposeddescription" //模塊名稱 android:value="模塊描述"/> android:name="xposedminversion" //最低版本號 android:value="54"/>
修改app目錄下的build.gradle文件:
進入app目錄下的build.gradle文件, compile fileTree(includes:['*.jar'],dir:'libs') 替換成 provided fileTree(includes:['*.jar'],dir:'libs')現在provided變為 compileOnly如果使用compile,可以正常編譯生成插件apk,但是當安裝到手機上后,xposed會報錯,無法正常工作
編寫hook類:
我們新建一個hook類xposed01,并實現接口IXposedHookLoadPackage,并實現里面關鍵方法handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam),該方法會在每個軟件被啟動的時候回調,所以一般需要通過目標包名過濾。
public class Xposed01 implements IXposedHookLoadPackage { @Override public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { if(loadPackageParam.packageName.equals("com.example.xposedlesson2")){ //判斷目標包名 XposedBridge.log("XLZH"+loadPackageParam.packageName); //打出包名的信息 Log.i("Xposed01",loadPackageParam.packageName); } }}
新建assets文件夾,然后在assets目錄下新建文件xposed_init,在里面寫上hook類的完整路徑。

這里面可以寫多個hook類,每個類寫一個,我們就完成了基本的Xposed框架的編寫。
最后勾選模塊,并重啟即可生效。


我們可以發現我們的xposed插件生效了,將我們系統中進程名打印出來了,說明hook成功了。
2.frida hook實操
(1)環境安裝
frida安裝,使用frida過程中我們可以安裝objection來進一步助力我們的hook工作,這個參考肉絲大佬的知識星球。
工具安裝(也可以選用其他版本):
pip install frida==12.8.0pip install frida-tools==5.3.0pip install objection==1.8.4
安裝成功后,查看frida和objection,確定版本正確。
frida --versionobjection --help
然后將frida_server推送到/data/local/tmp下,并啟動:(下載地址:https://github.com/frida/frida/releases)

(2)frida使用
然后我們就可以使用自動化工具objection和編寫js腳本進行hook了。
objection使用(詳細參考肉絲大佬github的教程):
常見的hook命令:objection -g com.android.settings explore //注入設置應用android hooking list activities //查看Activity,service相同android intent launch_activity com.android.settings.DisplaySettings //實現Activity跳轉android heap search instances com.android.settings.DisplaySettings //搜索類的實例android heap execute 0x2526 getPreferenceScreenResId //主動調用實例android hooking list classes //列出內存中所有類android hooking search methods display //列出內存中所有的方法android hooking watch class android.bluetooth.BluetoothDevice //hook相關類的所有方法android hooking watch class_method android.bluetooth.BluetoothDevice.getName --dump-args --dump-return --dump-backtrace //打印具體方法的參數、返回值、堆棧信息
編寫腳本:

啟動方式:
attach方式 frida -U com.example.test -l hook.jsspwan啟動 frida -U -f com.example.test -l demo1.js --no-pause
這樣我們就可以成功注入了,更加復雜的腳本編寫可以參考frida博客(https://github.com/hookmaster/frida-all-in-one)。
詳細案例實操,這里可以參考之前我的文章:Android惡意樣本分析——frida破解三層鎖機樣本(https://bbs.pediy.com/thread-269128.htm)。
3.inlinehook實操
這里我們分別實現基于inlinehook的兩個開源框架的具體使用方法。
(1)Android-lnine-Hook
開源地址:https://github.com/ele7enxxh/Android-Inline-Hook
該框只能針對32位的so文件進行hook。
我們對so文件進行hook時,可以按照如下步驟進行:
(1)查看so文件中的目標函數;
(2)編寫Xposed hook代碼,hook目標程序;
(3)編寫so層hook代碼,hook so中的函數地址;
(4)鏈接Java層和so層。
<1>編寫目標函數so文件

我們編寫案例,很明顯這里會打印失敗,然后我們使用inline-hook框架進行hook。
<2>導入文件
我們將該框架中如下文件導入我們的項目中。
我們需要使用inlineHook(https://github.com/ele7enxxh/Android-Inline-Hook)文件夾,并把這些文件直接拷貝到我們的工作目錄:


<3>修改配置文件

<4>編寫hook代碼
我們導入inlinehook頭文件就可以開始編寫hook代碼了。

編譯,報錯:

這是因為框架僅僅針對32位,所以我們需要在配置文件里面指定一下。

然后編譯,發現能正常通過。
首先聲明hook的就函數,然后編寫對應的新函數,這里我們hook的是strstr函數。

然后調用inlinehook進行hook。

最后我們發現就可以成功的hook。

代碼分析:
源碼解析: (1)dlopen:該函數將打開一個新庫,并把它裝入內存 void *dlopen(const char *filename, int flag); 參數1:文件名就是一個動態庫so文件,標志位:RTLD_NOW 的話,則立刻計算;設置的是 RTLD_LAZY,則在需要的時候才計算 libc.so是一個共享庫 ====================== 參數中的 libname 一般是庫的全路徑,這樣 dlopen 會直接裝載該文件;如果只是指定了庫名稱,在 dlopen 會按照下面的機制去搜尋: 根據環境變量 LD_LIBRARY_PATH 查找 根據 /etc/ld.so.cache 查找 查找依次在 /lib 和 /usr/lib 目錄查找。 flag 參數表示處理未定義函數的方式,可以使用 RTLD_LAZY 或 RTLD_NOW 。RTLD_LAZY 表示暫時不去處理未定義函數,先把庫裝載到內存,等用到沒定義的函數再說;RTLD_NOW 表示馬上檢查是否存在未定義的函數,若存在,則 dlopen 以失敗告終。 參考鏈接:https://blog.nowcoder.net/n/5b2c04bbcccf431e9f1ab34aa02717fe ======================= (2)dlsym:在 dlopen 之后,庫被裝載到內存。dlsym 可以獲得指定函數( symbol )在內存中的位置(指針)。 void *dlsym(void *handle,const char *symbol); 參數1:文件句柄 參數2:函數名
inlinehook框架使用正確姿勢:
我們對一個目標so文件hook步驟如下: (1)我們獲取so的handler,使用dlopen函數 void* libhandler = dlopen("libc.so",RTLD_NOW); (2)我們獲取hook目標函數的地址,使用dlsym函數 void* strstr_addr = dlsym(libhandler,函數名); (3)聲明原來的函數 void* (*oldmethod)(char*,char*); //這個格式需要參考hook的函數 聲明現在的函數 void* newmethod(char* a,char* b){ return (void *)oldmethod(a,b); } (3)使用registerInlinehook進行重定向,將hook函數地址重定向我們編寫的新函數上 (registerInlineHook((uint32_t) strstr_addr, (uint32_t) new_strstr, (uint32_t **) &old_strstr) != ELE7EN_OK //參數一:hook函數的地址 參數二:替換函數的地址 參數3:用來保存原來函數的地址 (5)我們判斷我們的hook操作是否成功,并且再次調用實現hook (inlineHook((uint32_t) strstr_addr) == ELE7EN_OK)
(2)SandHook實操
因為上面使用inline框架只支持32位,所以這里我們用SandHook實現對64位native函數的hook,sandHook(https://github.com/asLody/SandHook)既支持32位、又支持64位。
開源地址:https://github.com/asLody/SandHook
同樣是上面的案例,這里我們使用SandHook進行實操。
<1>導入文件
我們此路徑下SandHook/nativehook/src/main/cpp/文件全部導入。

<2>配置環境
首先我們在CMakeList中加入c文件。

然后在java代碼中修改導入的so庫。

直接編譯,報錯:

然后我們同理將配置信息加入:
cmake { arguments '-DBUILD_TESTING=OFF' cppFlags "-frtti -fexceptions -Wpointer-arith" abiFilters 'armeabi-v7a', 'arm64-v8a'}
再次編譯成功。
<3>編寫hook代碼
SandHook使用和上面inlinehook框架基本一樣。
首先聲明舊的函數,編寫新的函數(目標函數strstr)。

然后進行hook。

最后發現可以成功hook。

SandHook使用姿勢:
(1)導包,將SandHook中cpp文件夾下的包全部導入到項目中,并修改CMakeLists.txt中添加native.cpp, 修改java層導入so庫為sandHook-native(2)配置相關的環境 在配置文件build.gradle中配置 externalNativeBuild { cmake { arguments '-DBUILD_TESTING=OFF' cppFlags "-frtti -fexceptions -Wpointer-arith" abiFilters 'armeabi-v7a', 'arm64-v8a' } }(3)編譯可以成功通過(4)使用 const char * libc = "/system/lib64/libc.so"; old_fopen = reinterpret_cast<void *(*)(char *, char *)>(SandInlineHookSym(libc, "fopen", reinterpret_cast<void *>(new_fopen)));參數2:hook的函數 參數3:新的函數 添加原理hook舊函數的聲明void* (*old_fopen)(char*,char*);實現新的函數功能void* new_fopen(char* a,char* b){ __android_log_print(6,"windaa","I am from new open %s",a); return old_fopen(a,b);}(5)運行測試是否成功啟動
4.PLT/GOT hook實操
前面我們已經介紹了Got表hook的原理,下面我們實例操作一下導入表函數的hook。
參考博客:https://www.cnblogs.com/goodhacker/p/9306997.html
原理:
通過解析elf格式,分析Section header table找出靜態的.got表的位置,并在內存中找到相應的.got表位置,這個時候內存中.got表保存著導入函數的地址,讀取目標函數地址,與.got表每一項函數入口地址進行匹配,找到的話就直接替換新的函數地址,這樣就完成了一次導入表的Hook操作了。

首先,我們編寫demo。

我們編譯后使用010Editor打開libnative-lib.so。


然后我們用ida打開,并直接跳轉到該地址。

在got表中我們找到對應的mywin0函數。

<1>獲得so模塊的加載地址
我們可以使用/proc/self/maps去獲得so模塊的加載地址。
char line[1024]; int *start; int *end; int n=1; //1.拿到so的起始地址// 749e5d7000-749e5db000 r--p 000f4000 103:09 441 /system/bin/linker64// 749e5db000-749e5dc000 rw-p 000f8000 103:09 441 /system/bin/linker64 FILE *fd = fopen("/proc/self/maps","r"); while (fgets(line,sizeof(line),fd)){ if(strstr(line,"libnative-lib.so")){ __android_log_print(6,"windaa","%s",line); if(n==1){ start = reinterpret_cast<int *>(strtoul(strtok(line, "-"),NULL,16)); end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "),NULL,16)); } else{ strtok(line,"-"); end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "),NULL,16)); } n++; } }
<2>找到got表的位置
我們首先根據段頭找到section_header的首地址。


然后我們遍歷這個表就可以找到.got,然后根據got表地址再輪訓找到函數地址。
因為這種方法不能在內存中直接找到段頭,內存中會抹去段頭,所以我們可以通過加載so文件來定位。

<3>定位到節表的地址
然后我們來獲得節表的地址:
//讀取elf文件 Elf64_Ehdr ehd; int fp =open("/data/local/tmp/libnative-lib.so", O_RDONLY); if(fp == -1){ __android_log_print(4,"windaa","%s","error"); } //讀取elf文件的文件頭 read(fp,&ehd,sizeof(Elf64_Ehdr)); //讀取節表的地址 unsigned long shof = ehd.e_shoff; //讀取節表的數量 int shnum = ehd.e_shnum; //讀取每個節表的大小 int shsize = ehd.e_ehsize; //記錄一下str表的偏移,主要是獲取后面got的字符串值 int shstr = ehd.e_shstrndx;
我們打印一下此事shof的值,驗證一下節表的地址。

這里可以發現成功讀取。
<4>定位到got表的位置和函數位置
然后我們拿到字符串的偏移值進行定位到got表,再進一步定位到函數。
//2.拿到字符串表 Elf64_Shdr shdr; //定位字符串,節表地址加字符串表偏移×節表個數 lseek(fp,shof+shstr*shsize,SEEK_SET); //此時節表就定位到字符串表開頭 read(fp,&shdr,shsize); //分配一個字符串表大小 char* strtable = (char *)malloc(shdr.sh_size); __android_log_print(6,"windaa","shdrsize %p",shdr.sh_offset); //將字符串片指針移動到0x34104上 lseek(fp,shdr.sh_offset,SEEK_SET); read(fp,strtable,shdr.sh_size); //將指針移動到節表開頭 lseek(fp,shof,SEEK_SET); //遍歷查找到got for(int i=0;i //從節表開頭開始讀取字符串,每次讀取一個節表 read(fp,&shdr,shsize); //通過節表的索引找到字符串表中對應的值 if(strcmp(&strtable[shdr.sh_name], ".got")==0){ //定位到got表的地址 int* saddr = start+shdr.sh_addr/4; //整個got表的大小 int size = shdr.sh_size; //遍歷got表中的函數 for(int j=0;j8){ uint64_t value = *(uint64_t *)(saddr + j / 4); //找到mywind的地址 if(reinterpret_cast<uint64_t>(mywin0) == value) { __android_log_print(6,"windaa","value %p",value); //替換mywind地址 // 獲取當前內存分頁的大小 uint64_t page_size = getpagesize(); // 獲取內存分頁的起始地址(需要內存對齊) //page要保護的是函數的絕對地址,而不是相對地址 uint64_t entry_page_start = (uint64_t)(saddr+j/4) & (~(page_size - 1)); // 修改內存屬性為可讀可寫可執行 if(mprotect((uint64_t*)entry_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1){ __android_log_print(6,"windaa","%s","mprotect failed"); } value = (uint64_t)mywin1; //將mywind0函數的地址換成mywind1函數的地址 memcpy((saddr+j/4),&value,16); } } } }

這里我們就可以發現成功的hook。
got hook使用姿勢:
(1)使用/proc/self/maps去獲得so模塊的加載地址;
(2)使用ElfHeader找到Section的首地址,并計算offset和size來獲取StringTable;
(3)找到got表位置,計算其內存位置,并指針指向got表首地址;
(4)遍歷got表中的函數,找到要hook的函數,使用mprotect進行hook;
(5)將hook的函數地址替換為我們定義的函數地址。
5.Unicorn hook使用
這里我們簡單了解一下基于unicorn的框架Unidbg的hook使用
開源地址:https://github.com/zhkl0228/unidbg
這里我們直接idea將項目拉取下來,然后等下項目環境配置完成。

配置完成后,我們直接啟動里面的示例代碼查看hook效果。

這里unidbg使用了xHook,xHook是一種PLT hook的方式,當然這只是unidbg強大功能其中的一種,也是hook技術中一種,這里就簡單介紹到這,后續再詳細講如何使用。
unidbg使用參考博客:https://www.qinless.com/670六
實驗總結
本文從程序加載的原理出發,講解了當下常用的一些基本的hook方式和手段,后續對其中一些hook方式再次深入講解,實驗的一些樣本和代碼會上傳到知識星球和github,文章參考學了了很多大佬的文章和大佬星球的內容,參考文獻放在末尾,有什么問題,就請各位大佬一一指出了。