android hook,android基于plt/got的hook原理

 2023-11-30 阅读 23 评论 0

摘要:目錄 概述 簡單示例 ELF文件格式初探 裝載、動態鏈接與重定位 PLT與GOT 如何定位基址? 如何修改呢? 解析基址和偏移 思考和小結概述 我們日常開發中編寫的C/C++代碼經過NDK進行編譯和鏈接之后,生成的動態鏈接庫或可執行文件都是ELF格式的,它也是

目錄
概述
簡單示例
ELF文件格式初探
裝載、動態鏈接與重定位
PLT與GOT
如何定位基址?
如何修改呢?
解析基址和偏移
思考和小結
概述
我們日常開發中編寫的C/C++代碼經過NDK進行編譯和鏈接之后,生成的動態鏈接庫或可執行文件都是ELF格式的,它也是Linux的主要可執行文件格式。我們今天就要借助一個示例來理解一下android平臺下native層hook的操作和原理,不過在這之前,我們還是要先了解一下ELF相關的內容。

簡單示例
這里給了一段示例代碼:寫入一段文本到文件中去。
為了簡單起見,后面的都是以armeabi-v7a為例

void writeText(const char *path, const char *text) {FILE *fp = NULL;if ((fp = fopen(path, "w")) == NULL) {LOG_E("file cannot open");}//寫入數據fwrite(text, strlen(text), 1, fp);if (fclose(fp) != 0) {LOG_E("file cannot be closed");}
}


輸出目標共享庫:libnative-write.so,這個共享庫的作用是寫入一段文本,我們今天的目標就是對這個目標共享庫的fwrite函數進行hook操作。

android hook、ELF文件格式初探
ELF文件有兩種視圖形式:鏈接視圖和執行視圖

鏈接視圖:可以理解為目標文件的內容視圖
執行視圖:可以理解為目標文件的內存視圖

文件頭(elf_header)

android handler機制、

文件頭部定義了魔數,以及指向節頭表SHT(section_header_table)和程序頭表PHT(program_header_table)的偏移。

節頭表SHT(section_header_table)

ELF文件在鏈接視圖中是 以節(section)為單位來組織和管理各種信息。

安卓hook工具,

.dynsym:為了完成動態鏈接,最關鍵的還是所依賴的符號和相關文件的信息。為了表示動態鏈接這些模塊之間的符號導入導出關系,ELF有一個叫做動態符號表(Dynamic Symbol Table)的段用來保存這些信息。
.rel.dyn:實際上是對數據引用的修正,它所修正的位置位于.got以及數據段。
.rel.plt:是對函數引用的修正,它所修正的位置位于.got。
.plt:程序鏈接表(Procedure Link Table),外部調用的跳板。
.text:為代碼段,也是反匯編處理的部分,以機器碼的形式存儲。
.dynamic:描述了模塊動態鏈接相關的信息。
.got:全局偏移表(Global Offset Table),用于記錄外部調用的入口地址。
.data: 數據段,保存的那些已經初始化了的全局靜態變量和局部靜態變量。

程序頭表PHT(program_header_table)

ELF文件在執行視圖中是 以段(Segment)為單位來組織和管理各種信息。

android intent、

所有類型為 PT_LOAD 的段(segment)都會被動態鏈接器(linker)映射(mmap)到內存中。

裝載、動態鏈接與重定位
1、裝載
這個很好理解,我們在使用一個動態庫內的函數時,都要先對其進行加載,在android中,我們通常是使用System.loadLibrary的方式加載我們的目標共享庫,它的內部實現其實也是調用系統內部linker中的dlopen、dlsym、dlclose函數完成對目標共享庫的裝載。

2、動態鏈接
動態鏈接的基本思想是把程序按照模塊拆分成各個相對獨立部分,在程序運行時才將它們鏈接在一起形成一個完整的程序,而不是像靜態鏈接一樣把所有的程序模塊都鏈接成一個個單獨的可執行文件。
當共享庫被裝載的時候,動態鏈接器linker會將共享庫裝載到進程的地址空間,并且將程序中所有未決議的符號綁定到相應的動態鏈接庫中,并進行重定位工作。

