首頁 > 軟體

Android效能優化之plt hook與native執行緒監控詳解

2022-09-18 22:04:18

背景

我們在android超級優化-執行緒監控與執行緒統一可以知道,我們能夠通過asm插樁的方式,進行了執行緒的監控與執行緒的統一,通過一系列的黑科技,我們能夠將專案中的執行緒控制在一個非常可觀的水平,但是這個只侷限在java層執行緒的控制,如果我們專案中存在著native庫,或者存在著很多其他so庫,那麼native層的執行緒我們就沒辦法通過ASM或者其他位元組碼手段去監控了,但是並不是就沒有辦法,還有一個黑科技,就是我們的PIL Hook,目前行業上比較出名的就是xhook,和bhook了。

native 執行緒建立

瞭解PLT Hook之前,我們先了解一下native層常用的建立執行緒的手段,沒錯,就是pthread

int pthread_create(pthread_t* __pthread_ptr, pthread_attr_t const* __attr, void* (*__start_routine)(void*), void*);
  • __pthread_ptr:pthread_t型別的引數,成功時tidp指向的內容被設定為新建立執行緒的pthread_t
  • __attr 執行緒的屬性
  • __start_routine 執行函數,新建立執行緒從此函數開始執行
  • __start_routine中 需要執行的入參,如果__start_routine不需要入參,則該值為null

接下里我們用這個例子去說明,我們在MainActivity中設定了一個名叫threadCreate的jni呼叫,開啟一個新執行緒,在新執行緒裡面列印一些傳遞的資料。

libtest.so中的程式碼
/* 宣告結構體 */
struct member {
    int num;
    char *name;
};
/* 定義執行緒pthread */
static void *pthread(void *arg) {
    struct member *temp;
    /* 執行緒pthread開始執行 */
    printf("pthread start!n");
    /* 列印傳入引數 */
    temp = (struct member *) arg;
    printf("member->num:%dn", temp->num);
    printf("member->name:%sn", 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;
    /* 為結構體變數b賦值 */
    b = (struct member *) malloc(sizeof(struct member));
    b->num = 10086;
    b->name = "pika";
    /* 建立執行緒pthread */
    if ((pthread_create(&tidp, NULL, pthread, (void *) b)) == -1) {
        printf("create error!n");
    }
}

通過jni方式呼叫的pthread,我們就沒辦法用常規手段去監控了。所以我們才需要plt hook的方式

PLT

介紹plt hook之前,我們還是有必要了解一些前置的知識。在linux中,會存在很多地址無關的程式碼。在我們的編寫模組中,其實會遇到很多共用物件地址衝突的問題,如果相互依賴的物件是以絕對地址的方式存在的話,那麼執行的時候就會發生地址衝突,比如程序A裡面兩個方法都被定位到了同一個地址,所以才有了地址無關的程式碼。

地址無關的程式碼大多數採用執行時基地址+編譯時定向偏移,其中基地址可以在執行時確定,但是某個符號的執行時地址相對於基地址來說,就可以是一個確定的偏移數值。通過這種方式,函數可以在被需要的時候再進行繫結地址即可,在編譯時只需要記錄偏移就可以保證後期的執行定址的正常。這個儲存偏移地址的東西,就叫做GOT表(全域性偏移表),當程式碼需要參照到這個符號的時候,就可以通過GOT表間接定位到真正的地址,動態連結器(linker)執行重定位(relocate)操作時,這裡會被填入真實的外部呼叫的絕對地址。

通過這一種方式,linux已經能在符號地址繫結這塊得到了較好的效能,但是GOT表的生成也是連結過程的一個消耗,所以linux又提供了一種叫延遲繫結的手段,只有在函數真正用到的時候,才進行函數的地址定位。我們來了解一下步驟:

當我們進行連結的時候,連結器不進行函數符號的定址,而是通過一條push指令作為替代品(消耗非常小),push指令的入參可以是rel.plt等重定位表相關的下標,在執行時才進行真正的函數地址定址。

但是!!在我們Android體系中,目前只有 MIPS 架構支援 lazy binding,所以目前在android,對plt表的內容定位就不在執行時進行,而是直接在連結時確定,未來會不會更多支援延遲繫結呢,還不確定,所以這個我們作為了解即可。

PLT Hook

我們從上面呼叫可以看到,plt表的呼叫原理,所以我們的hook點也很明確,如果我們想要fun1-> fun2 變成 fun1 -> fun 3的話(fun2 跟 fun3 必須是外部函數,如果不是外部函數就不會生成plt表進行跳轉,因為是本模組就不需要藉助plt表,直接生成地址無關程式碼偏移即可)

以上面的例子出發,我們需要對libtest中的pthread_create進行hook,從而採集pthread_create的資料,因為我們實現plthook需要以下幾步。

定位出pthread_create的相對偏移(上面說過函數的真實地址是基地址+相對偏移),那麼這個偏移在哪呢?我們從上面流程圖可以看到,偏移就在.rel.plt中(並不是所有偏移都在這裡,重定位資訊可以分佈在.rel.plt.rela.plt.rel.dyn.rela.dyn.rel.android.rela.android等多個表中,但是一般的外部呼叫不需要經過全域性函數跳轉都在.rel.plt表中),我們可以通過readif -r libtest.so去檢視

就這樣我們找到了偏移地址 0x23f8

2.找到基地址,從前面我們可以知道,基地址是執行時決定的,我們可以在執行時檢索/proc/self/maps檔案,在裡面找到libtest.so的匹配項即可

格式如下

so的範圍地址 許可權 基地址(重點關注)  dev inode so名稱

3.通過基地址+偏移,我們得到了跳轉目標函數的地址,這個時候只需要把這個地址指向的函數更改為我們自定義函數即可,地址的概念,p->自定義函數

4.雖然我們實現了函數替換,但是這個被替換的函數地址可能會缺少相關的讀寫許可權,導致出現讀取該地址的時候發生讀寫異常,我們可以通過

int mprotect(void* __addr, size_t __size, int __prot);

進行讀寫許可權的新增,addr就是當前的地址,size就是大小,我們以當前頁大小執行即可(被修改許可權的地址[addr, addr+len-1]),prot當前許可權列舉

5.由於存在快取指令的影響,我們需要消除這部分可能已經被快取的指令,可以通過已提供的

void __builtin___clear_cache (char *begin, char *end);

去清除指令快取,以頁為單位。一個地址所處的頁與結束時的頁可以通過以下程式碼換算

#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)
其中PAGE_SIZE 由宏定義,這裡為
#define PAGE_SIZE 4096

通過以上步驟,我們就能夠實現了我們對pthread的hook,這裡給出完整的實現

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;
    //得到真實的函數地址 可由readif -r 看到
    addr = base_addr + 0x23f8;
    // 新增讀寫許可權
    mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);
    // 替換為函數地址
    *(void **)addr = (unsigned*)&my_pthread_create;
    // 清除快取
    __builtin___clear_cache(static_cast<char *>((void *) PAGE_START(addr)),
                            static_cast<char *>((void *) PAGE_END(addr)));
}

