文章翻譯:邵凱陽
發(fā)表時(shí)間:2015 年 7 月 27 日
原文作者:Thomas Finch
文章分類:移動(dòng)應(yīng)用開發(fā)
鉤子(Hook)是 Windows 消息處理機(jī)制的一個(gè)平臺(tái),該技術(shù)可以實(shí)現(xiàn)對(duì)消息的監(jiān)視,具有很強(qiáng)大的功能。本文就是基于鉤子的主要功能實(shí)現(xiàn)鉤子在 C 中的應(yīng)用,主要介紹了運(yùn)行時(shí)掛鉤 C 函數(shù)的基本步驟、相關(guān)代碼和一些局限性。
這是一份我最近嘗試的關(guān)于在 C 中運(yùn)行時(shí)函數(shù)掛鉤的快速記錄。對(duì)于鉤入一個(gè)函數(shù)最基本的思想是用您自己的代碼替換函數(shù)的代碼,所以在調(diào)用該函數(shù)時(shí),您的代碼將被執(zhí)行。運(yùn)行時(shí)掛鉤允許您在被執(zhí)行的程序沒有自己的代碼或者沒有以任何方式對(duì)其文件進(jìn)行實(shí)際修改的時(shí)候更改程序的運(yùn)行方式。運(yùn)行時(shí)函數(shù)掛鉤并不少見,并且用于 iOS 越獄調(diào)整(通過 Cydia Substrate 或 Substitute 平臺(tái)提供技術(shù)支持)以及 Xposed 框架 在 Android 程序中的使用。
如果您想在您自己的計(jì)算機(jī)上了解這篇文章,您需要使用 Xcode 和 Xcode 命令行工具安裝的 Mac。這些代碼能夠在 Github 上找到。
//testProgram.c
#include <stdio.h>
int hookTargetFunction() {
printf("Calling original function!\n");
return 5;
}
int main() {
printf("The number is: %d\n", hookTargetFunction());
return 0;
}
編譯和運(yùn)行該程序可以得到下面的輸出:
Calling original function!
The number is: 5
我們的目標(biāo)是鉤入 hookTargetFunction 這個(gè)函數(shù)并且更改該函數(shù)的返回值,使返回值不再是 5。
我們掛鉤目標(biāo)函數(shù)的方法是通過創(chuàng)建一個(gè)動(dòng)態(tài)庫,當(dāng)程序運(yùn)行時(shí)加載它。這個(gè)動(dòng)態(tài)庫的構(gòu)造函數(shù)會(huì)在 main 函數(shù)的目標(biāo)可執(zhí)行文件前執(zhí)行,所以我們就可以在目標(biāo)可執(zhí)行文件運(yùn)行之前在內(nèi)存中修改它。若要運(yùn)行我們替換的代碼,我們需要在掛鉤函數(shù)的開頭插入跳轉(zhuǎn)指令的機(jī)器代碼。換句話說,當(dāng)計(jì)算機(jī)嘗試運(yùn)行目標(biāo)函數(shù)時(shí),他將跳轉(zhuǎn)到我們替換函數(shù)所在的位置并運(yùn)行我們的代碼。
這個(gè)過程的第一步就是創(chuàng)建包含一個(gè)構(gòu)造函數(shù)和一個(gè)替換函數(shù)的動(dòng)態(tài)庫。
//inject.c
#include <stdio.h>
int hookReplacementFunction() {
printf("Calling replacement function!\n");
return 3;
}
__attribute__((constructor))
static void ctor(void) {
printf("Dylib constructor called!\n");
}
當(dāng)他使用了 DYLD_INSERT_LIBRARIES 環(huán)境變量的目標(biāo)程序被編譯和加載后,我們能夠看到他的構(gòu)造函數(shù)在主程序之前被執(zhí)行。
$ ls
inject.c testProgram testProgram.c
$ clang -dynamiclib inject.c -o inject.dylib
$ DYLD_INSERT_LIBRARIES=inject.dylib ./testProgram
Dylib constructor called!
Calling original function!
The number is: 5
為了鉤入目標(biāo)函數(shù),現(xiàn)在我們可以開始向構(gòu)造函數(shù)中添加代碼。由于 x86 跳轉(zhuǎn)指令使用相對(duì)尋址,所以我們不能簡單的在內(nèi)存中給計(jì)算機(jī)一個(gè)地址讓其跳轉(zhuǎn)。首先,我們需要從目標(biāo)函數(shù)中找到替換函數(shù)的抵消函數(shù),這些可以通過獲得進(jìn)入每個(gè)函數(shù)的指針,然后從另一個(gè)指針中減去一個(gè)函數(shù)的指針。
void *mainProgramHandle = dlopen(NULL, RTLD_NOW);
int64_t *origFunc = dlsym(mainProgramHandle , "hookTargetFunction");
int64_t *newFunc = (int64_t*)&hookReplacementFunction;
int32_t offset = (int64_t)newFunc - ((int64_t)origFunc + 5 * sizeof(char));
在這個(gè)示例代碼中有一些值得關(guān)注的事情。首先是使用 dlopen 來獲得進(jìn)入目標(biāo)可執(zhí)行文件的指針。dlopen 通常被用來加載共享庫,但是 根據(jù)其文檔,如果傳遞 NULL 作為文件名,它也可以用于訪問主可執(zhí)行文件。其次應(yīng)該注意的是,跳轉(zhuǎn)的偏移量實(shí)際采取的是下一條指令的地址,在這種情況下目標(biāo)函數(shù)將增加 5 bytes,因?yàn)椴迦胩D(zhuǎn)指令的大小為 5 bytes。
在這篇文章中我省略了一小步,那就是使目標(biāo)函數(shù)所在的內(nèi)存是可寫的,因?yàn)樘幱诎踩目紤],在默認(rèn)情況下內(nèi)存僅僅是可讀的和可執(zhí)行的。一旦這些被完成,最后一步就是創(chuàng)建和插入跳轉(zhuǎn)指令。x86 操作碼是 E9,他與立即數(shù)偏移尋址一起是無條件跳轉(zhuǎn),因此我們將這作為指令的第一個(gè)字節(jié),緊跟的是偏移。
int64_t instruction = 0xE9 | offset << 8;
*origFunc = instruction;
這里是完成的 inject.c 文件:
#include <stdio.h>
#include <dlfcn.h>
#include <stdint.h>
#include <sys/mman.h>
#include <unistd.h>
int hookReplacementFunction() {
printf("Calling replacement function!\n");
return 3;
}
__attribute__((constructor))
static void ctor(void) {
//Get pointers to the original and new functions and calculate the jump offset
void *mainProgramHandle = dlopen(NULL, RTLD_NOW);
int64_t *origFunc = dlsym(mainProgramHandle , "hookTargetFunction");
int64_t *newFunc = (int64_t*)&hookReplacementFunction;
int32_t offset = (int64_t)newFunc - ((int64_t)origFunc + 5 * sizeof(char));
//Make the memory containing the original funcion writable
//Code from http://stackoverflow.com/questions/20381812/mprotect-always-returns-invalid-arguments
size_t pageSize = sysconf(_SC_PAGESIZE);
uintptr_t start = (uintptr_t)origFunc;
uintptr_t end = start + 1;
uintptr_t pageStart = start & -pageSize;
mprotect((void *)pageStart, end - pageStart, PROT_READ | PROT_WRITE | PROT_EXEC);
//Insert the jump instruction at the beginning of the original function
int64_t instruction = 0xe9 | offset << 8;
*origFunc = instruction;
}
當(dāng)他編譯和執(zhí)行完后,他確實(shí)改變了主程序的輸出!
$ ls
inject.c testProgram testProgram.c
$ ./testProgram
Calling original function!
The number is: 5
$ clang -dynamiclib inject.c -o inject.dylib
$ DYLD_INSERT_LIBRARIES=inject.dylib ./testProgram
Calling replacement function!
The number is: 3
這里是另外一個(gè)執(zhí)行過程和一些調(diào)試輸出,顯示了跳轉(zhuǎn)指令插入目標(biāo)函數(shù)的開始:
$ DYLD_INSERT_LIBRARIES=inject.dylib ./testProgram
Original function address: 0x1078abee0
Replacement function address: 0x1078b4c40
Offset: 0x8d5b
Before replacement:
*(origFunc+0): 554889e5
*(origFunc+4): 488d3d73
*(origFunc+8): 00e84c00
*(origFunc+12): 00000089
After replacement:
*(origFunc+0): e95b8d00
*(origFunc+4): 488d3d73
*(origFunc+8): 00e84c00
*(origFunc+12): 00000089
Calling replacement function!
The number is: 3
掛鉤這種方法的一個(gè)局限性是他要求目標(biāo)函數(shù)至少是 5 bytes,用于插入跳轉(zhuǎn)指令。這看起來似乎是一個(gè)愚蠢的限制,但創(chuàng)建這樣小的函數(shù)也肯定是可能的(例如,只有單字節(jié)大小的 ret 指令)。我想不出解決這一問題的方式,畢竟對(duì)單字節(jié)進(jìn)行操作時(shí)很艱難的。最直截了當(dāng)?shù)慕鉀Q方法就是不掛鉤小于 5 bytes的指令。
我遇到的另外一個(gè)問題是讓這些代碼運(yùn)行在 Linux 上。出于某些原因,Linux 始終在一個(gè)高地址加載動(dòng)態(tài)庫,地址如此之高以至于偏移量溢出了可用的 32 bits。我不認(rèn)為這是可以修復(fù)的,盡管也使用了跳轉(zhuǎn)指令,因?yàn)槠屏康淖畲蟪叽缡?32 bits。但是,這個(gè)函數(shù)能夠被掛鉤通過另外一種方法——例如,將替換函數(shù)的地址壓入堆棧,然后通過 ret 指令跳轉(zhuǎn)到改地址。這種方法將會(huì)比簡單的跳轉(zhuǎn)花費(fèi)更多的空間,但是這是我現(xiàn)在僅能想到的方法。
我希望您能夠喜歡這篇文章!再一次,自由下載并且在您自己的計(jì)算機(jī)上測(cè)試這些代碼。當(dāng)您自己動(dòng)手嘗試時(shí),您會(huì)體會(huì)到更多的樂趣!
更多IT技術(shù)干貨: wiki.jikexueyuan.com
加入極客星球翻譯團(tuán)隊(duì): http://wiki.jikexueyuan.com/project/wiki-editors-guidelines/translators.html版權(quán)聲明:
本譯文僅用于學(xué)習(xí)和交流目的。非商業(yè)轉(zhuǎn)載請(qǐng)注明譯者、出處,并保留文章在極客學(xué)院的完整鏈接
商業(yè)合作請(qǐng)聯(lián)系 wiki@jikexueyuan.com
原文地址:[http://thomasfinch.me/blog/2015/07/24/Hooking-C-Functions-At-Runtime.html ](http://thomasfinch.me/blog/2015/07/24/Hooking-C-Functions-At-Runtime.html )