android dialogfragment?3、重定位

共享庫需要重定位的主要原因是導入符號的存在。動態鏈接下,無論是可執行文件或共享對象,一旦它依賴于其他共享對象,也就是說有導入的符號時(比如fwrite函數),那么它的代碼或數據中就會有對于導入符號的引用。在編譯時這些導入符號的地址未知,在運行時才確定,所以需要在運行時將這些導入符號的引用修正,即需要重定位。

動態鏈接的文件中,有專門的重定位表分別叫做.rel.dyn和.rel.plt:

# arm-linux-androideabi-readelf -r libnative-write.so

?

android handlerthread。R_ARM_GLOB_DAT和R_ARM_JUMP_SLOT是ARM下的重定位方式,這兩個類型的重定位入口表示,被修正的位置只需要直接填入符號的地址即可。比如我們看fwrite函數這個重定位入口,它的類型為R_ARM_JUMP_SLOT,它的偏移為0x0002FE0,它實際上位于.got中。

PLT與GOT
前面的過程裝載->動態鏈接->重定位完成之后,目標共享庫的基址已經確定了,當我們調用某個函數時(比如fwrite函數),調用函數并不是直接調用原始fwrite函數的函數地址,它會先經過PLT程序鏈接表(Procedure Link Table),跳轉至GOT全局偏移表(Global Offset Table)獲取目標函數fwrite函數的全局偏移,這時候就可以通過基址+偏移的方式定位真實的fwrite函數地址了,目前android平臺大部分CPU架構是沒有提供延遲綁定(Lazy Binding)機制的(只有MIPS架構支持延遲綁定),所有外部過程引用都在映像執行之前解析。

PLT:程序鏈接表(Procedure Link Table),外部調用的跳板,在ELF文件中以獨立的段存放,段名通常叫做".plt"
GOT:全局偏移表(Global Offset Table),用于記錄外部調用的入口地址,段名通常叫做".got"

前面的內容都是一些概念性的內容,比較枯燥,接下來會以writeText函數為入口,一步一步查看我們最終的目標函數fwrite的地址。

安卓hook能干嘛、從.dynsym開始

.dynsym:上面也說到了,這個節里只保存了與動態鏈接相關的符號導入導出關系。

# arm-linux-androideabi-readelf -s libnative-write.so

?

我們可以看到目標的writeText函數在0x705的地方,我們再看下對應的反匯編代碼:

# arm-linux-androideabi-objdump -D libnative-write.so

android tcpdump、?

這里會看到我們自己的writeText函數通過BLX(相對尋址)指令走到fwrite@plt里面,簡化上面的圖:

從上面的簡圖中,我們可以看到,當執行我們的代碼段.text中的writeText函數的時候,內部會通過BLX相對尋址的方式進入.plt節,計算程序計數器 PC 的當前值跳轉進入.got節。

00000668 <fwrite@plt>:668:	e28fc600 	add	ip, pc, #0, 12   //由于ARM三級流水,PC = 0x668 + 0x8;66c:	e28cca02 	add	ip, ip, #8192	; 0x2000  // ip = ip + 0x2000670:	e5bcf970 	ldr	pc, [ip, #2416]!	; 0x970  // pc = ip + 0x970

plt hook?
以上三條指令執行完,從0x668 + 0x8 + 0x2000 + 0x970 = 0x2FE0位置取值給PC,通過LDR完成間接尋址的跳轉。因此在.got(全局符號表)中偏移為0x2FE0的位置就是目標函數fwrite的偏移了。

可以看到,當我們通過libnative-write.so共享庫中的writeText函數調用libc中的導入函數fwrite的時候,還是經歷了一些曲折的過程,這里的過程,指的就是經過PLT和GOT的跳轉,到達我們最終的真實的導入函數的地址。

更快速的找到目標函數的偏移

