歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux技術 >> Linux系統調用Hook

Linux系統調用Hook

日期:2017/3/3 15:16:21   编辑:Linux技術
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指定的so文件中的符號。換句話說,可以用我們自己的so庫中的函數替換原來庫裡有的函數,從而達到hook的目的。這和:
view sourceprint?
1.
1
. Windows下通過修改
import

table來hook API

2.
2
. PHP中修改functions_table來hook function

從原理上來講很類似。
我們知道,Linux的用C庫的都是glibc,有一個叫libc.so.6的文件,這是幾乎所有Linux下命令的動態鏈接中,其中有標准C的各種函數,默認情況下,linux所編譯的程序中對標准C函數的鏈接,都是通過動態鏈接方式來鏈接libc.so.6這個函數庫的。這也意味著我們在通過我們注入的.so來實現函數覆蓋劫持之後需要從libc.so.6中取得原本的正常函數,讓程序繼續正常執行
正常程序main.c:
view sourceprint?
01.#include <stdio.h>
02.#include <string.h>
03. 
04.int main(int argc, char *argv[])
05.{
06.if( strcmp(argv[1], 'test') )
07.{
08.printf('Incorrect password
09.');
10.}
11.else
12.{
13.printf('Correct password
14.');
15.}
16.return 0;
17.}

用於劫持函數的.so代碼hook.c
view sourceprint?
01.#include <stdio.h>
02.#include <string.h>
03.#include <dlfcn.h>
04./*
05.hook的目標是strcmp,所以typedef了一個STRCMP函數指針
06.hook的目的是要控制函數行為,從原庫libc.so.6中拿到strcmp指針,保存成old_strcmp以備調用
07.*/
08.typedef int(*STRCMP)(const char*, const char*);
09. 
10.int strcmp(const char *s1, const char *s2)
11.{
12.static void *handle = NULL;
13.static STRCMP old_strcmp = NULL;
14. 
15.if( !handle )
16.{
17.handle = dlopen('libc.so.6', RTLD_LAZY);
18.old_strcmp = (STRCMP)dlsym(handle, 'strcmp');
19.}
20.printf('oops!!! hack function invoked. s1=<%s> s2=<%s>
21.', s1, s2);
22.return old_strcmp(s1, s2);
23.}

編譯:
view sourceprint?
1.gcc -o test main.c
2.gcc -fPIC -shared -o hook.so hook.c -ldl

運行:
view sourceprint?
1.LD_PRELOAD=./hook.so ./test 123

0x2: ...
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s ead_write.c
view sourceprint?
01.asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)
02.{
03.struct file *file;
04.ssize_t ret = -EBADF;
05.int fput_needed;
06. 
07.file = fget_light(fd, &fput_needed);
08.if (file)
09.{
10.loff_t pos = file_pos_read(file);
11.ret = vfs_read(file, buf, count, &pos);
12.file_pos_write(file, pos);
13.fput_light(file, fput_needed);
14.}
15. 
16.return ret;
17.}
18.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,就可以實現劫持目的了
view sourceprint?
1.
1
. 搜索sys_read的opcode

2.
2
. 如果發現是call指令,根據call後面的offset計算要跳轉的地址是不是我們要hook的函數地址

3.
1
) 如果
'不是'
就重新計算Hook函數的offset,用Hook函數的offset替換原來的offset

4.
2
) 如果
'已經是'
Hook函數的offset,則說明函數已經處於被劫持狀態了,我們的Hook引擎應該直接忽略跳過,避免重復劫持

