1. 背景
? ? ? ?在我們的日常工作中經(jīng)常會遇到一些BUG,而且這些BUG發(fā)生在native層,也就是在我們的so共享庫中,對于這些BUG有時我們可以修改代碼,重新編譯生成SO文件,但很多情況下這些SO文件都是外部提供,在沒有源碼的情況下我們沒法修改并編譯,這時候Hook技術(shù)就有了用武之地。
? ? ? 舉個例子,在之前的一個項目中,作者使用Android插件技術(shù)調(diào)起某款應(yīng)用,在使用的過程中,發(fā)現(xiàn)某功能無法使用,后來從對方研發(fā)那邊打聽到,他們的某個so苦文件中在調(diào)用標(biāo)準(zhǔn)C函數(shù)dlopen時,傳入了一個寫死的路徑,這個路徑類似于/data/data/com.yingyong,這是Android中應(yīng)用的數(shù)據(jù)存儲的私有路徑,如果應(yīng)用未安裝,該路徑也就不存在,所以也就導(dǎo)致了我們在使用插件技術(shù)調(diào)起對方應(yīng)用時發(fā)生了錯誤。在綜合考慮解決該問題的各種方法之后,最終我們決定hook住對方應(yīng)用內(nèi)的so文件,替換掉內(nèi)存中dlopen函數(shù)的入口地址,當(dāng)對方應(yīng)用so文件中調(diào)用該函數(shù)時,最終會調(diào)用到我們的代理函數(shù),我們在代理函數(shù)中預(yù)先做些處理,然后再去調(diào)用真實的dlopen函數(shù)。基于上面所說的方法,我們解決問題的思路大概如下:
1. 首先so庫肯定會加載到進程空間中來,我們在我們的進程空間中,找到這個so庫的起始地址
2. 基于對so文件格式的理解,想辦法找到dlopen這個函數(shù)調(diào)用的入口地址,我們把這個入口地址替換掉,替換成我們自定義的hook函數(shù)的地址,同時保存原來的dlopen函數(shù)的地址。
3. 當(dāng)我們把dlopen函數(shù)的入口地址替換掉之后,so文件中在調(diào)用dlopen函數(shù)時就會跑到我們的hook函數(shù)中,執(zhí)行我們的代碼,我們在我們的hook函數(shù)里面對傳進來的路徑進行判斷,如果是/data/data/com.yingyong那么函數(shù)就直接return,如果是其他的路徑則跳轉(zhuǎn)到原來的dlopen函數(shù)的入口地址里面去執(zhí)行。
要想達到上面所說的目的,我們有兩個障礙:
(1) 我們得熟悉so文件格式,了解so是如何被進程加載的,so在進程空間中是怎么存儲的,我們該如何從進程空間中找到dlopen函數(shù)的入口
(2)在找到dlopen函數(shù)的入口后,我們?nèi)绾稳バ薷倪M程空間把原來的dlopen函數(shù)的入口地址替換成我們自己寫的函數(shù)的地址
這兩個問題中,第一個問題比較復(fù)雜,我們放在后面講,先講第一個問題如何解決。
2.內(nèi)存映射鏡像
要想修改內(nèi)存中dlopen函數(shù)的入口地址,首先我們得先看看so在進程空間中是如何存在。
先讓我們來看一個典型的應(yīng)用進程的內(nèi)存鏡像:
上面的信息可以通過命令:cat /proc/pid/maps 來查看。注意,如果查看本進程,pid傳入的是字符串“self”,如果是其他進程,pid就是那個進程的進程號。通過上面這幅圖,我們可以查看到當(dāng)前進程空間的內(nèi)存映射情況,模塊加載情況以及虛擬地址和內(nèi)存讀寫執(zhí)行(rwxp)屬性等。我們以其中一行為例來解釋這里面數(shù)據(jù)的含義:
第一列“aec20000-aec2b000”表示該段數(shù)據(jù)的起始和結(jié)束地址; 第二列“r-xp”表示的是這段內(nèi)存的權(quán)限, r表示只讀,w表示可寫,x表示可執(zhí)行,p表示私有;第三列“00000000”表示在進程地址里面的偏移量,第四列“fe:00”是主設(shè)備號和次設(shè)備號,第五列“999598”是文件節(jié)點號inode
上面的這行有只讀和執(zhí)行權(quán)限,其實熟悉虛擬機的同學(xué)就可以判斷出這就是程序內(nèi)的代碼段,我們在c程序里面定義的一些函數(shù)的信息也就保存在這里,比如我們上面所說的dlopen函數(shù)的入口地址。但是現(xiàn)在問題來了,這段內(nèi)存是只讀的,那我們怎么去把dlopen的入口地址替換成我們自定義的函數(shù)地址呢?其實linux給我們提供了一個函數(shù)mprotect,該函數(shù)可以修改內(nèi)存塊的讀取權(quán)限,其定義如下:
#include?
intmprotect(void*addr,size_tlen,intprot);
參數(shù):
addr:要修改的內(nèi)存基址(必須頁面對齊,page size的倍數(shù),一般為4K對齊)
len:大小(bytes)
prot:修改后的rwx屬性,可以有以下值:
? ? ? ?PROT_EXEC??Pages?may?be?executed.//可執(zhí)行
? ? ? ?PROT_READ??Pages?may?be?read.//可讀
? ? ? ?PROT_WRITE?Pages?may?be?written.//可寫
? ? ? ?PROT_NONE??Pages?may?not?be?accessed.//不可訪問
當(dāng)我們找到存放dlopen入口地址的內(nèi)存塊時,首先調(diào)用mprotect函數(shù)修改該內(nèi)存塊的讀取權(quán)限,然后通過memcpy函數(shù),將該內(nèi)存塊存儲的dlopen入口地址替換成我們定義的函數(shù)地址,memcpy函數(shù)的定義如下:
#include?
intmemcpy(void*pDest,void*pSour,size_tpLen);
該函數(shù)的作用就是從pSour地址開始拷貝pLen長的數(shù)據(jù)到pDest開始的內(nèi)存中去。
到這里,上面的第二個問題就得到解決了 ,我們知道通過mprotect函數(shù)去修改內(nèi)存塊讀寫權(quán)限,然后我們可以通過memcpy將自己寫的hook函數(shù)的地址拷貝到保存dlopen函數(shù)地址的內(nèi)存塊中去,但現(xiàn)在的問題就是要找到dlopen函數(shù)的入口地址,這就需要我們?nèi)チ私釫LF文件格式了,只有了解了so文件的文件格式,我們才能寫出方法,找到存儲dlopen函數(shù)地址的地方。下面我們就來深入了解so文件格式,并著手解決第一個問題
3. ELF文件格式的理解
ELF是類Unix類系統(tǒng)當(dāng)然也包括Android系統(tǒng)上的對象文件的格式(包括.so和.o類文件)。可以理解為Android系統(tǒng)上的exe或者dll文件格式。理解ELF文件規(guī)范,是理解Android系統(tǒng)上進程加載、執(zhí)行的前提。
首先,你需要知道的是所謂對象文件(Object files)有三個種類:
1) 可重定位的對象文件(Relocatable file)。這是由匯編器匯編生成的 .o 文件。
2) 可執(zhí)行的對象文件(Executable file)。比如我們用的QQ、微信、瀏覽器等等
3) 可被共享的對象文件(Shared object file)這些就是所謂的動態(tài)庫文件,也即 .so 文件。
一個動態(tài)庫也就是so文件要想發(fā)揮作用,必須經(jīng)過兩個步驟:
a) 鏈接編輯器(link editor)拿它和其他Relocatable object file以及其他shared object file作為輸入,經(jīng)鏈接處理后,生存另外的 shared object file 或者 executable file。
b) 在運行時,動態(tài)鏈接器(dynamic linker)拿它和一個Executable file以及另外一些 Shared object file 來一起處理,在Linux系統(tǒng)里面創(chuàng)建一個進程映像。
下面用一張圖來對ELF的文件格式有個總覽:
左邊是靜態(tài)視圖,而右邊則是鏈接加載時的視圖,都是同一個文件的兩種狀態(tài)。
我覺得下面這兩張圖能夠更準(zhǔn)確的反映ELF文件的組成:
我們來解釋一下文件各部分的意義:
ELF header在文件開始處描述了整個文件的組織,
程序頭部表,后面我們叫Program header table,指出怎樣創(chuàng)建進程映像,含有每個program header的入口,
節(jié)區(qū),后面我們都叫Section,提供了目標(biāo)文件的各項信息(如指令、數(shù)據(jù)、符號表、重定位信息等)
節(jié)區(qū)頭部表,后面我們叫section header table,包含每一個section的入口,給出名字、大小等信息。
在我們的Android NDK環(huán)境中有一個工具arm-linux-androideabi-readelf,工具位于android-ndk-r10d/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/下面,這個工具可以讀取并顯示so文件中的一些信息。我們以之前免安裝工程里面寫的libhook.so共享庫為例,來看看so文件各部分信息的組成:
ELF Header信息
ELF Header:顧名思義,這是所有ELF文件都有的”頭“。里面包含了ELF文件的”綱領(lǐng)性“信息,如CPU架構(gòu),Section header在文件中的偏移字節(jié)數(shù),Section的個數(shù)等等,用C語言數(shù)據(jù)結(jié)構(gòu)來描述就是:
/*?ELF?Header?*/
typedefstructelfhdr?{
? ? ?unsignedchare_ident[EI_NIDENT];/*?ELF?Identification?*/
? ? ?Elf32_Half??e_type;/*?object?file?type?*/
? ? ?Elf32_Half??e_machine;/*?machine?*/
? ? ?Elf32_Word??e_version;/*?object?file?version?*/
? ? ?Elf32_Addr??e_entry;/*?virtual?entry?point?*/
? ? ?Elf32_Off???e_phoff;/*?program?header?table?offset?*/
? ? ?Elf32_Off???e_shoff;/*?section?header?table?offset?*/
? ? ?Elf32_Word??e_flags;/*?processor-specific?flags?*/
? ? ?Elf32_Half??e_ehsize;/*?ELF?header?size?*/
? ? ?Elf32_Half??e_phentsize;/*?program?header?entry?size?*/
? ? ?Elf32_Half??e_phnum;/*?number?of?program?header?entries?*/
? ? ?Elf32_Half??e_shentsize;/*?section?header?entry?size?*/
? ? ?Elf32_Half??e_shnum;/*?number?of?section?header?entries?*/
?Elf32_Half??e_shstrndx;/*?section?header?table's?"sectionheader?string?table"?entry?offset?*/
}?Elf32_Ehdr;
其中e_ident的16個字節(jié)標(biāo)明是個ELF文件。e_type表示文件類型,e_machine說明機器類別,e_entry給出進程開始的虛地址,即系統(tǒng)將控制轉(zhuǎn)移的位置, 可以理解成main函數(shù)的位置。e_phoff指出program header table的文件偏移,e_phentsize表示一個program header每一項的長度(字節(jié)數(shù)表示),e_phnum給出program header里面數(shù)據(jù)項的數(shù)目。類似的,e_shoff,e_shentsize,e_shnum分別表示section header table的文件偏移,表中每一個section項的的字節(jié)數(shù)和section的個數(shù)。e_flags給出與處理器相關(guān)的標(biāo)志,e_ehsize給出ELF文件頭的長度(字節(jié)數(shù)表示)。e_shstrndx表示section header string table在文件中的偏移位置。
我們要想定位到一個具體的Section,主要關(guān)注以下幾個值:
e_shoff: Section header table的起始地址
e_shnum: ? Section header的個數(shù)
e_shstrndx: ?有一個特殊的Section Header,它里面保存的相當(dāng)于一個String數(shù)組,每一項都是一個Section的名字。
Programe Header信息
一個可執(zhí)行文件及其依賴的共享目標(biāo)文件被完全成功地裝載到進程的內(nèi)存地址空間中之后,這個可執(zhí)行文件或共享目標(biāo)文件中的程序頭部表(Program Header Table)就是必須存在的、不可缺少的必需品,程序頭部表是一個數(shù)組,數(shù)組中的每一個元素就稱為一個程序頭(Program Header),每一個程序頭描述一個內(nèi)存段(Segment)或者一塊用于準(zhǔn)備執(zhí)行程序的信息;內(nèi)存中的一個目標(biāo)文件中的段包含一個或多個節(jié);也就是ELF文件在磁盤中的一個或多個節(jié)可能會被映射到內(nèi)存中的同一個段中;程序頭只對可執(zhí)行文件或共享目標(biāo)文件有意義,對于其它類型的目標(biāo)文件,該信息可以忽略;p_type=PT_LOAD時,段的內(nèi)容會被從文件中拷貝到內(nèi)存中,所有PT_LOAD類型的程序頭都按照p_vaddr的值做升序排列;
Section 信息
? ? ? Section Header Table則列出了所有包含在文件中的Section區(qū)信息列表。它是一個Elf32_Shdr結(jié)構(gòu)的數(shù)組,Section頭表的索引是這個數(shù)組的下標(biāo)。通過Section Header Table可以定位到所有的Section。在一個so文件中會有很多的Section,我們寫的libhook.so文件里面就有22個Section,如.interp,.text, .plt, ? ?.rel.plt等等
? ? ? 可以說,Section是在ELF文件里頭用以裝載內(nèi)容數(shù)據(jù)的最小容器。在ELF文件里面,每一個Section內(nèi)都裝載了性質(zhì)屬性都不一樣的內(nèi)容,比方:
1) .text section 里裝載了可執(zhí)行代碼;
2) .data section 里面裝載了被初始化的數(shù)據(jù);
3) .bss section 里面裝載了未被初始化的數(shù)據(jù);
4) 以 .rel 打頭的 sections 里面裝載了重定位條目,如.rel.plt里面存儲的就是外部函數(shù)的重定位信息
5) .symtab 或者 .dynsym section 里面裝載了符號信息;
6) .strtab 或者 .dynstr section 里面裝載了字符串信息;
7) 其他還有為滿足不同目的所設(shè)置的section,比方滿足調(diào)試的目的、滿足動態(tài)鏈接與加載的目的等等
下面我們再看一下Section Header的定義:
/*?Section?Header?*/
typedefstruct{
? ? ?Elf32_Word??sh_name;/*?name?-?index?into?section?header
? ? ?string?table?section?*/
? ? ?Elf32_Word??sh_type;/*?type?*/
? ? ?Elf32_Word??sh_flags;/*?flags?*/
? ? ?Elf32_Addr??sh_addr;/*?address?*/
? ? ?Elf32_Off???sh_offset;/*?file?offset?*/
? ? ?Elf32_Word??sh_size;/*?section?size?*/
? ? ?Elf32_Word??sh_link;/*?section?header?table?index?link?*/
? ? ?Elf32_Word??sh_info;/*?extra?information?*/
? ? ?Elf32_Word??sh_addralign;/*?address?alignment?*/
? ? ?Elf32_Word??sh_entsize;/*?section?entry?size?*/
}?Elf32_Shdr;
這個我們要詳細(xì)解釋一下,后面能用到:
字段:
sh_name:顧名思義,Section的名字,類型是Elf32_Word,實際上它是指向section header string table的索引值
sh_flags:類型。.dynsym的類型為DYNSYM表示該節(jié)區(qū)包含了要動態(tài)鏈接的符號等等
sh_addr:地址。該節(jié)區(qū)在內(nèi)存中,相對于基址的偏移
sh_offset:偏移。表示該節(jié)區(qū)到文件頭部的字節(jié)偏移。
sh_size:節(jié)區(qū)大小
sh_link:表示與當(dāng)前section有l(wèi)ink關(guān)系的section索引,不同類型的section,其解釋不同。如上面的libc.so,其.dynsym的link為2,而2正好是.dynstr的索引,實際上就是動態(tài)符號串表的索引
sh_info:一些附加信息
sh_addralign:節(jié)區(qū)的地址對齊
sh_entsize:節(jié)區(qū)項的大小(bytes)
? ? ? 上面這么多亂七八糟的看起來很多,實際上為了解決上面的第一個問題我們只需要關(guān)注三個Section:.dynsym, ?.dynstr,.rel.plt,因為,dlopen函數(shù)是標(biāo)準(zhǔn)的c庫函數(shù),在一個so文件中要想調(diào)用他,那么就得給這個函數(shù)生成一個重定位信息,這個信息就存儲在.rel.plt這個section里面,.rel.plt相當(dāng)于一個數(shù)組,這個數(shù)組里面的每一項都存儲了一項可重定位信息。所以我們要想找到dlopen的入口地址,就得到.rel.plt中去,一個一個的去遍歷數(shù)組,找到那個dlopen相關(guān)的那一項,但是.rel.plt不會存儲一個“dlopen”的字符串,我們首先得到.dynsym這個section里面去找代表“dlopen”的那個符號信息,這個符號信息的數(shù)據(jù)結(jié)構(gòu)里面也不會存儲具體的函數(shù)名稱(如“dlopen”),這些名稱會存儲在.dynstr這個section里面,所以又得去.dynstr里面去找。所以要想找到dlopen函數(shù)代表的重定位信息,首先就得遍歷.rel.plt, 對于里面的每一項通過.dynsym去.dynstr里面去找這個重定位函數(shù)的名稱是不是叫“dlopen”,如果是,那么這條重定位信息,就是代表dlopen的重定位信息,下面我們來具體想一看.rel.plt 、 .dynsym 、 .dynstr這三個節(jié)區(qū)里面代表每一項數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu):
.rel.plt:?該Section用來保存所有的可重定位信息, 對于每一條可重定位信息,可以用下面的數(shù)據(jù)結(jié)構(gòu)Elf32_Rel來表示:
typedefstruct{
Elf32_Addr ?r_offset;
Elf32_Word ?r_info;
}?Elf32_Rel;
在數(shù)據(jù)結(jié)構(gòu)Elf32_Rel中r_offset表示的就是在運行期間,該重定位條目相對于so基地址的偏移量,我們要找的也就是這個偏移量,然后加上so在內(nèi)存中的基地址,得到的就是我們要找的存儲函數(shù)入口地址的地方;r_info是由兩個值組成的:1. 該重定位在.dynsym中的索引, 2. 該重定位的類型。 ?我們可以通過ELF32_R_SYM這個宏獲取到該重定位在.dynsym中的索引,通過ELF32_R_TYPE獲取該重定位的類型。
typedefstructelf32_sym{
? ? ?Elf32_Word????st_name;
? ? ?Elf32_Addr????st_value;
? ? ?Elf32_Word????st_size;
? ? ?unsignedcharst_info;
? ? ?unsignedcharst_other;
? ? ?Elf32_Half?????st_shndx;
}?Elf32_Sym;
其中st_name包含指向符號表字符串表(.dynstr, ?也就是dynamic symbol string table)中的索引,從而可以獲得符號名。舉個例子,對于我們要找的dlopen這個函數(shù),就存在一個上面的Elf32_Sym對象,該對象的st_name值可能是15,這時候我們就能從.dynstr(Dynamic symble string table)里面找到第15項,這個第15項里面就存了一個字符串“dlopen”。
st_value指出符號的值,可能是一個絕對值、地址等。st_size指出符號相關(guān)的內(nèi)存大小,比如一個數(shù)據(jù)結(jié)構(gòu)包含的字節(jié)數(shù)等。st_info規(guī)定了符號的類型和綁定屬性,指出這個符號是一個數(shù)據(jù)名、函數(shù)名、section名還是源文件名;并且指出該符號的綁定屬性是local、global還是weak。
.dynstr:全稱叫dynamic symbol string table, 我們可以把他理解成一個字符串?dāng)?shù)組,即char* [ ];
到了這里我們的知識儲備就能夠找到運行時dlopen函數(shù)在進程空間中的地址了:
通過讀取ELF文件內(nèi)容,找到.dynsym, ?.dynstr,.rel.plt這三個section,讀取這三個section的內(nèi)容。然后從.rel.plt這個section中讀取到dlopen函數(shù)對應(yīng)的重定位項,怎么找到這個重定位項呢,那就是遍歷.rel.plt中的所有的重定位項信息,讀取這些重定位項對應(yīng)的Elf32_Rel結(jié)構(gòu)對象,從Elf32_Rel的r_info字段中通過ELF32_R_SYM讀取一個index值,這個index值就是dlopen函數(shù)在.dynsym中對應(yīng)的符號信息的index,通過這個index讀取dlopen這個符號對應(yīng)的Elf32_Sym結(jié)構(gòu)對象。而通過這個結(jié)構(gòu)對象的st_name去.dynstr中去找相應(yīng)的字符串,當(dāng)這個字符串等于“dlopen”時,我們就知道了,剛才在.rel.plt中找到的Elf32_Rel對應(yīng)的就是dlopen函數(shù)對應(yīng)的重定位項,通過讀取這個重定位項的r_offset就可以知道存儲dlopen函數(shù)入口地址的地方在整個so鏡像中的偏移量,用這個偏移量加上so映射到進程中的起始地址,就得到了dlopen這個函數(shù)在我們應(yīng)用進程中的絕對地址,這個地址保存的是dlopen函數(shù)的入口地址,我們把這個入口地址替換成我們函數(shù)的地址,這樣在調(diào)用dlopen函數(shù)時,就進入到我們定義的函數(shù)中了。
至此我們就完成了我們應(yīng)用包含的so中的dlopen函數(shù)的hook。