前面也提到過動態鏈接重定位表中的.rel.plt是對函數引用的修正,它所修正的位置位于.got。我們最終都是要通過.got確定目標函數的偏移,因此這里我們可以用readelf直接看到fwrite函數的偏移

安卓hook教程,通過如下可以查看ELF中需要重定位的函數,我們看下fwrite()函數。

# arm-linux-androideabi-readelf -r libnative-write.so

在這里插入圖片描述?

可以看到我們從libc庫中的導入函數fwrite,這個偏移和我們剛才計算的偏移是一致的都是:0x2FE0

如何定位基址?
我們首先來看基址的獲取,這里要用到linux系統的一些特性

# 進程的虛擬地址空間
?

# 進程的虛擬地址空間
# cat /proc/<pid>/maps

?

上圖已經列舉出了我們的應用加載的一些so庫,左邊標記紅色的地址就是各個so庫的基址

#在進程ID為32396的進程中加載的幾個庫中
libhook-simple.so庫的基址為:0xD40D8000
libnative-hook.so庫的基址為:0xD411B000
libnative-write.so庫的基址為:0xD414F000


因此我們實際需要hook的函數fwrite的地址為:

addr = base_addr + 0x2FE0


如何修改呢?
通過前面的分析,我們已經拿到目標函數fwrite()的地址指針了,理論上只要朝這個地址寫入我們目標函數的地址就可以了?
并不是!!!

注意:

1、目標函數的地址很可能沒有寫權限,因此需要提前調整目標函數地址的權限
2、由于ARM有緩存指令集,hook之后可能會不成功,讀取的是緩存中的指令,因此這里需要清除一下指令緩存

這時候我們就需要用到linux中的函數:

//調整目標內存區域的權限
int mprotect(void* __addr, size_t __size, int __prot);
//清除緩存指令
__builtin___clear_cache(void * __page_start,void * __page_end)


操作如下:

//調整寫權限
mprotect((void *) PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);
//朝目標函數的地址寫新的地址
*(void **) addr = hook_fwrite;
//清除指令緩存
__builtin___clear_cache((void *) PAGE_START(addr), (void *) PAGE_END(addr));


完整的hook操作:

#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <inttypes.h>
#include <sys/mman.h>
#include "hook_simple.h"
#include "logger.h"#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)size_t hook_fwrite(const void *buf, size_t size, size_t count, FILE *fp) {LOG_D("hook fwrite success");//這里插入一段文本const char *text = "hello ";fwrite(text, strlen(text), 1, fp);return fwrite(buf, size, count, fp);
}/*** 直接硬編碼的方式進行* hook演示操作* @param env* @param obj* @param jSoName*/
void Java_com_feature_hook_NativeHook_hookSimple(JNIEnv *env, jobject obj, jstring jSoName) {const char *soName = (*env)->GetStringUTFChars(env, jSoName, 0);LOG_D("soName=%s", soName);char line[1024] = "\n";FILE *fp = NULL;uintptr_t base_addr = 0;uintptr_t addr = 0;// 1. 查找自身對應的基址if (NULL == (fp = fopen("/proc/self/maps", "r"))) return;while (fgets(line, sizeof(line), fp)) {if (NULL != strstr(line, soName) &&sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1)break;}fclose(fp);LOG_D("base_addr=0x%08X", base_addr);if (0 == base_addr) return;//2. 基址+偏移=真實的地址addr = base_addr + 0x2FE0;LOG_D("addr=0x%08X", addr);//注意:調整寫權限mprotect((void *) PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);//替換目標地址*(void **) addr = hook_fwrite;//注意:清除指令緩存__builtin___clear_cache((void *) PAGE_START(addr), (void *) PAGE_END(addr));
}


可以看到這里已經成功完成了hook操作

看了上面的例子,大家覺得native-hook復雜嗎?看上去不復雜?那如果讓你來設計一個類似于xHook的庫,你能直接在框架里硬編碼0x2FE0嗎?,當然不行,因此需要一個通用的邏輯來定位具體的偏移和基址才行,接下來我們重點來看下偏移和基址如何通過通用的代碼來動態確定

