安卓進(jìn)階漲薪訓(xùn)練營,讓一部分人先進(jìn)大廠
大家好,我是皇叔,最近開了一個(gè)安卓進(jìn)階漲薪訓(xùn)練營,可以幫助大家突破技術(shù)&職場瓶頸,從而度過難關(guān),進(jìn)入心儀的公司。
詳情見文章:沒錯(cuò)!皇叔開了個(gè)訓(xùn)練營
來源:看雪論壇
看雪論壇作者ID:隨風(fēng)而行aa
最近一段時(shí)間在研究Android加殼和脫殼技術(shù),其中涉及到了一些hook技術(shù),于是將自己學(xué)習(xí)的一些hook技術(shù)進(jìn)行了一下梳理,以便后面回顧和大家學(xué)習(xí)。本文第二節(jié)主要講述編譯原理,了解編譯原理可以幫助進(jìn)一步理解hook技術(shù)。本文第三節(jié)主要講述NDK開發(fā)的一些基礎(chǔ)知識。本文第四節(jié)主要講述各類hook技術(shù)的實(shí)現(xiàn)原理。本文第五節(jié)主要講述各hook技術(shù)的實(shí)現(xiàn)步驟和案例演示。
1.編譯過程
我們可以借助gcc來實(shí)現(xiàn)上面的過程:
預(yù)處理階段:預(yù)處理器(cpp)根據(jù)以字符#開頭的命令修給原始的C程序,結(jié)果得到另一個(gè)C程序,通常以.i作為文件擴(kuò)展名。主要是進(jìn)行文本替換、宏展開、刪除注釋這類簡單工作。
命令行:gcc -E hello.c hello.i
編譯階段:將文本文件hello.i翻譯成hello.s,包含相應(yīng)的匯編語言程序。
匯編階段:將.S文件翻譯成機(jī)器指令,然后把這些指令打包成一種可重定位目標(biāo)程序的格式,并把結(jié)果保存在目標(biāo)文件.o中(匯編——>機(jī)器)。
命令行:gcc -c hello.c hello.o
鏈接階段:hello程序調(diào)用了printf函數(shù),鏈接器(Id)就把printf.o文件并入hello.o文件中,得到hello可執(zhí)行文件,然后加載到存儲器中由系統(tǒng)執(zhí)行。
函數(shù)庫包括靜態(tài)庫和動態(tài)庫
靜態(tài)庫:編譯鏈接時(shí),把庫文件代碼全部加入可執(zhí)行文件中,運(yùn)行時(shí)不需要庫文件,后綴為.a。
動態(tài)庫:編譯鏈接時(shí),不加入,在程序執(zhí)行時(shí),由運(yùn)行時(shí)鏈接文件加載庫,這樣節(jié)省開銷,后綴為.so。(gcc編譯時(shí)默認(rèn)使用動態(tài)庫)
再經(jīng)過匯編器和連接器的作用后輸出一個(gè)目標(biāo)文件,這個(gè)目標(biāo)文件為可執(zhí)行文件。
這里我們對編譯過程做了一個(gè)初步的講解,詳細(xì)大家可以去看《程序員的自我修養(yǎng)——鏈接、裝載與庫》一書,下面我們主要介紹鏈接方式、鏈接庫、可執(zhí)行目標(biāo)文件幾個(gè)基本概念。(1)鏈接方式
對于靜態(tài)庫,程序在編譯鏈接時(shí),將庫的代碼鏈接到可執(zhí)行文件中,程序運(yùn)行時(shí)不再需要靜態(tài)庫。在使用過程中只需要將庫和我們的程序編譯后的文件鏈接在一起就可形成一個(gè)可執(zhí)行文件。1、內(nèi)存和磁盤空間浪費(fèi):靜態(tài)鏈接方式對于計(jì)算機(jī)內(nèi)存和磁盤的空間浪費(fèi)十分嚴(yán)重。假如一個(gè)c語言的靜態(tài)庫大小為1MB,系統(tǒng)中有100個(gè)需要使用到該庫文件,采用靜態(tài)鏈接的話,就要浪費(fèi)進(jìn)100M的內(nèi)存,若數(shù)量再大,那浪費(fèi)的也就更多。2.更新麻煩:比如一個(gè)程序20個(gè)模塊,每個(gè)模塊只有1MB,那么每次更新任何一個(gè)模塊,用戶都得重新下載20M的程序。由于靜態(tài)鏈接具有浪費(fèi)內(nèi)存和模塊更新困難等問題,提出了動態(tài)鏈接。基本實(shí)現(xiàn)思想是把程序按照模塊拆分成各個(gè)相對獨(dú)立部分,在程序運(yùn)行時(shí)才將他們鏈接在一起形成一個(gè)完整的程序,而不是像靜態(tài)鏈接那樣把所有的程序模塊都鏈接成一個(gè)單獨(dú)的可執(zhí)行文件。所以動態(tài)鏈接是將鏈接過程推遲到了運(yùn)行時(shí)才進(jìn)行。同樣,假如有程序1,程序2,和Lib.o三個(gè)文件,程序1和程序2在執(zhí)行時(shí)都需要用到Lib.o文件,當(dāng)運(yùn)行程序1時(shí),系統(tǒng)首先加載程序1,當(dāng)發(fā)現(xiàn)需要Lib.o文件時(shí),也同樣加載到內(nèi)存,再去加載程序2當(dāng)發(fā)現(xiàn)也同樣需要用到Lib.o文件時(shí),則不需要重新加載Lib.o,只需要將程序2和Lib.o文件鏈接起來即可,內(nèi)存中始終只存在一份Lib.o文件。③ 在升級某個(gè)模塊時(shí),理論上只需要將對應(yīng)舊的目標(biāo)文件覆蓋掉即可。新版本的目標(biāo)文件會被自動裝載到內(nèi)存中并且鏈接起來;④ 程序在運(yùn)行時(shí)可以動態(tài)的選擇加載各種程序模塊,實(shí)現(xiàn)程序的擴(kuò)展。
(2)鏈接庫
我們在鏈接的過程中,一般會鏈接一些庫文件,主要分為靜態(tài)鏈接庫和動態(tài)鏈接庫。靜態(tài)鏈接庫一般為Windows下的.lib和Linux下的.a,動態(tài)鏈接庫一般為Windows下的.dll和Linux下的.so,這里考慮到我們主要是對so文件hook講解,下面我們主要介紹linux系統(tǒng)下的情況。命名規(guī)范為libXXX.a庫函數(shù)會被連接進(jìn)可執(zhí)行程序,可執(zhí)行文件體積較大可執(zhí)行文件運(yùn)行時(shí),不需要從磁盤載入庫函數(shù),執(zhí)行效率較高庫函數(shù)更新后,需要重新編譯可執(zhí)行程序
命名規(guī)范為libXXX.so庫函數(shù)不被連接進(jìn)可執(zhí)行程序,可執(zhí)行文件體積較小可執(zhí)行文件運(yùn)行時(shí),庫函數(shù)動態(tài)載入使用靈活,庫函數(shù)更新后,不需要重新編譯可執(zhí)行程序
2.可執(zhí)行文件(ELF)
目前PC平臺比較流行的可執(zhí)行文件格式主要是Windows下的PE和Linux下的ELF,它們都是COFF格式的變種。在Windows平臺下就是我們比較熟悉的.exe文件,而Linux平臺下現(xiàn)在便是統(tǒng)稱的ELF文件。這里我們主要介紹一下Linux下的ELF文件。可重定位目標(biāo)文件:包含二進(jìn)制代碼和數(shù)據(jù),其形式可以和其他目標(biāo)文件進(jìn)行合并,創(chuàng)建一個(gè)可執(zhí)行目標(biāo)文件。比如linux下的.o文件。可執(zhí)行目標(biāo)文件:包含二進(jìn)制代碼和數(shù)據(jù),可直接被加載器加載執(zhí)行。比如/bin/sh文件。共享目標(biāo)文件:可被動態(tài)的加載和鏈接。比如.so文件。elf文件在不同的平臺上有不同的格式,在Unix和x86-64 Linux上稱ELF:(1)ELF文件結(jié)構(gòu)
目標(biāo)文件既要參與程序鏈接,又要參與程序執(zhí)行:(1)文件開始處:是一個(gè)ELF頭部(ELF Header),用來描述整個(gè)文件的組織。節(jié)區(qū)部分包含鏈接視圖的大量信息:指令、數(shù)據(jù)、符號表、重定位信息等。(2)程序頭部表(Program Header Table):如果存在的話,會告訴系統(tǒng)如何創(chuàng)建進(jìn)程映像。用來構(gòu)造進(jìn)程映像的目標(biāo)文件必須具有程序頭部表,可重定位文件不需要這個(gè)表。(3)節(jié)區(qū)頭部表(Section Header Table):包含了描述文件節(jié)區(qū)的信息,每個(gè)節(jié)區(qū)在表中都有一項(xiàng),每一項(xiàng)給出諸如節(jié)區(qū)名稱、節(jié)區(qū)大小這類信息。用于鏈接的目標(biāo)文件必須包含節(jié)區(qū)頭部表,其他目標(biāo)文件可以有,也可以沒有這個(gè)表。下面我們來從分別從連接視角和程序執(zhí)行的視角來看ELF文件:ELF Header:描述了描述了體系結(jié)構(gòu)和操作系統(tǒng)等基本信息并指出Section Header Table和Program Header Table在文件中的什么位置。Program Header Table: 保存了所有Segment的描述信息;在匯編和鏈接過程中沒有用到,所以是可有可無的。Section Header Table:保存了所有Section的描述信息;Section Header Table在加載過程中沒有用到,所以是可有可無的。下面我們來看一張更加詳細(xì)的ELF結(jié)構(gòu)圖:從中我們可以詳細(xì)的知道ELF文件各個(gè)字段的含義,其他字段的含義如下圖:(2)GOT和PLT
上面我們簡單的分析了ELF的文件結(jié)構(gòu),而這里我們介紹一下其中兩個(gè)重要的節(jié)表GOT(全局偏移表)和PLT(程序鏈接表)。經(jīng)過上面的分析,我們知道程序在經(jīng)歷了編譯流程后,就來到了鏈接過程,鏈接過程就是將一個(gè)或者多個(gè)中間文件(.o文件)通過鏈接器將它們鏈接成一個(gè)可執(zhí)行文件,主要要完成以下事情:① 各個(gè)中間文之間的同名section合并。② 對代碼段,數(shù)據(jù)段以及各符號進(jìn)行地址分配。但是當(dāng)我們程序運(yùn)行起來,glibc動態(tài)庫也裝載了,函數(shù)地址也確定了,那我們程序如何去調(diào)用動態(tài)庫中的函數(shù)呢,這個(gè)時(shí)候就需要理解一下重定位的概念:1.鏈接重定位:將一個(gè)或多個(gè)中間文件(.o文件)通過鏈接器將它們鏈接成一個(gè)可執(zhí)行文件,一般分為兩種情況: (1)如果是在其他中間文件中已經(jīng)定義了的函數(shù),鏈接階段可以直接重定位到函數(shù)地址,比如我們從頭文件訪問另一個(gè)函數(shù)。
(2)如果是在動態(tài)庫中定義了的函數(shù),鏈接階段無法直接重定位到函數(shù)地址,只能生成額外的小片段代碼,也就是PLT表,然后重定位到該代碼片段。
2.運(yùn)行重定位:運(yùn)行后加載動態(tài)庫,把動態(tài)庫中的相應(yīng)函數(shù)地址填入GOT表,由于PLT表是跳轉(zhuǎn)到GOT表的,這就構(gòu)成了運(yùn)行時(shí)重定位。3.延遲重定位:只有動態(tài)庫函數(shù)在被調(diào)用時(shí),才會進(jìn)行地址解析和重定位工作,這時(shí)候動態(tài)庫函數(shù)的地址才會被寫入到GOT表項(xiàng)中。這里我們就可以明白流程,程序在加載動態(tài)庫中函數(shù)時(shí),需要兩部分:需要存放外部函數(shù)的代碼段表(PLT表);存放函數(shù)地址的數(shù)據(jù)表(GOT表)。這里我用一個(gè)實(shí)例加深大家的理解,例如程序在鏈接時(shí)發(fā)現(xiàn)scanf定義在動態(tài)庫時(shí),鏈接器生成一小段代碼scanf_stub,這就是我們的PLT表,然后scanf_stub地址取代原來的scanf,因此程序此時(shí)就轉(zhuǎn)換為鏈接scanf_stub,這個(gè)過程叫鏈接重定位,然后在運(yùn)行時(shí)動態(tài)庫glibc中的scanf_libc地址填入GOT表,然后程序通過scanf_stub訪問到scanf_libc,這個(gè)過程叫運(yùn)行時(shí)重定位。講到這里,其實(shí)我們對PLT和GOT表的作用已經(jīng)了解了,PLT(程序鏈接表)就是鏈接時(shí)需要存放外部函數(shù)的數(shù)據(jù)段,GOT(全局偏移表)是存放函數(shù)地址的代碼PLT表中的第一項(xiàng)為公共表項(xiàng),剩下的是每個(gè)動態(tài)庫函數(shù)為一項(xiàng),每項(xiàng)PLT都從對應(yīng)的GOT表項(xiàng)中讀取目標(biāo)函數(shù)地址。GOT表中前3個(gè)為特殊項(xiàng),分別用于保存 .dynamic段地址、本鏡像的link_map數(shù)據(jù)結(jié)構(gòu)地址和_dl_runtime_resolve函數(shù)地址。dynamic段:提供動態(tài)鏈接的信息,例如動態(tài)鏈接中各個(gè)表的位置。link_map:已加載庫的鏈表,由動態(tài)庫函數(shù)的地址構(gòu)成的鏈表。_dl_runtime_resolve:在第一次運(yùn)行時(shí)進(jìn)行地址解析和重定位工作。根據(jù)操作系統(tǒng)規(guī)定不允許修改代碼段,只能修改數(shù)據(jù)段,所以PLT表是不變的,GOT表是可以改變的。因此我們可以看一下程序調(diào)用PLT表和GOT表的邏輯。最后我們來詳細(xì)看一下程序調(diào)用函數(shù)的變化流程:程序第一次調(diào)用函數(shù)時(shí):此時(shí)第一步由函數(shù)調(diào)用跳入到PLT表中,然后第二步PLT表跳到GOT表中,可以看到第三步由GOT表回跳到PLT表中,這時(shí)候進(jìn)行壓棧,把代表函數(shù)的ID壓棧,接著第四步跳轉(zhuǎn)到公共的PLT表項(xiàng)中,第5步進(jìn)入到GOT表中,然后_dl_runtime_resolve對動態(tài)函數(shù)進(jìn)行地址解析和重定位,第七步把動態(tài)函數(shù)真實(shí)的地址寫入到GOT表項(xiàng)中,然后執(zhí)行函數(shù)并返回,此時(shí)GOT表中就存放了函數(shù)的真實(shí)地址。之后函數(shù)被調(diào)用時(shí):第一步還是由函數(shù)調(diào)用跳入到PLT表,但是第二步跳入到GOT表中時(shí),由于這個(gè)時(shí)候該表項(xiàng)已經(jīng)是動態(tài)函數(shù)的真實(shí)地址了,所以可以直接執(zhí)行然后返回。這里我們主要介紹Android中的so文件加載的原理,為后面hook技術(shù)講解做鋪墊:
1.Android so文件的類型
NDK開發(fā)的so不再具備跨平臺特性,需要編譯提供不同平臺支持。我們從官網(wǎng)可以得知so文件在不同架構(gòu)下也不同,這里依次對應(yīng)arm32位和64位,x86_32位和64位。我們可以使用指令查看我們手機(jī)的架構(gòu):adb shellcat /proc/cpuinfo
2.so文件加載
Android中我們通常使用系統(tǒng)提供的兩種API:System.loadLibrary或者System.load來加載so文件:System.loadLibrary("native-lib");System.load("/data/data/應(yīng)用包名/lib/libnative-lib.so")
System.loadLibrary()和System.load()的區(qū)別:(1)loadLibray傳入的是編譯腳本指定生成的so文件名稱,一般不需要包含開頭的lib和結(jié)尾的.so,而load傳入的是so文件所在的絕對路徑。(2)loadLibrary傳入的不能是路徑,查找so時(shí)會優(yōu)先從應(yīng)用本地路徑下(/data/data/${package-name}/lib/arm/)進(jìn)行查找,不存在的話才會從系統(tǒng)lib路徑下(/system/lib、/vendor/lib等)進(jìn)行查找;而load則沒有路徑查找的過程。(3)load傳入的不能是sdcard路徑,會導(dǎo)致加載失敗,一般只支持應(yīng)用本地存儲路徑/data/data/${package-name}/,或者是系統(tǒng)lib路徑system/lib等這2類路徑。(4)loadLibrary加載的都是一開始就已經(jīng)打包進(jìn)apk或系統(tǒng)的so文件了,而load可以是一開始就打包進(jìn)來的so文件,也可以是后續(xù)從網(wǎng)絡(luò)下載,外部導(dǎo)入的so文件。(5)重復(fù)調(diào)用loadLibrar,load并不會重復(fù)加載so,會優(yōu)先從已加載的緩存中讀取,所以只會加載一次。(6)加載成功后會去搜索so是否有"JNI_OnLoad",有的話則進(jìn)行調(diào)用,所以"JNI_OnLoad"只會在加載成功后被主動回調(diào)一次,一般可以用來做一些初始化的操作,比如動態(tài)注冊jni相關(guān)方法等。[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:我們可以發(fā)現(xiàn)System.loadLibrary()中會修改類加載器,這個(gè)在我們后面hook過程可能會報(bào)錯(cuò),而Runtime.loadLibray()中有重寫的方法,則可以正確實(shí)現(xiàn)。[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); }
我們可以發(fā)現(xiàn)不同版本的區(qū)別:Android 6.0采用的是loadLibrary,6.0之后都采用的是loadLibrary0; 同理 load函數(shù)也一樣,6.0之后采用的是load0。同時(shí)我們分析了loadLibrary0:① classLoader存在時(shí),通過classLoader.findLibrary(libraryName)來獲取存放指定so文件的路徑。② classLoader不存在時(shí),則通過getLibPaths()接口來獲取。③ 最終調(diào)用nativeLoad加載指定路徑的so文件。
hook技術(shù)就是指截獲進(jìn)程對某個(gè)API函數(shù)的調(diào)用,使得API的執(zhí)行流程轉(zhuǎn)向我們實(shí)現(xiàn)的代碼片段,從而實(shí)現(xiàn)我們要的功能,在Android中使用hook的方法有很多,常用的Xposed和frida hook技術(shù)、inlinehook技術(shù)、基于inlinehook的開源框架Sandhook、PLT/Got hook技術(shù)、以及當(dāng)下模擬cpu的Unicorn的hook技術(shù),下面我們將逐一介紹其原理。1.Xposed hook技術(shù)
Xposed的基本原理,我在源碼編譯(3)——Xposed框架定制(https://bbs.pediy.com/thread-269627.htm)中已經(jīng)給大家做了詳細(xì)的講解,其主要就是Android應(yīng)用進(jìn)程都是由 zygote 進(jìn)程孵化而來,zygote對應(yīng)的可執(zhí)行程序就是app_process,posed 框架通過替換系統(tǒng)的 app_process 可執(zhí)行文件以及虛擬機(jī)動態(tài)鏈接庫,讓 zygote 在啟動應(yīng)用程序進(jìn)程時(shí)注入框架代碼,進(jìn)而實(shí)現(xiàn)對應(yīng)用程序進(jìn)程的劫持。具體怎么實(shí)現(xiàn)hook技術(shù),Xposed就是通過修改了Art虛擬機(jī),將需要hook的函數(shù)注冊為Native函數(shù),當(dāng)執(zhí)行這一函數(shù)時(shí),虛擬機(jī)會優(yōu)先執(zhí)行Native函數(shù),然后執(zhí)行java函數(shù),這樣就成功完成了函數(shù)的hook。在 Android 系統(tǒng)啟動的時(shí)候, zygote 進(jìn)程加載 XposedBridge 將所有需要替換的 Method 通過 JNI 方法 hookMethodNative 指向 Native 方法 xposedCallHandler , xposedCallHandler 在轉(zhuǎn)入 handleHookedMethod 這個(gè) Java 方法執(zhí)行用戶規(guī)定的 Hook Func。dvmCallMethodV會根據(jù)accessFlags決定調(diào)用native還是java函數(shù),因此修改accessFlags后,Dalvik會認(rèn)為這個(gè)函數(shù)是一個(gè)native函數(shù),便走向了native分支也就是說Xposed在對java方法進(jìn)行hook時(shí),先將虛擬機(jī)里面這個(gè)方法的Method的accessFlag改為native對應(yīng)的值,然后將該方法的nativeFunc指向自己實(shí)現(xiàn)的一個(gè)native方法,這樣方法在調(diào)用時(shí),就會調(diào)用到這個(gè)native方法,接管了控制權(quán)。2.Frida hook技術(shù)
frida 也是一種動態(tài)插樁工具,原理和Xposed hook一樣,也是把java method轉(zhuǎn)為native method,但是Art下的實(shí)現(xiàn)與Dalivk有所不同,這里就需要了解ART的運(yùn)行機(jī)制,這里主要參考博客:Frida源碼分析。ART 是一種代替 Dalivk 的新的運(yùn)行時(shí),它具有更高的執(zhí)行效率。ART虛擬機(jī)執(zhí)行 Java 方法主要有兩種模式:quick code 模式和 Interpreter 模式。quick code 模式:執(zhí)行 arm 匯編指令Interpreter 模式:由解釋器解釋執(zhí)行 Dalvik 字節(jié)碼即使是在quick code模式中,也有類方法可能需要以Interpreter模式執(zhí)行。反之亦然。解釋執(zhí)行的類方法通過函數(shù)artInterpreterToCompiledCodeBridge的返回值調(diào)用本地機(jī)器指令執(zhí)行的類方法;本地機(jī)器指令執(zhí)行的類方法通過函數(shù)GetQuickToInterpreterBridge的返回值調(diào)用解釋執(zhí)行的類方法。如圖,對于一個(gè)native方法,ART虛擬機(jī)會先嘗試使用quickcode的模式去執(zhí)行,并檢查ARTMethod結(jié)構(gòu)中的entry_point_from_quick_compiledcode成員,這里分3種情況:① 如果函數(shù)已經(jīng)存在quick code, 則指向這個(gè)函數(shù)對應(yīng)的 quick code的起始地址,而當(dāng)quick code不存在時(shí),它的值則會代表其他的意義;② 當(dāng)一個(gè) java 函數(shù)不存在 quick code時(shí),它的值是函數(shù) artQuickToInterpreterBridge 的地址,用以從 quick 模式切換到 Interpreter 模式來解釋執(zhí)行 java 函數(shù)代碼;③ 當(dāng)一個(gè) java native(JNI)函數(shù)不存在 quick code時(shí),它的值是函數(shù) art_quick_generic_jni_trampoline 的地址,用以執(zhí)行沒有quick code的 jni 函數(shù)。因此,frida將一個(gè)java method修改jni mthod 顯然是不存在quick code,這時(shí)需要將entry_point_from_quick_compiledcode值修改為art_quick_generic_jni_trampoline 的地址。總結(jié),frida把java method改為jni method,需要修改ARTMethod結(jié)構(gòu)體中的這幾個(gè)值:accessflags = nativeentry_point_fromjni = 自定義代碼的入口entry_point_from_quick_compiledcode = art_quick_generic_jni_trampoline函數(shù)的地址entry_point_frominterpreter = artInterpreterToCompiledCodeBridge函數(shù)地址
3.inlinehook 技術(shù)
(1)基本原理
首先,我們先介紹一下什么是inline Hook:inline Hook是一種攔截目標(biāo)函數(shù)調(diào)用的方法,主要用于殺毒軟件、沙箱和惡意軟件。一般的想法是將一個(gè)函數(shù)重定向到我們自己的函數(shù),以便我們可以在函數(shù)執(zhí)行它之前和/或之后執(zhí)行處理;這可能包括:檢查參數(shù)、填充、記錄、欺騙返回的數(shù)據(jù)和過濾調(diào)用。hook是通過直接修改目標(biāo)函數(shù)內(nèi)的代碼來放置,通常是用跳轉(zhuǎn)覆蓋的前幾個(gè)字節(jié),允許在函數(shù)進(jìn)行任何處理之前重定向執(zhí)行。(2)inlineHook組成
hook:一個(gè)5字節(jié)的相對跳轉(zhuǎn),在被寫入目標(biāo)函數(shù)以鉤住它,跳轉(zhuǎn)將從被鉤住的函數(shù)跳轉(zhuǎn)到我們的代碼。proxy:這是我們指定的函數(shù)(或代碼),放置在目標(biāo)函數(shù)上的鉤子將跳轉(zhuǎn)到該函數(shù)(或代碼)。Trampoline:用于繞過鉤子,以便我們可以正常調(diào)用鉤子函數(shù)。(3)inlineHook實(shí)現(xiàn)
我們將目標(biāo)函數(shù)MessgeBoxA()中的地址拿出來,然后我們用重寫的hook函數(shù)替換,然后我們執(zhí)行完成之后,再回調(diào)到函數(shù)的執(zhí)行地址出,保證程序的正常運(yùn)行。我們也可以通過上述示意圖去理解inlinehook的基本原理。(4)Android-Inline-Hook和SandHook 技術(shù)
Android-lnline-Hook和SandHook都是基于inlinehook的兩種開源框架,在Android中對native層hook,使用的比較常見,前者主要針對32位進(jìn)行hook,后者即可以用于32位也可以用于64位,但是官方表示32位并未進(jìn)行測試,所以應(yīng)用在64位上仍然更多。4.PLT/GOT hook技術(shù)
前面我們已經(jīng)很詳細(xì)的講述了全局偏移表(GOT)和動態(tài)鏈接表(PLT),Inline Hook能Hook幾乎所有函數(shù),但是兼容性較差,不能達(dá)到上線標(biāo)準(zhǔn),相比于inlineHook,GOT Hook兼容性比較好,可以達(dá)到上線標(biāo)準(zhǔn),但是只能Hook基于GOT表的一些函數(shù)。GOT/PLT Hook 主要是通過解析SO文件,將待hook函數(shù)在got表的地址替換為自己函數(shù)的入口地址,這樣目標(biāo)進(jìn)程每次調(diào)用待hook函數(shù)時(shí),實(shí)際上是執(zhí)行了我們自己的函數(shù)。這里我們還要理解GOT表中含包含了導(dǎo)入表和導(dǎo)出表:導(dǎo)出表指將當(dāng)前動態(tài)庫的一些函數(shù)符號保留,供外部調(diào)用;導(dǎo)入表中的函數(shù)實(shí)際是在該動態(tài)庫中調(diào)用外部的導(dǎo)出函數(shù)。例如導(dǎo)入表存放的是一些其他so的函數(shù),例如libc的open,而導(dǎo)出表存放的是一些共其他so調(diào)用的函數(shù),比如自己so中編寫的函數(shù),而無論導(dǎo)入表還是導(dǎo)出表基本都是針對導(dǎo)出函數(shù),針對非導(dǎo)出函用inlinehook更常用一些。5.Unicorn hook技術(shù)
Unicore是一款非常優(yōu)秀的跨平臺模擬執(zhí)行框架,該框架可以跨平臺執(zhí)行Arm, Arm64 (Armv8), M68K, Mips, Sparc, & X86 (include X86_64)等指令集的原生程序,通過模擬CPU,可以實(shí)現(xiàn)很多強(qiáng)大的功能,也可以實(shí)現(xiàn)函數(shù)級別的Hook。參考資料:無名大佬文章Unicorn 在 Android 的應(yīng)用(https://bbs.pediy.com/thread-253868.htm#msg_header_h1_7)nicorn 內(nèi)部并沒有函數(shù)的概念,它只是一個(gè)單純的CPU, 沒有HOOK_FUNCTION的callback,AndroidNativeEmu 中的函數(shù)級Hook 并不是真正意義上的Hook,它不僅能Hook存在的函數(shù),還能Hook不存在的函數(shù)。AndroidNativeEmu 使用這種技術(shù)實(shí)現(xiàn)了JNI函數(shù)Hook、庫函數(shù)Hook。Jni函數(shù)是不存的,Hook它只是為了能夠用Python 實(shí)現(xiàn) Jni Functions。有一些庫函數(shù)是存在的,Hook只是為了重新實(shí)現(xiàn)它。
1.Xposed hook實(shí)操
(1)環(huán)境安裝
Xposed環(huán)境安裝詳細(xì)可以參考我寫的Xposed系列文章,這里只是簡單的總結(jié)一下:(1) 4.4以下Android版本安裝比較簡單,只需要兩步即可 1.對需要安裝Xposed的手機(jī)進(jìn)行root 2.下載并安裝xposedInstaller,之后授權(quán)其root權(quán)限,進(jìn)入app點(diǎn)擊安裝即可 但是由于官網(wǎng)不在維護(hù),導(dǎo)致無法直接通過xposedinstaller下載補(bǔ)丁包(2)Android 5.0-8.0 由于5.0后出現(xiàn)ART,所以安裝步驟分成兩個(gè)部分:xposed.zip 和 XposedInstaller.apk,zip文件是框架主體,需要進(jìn)入Recovery后刷入,apk文件用于Xposed管理 1.完成對手機(jī)的root,并刷入reconvery(比如twrp),使用Superroot 2.下載你對應(yīng)的zip補(bǔ)丁包,并進(jìn)入recovery刷入 3.重啟手機(jī),安裝xposedInstaller并授予root權(quán)限即可 官網(wǎng)地址:https:(3)由于Android 8.0后,Xposed官方作者沒有再對其更新,我們一般就使用國內(nèi)大佬riyu的Edxposed框架 Magisk + riyu + Edxposed
這里我們用的是nexus5進(jìn)行操作,簡單演示一下android6.0的Xposed安裝。asop鏡像:https:twrp: https:xposed: https:xposed installer https:
首先我們先下載n5鏡像,然后刷機(jī),這里我們已經(jīng)安裝就不再安裝了。然后我們刷入 twrp-3.4.0-0-hammerhead.imgfastboot flash recovery twrp-3.4.0-0-hammerhead.img
然后我們就可以進(jìn)入recovery模式了。然后我們將Supersu拷貝進(jìn)去,然后將Xposed-v89-sdk.zip拷貝進(jìn)去。然后我們進(jìn)入recovery模式,將兩個(gè)文件依次刷入即可。接下來我們安裝XposedInstall.apk,來管理Xposed。如果我們開機(jī)后發(fā)現(xiàn)xposed框架沒有激活,嘗試再重啟一下,我們可以看見:(2)Xposed插件編寫
Xposed插件編寫的流程網(wǎng)上已經(jīng)有很多了,這里我就簡單的講解一下。 (1)拷貝XposedBridgeApi.jar到新建工程的libs目錄; (2)修改app目錄下的build.gradle文件,在AndroidManifest.xml中增加Xposed相關(guān)內(nèi)容; (4)新建assets文件夾,然后在assets目錄下新建文件xposed_init,在里面寫上hook類的完整路徑。首先,我們查找XposedBridgeApi.jar到新建工程的libs目錄:然后,修改AndroidManifest.xml文件,在Application標(biāo)簽下增加內(nèi)容如下:<meta-data android:name="xposedmodule" android:value="true"/><meta-data android:name="xposeddescription" android:value="模塊描述"/><meta-data android:name="xposedminversion" android:value="54"/>
進(jìn)入app目錄下的build.gradle文件, compile fileTree(includes:['*.jar'],dir:'libs') 替換成 provided fileTree(includes:['*.jar'],dir:'libs')現(xiàn)在provided變?yōu)?compileOnly如果使用compile,可以正常編譯生成插件apk,但是當(dāng)安裝到手機(jī)上后,xposed會報(bào)錯(cuò),無法正常工作
我們新建一個(gè)hook類xposed01,并實(shí)現(xiàn)接口IXposedHookLoadPackage,并實(shí)現(xiàn)里面關(guān)鍵方法handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam),該方法會在每個(gè)軟件被啟動的時(shí)候回調(diào),所以一般需要通過目標(biāo)包名過濾。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類的完整路徑。這里面可以寫多個(gè)hook類,每個(gè)類寫一個(gè),我們就完成了基本的Xposed框架的編寫。我們可以發(fā)現(xiàn)我們的xposed插件生效了,將我們系統(tǒng)中進(jìn)程名打印出來了,說明hook成功了。2.frida hook實(shí)操
(1)環(huán)境安裝
frida安裝,使用frida過程中我們可以安裝objection來進(jìn)一步助力我們的hook工作,這個(gè)參考肉絲大佬的知識星球。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腳本進(jìn)行hook了。objection使用(詳細(xì)參考肉絲大佬g(shù)ithub的教程):常見的hook命令:objection -g com.android.settings explore android hooking list activities android intent launch_activity com.android.settings.DisplaySettings 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相關(guān)類的所有方法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
這樣我們就可以成功注入了,更加復(fù)雜的腳本編寫可以參考frida博客(https://github.com/hookmaster/frida-all-in-one)。詳細(xì)案例實(shí)操,這里可以參考之前我的文章:Android惡意樣本分析——frida破解三層鎖機(jī)樣本(https://bbs.pediy.com/thread-269128.htm)。
3.inlinehook實(shí)操
這里我們分別實(shí)現(xiàn)基于inlinehook的兩個(gè)開源框架的具體使用方法。(1)Android-lnine-Hook
開源地址:https://github.com/ele7enxxh/Android-Inline-Hook該框架只能針對32位的so文件進(jìn)行hook。我們對so文件進(jìn)行hook時(shí),可以按照如下步驟進(jìn)行:(1)查看so文件中的目標(biāo)函數(shù);(2)編寫Xposed hook代碼,hook目標(biāo)程序;(3)編寫so層hook代碼,hook so中的函數(shù)地址;<1>編寫目標(biāo)函數(shù)so文件
我們編寫案例,很明顯這里會打印失敗,然后我們使用inline-hook框架進(jìn)行hook。<2>導(dǎo)入文件
我們將該框架中如下文件導(dǎo)入我們的項(xiàng)目中。我們需要使用inlineHook(https://github.com/ele7enxxh/Android-Inline-Hook)文件夾,并把這些文件直接拷貝到我們的工作目錄:
<3>修改配置文件
<4>編寫hook代碼
我們導(dǎo)入inlinehook頭文件就可以開始編寫hook代碼了。這是因?yàn)榭蚣軆H僅針對32位,所以我們需要在配置文件里面指定一下。首先聲明hook的就函數(shù),然后編寫對應(yīng)的新函數(shù),這里我們hook的是strstr函數(shù)。然后調(diào)用inlinehook進(jìn)行hook。最后我們發(fā)現(xiàn)就可以成功的hook。源碼解析: (1)dlopen:該函數(shù)將打開一個(gè)新庫,并把它裝入內(nèi)存 void *dlopen(const char *filename, int flag); 參數(shù)1:文件名就是一個(gè)動態(tài)庫so文件,標(biāo)志位:RTLD_NOW 的話,則立刻計(jì)算;設(shè)置的是 RTLD_LAZY,則在需要的時(shí)候才計(jì)算 libc.so是一個(gè)共享庫 ====================== 參數(shù)中的 libname 一般是庫的全路徑,這樣 dlopen 會直接裝載該文件;如果只是指定了庫名稱,在 dlopen 會按照下面的機(jī)制去搜尋: 根據(jù)環(huán)境變量 LD_LIBRARY_PATH 查找 根據(jù) /etc/ld.so.cache 查找 查找依次在 /lib 和 /usr/lib 目錄查找。 flag 參數(shù)表示處理未定義函數(shù)的方式,可以使用 RTLD_LAZY 或 RTLD_NOW 。RTLD_LAZY 表示暫時(shí)不去處理未定義函數(shù),先把庫裝載到內(nèi)存,等用到?jīng)]定義的函數(shù)再說;RTLD_NOW 表示馬上檢查是否存在未定義的函數(shù),若存在,則 dlopen 以失敗告終。 參考鏈接:https: ======================= (2)dlsym:在 dlopen 之后,庫被裝載到內(nèi)存。dlsym 可以獲得指定函數(shù)( symbol )在內(nèi)存中的位置(指針)。 void *dlsym(void *handle,const char *symbol); 參數(shù)1:文件句柄 參數(shù)2:函數(shù)名
我們對一個(gè)目標(biāo)so文件hook步驟如下: (1)我們獲取so的handler,使用dlopen函數(shù) void* libhandler = dlopen("libc.so",RTLD_NOW); (2)我們獲取hook目標(biāo)函數(shù)的地址,使用dlsym函數(shù) void* strstr_addr = dlsym(libhandler,函數(shù)名); (3)聲明原來的函數(shù) void* (*oldmethod)(char*,char*); 聲明現(xiàn)在的函數(shù) void* newmethod(char* a,char* b){ return (void *)oldmethod(a,b); } (3)使用registerInlinehook進(jìn)行重定向,將hook函數(shù)地址重定向我們編寫的新函數(shù)上 (registerInlineHook((uint32_t) strstr_addr, (uint32_t) new_strstr, (uint32_t **) &old_strstr) != ELE7EN_OK (5)我們判斷我們的hook操作是否成功,并且再次調(diào)用實(shí)現(xiàn)hook (inlineHook((uint32_t) strstr_addr) == ELE7EN_OK)
(2)SandHook實(shí)操
因?yàn)樯厦媸褂胕nline框架只支持32位,所以這里我們用SandHook實(shí)現(xiàn)對64位native函數(shù)的hook,sandHook(https://github.com/asLody/SandHook)既支持32位、又支持64位。開源地址:https://github.com/asLody/SandHook同樣是上面的案例,這里我們使用SandHook進(jìn)行實(shí)操。<1>導(dǎo)入文件
我們此路徑下SandHook/nativehook/src/main/cpp/文件全部導(dǎo)入。
<2>配置環(huán)境
cmake { arguments '-DBUILD_TESTING=OFF' cppFlags "-frtti -fexceptions -Wpointer-arith" abiFilters 'armeabi-v7a', 'arm64-v8a'}
<3>編寫hook代碼
SandHook使用和上面inlinehook框架基本一樣。首先聲明舊的函數(shù),編寫新的函數(shù)(目標(biāo)函數(shù)strstr)。(1)導(dǎo)包,將SandHook中cpp文件夾下的包全部導(dǎo)入到項(xiàng)目中,并修改CMakeLists.txt中添加native.cpp, 修改java層導(dǎo)入so庫為sandHook-native(2)配置相關(guān)的環(huán)境 在配置文件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)));參數(shù)2:hook的函數(shù) 參數(shù)3:新的函數(shù) 添加原理hook舊函數(shù)的聲明void* (*old_fopen)(char*,char*);實(shí)現(xiàn)新的函數(shù)功能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)運(yùn)行測試是否成功啟動
4.PLT/GOT hook實(shí)操
前面我們已經(jīng)介紹了Got表hook的原理,下面我們實(shí)例操作一下導(dǎo)入表函數(shù)的hook。參考博客:https://www.cnblogs.com/goodhacker/p/9306997.html通過解析elf格式,分析Section header table找出靜態(tài)的.got表的位置,并在內(nèi)存中找到相應(yīng)的.got表位置,這個(gè)時(shí)候內(nèi)存中.got表保存著導(dǎo)入函數(shù)的地址,讀取目標(biāo)函數(shù)地址,與.got表每一項(xiàng)函數(shù)入口地址進(jìn)行匹配,找到的話就直接替換新的函數(shù)地址,這樣就完成了一次導(dǎo)入表的Hook操作了。我們編譯后使用010Editor打開libnative-lib.so。然后我們用ida打開,并直接跳轉(zhuǎn)到該地址。在got表中我們找到對應(yīng)的mywin0函數(shù)。<1>獲得so模塊的加載地址
我們可以使用/proc/self/maps去獲得so模塊的加載地址。char line[1024]; int *start; int *end; int n=1; 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表的位置
我們首先根據(jù)段頭找到section_header的首地址。
然后我們遍歷這個(gè)表就可以找到.got,然后根據(jù)got表地址再輪訓(xùn)找到函數(shù)地址。因?yàn)檫@種方法不能在內(nèi)存中直接找到段頭,內(nèi)存中會抹去段頭,所以我們可以通過加載so文件來定位。
<3>定位到節(jié)表的地址
Elf64_Ehdr ehd; int fp =open("/data/local/tmp/libnative-lib.so", O_RDONLY); if(fp == -1){ __android_log_print(4,"windaa","%s","error"); } read(fp,&ehd,sizeof(Elf64_Ehdr)); unsigned long shof = ehd.e_shoff; int shnum = ehd.e_shnum; int shsize = ehd.e_ehsize; int shstr = ehd.e_shstrndx;
我們打印一下此事shof的值,驗(yàn)證一下節(jié)表的地址。
<4>定位到got表的位置和函數(shù)位置
然后我們拿到字符串的偏移值進(jìn)行定位到got表,再進(jìn)一步定位到函數(shù)。 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); lseek(fp,shdr.sh_offset,SEEK_SET); read(fp,strtable,shdr.sh_size); lseek(fp,shof,SEEK_SET); for(int i=0;i<shnum;i++){ read(fp,&shdr,shsize); if(strcmp(&strtable[shdr.sh_name], ".got")==0){ int* saddr = start+shdr.sh_addr/4; int size = shdr.sh_size; for(int j=0;j<size;j=j+8){ uint64_t value = *(uint64_t *)(saddr + j / 4); if(reinterpret_cast<uint64_t>(mywin0) == value) { __android_log_print(6,"windaa","value %p",value); uint64_t page_size = getpagesize(); 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; memcpy((saddr+j/4),&value,16); } } } }
這里我們就可以發(fā)現(xiàn)成功的hook。(1)使用/proc/self/maps去獲得so模塊的加載地址;(2)使用ElfHeader找到Section的首地址,并計(jì)算offset和size來獲取StringTable;(3)找到got表位置,計(jì)算其內(nèi)存位置,并指針指向got表首地址;(4)遍歷got表中的函數(shù),找到要hook的函數(shù),使用mprotect進(jìn)行hook;(5)將hook的函數(shù)地址替換為我們定義的函數(shù)地址。5.Unicorn hook使用
這里我們簡單了解一下基于unicorn的框架Unidbg的hook使用開源地址:https://github.com/zhkl0228/unidbg這里我們直接idea將項(xiàng)目拉取下來,然后等下項(xiàng)目環(huán)境配置完成。配置完成后,我們直接啟動里面的示例代碼查看hook效果。這里unidbg使用了xHook,xHook是一種PLT hook的方式,當(dāng)然這只是unidbg強(qiáng)大功能其中的一種,也是hook技術(shù)中一種,這里就簡單介紹到這,后續(xù)再詳細(xì)講如何使用。unidbg使用參考博客:https://www.qinless.com/670本文從程序加載的原理出發(fā),講解了當(dāng)下常用的一些基本的hook方式和手段,后續(xù)對其中一些hook方式再次深入講解,實(shí)驗(yàn)的一些樣本和代碼會上傳到知識星球和github,文章參考學(xué)了了很多大佬的文章和大佬星球的內(nèi)容,參考文獻(xiàn)放在末尾,有什么問題,就請各位大佬一一指出了。github的地址:https://github.com/WindXaa
參考文獻(xiàn)
參考書目:《程序員的自我修養(yǎng)——鏈接、裝載與庫》
https://zhuanlan.zhihu.com/p/389889716https://mabin004.github.io/2018/07/31/Mac%E4%B8%8A%E7%BC%96%E8%AF%91Frida/https://zhuanlan.zhihu.com/p/269441842https://blog.csdn.net/sdoyuxuan/article/details/78481239https://www.cnblogs.com/codingmengmeng/p/6046481.htmlhttps://blog.csdn.net/sssssuuuuu666/article/details/78788369https://www.malwaretech.com/2015/01/inline-hooking-for-programmers-part-1.htmlhttps://juejin.cn/post/6844903993668272141
https://www.likecs.com/show-203321775.htmlhttps://www.lmlphp.com/user/65342/article/item/709806/

為了防止失聯(lián),歡迎關(guān)注我防備的小號
微信改了推送機(jī)制,真愛請星標(biāo)本公號??