網(wǎng)站首頁 編程語言 正文
背景
我們在android超級優(yōu)化-線程監(jiān)控與線程統(tǒng)一可以知道,我們能夠通過asm插樁的方式,進行了線程的監(jiān)控與線程的統(tǒng)一,通過一系列的黑科技,我們能夠?qū)㈨椖恐械木€程控制在一個非??捎^的水平,但是這個只局限在java層線程的控制,如果我們項目中存在著native庫,或者存在著很多其他so庫,那么native層的線程我們就沒辦法通過ASM或者其他字節(jié)碼手段去監(jiān)控了,但是并不是就沒有辦法,還有一個黑科技,就是我們的PIL Hook,目前行業(yè)上比較出名的就是xhook,和bhook了。
native 線程創(chuàng)建
了解PLT Hook之前,我們先了解一下native層常用的創(chuàng)建線程的手段,沒錯,就是pthread
int pthread_create(pthread_t* __pthread_ptr, pthread_attr_t const* __attr, void* (*__start_routine)(void*), void*);
- __pthread_ptr:pthread_t類型的參數(shù),成功時tidp指向的內(nèi)容被設(shè)置為新創(chuàng)建線程的pthread_t
- __attr 線程的屬性
- __start_routine 執(zhí)行函數(shù),新創(chuàng)建線程從此函數(shù)開始運行
- __start_routine中 需要運行的入?yún)ⅲ绻鸰_start_routine不需要入?yún)?,則該值為null
接下里我們用這個例子去說明,我們在MainActivity中設(shè)定了一個名叫threadCreate的jni調(diào)用,開啟一個新線程,在新線程里面打印一些傳遞的數(shù)據(jù)。
libtest.so中的代碼 /* 聲明結(jié)構(gòu)體 */ struct member { int num; char *name; }; /* 定義線程pthread */ static void *pthread(void *arg) { struct member *temp; /* 線程pthread開始運行 */ printf("pthread start!\n"); /* 打印傳入?yún)?shù) */ temp = (struct member *) arg; printf("member->num:%d\n", temp->num); printf("member->name:%s\n", temp->name); return NULL; } extern "C" JNIEXPORT void JNICALL Java_com_example_signal_MainActivity_threadCreate(JNIEnv *env, jobject thiz) { pthread_t tidp; struct member *b; /* 為結(jié)構(gòu)體變量b賦值 */ b = (struct member *) malloc(sizeof(struct member)); b->num = 10086; b->name = "pika"; /* 創(chuàng)建線程pthread */ if ((pthread_create(&tidp, NULL, pthread, (void *) b)) == -1) { printf("create error!\n"); } }
通過jni方式調(diào)用的pthread,我們就沒辦法用常規(guī)手段去監(jiān)控了。所以我們才需要plt hook的方式
PLT
介紹plt hook之前,我們還是有必要了解一些前置的知識。在linux中,會存在很多地址無關(guān)的代碼。在我們的編寫模塊中,其實會遇到很多共享對象地址沖突的問題,如果相互依賴的對象是以絕對地址的方式存在的話,那么運行的時候就會發(fā)生地址沖突,比如進程A里面兩個方法都被定位到了同一個地址,所以才有了地址無關(guān)的代碼。
地址無關(guān)的代碼大多數(shù)采用運行時基地址+編譯時定向偏移,其中基地址可以在運行時確定,但是某個符號的運行時地址相對于基地址來說,就可以是一個確定的偏移數(shù)值。通過這種方式,函數(shù)可以在被需要的時候再進行綁定地址即可,在編譯時只需要記錄偏移就可以保證后期的運行尋址的正常。這個保存偏移地址的東西,就叫做GOT表(全局偏移表),當代碼需要引用到這個符號的時候,就可以通過GOT表間接定位到真正的地址,動態(tài)鏈接器(linker)執(zhí)行重定位(relocate)操作時,這里會被填入真實的外部調(diào)用的絕對地址。
通過這一種方式,linux已經(jīng)能在符號地址綁定這塊得到了較好的性能,但是GOT表的生成也是鏈接過程的一個消耗,所以linux又提供了一種叫延遲綁定的手段,只有在函數(shù)真正用到的時候,才進行函數(shù)的地址定位。我們來了解一下步驟:
當我們進行鏈接的時候,鏈接器不進行函數(shù)符號的尋址,而是通過一條push指令作為替代品(消耗非常?。?,push指令的入?yún)⒖梢允莚el.plt等重定位表相關(guān)的下標,在運行時才進行真正的函數(shù)地址尋址。
但是??!在我們Android體系中,目前只有 MIPS 架構(gòu)支持 lazy binding,所以目前在android,對plt表的內(nèi)容定位就不在運行時進行,而是直接在鏈接時確定,未來會不會更多支持延遲綁定呢,還不確定,所以這個我們作為了解即可。
PLT Hook
我們從上面調(diào)用可以看到,plt表的調(diào)用原理,所以我們的hook點也很明確,如果我們想要fun1-> fun2 變成 fun1 -> fun 3的話(fun2 跟 fun3 必須是外部函數(shù),如果不是外部函數(shù)就不會生成plt表進行跳轉(zhuǎn),因為是本模塊就不需要借助plt表,直接生成地址無關(guān)代碼偏移即可)
以上面的例子出發(fā),我們需要對libtest中的pthread_create進行hook,從而采集pthread_create的數(shù)據(jù),因為我們實現(xiàn)plthook需要以下幾步。
定位出pthread_create的相對偏移(上面說過函數(shù)的真實地址是基地址+相對偏移),那么這個偏移在哪呢?我們從上面流程圖可以看到,偏移就在.rel.plt中(并不是所有偏移都在這里,重定位信息可以分布在.rel.plt
,?.rela.plt
,?.rel.dyn
,?.rela.dyn
,?.rel.android
,?.rela.android
等多個表中,但是一般的外部調(diào)用不需要經(jīng)過全局函數(shù)跳轉(zhuǎn)都在.rel.plt表中),我們可以通過readif -r libtest.so去查看
就這樣我們找到了偏移地址 0x23f8
2.找到基地址,從前面我們可以知道,基地址是運行時決定的,我們可以在運行時檢索/proc/self/maps文件,在里面找到libtest.so的匹配項即可
格式如下
so的范圍地址 權(quán)限 基地址(重點關(guān)注) ?dev inode so名稱
3.通過基地址+偏移,我們得到了跳轉(zhuǎn)目標函數(shù)的地址,這個時候只需要把這個地址指向的函數(shù)更改為我們自定義函數(shù)即可,地址的概念,p->自定義函數(shù)
4.雖然我們實現(xiàn)了函數(shù)替換,但是這個被替換的函數(shù)地址可能會缺少相關(guān)的讀寫權(quán)限,導(dǎo)致出現(xiàn)讀取該地址的時候發(fā)生讀寫異常,我們可以通過
int mprotect(void* __addr, size_t __size, int __prot);
進行讀寫權(quán)限的添加,addr就是當前的地址,size就是大小,我們以當前頁大小執(zhí)行即可(被修改權(quán)限的地址[addr, addr+len-1]),prot當前權(quán)限枚舉
5.由于存在緩存指令的影響,我們需要消除這部分可能已經(jīng)被緩存的指令,可以通過已提供的
void __builtin___clear_cache (char *begin, char *end);
去清除指令緩存,以頁為單位。一個地址所處的頁與結(jié)束時的頁可以通過以下代碼換算
#define PAGE_START(addr) ((addr) & PAGE_MASK) #define PAGE_END(addr) (PAGE_START(addr) + PAGE_SIZE)
其中PAGE_SIZE 由宏定義,這里為 #define PAGE_SIZE 4096
通過以上步驟,我們就能夠?qū)崿F(xiàn)了我們對pthread的hook,這里給出完整的實現(xiàn)
bool isHook = true; int my_pthread_create(pthread_t* __pthread_ptr, pthread_attr_t const* __attr, void* (*__start_routine)(void*), void* p1) { if(isHook){ isHook = false; __android_log_print(ANDROID_LOG_INFO, "hello", "%s","pthread hook power by pika"); return pthread_create(__pthread_ptr,__attr,__start_routine,p1); } else{ return 0; } } #define PAGE_START(addr) ((addr) & PAGE_MASK) #define PAGE_END(addr) (PAGE_START(addr) + PAGE_SIZE) void hook() { char line[512]; FILE *fp; uintptr_t base_addr = 0; uintptr_t addr; //尋找基地址 if(NULL == (fp = fopen("/proc/self/maps", "r"))) return; while(fgets(line, sizeof(line), fp)) { if(NULL != strstr(line, "libtest.so") && sscanf(line, "%" PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1) break; } fclose(fp); __android_log_print(ANDROID_LOG_INFO, "hello", "%u", base_addr); if(0 == base_addr) return; //得到真實的函數(shù)地址 可由readif -r 看到 addr = base_addr + 0x23f8; // 添加讀寫權(quán)限 mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE); // 替換為函數(shù)地址 *(void **)addr = (unsigned*)&my_pthread_create; // 清除緩存 __builtin___clear_cache(static_cast<char *>((void *) PAGE_START(addr)), static_cast<char *>((void *) PAGE_END(addr))); }
調(diào)用hook()后,libtest中pthread_create 就會被轉(zhuǎn)化為my_pthread_create的調(diào)用,這樣我們就實現(xiàn)了一次plt hook!
xhook bhook
上面我們hook的偏移都是基于通過readif看到的偏移地址,但是實際上這個地址都用readif可能會非常不方便,而且我們也只是檢索了rel.plt表,實際上會存在多個復(fù)雜的跳轉(zhuǎn)現(xiàn)象時,就需要檢索所有的重定位表。但是沒關(guān)系,這些xhook bhook都幫我們做了,只需要調(diào)用封裝好的方法即可,我們這里就不結(jié)束api了,感興趣讀者可自行觀看readme
plt hook總結(jié)
最后我們來總結(jié)一下plt hook相關(guān)優(yōu)缺點
優(yōu)點 | 缺點 |
---|---|
可操作性強,原理簡單易用 | 局限性 plt hook 只能作用在外部函數(shù),即調(diào)用生成重定位表的方法中 |
適配成本低,只需要hook 相關(guān)重定位表即可,由elf文件保證其規(guī)范 | ? |
原文鏈接:https://juejin.cn/post/7143945091366223903
相關(guān)推薦
- 2022-08-03 C++?sort排序函數(shù)用法詳解_C 語言
- 2023-11-19 Linux虛擬機VMware的Ubuntu使用vi指令的方向鍵和backspace空格鍵亂碼
- 2023-01-15 React?18中的state概念與使用、注意問題解決_React
- 2022-08-05 從List<Map>中截取指定的范圍數(shù)據(jù)集合
- 2022-08-18 Go?Web編程添加服務(wù)器錯誤和訪問日志_Golang
- 2022-07-10 深拷貝的三種實現(xiàn)方式
- 2022-08-22 詳解C#對Dictionary內(nèi)容的通用操作_C#教程
- 2022-10-09 淺談Redis處理接口冪等性的兩種方案_Redis
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支