解析基址和偏移
我們接下來要做的重要的工作是在運行期間,動態定位目標共享庫中的基址和偏移。

這里主要如下幾個步驟:

1、獲取目標so庫的基址
 基址很好確定:

void *get_module_base(pid_t pid, const char *module_name) {FILE *fp;long addr = 0;char filename[32] = "\n";char line[1024] = "\n";LOG_D("pid=%d ", pid);if (pid < 0) {snprintf(filename, sizeof(filename), "/proc/self/maps");} else {snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);}// 獲取指定pid進程加載的內存模塊信息fp = fopen(filename, "r");while (fgets(line, sizeof(line), fp)) {if (NULL != strstr(line, module_name) &&sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000", &addr) == 1)break;}fclose(fp);return (void *) addr;
}


我們只需要讀取自身進程的/proc/self/maps就可以獲取當前進程裝載的模塊信息,這個不算復雜。
2、保存原始的調用地址
當我們自己的共享庫完成對目標共享庫的hook操作之后,要保證功能正常運行,需要先保存原始的函數調用地址。
3、解析ELF文件頭部
這里先根據魔數來確定是否為ELF文件格式,而且文件頭部里實際已經指明了SHT和PHT的偏移信息了

4、根據(基址 + e_phoff)確定程序頭表PHT(Program Header Table)的地址
上圖中的這個e_phoff的值是指向程序頭表PHT的偏移,0x34 = 52
5、遍歷程序頭表PHT(Program Header Table)
看上面的圖示,程序頭表PHT內的元素是個數組,但是我們目前只關心類型為PT_DYNAMIC(指定動態鏈接信息)的項,獲取對應的p_vaddr

6、根據(基址+p_vaddr)確定.dynamic段的地址,遍歷dynamic link table
接著遍歷出d_tag=DT_JMPREL類型的項的d_val值,這個值是指向重定位表的偏移,不要疑惑下圖中的偏移是0x2E7C,為什么下面Start卻是0x1E7C,剛才也說了ELF文件有兩種視圖,一個鏈接視圖,一個執行視圖,下面的圖是鏈接視圖,但我們最終要以執行視圖里的結果為準。

7、根據(基址+d_val)確定重定位表的地址,接下來我們遍歷函數名稱對比即可找到目標函數的偏移
 參考下面這張圖吧

在這里插入圖片描述

也就是說上面的那么多步驟,實際目的就是確定運行期間的目標共享庫中的重定位表的地址。


實際應用
筆者只是借助一個示例來理解基于PLT/GOT進行hook操作的原理,實際項目中,我們完全可以借助這種方案對目標共享庫中的malloc,free進行hook操作,在沒有源碼的情況下,以此來分析第三方共享庫中可能存在的內存泄露問題。
具體可以看看:LoliProfiler的實現。

思考
Q:比如我要hook我當前應用中的malloc函數,是否只對某個共享庫進行hook即可?
A:并不是,每一個共享庫都有它自己的PLT/GOT表,因此需要對每個共享庫都要進行hook操作才行。

Q:我在共享庫中通過dlopen、dlsym的方式調用系統導入函數,這中方式可以被hook住嗎?
A:不可以,上面的整個內容其實都是基于PLT/GOT表定位目標函數進行hook操作,而dlopen、dlsym是目標共享庫在運行期間,動態定位導入函數,這種方式并不生效。

小結
其實hook操作本身的技術原理并不復雜,但是要針對android平臺下的共享庫進行hook操作,僅僅只了解hook操作是不夠的,可以看到上面大部分的內容其實是在跟ELF文件周旋,要結合它的加載、動態鏈接、重定位過程,才能更好的理解基于PLT/GOT的hook原理,由于筆者能力有限,在部分細節的描述可能不全面或者會有偏差,歡迎指正!

項目地址
native-hook

參考
《程序員的自我修養:鏈接、裝載與庫》
https://github.com/iqiyi/xHook/
https://www.cnblogs.com/goodhacker/p/9306997.html

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/2/185555.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息