Linux系統(tǒng)調(diào)用Hook:如何給內(nèi)核接口 “裝監(jiān)控”?
在 Linux 系統(tǒng)的運行脈絡(luò)中,系統(tǒng)調(diào)用是用戶程序與內(nèi)核交互的核心通道。但你是否想過,如何實時追蹤這些交互?如何在不破壞原有邏輯的前提下,給內(nèi)核接口裝上 “監(jiān)控”?這正是 Linux 系統(tǒng)調(diào)用 Hook 技術(shù)的魅力所在。它像一把精巧的 “鉤子”,能悄無聲息地附著在系統(tǒng)調(diào)用的關(guān)鍵節(jié)點上,既不阻斷正常的調(diào)用流程,又能精準捕獲每一次交互細節(jié) —— 從參數(shù)傳遞到返回值生成,從調(diào)用頻率到異常行為。
無論是開發(fā)調(diào)試時追蹤程序行為,還是安全防護中監(jiān)測惡意調(diào)用,甚至是性能優(yōu)化時分析資源消耗,這種 “監(jiān)控” 能力都不可或缺。然而,給內(nèi)核接口裝 “監(jiān)控” 絕非易事。內(nèi)核空間的嚴格權(quán)限控制、不同版本的兼容性差異、以及誤操作可能引發(fā)的系統(tǒng)崩潰風(fēng)險,都讓這項技術(shù)充滿挑戰(zhàn)。接下來,我們就揭開 Linux 系統(tǒng)調(diào)用 Hook 的神秘面紗,看看它如何實現(xiàn)對內(nèi)核接口的 “隱形監(jiān)控”。
一、Linux系統(tǒng)調(diào)用簡介
系統(tǒng)調(diào)用(syscall)是一個通用的概念,它既包括應(yīng)用層系統(tǒng)函數(shù)庫的調(diào)用,也包括ring0層系統(tǒng)提供的syscall_table提供的系統(tǒng)api。
1.1系統(tǒng)調(diào)用的概念
系統(tǒng)調(diào)用是操作系統(tǒng)內(nèi)核提供給用戶空間應(yīng)用程序使用的接口。當應(yīng)用程序需要訪問硬件資源(如磁盤、網(wǎng)絡(luò))、創(chuàng)建進程、分配內(nèi)存等操作時,就會通過系統(tǒng)調(diào)用陷入內(nèi)核態(tài),由內(nèi)核來完成這些任務(wù)。例如,常見的文件讀寫函數(shù)read和write,在底層實際上就是通過系統(tǒng)調(diào)用實現(xiàn)的。
1.2系統(tǒng)調(diào)用的實現(xiàn)機制
在 Linux 中,系統(tǒng)調(diào)用通過軟件中斷實現(xiàn)。以 x86 架構(gòu)為例,應(yīng)用程序執(zhí)行int 0x80指令(在較新的內(nèi)核中,也使用sysenter指令),觸發(fā)一個軟件中斷,CPU 會切換到內(nèi)核態(tài),然后根據(jù)系統(tǒng)調(diào)用號在系統(tǒng)調(diào)用表中找到對應(yīng)的內(nèi)核函數(shù)進行執(zhí)行。系統(tǒng)調(diào)用表是一個存儲了所有系統(tǒng)調(diào)用函數(shù)指針的數(shù)組,每個系統(tǒng)調(diào)用都有唯一的編號,通過這個編號可以快速定位到相應(yīng)的處理函數(shù)。
我們必須要明白,Hook技術(shù)是一個相對較寬的話題,因為操作系統(tǒng)從ring3到ring0是分層次的結(jié)構(gòu),在每一個層次上都可以進行相應(yīng)的Hook,它們使用的技術(shù)方法以及取得的效果也是不盡相同的。本文的主題是"系統(tǒng)調(diào)用的Hook學(xué)習(xí)","系統(tǒng)調(diào)用的Hook"是我們的目的,而要實現(xiàn)這個目的可以有很多方法,本文試圖盡量覆蓋從ring3到ring0中所涉及到的Hook技術(shù),來實現(xiàn)系統(tǒng)調(diào)用的監(jiān)控功能。
二、Hook技術(shù)詳解
2.1Ring3中Hook技術(shù)
⑴LD_PRELOAD動態(tài)連接.so函數(shù)劫持
LD_PRELOAD hook技術(shù)屬于so依賴劫持技術(shù)的一種實現(xiàn),所以要討論這種技術(shù)的技術(shù)原理,我們先來看一下linux操作系統(tǒng)加載so的底層原理。
括Linux系統(tǒng)在內(nèi)的很多開源系統(tǒng)都是基于Glibc的,動態(tài)鏈接的ELF可執(zhí)行文件在啟動時同時會啟動動態(tài)鏈接器(/lib/ld-linux.so.X),程序所依賴的共享對象全部由動態(tài)鏈接器負責(zé)裝載和初始化,所以這里所謂的共享庫的查找過程,本質(zhì)上就是動態(tài)鏈接器(/lib/ld-linux.so.X)對共享庫路徑的搜索過程,搜索過程如下:
/etc/ld.so.cache:Linux為了加速LD_PRELOAD的搜索過程,在系統(tǒng)中建立了一個ldconfig程序,這個程序負責(zé)
- 將共享庫下的各個共享庫維護一個SO-NAME(一一對應(yīng)的符號鏈接),這樣每個共享庫的SO-NAME就能夠指向正確的共享庫文件
- 將全部SO-NAME收集起來,集中放到/etc/ld.so.cache文件里面,并建立一個SO-NAME的緩存
- 當動態(tài)鏈接器要查找共享庫時,它可以直接從/etc/ld.so.cache里面查找。所以,如果我們在系統(tǒng)指定的共享庫目錄下添加、刪除或更新任何一個共享庫,或者我們更改了/etc/ld.so.conf、/etc/ld.preload的配置,都應(yīng)該運行一次ldconfig這個程序,以便更新SO-NAME和/etc/ld.so.cache。很多軟件包的安裝程序在結(jié)束共享庫安裝以后都會調(diào)用ldconfig
根據(jù)/etc/ld.so.preload中的配置進行搜索(LD_PRELOAD):這個配置文件中保存了需要搜索的共享庫路徑,Linux動態(tài)共享庫加載器根據(jù)順序進行逐行廣度搜索
根據(jù)環(huán)境變量LD_LIBRARY_PATH指定的動態(tài)庫搜索路徑:
根據(jù)ELF文件中的配置信息:任何一個動態(tài)鏈接的模塊所依賴的模塊路徑保存在".dynamic"段中,由DT_NEED類型的項表示,動態(tài)鏈接器會按照這個路徑去查找DT_RPATH所指定的路徑,編譯目標代碼時,可以對gcc加入鏈接參數(shù)"-Wl,-rpath"指定動態(tài)庫搜索路徑。
- DT_NEED段中保存的是絕對路徑,則動態(tài)鏈接器直接按照這個路徑進行直接加載
- DT_NEED段中保存的是相對路徑,動態(tài)鏈接器會在按照一個約定的順序進行庫文件查找下列路徑:/lib、/usr/lib、/etc/ld.so.conf中配置指定的搜索路徑
可以看到,LD_PRELOAD是Linux系統(tǒng)中啟動新進程首先要加載so的搜索路徑,所以它可以影響程序的運行時的鏈接(Runtime linker),它允許你定義在程序運行前"優(yōu)先加載"的動態(tài)鏈接庫。
我們只要在通過LD_PRELOAD加載的.so中編寫我們需要hook的同名函數(shù),根據(jù)Linux對外部動態(tài)共享庫的符號引入全局符號表的處理,后引入的符號會被省略,即系統(tǒng)原始的.so(/lib64/libc.so.6)中的符號會被省略。
通過strace program也可以看到,Linux是優(yōu)先加載LD_PRELOAD指明的.so,然后再加載系統(tǒng)默認的.so的:
圖片
⑵通過自寫.so文件劫持LD_PRELOAD
①demo例子
正常程序main.c:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
if( strcmp(argv[1], "test") )
{
printf("Incorrect password\n");
}
else
{
printf("Correct password\n");
}
return 0;
}用于劫持函數(shù)的.so代碼hook.c
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
/*
hook的目標是strcmp,所以typedef了一個STRCMP函數(shù)指針
hook的目的是要控制函數(shù)行為,從原庫libc.so.6中拿到strcmp指針,保存成old_strcmp以備調(diào)用
*/
typedef int(*STRCMP)(const char*, const char*);
int strcmp(const char *s1, const char *s2)
{
static void *handle = NULL;
static STRCMP old_strcmp = NULL;
if( !handle )
{
handle = dlopen("libc.so.6", RTLD_LAZY);
old_strcmp = (STRCMP)dlsym(handle, "strcmp");
}
printf("oops!!! hack function invoked. s1=<%s> s2=<%s>\n", s1, s2);
return old_strcmp(s1, s2);
}編譯:
gcc -o test main.c
gcc -fPIC -shared -o hook.so hook.c -ldl運行:
LD_PRELOAD=./hook.so ./test 123②hook function注意事項
在編寫用于function hook的.so文件的時候,要考慮以下幾個因素
1. Hook函數(shù)的覆蓋完備性
對于Linux下的指令執(zhí)行來說,有7個Glibc API都可是實現(xiàn)指令執(zhí)行功能,對這些API對要進行Hook
/*
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, .../* (char *)0, char *const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
http://www.2cto.com/os/201410/342362.html
*/
2. 當前系統(tǒng)中存在function hook的重名覆蓋問題
1) /etc/ld.so.preload中填寫了多條.so加載條目
2) 其他程序通過"export LD_PRELOAD=.."臨時指定了待加載so的路徑
在很多情況下,出于系統(tǒng)管理或者集群系統(tǒng)日志收集的目的,運維人員會向系統(tǒng)中注入.so文件,對特定function函數(shù)進行hook,這個時候,當我們注入的.so文件中的hook function和原有的hook function存在同名的情況,Linux會自動忽略之后載入了hook function,這種情況我們稱之為"共享對象全局符號介入"
3. 注入.so對特定function函數(shù)進行hook要保持原始業(yè)務(wù)的兼容性
典型的hook的做法應(yīng)該是
hook_function()
{
save ori_function_address;
/*
do something in here
span some time delay
*/
call ori_function;
}
hook函數(shù)在執(zhí)行完自己的邏輯后,應(yīng)該要及時調(diào)用被hook前的"原始函數(shù)",保持對原有業(yè)務(wù)邏輯的透明
4. 盡量減小hook函數(shù)對原有調(diào)用邏輯的延時
hook_function()
{
save ori_function_address;
/*
do something in here
span some time delay
*/
call ori_function;
}
hook這個操作是一定會對原有的代碼調(diào)用執(zhí)行邏輯產(chǎn)生延時的,我們需要盡量減少從函數(shù)入口到"call ori_function"這塊的代碼邏輯,讓代碼邏輯盡可能早的去"call ori_function"
在一些極端特殊的場景下,存在對單次API調(diào)用延時極其嚴格的情況,如果延時過長可能會導(dǎo)致原始業(yè)務(wù)邏輯代碼執(zhí)行失敗如果需要不僅僅是替換掉原有庫函數(shù),而且還希望最終將函數(shù)邏輯傳遞到原有系統(tǒng)函數(shù),實現(xiàn)透明hook(完成業(yè)務(wù)邏輯的同時不影響正常的系統(tǒng)行為)、維持調(diào)用鏈,那么需要用到RTLD_NEXT
當調(diào)用dlsym的時候傳入RTLD_NEXT參數(shù),gcc的共享庫加載器會按照"裝載順序(load order)(即先來后到的順序)"獲取"下一個共享庫"中的符號地址
/*
Specifies the next object after this one that defines name. This one refers to the object containing the invocation of dlsym(). The next object is the one found upon the application of a load order symbol resolution algorithm (see dlopen()). The next object is either one of global scope (because it was introduced as part of the original process image or because it was added with a dlopen() operation including the RTLD_GLOBAL flag), or is an object that was included in the same dlopen() operation that loaded this one.
The RTLD_NEXT flag is useful to navigate an intentionally created hierarchy of multiply-defined symbols created through interposition. For example, if a program wished to create an implementation of malloc() that embedded some statistics gathering about memory allocations, such an implementation could use the real malloc() definition to perform the memory allocation-and itself only embed the necessary logic to implement the statistics gathering function.
http://pubs.opengroup.org/onlinepubs/009695399/functions/dlsym.html
http://www.newsmth.net/nForum/#!article/KernelTech/413
*/code example
// used for getting the orginal exported function address
#if defined(RTLD_NEXT)
# define REAL_LIBC RTLD_NEXT
#else
# define REAL_LIBC ((void *) -1L)
#endif
//REAL_LIBC代表當前調(diào)用鏈中緊接著下一個共享庫,從調(diào)用方鏈接映射列表中的下一個關(guān)聯(lián)目標文件獲取符號
#define FN(ptr,type,name,args) ptr = (type (*)args)dlsym (REAL_LIBC, name)
...
FN(func,int,"execve",(const char *, char **const, char **const));我們知道,如果當前進程空間中已經(jīng)存在某個同名的符號,則后載入的so的同名函數(shù)符號會被忽略,但是不影響so的載入,先后載入的so會形成一個鏈式的依賴關(guān)系,通過RTLD_NEXT可以遍歷
③SO功能代碼編寫
這個小節(jié)我們來完成一個基本的進程、網(wǎng)絡(luò)、模塊加載監(jiān)控的小demo。
1. 指令執(zhí)行
1) execve
2) execv
2. 網(wǎng)絡(luò)連接
1) connect
3. LKM模塊加載
1) init_modulehook.c
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <netinet/in.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#if defined(RTLD_NEXT)
# define REAL_LIBC RTLD_NEXT
#else
# define REAL_LIBC ((void *) -1L)
#endif
#define FN(ptr, type, name, args) ptr = (type (*)args)dlsym (REAL_LIBC, name)
int execve(const char *filename, char *const argv[], char *const envp[])
{
static int (*func)(const char *, char **, char **);
FN(func,int,"execve",(const char *, char **const, char **const));
//print the log
printf("filename: %s, argv[0]: %s, envp:%s\n", filename, argv[0], envp);
return (*func) (filename, (char**) argv, (char **) envp);
}
int execv(const char *filename, char *const argv[])
{
static int (*func)(const char *, char **);
FN(func,int,"execv", (const char *, char **const));
//print the log
printf("filename: %s, argv[0]: %s\n", filename, argv[0]);
return (*func) (filename, (char **) argv);
}
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
{
static int (*func)(int, const struct sockaddr *, socklen_t);
FN(func,int,"connect", (int, const struct sockaddr *, socklen_t));
/*
print the log
獲取、打印參數(shù)信息的時候需要注意
1. 加鎖
2. 拷貝到本地棧區(qū)變量中
3. 然后再打印
調(diào)試的時候發(fā)現(xiàn)直接獲取打印會導(dǎo)致core dump
*/
printf("socket connect hooked!!\n");
//return (*func) (sockfd, (const struct sockaddr *) addr, (socklen_t)addrlen);
return (*func) (sockfd, addr, addrlen);
}
int init_module(void *module_image, unsigned long len, const char *param_values)
{
static int (*func)(void *, unsigned long, const char *);
FN(func,int,"init_module",(void *, unsigned long, const char *));
/*
print the log
lkm的加載不需要取參數(shù),只需要捕獲事件本身即可
*/
printf("lkm load hooked!!\n");
return (*func) ((void *)module_image, (unsigned long)len, (const char *)param_values);
}編譯,并裝載。
//編譯出一個so文件
gcc -fPIC -shared -o hook.so hook.c -ldl添加LD_PRELOAD有很多種方式。
1. 臨時一次性添加(當條指令有效)
LD_PRELOAD=./hook.so nc www.baidu.com 80
/*
LD_PRELOAD后面接的是具體的庫文件全路徑,可以連接多個路徑
程序加載時,LD_PRELOAD加載路徑優(yōu)先級高于/etc/ld.so.preload
*/
2. 添加到環(huán)境變量LD_PRELOAD中(當前會話SESSION有效)
export LD_PRELOAD=/zhenghan/snoopylog/hook.so
//"/zhenghan/snoopylog/"是編譯.so文件的目錄
unset LD_PRELOAD
3. 添加到環(huán)境變量LD_LIBRARY_PATH中
假如現(xiàn)在需要在已有的環(huán)境變量上添加新的路徑名,則采用如下方式
LD_LIBRARY_PATH=/zhenghan/snoopylog/hook.so:$LD_LIBRARY_PATH.(newdirs是新的路徑串)
/*
LD_LIBRARY_PATH指定查找路徑,這個路徑優(yōu)先級別高于系統(tǒng)預(yù)設(shè)的路徑
*/
4. 添加到系統(tǒng)配置文件中
vim /etc/ld.so.preload
add /zhenghan/snoopylog/hook.so
5. 添加到配置文件目錄中
cat /etc/ld.so.conf
//include ld.so.conf.d/*.conf效果測試:
1. 指令執(zhí)行
在代碼中手動調(diào)用: execve(argv[1], newargv, newenviron);
2. 網(wǎng)絡(luò)連接
執(zhí)行: nc www.baidu.com 80
3. LKM模塊加載
編寫測試LKM模塊,執(zhí)行: insmod hello.ko在真實的環(huán)境中,socket的網(wǎng)絡(luò)連接存在大量的連接失敗,非阻塞等待等等情況,這些都會觸發(fā)connect的hook調(diào)用,對于connect的hook來說,我們需要對以下的事情進行過濾。
1. 區(qū)分IPv4、IPv6
根據(jù)connect參數(shù)中的(struct sockaddr *addr)->sa_family進行判斷
2. 區(qū)分執(zhí)行成功、執(zhí)行失敗
如果本次connect調(diào)用執(zhí)行失敗,則不應(yīng)該繼續(xù)進行參數(shù)獲取
int ret_code = (*func) (sockfd, addr, addrlen);
int tmp_errno = errno;
if (ret_code == -1 && tmp_errno != EINPROGRESS)
{
return ret_code;
}
3. 區(qū)分TCP、UDP連接
對于TCP和UDP來說,它們都可以發(fā)起connect請求,我們需要從中過濾出TCP Connect請求
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
/*
#include <sys/types.h>
#include <sys/socket.h>
main()
{
int s;
int optval;
int optlen = sizeof(int);
if((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)
perror("socket");
getsockopt(s, SOL_SOCKET, SO_TYPE, &optval, &optlen);
printf("optval = %d\n", optval);
close(s);
}
*/
執(zhí)行:
optval = 1 //SOCK_STREAM 的定義正是此值④劫持效果測試
指令執(zhí)行監(jiān)控
execve.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
char *newargv[] = { NULL, "hello", "world", NULL };
char *newenviron[] = { NULL };
if (argc != 2)
{
fprintf(stderr, "Usage: %s <file-to-exec>\n", argv[0]);
exit(EXIT_FAILURE);
}
newargv[0] = argv[1];
execve(argv[1], newargv, newenviron);
perror("execve"); /* execve() only returns on error */
exit(EXIT_FAILURE);
}
//gcc -o execve execve.cmyecho.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int j;
for (j = 0; j < argc; j++)
printf("argv[%d]: %s\n", j, argv[j]);
exit(EXIT_SUCCESS);
}
//gcc -o myecho myecho.c
圖片
可以看到,LD_PRELOAD在所有程序代碼庫加載前優(yōu)先加載,對glibc中的導(dǎo)出函數(shù)進行了hook
網(wǎng)絡(luò)連接監(jiān)控
圖片
模塊加載監(jiān)控:hello.c
#include <linux/module.h> // included for all kernel modules
#include <linux/kernel.h> // included for KERN_INFO
#include <linux/init.h> // included for __init and __exit macros
#include <linux/cred.h>
#include <linux/sched.h>
static int __init hello_init(void)
{
struct cred *currentCred;
currentCred = current->cred;
printk(KERN_INFO "uid = %d\n", currentCred->uid);
printk(KERN_INFO "gid = %d\n", currentCred->gid);
printk(KERN_INFO "suid = %d\n", currentCred->suid);
printk(KERN_INFO "sgid = %d\n", currentCred->sgid);
printk(KERN_INFO "euid = %d\n", currentCred->euid);
printk(KERN_INFO "egid = %d\n", currentCred->egid);
printk(KERN_INFO "Hello world!\n");
return 0; // Non-zero return means that the module couldn't be loaded.
}
static void __exit hello_cleanup(void)
{
printk(KERN_INFO "Cleaning up module.\n");
}
module_init(hello_init);
module_exit(hello_cleanup);Makefile
obj-m := hello.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean加載模塊:insmod hello.ko
圖片
使用snoopy進行execve/execv、connect、init_module hook
snoopy會監(jiān)控服務(wù)器上的命令執(zhí)行,并記錄到syslog。
本質(zhì)上,snoopy是利用ld_preload技術(shù)實現(xiàn)so依賴劫持的,只是它的工程化完善度更高,日志采集和日志整理傳輸這方面已經(jīng)幫助我們完成了。
#cat /etc/ld.so.preload
/usr/local/snoopy/lib/snoopy.so基于PD_PRELOAD、LD_LIBRARY_PATH環(huán)境變量劫持繞過Hook模塊
我們知道,snoopy監(jiān)控服務(wù)器上的指令執(zhí)行,是通過修改系統(tǒng)的共享庫預(yù)加載配置文件(/etc/ld.so.preload)實現(xiàn),但是這種方式存在一個被黑客繞過的可能
圖片
LD_PRELOAD的加載順序優(yōu)先于/etc/ld.so.preload的配置項,黑客可以利用這點來強制覆蓋共享庫的加載順序
1. 強制指定LD_PRELOAD的環(huán)境變量
export LD_PRELOAD=/lib64/libc.so.6
bash
/*
新啟動的bash終端默認會使用LD_PRELOAD的共享庫路徑
*/
2. LD_PRELOAD="/lib64/libc.so.6" bash
/*
重新開啟一個加載了默認libc.so.6共享庫的bash session
因為對于libc.so.6來說,它沒有使用dlsym去動態(tài)獲取API Function調(diào)用鏈條的RTL_NEXT函數(shù),即調(diào)用鏈是斷開的
*/在這個新的Bash下執(zhí)行的指令,因為都不會調(diào)用到snoopy的hook函數(shù),所以也不會被記錄下來。
基于ptrace()調(diào)試技術(shù)進行API Hook
在Linux下,除了使用LD_PRELOAD這種被動Glibc API注入方式,還可以使用基于調(diào)試器(Debuger)思想的ptrace()主動注入方式,總體思路如下:
- 使用Linux Module、或者LSM掛載點對進程的啟動動作進行實時的監(jiān)控,并通過Ring0-Ring3通信,通知到Ring3程序有新進程啟動的動作
- 用ptrace函數(shù)attach上目標進程
- 讓目標進程的執(zhí)行流程跳轉(zhuǎn)到mmap函數(shù)來分配一小段內(nèi)存空間
- 把一段機器碼拷貝到目標進程中剛分配的內(nèi)存中去
- 最后讓目標進程的執(zhí)行流程跳轉(zhuǎn)到注入的代碼執(zhí)行
通過靜態(tài)編碼繞過LD_PRELOAD機制監(jiān)控
通過靜態(tài)鏈接方式編譯so模塊:
gcc -o test test.c -static在靜態(tài)鏈接的模式下,程序不會去搜索系統(tǒng)中的so文件(不同是系統(tǒng)默認的、還是第三方加入的),所以也就不會調(diào)用到Hook SO模塊。
通過內(nèi)聯(lián)匯編的方式繞過LD_PRELOAD機制監(jiān)控
使用內(nèi)嵌匯編的形式直接通過syscall指令使用系統(tǒng)調(diào)用功能,同樣也不會調(diào)用到Glibc提供的API。
asm("movq $2, %%rax\n\t syscal:"=a"(ret));2.2Ring0中Hook技術(shù)
傳統(tǒng)的kernel inline hook技術(shù)就是修改內(nèi)核函數(shù)的opcode,通過寫入jmp或push ret等指令跳轉(zhuǎn)到新的內(nèi)核函數(shù)中,從何達到劫持的目的。
- 我們知道實現(xiàn)一個系統(tǒng)調(diào)用的函數(shù)中一定會遞歸的嵌套有很多的子函數(shù),即它必定要調(diào)用它的下層函數(shù)。
- 從匯編的角度來說,對一個子函數(shù)的調(diào)用是采用"段內(nèi)相對短跳轉(zhuǎn) jmp offset"來實現(xiàn)的,即CPU根據(jù)offset來進行一個偏移量的跳轉(zhuǎn)。如果我們把下層函數(shù)在上層函數(shù)中的offset替換成我們"Hook函數(shù)"的offset,這樣上層函數(shù)調(diào)用下層函數(shù)時,就會跳到我們的"Hook函數(shù)"中。
- 我們就可以在"Hook函數(shù)"中做過濾和劫持內(nèi)容的工作
以sys_read作為例子:
\linux-2.6.32.63\fs\read_write.c
asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
file = fget_light(fd, &fput_needed);
if (file)
{
loff_t pos = file_pos_read(file);
ret = vfs_read(file, buf, count, &pos);
file_pos_write(file, pos);
fput_light(file, fput_needed);
}
return ret;
}
EXPORT_SYMBOL_GPL(sys_read);在sys_read()中,調(diào)用了子函數(shù)vfs_read()來完成讀取數(shù)據(jù)的操作,在sys_read()中調(diào)用子函數(shù)vfs_read()的匯編命令是:
call 0xc106d75c <vfs_read>等同于:
jmp offset(相對于sys_read()的基址偏移)所以,我們的思路很明確,找到call 0xc106d75c <vfs_read>這條匯編,把其中的offset改成我們的Hook函數(shù)對應(yīng)的offset,就可以實現(xiàn)劫持目的了
1. 搜索sys_read的opcode 2. 如果發(fā)現(xiàn)是call指令,根據(jù)call后面的offset計算要跳轉(zhuǎn)的地址是不是我們要hook的函數(shù)地址 1) 如果"不是"就重新計算Hook函數(shù)的offset,用Hook函數(shù)的offset替換原來的offset 2) 如果"已經(jīng)是"Hook函數(shù)的offset,則說明函數(shù)已經(jīng)處于被劫持狀態(tài)了,我們的Hook引擎應(yīng)該直接忽略跳過,避免重復(fù)劫持
poc:
/*
參數(shù):
1. handler是上層函數(shù)的地址,這里就是sys_read的地址
2. old_func是要替換的函數(shù)地址,這里就是vfs_read
3. new_func是新函數(shù)的地址,這里就是new_vfs_read的地址
*/
unsigned int patch_kernel_func(unsigned int handler, unsigned int old_func,
unsigned int new_func)
{
unsigned char *p = (unsigned char *)handler;
unsigned char buf[4] = "\x00\x00\x00\x00";
unsigned int offset = 0;
unsigned int orig = 0;
int i = 0;
DbgPrint("\n*** hook engine: start patch func at: 0x%08x\n", old_func);
while (1) {
if (i > 512)
return 0;
if (p[0] == 0xe8) {
DbgPrint("*** hook engine: found opcode 0x%02x\n", p[0]);
DbgPrint("*** hook engine: call addr: 0x%08x\n",
(unsigned int)p);
buf[0] = p[1];
buf[1] = p[2];
buf[2] = p[3];
buf[3] = p[4];
DbgPrint("*** hook engine: 0x%02x 0x%02x 0x%02x 0x%02x\n",
p[1], p[2], p[3], p[4]);
offset = *(unsigned int *)buf;
DbgPrint("*** hook engine: offset: 0x%08x\n", offset);
orig = offset + (unsigned int)p + 5;
DbgPrint("*** hook engine: original func: 0x%08x\n", orig);
if (orig == old_func) {
DbgPrint("*** hook engine: found old func at"
" 0x%08x\n",
old_func);
DbgPrint("%d\n", i);
break;
}
}
p++;
i++;
}
offset = new_func - (unsigned int)p - 5;
DbgPrint("*** hook engine: new func offset: 0x%08x\n", offset);
p[1] = (offset & 0x000000ff);
p[2] = (offset & 0x0000ff00) >> 8;
p[3] = (offset & 0x00ff0000) >> 16;
p[4] = (offset & 0xff000000) >> 24;
DbgPrint("*** hook engine: pachted new func offset.\n");
return orig;
}對于這類劫持攻擊,目前常見的做法是fireeye的"函數(shù)返回地址污點檢測",通過對原有指令返回位置的匯編代碼作污點標記,通過查找jmp,push ret等指令來進行防御。
⑴利用0x80中斷劫持system_call->sys_call_table進行系統(tǒng)調(diào)用Hook
我們知道,要對系統(tǒng)調(diào)用(sys_call_table)進行替換,卻必須要獲取該地址后才可以進行替換。但是Linux 2.6版的內(nèi)核出于安全的考慮沒有將系統(tǒng)調(diào)用列表基地址的符號sys_call_table導(dǎo)出,但是我們可以采取一些hacking的方式進行獲取。
因為系統(tǒng)調(diào)用都是通過0x80中斷來進行的,故可以通過查找0x80中斷的處理程序來獲得sys_call_table的地址。其基本步驟是
1. 獲取中斷描述符表(IDT)的地址(使用C ASM匯編) 2. 從中查找0x80中斷(系統(tǒng)調(diào)用中斷)的服務(wù)例程(8*0x80偏移) 3. 搜索該例程的內(nèi)存空間, 4. 從其中獲取sys_call_table(保存所有系統(tǒng)調(diào)用例程的入口地址)的地址
編程示例
find_sys_call_table.c
#include <linux/module.h>
#include <linux/kernel.h>
// 中斷描述符表寄存器結(jié)構(gòu)
struct
{
unsigned short limit;
unsigned int base;
} __attribute__((packed)) idtr;
// 中斷描述符表結(jié)構(gòu)
struct
{
unsigned short off1;
unsigned short sel;
unsigned char none, flags;
unsigned short off2;
} __attribute__((packed)) idt;
// 查找sys_call_table的地址
void disp_sys_call_table(void)
{
unsigned int sys_call_off;
unsigned int sys_call_table;
char* p;
int i;
// 獲取中斷描述符表寄存器的地址
asm("sidt %0":"=m"(idtr));
printk("addr of idtr: %x\n", &idtr);
// 獲取0x80中斷處理程序的地址
memcpy(&idt, idtr.base+8*0x80, sizeof(idt));
sys_call_off=((idt.off2<<16)|idt.off1);
printk("addr of idt 0x80: %x\n", sys_call_off);
// 從0x80中斷服務(wù)例程中搜索sys_call_table的地址
p=sys_call_off;
for (i=0; i<100; i++)
{
if (p=='\xff' && p[i+1]=='\x14' && p[i+2]=='\x85')
{
sys_call_table=*(unsigned int*)(p+i+3);
printk("addr of sys_call_table: %x\n", sys_call_table);
return ;
}
}
}
// 模塊載入時被調(diào)用
static int __init init_get_sys_call_table(void)
{
disp_sys_call_table();
return 0;
}
module_init(init_get_sys_call_table);
// 模塊卸載時被調(diào)用
static void __exit exit_get_sys_call_table(void)
{
}
module_exit(exit_get_sys_call_table);
// 模塊信息
MODULE_LICENSE("GPL2.0");
MODULE_AUTHOR("LittleHann");Makefile
obj-m := find_sys_call_table.o編譯
make -C /usr/src/kernels/2.6.32-358.el6.i686 M=$(pwd) modules測試效果
dmesg| tail
圖片
獲取到了sys_call_table的基地址之后,我們就可以修改指定offset對應(yīng)的系統(tǒng)調(diào)用了,從而達到劫持系統(tǒng)調(diào)用的目的。
⑵獲取sys_call_table的常用方法
①通過dump獲取絕對地址
模擬出一個call *sys_call_table(,%eax,4),然后看其機器碼,然后在system_call的附近基于這個特征進行尋找
#include <stdio.h>
void fun1()
{
printf("fun1/n");
}
void fun2()
{
printf("fun2/n");
}
unsigned int sys_call_table[2] = {fun1, fun2};
int main(int argc, char **argv)
{
asm("call *sys_call_table(%eax,4");
}
編譯
gcc test.c -o test
objdump進行dump
objdump -D ./test | grep sys_call_table②通過/boot/System.map-2.6.32-358.el6.i686文件查找
cd /boot
grep sys_call_table System.map-2.6.32-358.el6.i686③通過讀取/dev/kmem虛擬內(nèi)存全鏡像設(shè)備文件獲得sys_call_table地址
Linux下/dev/mem和/dev/kmem的區(qū)別:
/dev/mem: 物理內(nèi)存的全鏡像??梢杂脕碓L問物理內(nèi)存。比如: 1) X用來訪問顯卡的物理內(nèi)存, 2) 嵌入式中訪問GPIO。用法一般就是open,然后mmap,接著可以使用map之后的地址來訪問物理內(nèi)存。這其實就是實現(xiàn)用戶空間驅(qū)動的一種方法。2. /dev/kmem: kernel看到的虛擬內(nèi)存的全鏡像??梢杂脕? 1) 訪問kernel的內(nèi)容,查看kernel的變量, 2) 用作rootkit之類的
code
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
int kfd;
struct
{
unsigned short limit;
unsigned int base;
} __attribute__ ((packed)) idtr;
struct
{
unsigned short off1;
unsigned short sel;
unsigned char none, flags;
unsigned short off2;
} __attribute__ ((packed)) idt;
int readkmem (unsigned char *mem, unsigned off, int bytes)
{
if (lseek64 (kfd, (unsigned long long) off, SEEK_SET) != off)
{
return -1;
}
if (read (kfd, mem, bytes) != bytes)
{
return -1;
}
}
int main (void)
{
unsigned long sct_off;
unsigned long sct;
unsigned char *p, code[255];
int i;
/* request IDT and fill struct */
asm ("sidt %0":"=m" (idtr));
if ((kfd = open ("/dev/kmem", O_RDONLY)) == -1)
{
perror("open");
exit(-1);
}
if (readkmem ((unsigned char *)&idt, idtr.base + 8 * 0x80, sizeof (idt)) == -1)
{
printf("Failed to read from /dev/kmem\n");
exit(-1);
}
sct_off = (idt.off2 << 16) | idt.off1;
if (readkmem (code, sct_off, 0x100) == -1)
{
printf("Failed to read from /dev/kmem\n");
exit(-1);
}
/* find the code sequence that calls SCT */
sct = 0;
for (i = 0; i < 255; i++)
{
if (code[i] == 0xff && code[i+1] == 0x14 && code[i+2] == 0x85)
{
sct = code[i+3] + (code[i+4] << 8) + (code[i+5] << 16) + (code[i+6] << 24);
}
}
if (sct)
{
printf ("sys_call_table: 0x%x\n", sct);
}
close (kfd);
}④通過函數(shù)特征碼循環(huán)搜索獲取sys_call_table地址 (64 bit)
unsigned long **find_sys_call_table()
{
unsigned long ptr;
unsigned long *p;
for (ptr = (unsigned long)sys_close; ptr < (unsigned long)&loops_per_jiffy; ptr += sizeof(void *))
{
p = (unsigned long *)ptr;
if (p[__NR_close] == (unsigned long)sys_close)
{
printk(KERN_DEBUG "Found the sys_call_table!!!\n");
return (unsigned long **)p;
}
}
return NULL;
}要特別注意的是代碼中進行函數(shù)地址搜索的代碼:if (p[__NR_close] == (unsigned long)sys_close)
在64bit Linux下,函數(shù)的地址是8字節(jié)的,所以要使用unsigned long
我們可以在linux下執(zhí)行以下兩條指令
grep sys_close System.map-2.6.32-358.el6.i686
grep loops_per_jiffy System.map-2.6.32-358.el6.i686
圖片
可以看到,系統(tǒng)調(diào)用表sys_call_table中的函數(shù)地址都落在這個地址區(qū)間中,因此我們可以使用loop搜索的方法去獲取sys_call_table的基地址
⑶通過kprobe方式動態(tài)獲取kallsyms_lookup_name,然后利用kallsyms_lookup_name獲取sys_call_table的地址
通過kprobe的函數(shù)hook掛鉤機制,可以獲取內(nèi)核中任意函數(shù)的入口地址,我們可以先獲取"kallsyms_lookup_name"函數(shù)的入口地址
//get symbol name by "kprobe.addr"
//when register a kprobe on succefully return,the structure of kprobe save the symbol address at "kprobe.addr"
//just return this value
static void* aquire_symbol_by_kprobe(char* symbol_name)
{
void *symbol_addr=NULL;
struct kprobe kp;
do
{
memset(&kp,0,sizeof(kp));
kp.symbol_name=symbol_name;
kp.pre_handler=kprobe_pre;
if(register_kprobe(&kp)!=0)
{
break;
}
//this is the address of "symbol_name"
symbol_addr=(void*)kp.addr;
//now kprobe is not used any more,so unregister it
unregister_kprobe(&kp);
}while(false);
return symbol_addr;
}
//調(diào)用之
tmp_lookup_func = aquire_symbol_by_kprobe("kallsyms_lookup_name");kallsyms_lookup_name()可以用于獲取內(nèi)核導(dǎo)出符號表中的符號地址,而sys_call_table的地址也存在于內(nèi)核導(dǎo)出符號表中,我們可以使用kallsyms_lookup_name()獲取到sys_call_table的基地址
(void**)kallsyms_lookup_name("sys_call_table");⑷利用Linux內(nèi)核機制kprobe機制(kprobes, jprobe和kretprobe)進行系統(tǒng)調(diào)用Hook
kprobe是一個動態(tài)地收集調(diào)試和性能信息的工具,它從Dprobe項目派生而來,它幾乎可以跟蹤任何函數(shù)或被執(zhí)行的指令以及一些異步事件。它的基本工作機制是:
- 1. 用戶指定一個探測點,并把一個用戶定義的處理函數(shù)關(guān)聯(lián)到該探測點
- 2. 在注冊探測點的時候,對被探測函數(shù)的指令碼進行替換,替換為int 3的指令碼
- 3. 在執(zhí)行int 3的異常執(zhí)行中,通過通知鏈的方式調(diào)用kprobe的異常處理函數(shù)
- 4. 在kprobe的異常出來函數(shù)中,判斷是否存在pre_handler鉤子,存在則執(zhí)行
- 5. 執(zhí)行完后,準備進入單步調(diào)試,通過設(shè)置EFLAGS中的TF標志位,并且把異常返回的地址修改為保存的原指令
- 6. 代碼返回,執(zhí)行原有指令,執(zhí)行結(jié)束后觸發(fā)單步異常 7. 在單步異常的處理中,清除單步標志,執(zhí)行post_handler流程,并最終返回
從原理上來說,kprobe的這種機制屬于系統(tǒng)提供的"回調(diào)訂閱",和netfilter是類似的,linux內(nèi)核通過在某些代碼執(zhí)行流程中給出回調(diào)函數(shù)接口供程序員訂閱,內(nèi)核開發(fā)人員可以在這些回調(diào)點上注冊(訂閱)自定義的處理函數(shù),同時還可以獲取到相應(yīng)的狀態(tài)信息,方便進行過濾、分析
kprobe實現(xiàn)了三種類型的探測點:
- 1. kprobes kprobes是可以被插入到內(nèi)核的任何指令位置的探測點,kprobe允許在同一地址注冊多個kprobes,但是不能同時在該地址上有多個jprobes
- 2. jprobe jprobe則只能被插入到一個內(nèi)核函數(shù)的入口
- 3. kretprobe(也叫返回探測點) 而kretprobe則是在指定的內(nèi)核函數(shù)返回時才被執(zhí)行
在本文中,我們可以使用kprobe的程序?qū)崿F(xiàn)作一個內(nèi)核模塊,模塊的初始化函數(shù)來負責(zé)安裝探測點,退出函數(shù)卸載那些被安裝的探測點。kprobe提供了接口函數(shù)(APIs)來安裝或卸載探測點。目前kprobe支持如下架構(gòu):i386、x86_64、ppc64、ia64(不支持對slot1指令的探測)、sparc64 (返回探測還沒有實現(xiàn))
三、Linux系統(tǒng)調(diào)用中常見的Hook技術(shù)方法
3.1基于函數(shù)指針的 Hook
在 Linux 內(nèi)核中,系統(tǒng)調(diào)用函數(shù)的入口地址存儲在系統(tǒng)調(diào)用表中,這個表實際上是一個函數(shù)指針數(shù)組?;诤瘮?shù)指針的 Hook 方法就是直接修改系統(tǒng)調(diào)用表中對應(yīng)函數(shù)的指針,使其指向我們自定義的 Hook 函數(shù)。
實現(xiàn)步驟:首先需要找到系統(tǒng)調(diào)用表的地址,這在不同的內(nèi)核版本和架構(gòu)上可能有所不同。然后,通過修改內(nèi)存中的函數(shù)指針,將其指向自定義的 Hook 函數(shù)。在 Hook 函數(shù)中,可以先執(zhí)行自己的邏輯,然后再調(diào)用原來的系統(tǒng)調(diào)用函數(shù)(如果需要的話)。
示例代碼(簡化示意):
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/syscalls.h>
// 保存原來的系統(tǒng)調(diào)用函數(shù)指針
asmlinkage long (*original_sys_open)(const char __user *, int, umode_t);
// 自定義的Hook函數(shù)
asmlinkage long my_sys_open(const char __user *filename, int flags, umode_t mode) {
printk(KERN_INFO "MyHook: Opening file: %s\n", filename);
// 調(diào)用原來的系統(tǒng)調(diào)用函數(shù)
return original_sys_open(filename, flags, mode);
}
static int __init my_init(void) {
// 獲取系統(tǒng)調(diào)用表地址
unsigned long *sys_call_table = (unsigned long *)sys_call_table;
// 保存原來的系統(tǒng)調(diào)用函數(shù)指針
original_sys_open = (void *)sys_call_table[__NR_open];
// 修改系統(tǒng)調(diào)用表中的函數(shù)指針
sys_call_table[__NR_open] = (unsigned long)my_sys_open;
return 0;
}
static void __exit my_exit(void) {
// 恢復(fù)原來的系統(tǒng)調(diào)用函數(shù)指針
unsigned long *sys_call_table = (unsigned long *)sys_call_table;
sys_call_table[__NR_open] = (unsigned long)original_sys_open;
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");優(yōu)點與局限性:這種方法簡單直接,效果顯著。但是,由于直接修改系統(tǒng)調(diào)用表,可能會導(dǎo)致內(nèi)核的穩(wěn)定性問題,并且在不同的內(nèi)核版本之間移植性較差,因為系統(tǒng)調(diào)用表的結(jié)構(gòu)和地址可能會發(fā)生變化。
3.2基于 GOT(Global Offset Table)的 Hook
在用戶空間的動態(tài)鏈接庫中,GOT 是一個重要的數(shù)據(jù)結(jié)構(gòu),用于存儲外部函數(shù)的地址?;?GOT 的 Hook 方法主要應(yīng)用于用戶空間的程序,通過修改 GOT 表中函數(shù)的地址,實現(xiàn)對系統(tǒng)調(diào)用的 Hook。
實現(xiàn)步驟:首先需要找到目標函數(shù)在 GOT 表中的項,然后修改該項的內(nèi)容,使其指向自定義的 Hook 函數(shù)。在 Hook 函數(shù)中,可以進行自己的邏輯處理,最后再通過調(diào)用原來的函數(shù)地址(保存在一個臨時變量中)來執(zhí)行原函數(shù)的功能。
示例代碼(以 C 語言和動態(tài)鏈接庫為例):
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
// 自定義的Hook函數(shù)
int my_open(const char *pathname, int flags, mode_t mode) {
printf("MyHook: Opening file: %s\n", pathname);
// 獲取原來的open函數(shù)指針
int (*original_open)(const char *, int, mode_t) = dlsym(RTLD_NEXT, "open");
// 調(diào)用原來的open函數(shù)
return original_open(pathname, flags, mode);
}
// 通過環(huán)境變量LD_PRELOAD加載這個庫時,會優(yōu)先使用這個函數(shù)
__attribute__((constructor)) void my_init(void) {
void *handle = dlopen(NULL, RTLD_NOW);
if (!handle) {
fprintf(stderr, "Error opening library: %s\n", dlerror());
exit(EXIT_FAILURE);
}
// 使用dlsym獲取原來的open函數(shù)指針(這里只是示例,實際可能需要更復(fù)雜的處理)
int (*original_open)(const char *, int, mode_t) = dlsym(handle, "open");
if (!original_open) {
fprintf(stderr, "Error getting original open function: %s\n", dlerror());
dlclose(handle);
exit(EXIT_FAILURE);
}
// 這里可以通過一些技巧修改GOT表中open函數(shù)的地址,使其指向my_open函數(shù)
// 具體實現(xiàn)因不同系統(tǒng)和編譯器而異,此處簡化示意
}優(yōu)點與局限性:這種方法主要在用戶空間操作,對內(nèi)核的影響較小,相對安全穩(wěn)定。而且,它可以針對特定的用戶程序進行 Hook,具有較好的靈活性。但是,它只能 Hook 用戶空間調(diào)用的系統(tǒng)調(diào)用函數(shù),對于內(nèi)核內(nèi)部直接調(diào)用的系統(tǒng)調(diào)用則無法生效。
3.3基于內(nèi)核模塊的 Kprobes Hook
Kprobes 是 Linux 內(nèi)核提供的一種動態(tài)探測機制,它允許開發(fā)者在不修改內(nèi)核源代碼的情況下,對內(nèi)核函數(shù)的執(zhí)行進行探測和干預(yù)?;?Kprobes 的 Hook 技術(shù)就是利用這一機制來實現(xiàn)對系統(tǒng)調(diào)用的 Hook。
實現(xiàn)步驟:首先需要注冊一個 Kprobe 結(jié)構(gòu)體,指定要 Hook 的內(nèi)核函數(shù)以及在函數(shù)執(zhí)行前、執(zhí)行后和發(fā)生異常時的回調(diào)函數(shù)。然后,通過內(nèi)核提供的函數(shù)將 Kprobe 注冊到內(nèi)核中。在回調(diào)函數(shù)中,可以編寫自己的 Hook 邏輯。
示例代碼(簡化示意):
#include <linux/module.h>
#include <linux/kprobes.h>
// 定義一個Kprobe結(jié)構(gòu)體
static struct kprobe my_kprobe = {
.symbol_name = "__x64_sys_open", // 要Hook的系統(tǒng)調(diào)用函數(shù)名
};
// 函數(shù)執(zhí)行前的回調(diào)函數(shù)
static int pre_handler(struct kprobe *p, struct pt_regs *regs) {
printk(KERN_INFO "MyHook: Before open system call\n");
return 0;
}
// 函數(shù)執(zhí)行后的回調(diào)函數(shù)
static void post_handler(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
printk(KERN_INFO "MyHook: After open system call\n");
}
// 發(fā)生異常時的回調(diào)函數(shù)
static void fault_handler(struct kprobe *p, struct pt_regs *regs, int trapnr) {
printk(KERN_INFO "MyHook: Fault in open system call\n");
}
static int __init my_init(void) {
// 設(shè)置回調(diào)函數(shù)
my_kprobe.pre_handler = pre_handler;
my_kprobe.post_handler = post_handler;
my_kprobe.fault_handler = fault_handler;
// 注冊Kprobe
if (register_kprobe(&my_kprobe) < 0) {
printk(KERN_INFO "Failed to register kprobe\n");
return -1;
}
return 0;
}
static void __exit my_exit(void) {
// 注銷Kprobe
unregister_kprobe(&my_kprobe);
printk(KERN_INFO "Kprobe unregistered\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");優(yōu)點與局限性:Kprobes Hook 技術(shù)功能強大,能夠深入內(nèi)核內(nèi)部進行 Hook 操作,并且對內(nèi)核的影響相對較小,因為它是一種動態(tài)探測機制,不需要修改內(nèi)核的靜態(tài)代碼。然而,它的實現(xiàn)相對復(fù)雜,需要對內(nèi)核機制有較深入的理解,并且在使用不當?shù)那闆r下,可能會對內(nèi)核性能產(chǎn)生一定的影響。
四、Hook技術(shù)挑戰(zhàn)與應(yīng)對
4.1內(nèi)核穩(wěn)定性問題
無論是直接修改系統(tǒng)調(diào)用表還是使用 Kprobes 等技術(shù),都可能對內(nèi)核的穩(wěn)定性造成潛在威脅。一旦 Hook 代碼出現(xiàn)錯誤,可能導(dǎo)致內(nèi)核崩潰或者出現(xiàn)不可預(yù)測的行為。
應(yīng)對方法:在編寫 Hook 代碼時,要進行嚴格的測試和調(diào)試,確保代碼的正確性和穩(wěn)定性。同時,可以采用一些安全機制,如對修改的內(nèi)存區(qū)域進行備份,以便在出現(xiàn)問題時能夠快速恢復(fù)。
4.2兼容性問題
不同的 Linux 內(nèi)核版本和架構(gòu)在系統(tǒng)調(diào)用表結(jié)構(gòu)、函數(shù)命名等方面可能存在差異,這給 Hook 技術(shù)的跨版本和跨架構(gòu)應(yīng)用帶來了困難。
應(yīng)對方法:在編寫 Hook 代碼時,要充分考慮內(nèi)核版本和架構(gòu)的兼容性??梢酝ㄟ^宏定義等方式,根據(jù)不同的內(nèi)核版本和架構(gòu)進行不同的代碼處理。同時,關(guān)注內(nèi)核的更新和變化,及時調(diào)整 Hook 代碼。
4.3安全風(fēng)險
Hook 技術(shù)如果被惡意利用,可能會對系統(tǒng)安全造成嚴重威脅。例如,惡意軟件可以通過 Hook 關(guān)鍵系統(tǒng)調(diào)用,隱藏自己的行為或者獲取敏感信息。
應(yīng)對方法:加強系統(tǒng)的安全防護,使用安全檢測工具及時發(fā)現(xiàn)和阻止惡意的 Hook 行為。同時,對于合法的 Hook 應(yīng)用,要進行嚴格的權(quán)限管理和審計,確保 Hook 技術(shù)的使用是安全可靠的。




