poc:
view sourceprint?
01./*
02.參數:
03.1. handler是上層函數的地址,這裡就是sys_read的地址
04.2. old_func是要替換的函數地址,這裡就是vfs_read
05.3. new_func是新函數的地址,這裡就是new_vfs_read的地址
06.*/
07.unsigned int patch_kernel_func(unsigned int handler, unsigned int old_func,
08.unsigned int new_func)
09.{
10.unsigned char *p = (unsigned char *)handler;
11.unsigned char buf[4] = '';
12.unsigned int offset = 0;
13.unsigned int orig = 0;
14.int i = 0;
15. 
16.DbgPrint('
17.*** hook engine: start patch func at: 0x%08x
18.', old_func);
19. 
20.while (1) {
21.if (i > 512)
22.return 0;
23. 
24.if (p[0] == 0xe8) {
25.DbgPrint('*** hook engine: found opcode 0x%02x
26.', p[0]);
27. 
28.DbgPrint('*** hook engine: call addr: 0x%08x
29.',
30.(unsigned int)p);
31.buf[0] = p[1];
32.buf[1] = p[2];
33.buf[2] = p[3];
34.buf[3] = p[4];
35. 
36.DbgPrint('*** hook engine: 0x%02x 0x%02x 0x%02x 0x%02x
37.',
38.p[1], p[2], p[3], p[4]);
39. 
40.offset = *(unsigned int *)buf;
41.DbgPrint('*** hook engine: offset: 0x%08x
42.', offset);
43. 
44.orig = offset + (unsigned int)p + 5;
45.DbgPrint('*** hook engine: original func: 0x%08x
46.', orig);
47. 
48.if (orig == old_func) {
49.DbgPrint('*** hook engine: found old func at'
50.' 0x%08x
51.',
52.old_func);
53. 
54.DbgPrint('%d
55.', i);
56.break;
57.}
58.}
59.p++;
60.i++;
61.}
62. 
63.offset = new_func - (unsigned int)p - 5;
64.DbgPrint('*** hook engine: new func offset: 0x%08x
65.', offset);
66. 
67.p[1] = (offset & 0x000000ff);
68.p[2] = (offset & 0x0000ff00) >> 8;
69.p[3] = (offset & 0x00ff0000) >> 16;
70.p[4] = (offset & 0xff000000) >> 24;
71. 
72.DbgPrint('*** hook engine: pachted new func offset.
73.');
74. 
75.return orig;
76.}

0x2: 利用0x80中斷劫持system_call->sys_call_table進行系統調用Hook
我們知道,要對系統調用(sys_call_table)進行替換,卻必須要獲取該地址後才可以進行替換。但是Linux 2.6版的內核出於安全的考慮沒有將系統調用列表基地址的符號sys_call_table導出,但是我們可以采取一些hacking的方式進行獲取。
因為系統調用都是通過0x80中斷來進行的,故可以通過查找0x80中斷的處理程序來獲得sys_call_table的地址。其基本步驟是
view sourceprint?
1.
1
. 獲取中斷描述符表(IDT)的地址(使用C ASM匯編)

2.
2
. 從中查找
0x80
中斷(系統調用中斷)的服務例程(
8
*
0x80
偏移)

3.
3
. 搜索該例程的內存空間,

4.
4
. 從其中獲取sys_call_table(保存所有系統調用例程的入口地址)的地址

編程示例
find_sys_call_table.c
view sourceprint?
01.#include <linux/module.h>
02.#include <linux/kernel.h>
03. 
04.// 中斷描述符表寄存器結構
05.struct
06.{
07.unsigned short limit;
08.unsigned int base;
09.} __attribute__((packed)) idtr;
10. 
11. 
12.// 中斷描述符表結構
13.struct
14.{
15.unsigned short off1;
16.unsigned short sel;
17.unsigned char none, flags;
18.unsigned short off2;
19.} __attribute__((packed)) idt;
20. 
21.// 查找sys_call_table的地址
22.void disp_sys_call_table(void)
23.{
24.unsigned int sys_call_off;
25.unsigned int sys_call_table;
26.char* p;
27.int i;
28. 
29.// 獲取中斷描述符表寄存器的地址
30.asm('sidt %0':'=m'(idtr));
31.printk('addr of idtr: %x
32.', &idtr);
33. 
34.// 獲取0x80中斷處理程序的地址
35.memcpy(&idt, idtr.base+8*0x80, sizeof(idt));
36.sys_call_off=((idt.off2<<16)|idt.off1);
37.printk('addr of idt 0x80: %x
38.', sys_call_off);
39. 
40.// 從0x80中斷服務例程中搜索sys_call_table的地址
41.p=sys_call_off;
42.for (i=0; i<100; i++)
43.{
44.if (p=='ÿ' && p[i+1]=='' && p[i+2]=='?')
45.{
46.sys_call_table=*(unsigned int*)(p+i+3);
47.printk('addr of sys_call_table: %x
48.', sys_call_table);
49.return ;
50.}
51.}
52.}
53. 
54.// 模塊載入時被調用
55.static int __init init_get_sys_call_table(void)
56.{
57.disp_sys_call_table();
58.return 0;
59.}
60. 
61.module_init(init_get_sys_call_table);
62. 
63.// 模塊卸載時被調用
64.static void __exit exit_get_sys_call_table(void)
65.{
66.}
67. 
68.module_exit(exit_get_sys_call_table);
69. 
70.// 模塊信息
71.MODULE_LICENSE('GPL2.0');
72.MODULE_AUTHOR('LittleHann');

Makefile
view sourceprint?
1.obj-m := find_sys_call_table.o

編譯
view sourceprint?
1.make -C /usr/src/kernels/2.6.32-358.el6.i686 M=$(pwd) modules

測試效果

dmesg| tail

獲取到了sys_call_table的基地址之後,我們就可以修改指定offset對應的系統調用了,從而達到劫持系統調用的目的
0x3: 獲取sys_call_table的其他方法
1. 常用方法
模擬出一個call *sys_call_table(,%eax,4),然後看其機器碼,然後在system_call的附近基於這個特征進行尋找
view sourceprint?
01.#include <stdio.h>
02.void fun1()
03.{
04.printf('fun1/n');
05.}
06.void fun2()
07.{
08.printf('fun2/n');
09.}
10.unsigned int sys_call_table[2] = {fun1, fun2};
11.int main(int argc, char **argv)
12.{
13.asm('call *sys_call_table(%eax,4');
14.}
15. 
16.編譯
17.gcc test.c -o test
18. 
19.objdump進行dump
20.objdump -D ./test | grep sys_call_table

2. 通過/boot/System.map-2.6.32-358.el6.i686文件查找
view sourceprint?
1.
cd /boot

2.
grep sys_call_table System.map-
2.6
.
32
-
358
.el6.i686

0x4: 利用Linux內核機制kprobe機制進行系統調用Hook
kprobe簡介
kprobe是一個動態地收集調試和性能信息的工具,它從Dprobe項目派生而來,它幾乎可以跟蹤任何函數或被執行的指令以及一些異步事件。它的基本工作機制是:
view sourceprint?
1.
1
. 用戶指定一個探測點,並把一個用戶定義的處理函數關聯到該探測點

2.
2
. 在注冊探測點的時候,對被探測函數的指令碼進行替換,替換為
int

3
的指令碼

3.
3
. 在執行
int

3
的異常執行中,通過通知鏈的方式調用kprobe的異常處理函數

4.
4
. 在kprobe的異常出來函數中,判斷是否存在pre_handler鉤子,存在則執行

5.
5
. 執行完後,准備進入單步調試,通過設置EFLAGS中的TF標志位,並且把異常返回的地址修改為保存的原指令碼

6.
6
. 代碼返回,執行原有指令,執行結束後觸發單步異常

7.
7
. 在單步異常的處理中,清除單步標志,執行post_handler流程,並最終返回

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

2.
kprobes是可以被插入到內核的任何指令位置的探測點,kprobe允許在同一地址注冊多個kprobes,但是不能同時在該地址上有多個jprobes

3.

4.
2
. jprobes

5.
jprobes則只能被插入到一個內核函數的入口

6.

7.
3
. kretprobes(也叫返回探測點)

8.
而kretprobes則是在指定的內核函數返回時才被執行

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

02.
kprobes執行流程

03.
*/

04.
1
. 當安裝一個kprobes探測點時,kprobe首先備份被探測的指令

05.
2
. 使用斷點指令(
int

3
指令)來取代被探測指令的頭一個或幾個字節(這點和OD很像)

06.
3
. CPU執行到探測點時,將因運行斷點指令而執行trap操作,那將導致保存CPU的寄存器,調用相應的trap處理函數

07.
4
. trap處理函數將調用相應的notifier_call_chain(內核中一種異步工作機制)中注冊的所有notifier函數

08.
5
. kprobe正是通過向trap對應的notifier_call_chain注冊關聯到探測點的處理函數來實現探測處理的

09.
6
. 當kprobe注冊的notifier被執行時

10.
6.1

它首先執行關聯到探測點的pre_handler函數,並把相應的kprobe struct和保存的寄存器作為該函數的參數

11.
6.2

然後,kprobe單步執行被探測指令的備份(原始函數)

12.
6.3

最後,kprobe執行post_handler

13.
7
. 等所有這些運行完畢後,緊跟在被探測指令後的指令流將被正常執行

#include linux/kprobes.h
view sourceprint?
1.
int

register_kprobe(struct kprobe *kp);

2.

3.
int

pre_handler(struct kprobe *p, struct pt_regs *regs);

4.
void

post_handler(struct kprobe *p, struct pt_regs *regs,

5.
unsigned
long
flags);

6.
<em><strong>fault_handler()

7.
</strong></em>

整個順序為:
pre_handler->被Hook原函數->post_handler
2. jprobe
view sourceprint?
01.
/*

02.
jprobe執行流程

03.
*/

04.
1
. jprobe通過注冊kprobes在被探測函數入口的來實現,它能無縫地訪問被探測函數的參數

05.
2
. jprobe處理函數應當和被探測函數有同樣的原型,而且該處理函數在函數末必須調用kprobe提供的函數jprobe_return()

06.
3
. 當執行到該探測點時,kprobe備份CPU寄存器和棧的一些部分,然後修改指令寄存器指向jprobe處理函數

07.
4
. 當執行該jprobe處理函數時,寄存器和棧內容與執行真正的被探測函數一模一樣,因此它不需要任何特別的處理就能訪問函數參數, 在該處理函數執行到最後

08.
時,它調用jprobe_return(),那導致寄存器和棧恢復到執行探測點時的狀態,因此被探測函數能被正常運行

09.
5
. 需要注意,被探測函數的參數可能通過棧傳遞,也可能通過寄存器傳遞,但是jprobe對於兩種情況都能工作,因為它既備份了棧,又備份了寄存器,當然,前提

10.
是jprobe處理函數原型必須與被探測函數完全一樣

#include linux/kprobes.h
view sourceprint?
1.
int

register_jprobe(struct jprobe *jp);

3. kretprobe
view sourceprint?
01.
/*

02.
kretprobe執行流程

03.
*/

04.
1
. kretprobe也使用了kprobes來實現
2

05.
2
. 當用戶調用register_kretprobe()時,kprobe在被探測函數的入口建立了一個探測點

06.
3
. 當執行到探測點時,kprobe保存了被探測函數的返回地址並取代返回地址為一個trampoline的地址,kprobe在初始化時定義了該trampoline並且為該

07.
trampoline注冊了一個kprobe

08.
4
. 當被探測函數執行它的返回指令時,控制傳遞到該trampoline,因此kprobe已經注冊的對應於trampoline的處理函數將被執行,而該處理函數會調用用戶

09.
關聯到該kretprobe上的處理函數

10.
5
. 處理完畢後,設置指令寄存器指向已經備份的函數返回地址,因而原來的函數返回被正常執行。

11.
6
. 被探測函數的返回地址保存在類型為kretprobe_instance的變量中,結構kretprobe的maxactive字段指定了被探測函數可以被同時探測的實例數

12.
7
. 函數register_kretprobe()將預分配指定數量的kretprobe_instance:

13.
7.1

如果被探測函數是非遞歸的並且調用時已經保持了自旋鎖(spinlock),那麼maxactive為
1
就足夠了

14.
7.2

如果被探測函數是非遞歸的且運行時是搶占失效的,那麼maxactive為NR_CPUS就可以了

15.
7.3

如果maxactive被設置為小於等於
0
, 它被設置到缺省值(如果搶占使能, 即配置了 CONFIG_PREEMPT,缺省值為
10
2
*NR_CPUS中的最大值,否則

16.
缺省值為NR_CPUS)

17.
7.4

如果maxactive被設置的太小了,一些探測點的執行可能被丟失,但是不影響系統的正常運行,在結構kretprobe中nmissed字段將記錄被丟失的探測

18.
點執行數,它在返回探測點被注冊時設置為
0
,每次當執行探測函數而沒有kretprobe_instance可用時,它就加
1

#include linux/kprobes.h
view sourceprint?
1.
int

register_kretprobe(struct kretprobe *rp);

2.

3.
int

kretprobe_handler(struct kretprobe_instance *ri, struct pt_regs *regs);

對應於每一個注冊函數,有相應的卸載函數
view sourceprint?
1.
void

unregister_kprobe(struct kprobe *kp);

2.
void

unregister_jprobe(struct jprobe *jp);

3.
void

unregister_kretprobe(struct kretprobe *rp);

了解了kprobe的基本原理之後,我們要回到我們本文的主題,系統調用的Hook上來,由於kprobe是linux提供的穩定的回調注冊機制,linux天生就穩定地支持在我們指定的某個函數的執行流上進行注冊回調,我們很方便地使用它來進行系統調用(例如sys_execv()、網絡連接等)的執行Hook,從而劫持linux系統的系統調用流程,為下一步的惡意入侵行為分析作准備
kprobe編程示例
do_fork.c
view sourceprint?
01./*
02.* * You will see the trace data in /var/log/messages and on the console
03.* * whenever do_fork() is invoked to create a new process.
04.* */
05. 
06.#include <linux/kernel.h>
07.#include <linux/module.h>
08.#include <linux/kprobes.h>
09. 
10.//定義要Hook的函數,本例中do_fork
11.static struct kprobe kp =
12.{
13..symbol_name = 'do_fork',
14.};
15. 
16.static int handler_pre(struct kprobe *p, struct pt_regs *regs)
17.{
18.struct thread_info *thread = current_thread_info();
19. 
20.printk(KERN_INFO 'pre-handler thread info: flags = %x, status = %d, cpu = %d, task->pid = %d
21.',
22.thread->flags, thread->status, thread->cpu, thread->task->pid);
23. 
24.return 0;
25.}
26. 
27.static void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags)
28.{ 
29.struct thread_info *thread = current_thread_info();
30. 
31.printk(KERN_INFO 'post-handler thread info: flags = %x, status = %d, cpu = %d, task->pid = %d
32.',
33.thread->flags, thread->status, thread->cpu, thread->task->pid);
34.}
35. 
36.static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
37.{
38.printk(KERN_INFO 'fault_handler: p->addr = 0x%p, trap #%dn',
39.p->addr, trapnr);
40.return 0;
41.}
42. 
43./*
44.內核模塊加載初始化,這個過程和windows下的內核驅動注冊分發例程很類似
45.*/
46.static int __init kprobe_init(void)
47.{
48.int ret;
49.kp.pre_handler = handler_pre;
50.kp.post_handler = handler_post;
51.kp.fault_handler = handler_fault;
52. 
53.ret = register_kprobe(&kp);
54.if (ret < 0)
55.{
56.printk(KERN_INFO 'register_kprobe failed, returned %d
57.', ret);
58.return ret;
59.}
60.printk(KERN_INFO 'Planted kprobe at %p
61.', kp.addr);
62.return 0;
63.}
64. 
65.static void __exit kprobe_exit(void)
66.{
67.unregister_kprobe(&kp);
68.printk(KERN_INFO 'kprobe at %p unregistered
69.', kp.addr);
70.}
71. 
72.module_init(kprobe_init)
73.module_exit(kprobe_exit)
74.MODULE_LICENSE('GPL');

Makefile
view sourceprint?
1.obj-m := do_fork.o

編譯:
view sourceprint?
1.make -C /usr/src/kernels/2.6.32-358.el6.i686 M=$(pwd) modules

加載內核模塊:
view sourceprint?
1.insmod do_fork.ko

測試效果:
dmesg| tail
cat /proc/kallsyms | grep do_fork
do_fork的地址與kprobe注冊的地址一致,可見,在kprobe調試模塊在內核停留期間,我們編寫的內核監控模塊劫持並記錄了系統fork出了新的進程信息
4. 後記
Hook技術是進行主動防御、動態入侵檢測的關鍵技術,從技術上來說,目前的很多Hook技術都屬於'猥瑣流',即:
view sourceprint?
1.
1
. 通過
'劫持'
在關鍵流程上的某些函數的執行地址,在Hook函數執行完之後,再跳回原始的函數繼續執行(做好現場保護)

2.
2
. 或者通過dll、進程、線程注入比原始程序提前獲得CPU執行權限

但是隨著windows的PatchGuard的出現,這些出於'安全性'的內核patct將被一視同仁地看作內核完整性的威脅者
更加優美、穩定的方法應該是:
view sourceprint?
01.
1
. 注冊標准的回調方法,包括:

02.
1
) 進程

03.
2
) 線程

04.
3
) 模塊的創建

05.
4
) 卸載回調函數

06.
5
) 文件/網絡等各種過濾驅動
2
. 內核提供的標准處理流程Hook點

07.
1
) kprobe機制

08.
2
. 網絡協議棧提供的標准Hook點

09.
1
) netfilter的鏈式處理流程
Copyright © Linux教程網 All Rights Reserved