呼叫hook()後,libtest中pthread_create 就會被轉化為my_pthread_create的呼叫,這樣我們就實現了一次plt hook!

xhook bhook

上面我們hook的偏移都是基於通過readif看到的偏移地址,但是實際上這個地址都用readif可能會非常不方便,而且我們也只是檢索了rel.plt表,實際上會存在多個複雜的跳轉現象時,就需要檢索所有的重定位表。但是沒關係,這些xhook bhook都幫我們做了,只需要呼叫封裝好的方法即可,我們這裡就不結束api了,感興趣讀者可自行觀看readme

plt hook總結

最後我們來總結一下plt hook相關優缺點

優點缺點
可操作性強,原理簡單易用侷限性 plt hook 只能作用在外部函數,即呼叫生成重定位表的方法中
適配成本低,只需要hook 相關重定位表即可,由elf檔案保證其規範 

當前,為了解決plt hook的侷限性問題,同時也有對inline hook 的開源框架,但是inline hook存在適配成本較高穩定性較差的問題,一直沒有得到非常大的推廣,一般只在特殊場景下的使用,這裡普及一下並不詳細展開說明!看完這裡讀者朋友們應該能夠理解plt hook在pthread_create的應用,由於裡面涉及了一些elf檔案的內容,我們先粗略瞭解,必要的時候需要進一步學習查詢即可,我們在以後會推出elf檔案相關的介紹文章,歡迎繼續關注!到這裡,android效能優化執行緒相關的優化就到此結束,更多關於Android plt hook native執行緒監控的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com