原文链接:http://blog.chinaunix.net/u/33048/showart_2109264.html
目录 一、代码及实现 (一) 劫持open系统调用的代码 (二) 编译及实践 二、实现原理分析 (一)中断向量表地址的获取 (二)系统调用中断向量地址的获取 (三)系统调用处理例程地址的获取 (四)系统调用表地址的获取 (五)系统调用的替换 参考链接
Linux内核版本2.6中已经不再导出系统调用符号表了。因此,如果想实现劫持系统调用,就得想办法找到系统调用表的地址。网上应该可以搜到相关的实现。我这里找到了albcamus兄的精华文章,并在内核版本2.6.18.3上实践了其中的代码。这里总结一下。
本文欢迎自由转载,但请标明出处和本文链接,并保持本文的完整性。 CU: Godbach Blog:http://blog.chinaunix.net/u/33048/index.html Dec 2, 2009
一、代码及实现 (一) 劫持open系统调用的代码 内核态实现劫持系统调用的代码如下,来自参考链接1,即albcamus兄提供的代码。我这里屏蔽了一些代码,仅实现了劫持open系统调用。
- #include <linux/kernel.h>
- #include
- #include module.h>
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- MODULE_DESCRIPTION("Intercept the system call table in Linux");
- MODULE_AUTHOR("alert7 ([email]alert7@xfocus.org[/email]) \n\t\talbcamus <[email]albcamus@gmail.com[/email]>");
- MODULE_LICENSE("GPL");
- /* comment the following line to shut me up */
- #define INTERCEPT_DEBUG
- #ifdef INTERCEPT_DEBUG
- #define dbgprint(format,args...) \
- printk("intercept: function:%s-L%d: "format, __FUNCTION__, __LINE__, ##args);
- #else
- #define dbgprint(format,args...) do {} while(0);
- #endif
- /**
- * the system call table
- */
- void **my_table;
- unsigned int orig_cr0;
- /**
- * the original syscall functions
- */
- asmlinkage long (*old_open) (char __user *filename, int flags, int mode);
- asmlinkage int (*old_execve) (struct pt_regs regs);
- /** do_execve and do_fork */
- unsigned int can_exec_fork = 0;
- int (*new_do_execve) (char * filename,
- char __user *__user *argv,
- char __user *__user *envp,
- struct pt_regs * regs);
- struct idtr {
- unsigned short limit;
- unsigned int base;
- } __attribute__ ((packed));
- struct idt {
- unsigned short off1;
- unsigned short sel;
- unsigned char none, flags;
- unsigned short off2;
- } __attribute__ ((packed));
- #if 0
- /**
- * check if we can intercept fork/vfork/clone/execve or not
- *
- * return : 0 for no, 1 for yes
- */
- struct kprobe kp_exec;
- unsigned int can_intercept_fork_exec(void)
- {
- int ret = 0;
- #ifndef CONFIG_KPROBES
- return ret;
- #endif
- kp_exec.symbol_name = "do_execve";
- ret = register_kprobe(&kp_exec);
- if (ret != 0 ) {
- dbgprint("cannot find do_execve by kprobe.\n");
- return 0;
- }
- new_do_execve = ( int (*)
- (char *,
- char __user * __user *,
- char __user * __user *,
- struct pt_regs *
- )
- ) kp_exec.addr;
- dbgprint("do_execve at %p\n", (void *)kp_exec.addr);
- unregister_kprobe(&kp_exec);
- return 1;
- }
- #endif
- /**
- * clear WP bit of CR0, and return the original value
- */
- unsigned int clear_and_return_cr0(void)
- {
- unsigned int cr0 = 0;
- unsigned int ret;
- asm volatile ("movl %%cr0, %%eax"
- : "=a"(cr0)
- );
- ret = cr0;
- /* clear the 16 bit of CR0, a.k.a WP bit */
- cr0 &= 0xfffeffff;
- asm volatile ("movl %%eax, %%cr0"
- :
- : "a"(cr0)
- );
- return ret;
- }
- /** set CR0 with new value
- *
- * @val : new value to set in cr0
- */
- void setback_cr0(unsigned int val)
- {
- asm volatile ("movl %%eax, %%cr0"
- :
- : "a"(val)
- );
- }
- /**
- * Return the first appearence of NEEDLE in HAYSTACK.
- * */
- static void *memmem(const void *haystack, size_t haystack_len,
- const void *needle, size_t needle_len)
- {/*{{{*/
- const char *begin;
- const char *const last_possible
- = (const char *) haystack + haystack_len - needle_len;
- if (needle_len == 0)
- /* The first occurrence of the empty string is deemed to occur at
- the beginning of the string. */
- return (void *) haystack;
- /* Sanity check, otherwise the loop might search through the whole
- memory. */
- if (__builtin_expect(haystack_len < needle_len, 0))
- return NULL;
- for (begin = (const char *) haystack; begin <= last_possible;
- ++begin)
- if (begin[0] == ((const char *) needle)[0]
- && !memcmp((const void *) &begin[1],
- (const void *) ((const char *) needle + 1),
- needle_len - 1))
- return (void *) begin;
- return NULL;
- }/*}}}*/
- /**
- * Find the location of sys_call_table
- */
- static unsigned long get_sys_call_table(void)
- {/*{{{*/
- /* we'll read first 100 bytes of int $0x80 */
- #define OFFSET_SYSCALL 100
- struct idtr idtr;
- struct idt idt;
- unsigned sys_call_off;
- unsigned retval;
- char sc_asm[OFFSET_SYSCALL], *p;
- /* well, let's read IDTR */
- asm("sidt %0":"=m"(idtr)
- :
- :"memory" );
- dbgprint("idtr base at 0x%X, limit at 0x%X\n", (unsigned int)idtr.base,(unsigned short)idtr.limit);
- /* Read in IDT for vector 0x80 (syscall) */
- memcpy(&idt, (char *) idtr.base + 8 * 0x80, sizeof(idt));
- sys_call_off = (idt.off2 << 16) | idt.off1;
- dbgprint("idt80: flags=%X sel=%X off=%X\n",
- (unsigned) idt.flags, (unsigned) idt.sel, sys_call_off);
- /* we have syscall routine address now, look for syscall table
- dispatch (indirect call) */
- memcpy(sc_asm, (void *)sys_call_off, OFFSET_SYSCALL);
- /**
- * Search opcode of `call sys_call_table(,eax,4)'
- */
- p = (char *) memmem(sc_asm, OFFSET_SYSCALL, "\xff\x14\x85", 3);
- if (p == NULL)
- return 0;
- retval = *(unsigned *) (p + 3);
- if (p) {
- dbgprint("sys_call_table at 0x%x, call dispatch at 0x%x\n",
- retval, (unsigned int) p);
- }
- return retval;
- #undef OFFSET_SYSCALL
- }/*}}}*/
- /**
- * new_open - replace the original sys_open when initilazing,
- * as well as be got rid of when removed
- */
- asmlinkage long new_open(char *filename, int flags, int mode)
- {
- dbgprint("call open()\n");
- return old_open (filename, flags, mode);
- }
- /**
- * new_execve - you should change this function whenever the kernel's sys_execve()
- * changes
- */
- asmlinkage int new_execve(struct pt_regs regs)
- {
- int error;
- char *filename;
- dbgprint("Hello\n");
- filename = getname( (char __user *) regs.ebx );
- error = PTR_ERR(filename);
- if ( IS_ERR(filename) )
- goto out;
- dbgprint("file to execve: %s\n", filename);
- error = new_do_execve(filename,
- (char __user * __user *) regs.ecx,
- (char __user * __user *) regs.edx,
- ®s);
- if (error == 0) {
- task_lock(current);
- current->ptrace &= ~PT_DTRACE;
- task_unlock(current);
- set_thread_flag(TIF_IRET);
- }
- putname (filename);
- out:
- return error;
- }
- static int intercept_init(void)
- {
- my_table = (void **)get_sys_call_table();
- if (my_table == NULL)
- return -1;
- dbgprint("sys call table address %p\n", (void *) my_table);
- #define REPLACE(x) old_##x = my_table[__NR_##x];\
- my_table[__NR_##x] = new_##x
-
- REPLACE(open);
- #if 0
- can_exec_fork = can_intercept_fork_exec();
- if(can_exec_fork == 1)
- REPLACE(execve);
- #endif
- #undef REPLACE
- return 0;
- }
- static int __init this_init(void)
- {
- int ret;
- printk("syscall intercept: Hi, poor linux!\n");
- orig_cr0 = clear_and_return_cr0();
- ret = intercept_init();
- setback_cr0(orig_cr0);
- return ret;
- }
- static void __exit this_fini(void)
- {
- printk("syscall intercept: bye, poor linux!\n");
- #define RESTORE(x) my_table[__NR_##x] = old_##x
- orig_cr0 = clear_and_return_cr0();
- RESTORE(open);
- #if 0
- if(can_exec_fork == 1)
- RESTORE(execve);
- #endif
- setback_cr0(orig_cr0);
- #undef RESTORE
- }
- module_init(this_init);
- module_exit(this_fini);
复制代码 (二) 编译及实践 Makefile如下:
obj-m :=hack_open.o EXTRA_CFLAGS := -Dsymname=sys_call_table KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: make -C $(KDIR) SUBDIRS=$(PWD) modules clean: rm -rf .*.cmd *.mod.c *.o *.ko .tmp* *.symvers
编译之后,加载模块,然后查看日志信息:
Sep 24 19:06:49 localhost kernel: intercept: function:get_sys_call_table-L220: sys_call_table at 0xc11f14e0, call dispatch at 0xcebeceaa Sep 24 19:06:49 localhost kernel: intercept: function:intercept_init-L276: sys call table address c11f14e0 Sep 24 19:06:50 localhost kernel: intercept: function:new_open-L234: hello Sep 24 19:07:00 localhost last message repeated 460 times
可见open系统调用执行次数之频繁。
二、实现原理分析 实现的方法就是通过中断向量表,找到系统调用的中断向量,再通过系统调用时执行的指令,最终找到系统调用表的地址, 进行系统调用的替换。。
(一)中断向量表地址的获取 中断向量表(IDT)的入口地址是通过IDTR寄存器来确定的。IDTR寄存器的内容如下图所示。
这就是上面代码中定义结构体
- struct idtr {
- unsigned short limit;
- unsigned int base;
- } __attribute__ ((packed))
复制代码
的原因。 idtr寄存器的内容可以通过汇编指令sidt取出,然后就可以IDT的入口地址idtr.base,即高32bit。
(二)系统调用中断向量地址的获取 下一步就通过IDT找到系统调用中断即(0x80)的地址。下图即一个中断向量的组成。由此可以见,一个中断向量占用8个字节。因此,系统调用中断的地址可以表示为: idt = idtr.base + 8 * 0x80
(三)系统调用处理例程地址的获取 获取到系统调用中断的地址后,同样需要一个数据结构,将中断向量描述符的相关内容保存。数据结构的定义如下:
- struct idt {
- unsigned short off1;
- unsigned short sel;
- unsigned char none, flags;
- unsigned short off2;
- } __attribute__ ((packed));
复制代码
通过以上数据结构就可以得到系统调用中断发生时的中断处理例程的地址,即函数system_call()函数的地址:
- sys_call_off = idt.off2 << 16 | idt.off1
复制代码
该函数是用汇编实现的,位于arch/i386/kernel/entry.S,下面截取该函数的部分实现:
# system call handler stub ENTRY(system_call) RING0_INT_FRAME # can't unwind into user space anyway pushl %eax # save orig_eax CFI_ADJUST_CFA_OFFSET 4 SAVE_ALL GET_THREAD_INFO(%ebp) testl $TF_MASK,EFLAGS(%esp) jz no_singlestep orl $_TIF_SINGLESTEP,TI_flags(%ebp) no_singlestep: # system call tracing in operation / emulation /* Note, _TIF_SECCOMP is bit number 8, and so it needs testw and not testb */ testw $(_TIF_SYSCALL_EMU|_TIF_SYSCALL_TRACE|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%ebp) jnz syscall_trace_entry cmpl $(nr_syscalls), %eax jae syscall_badsys syscall_call: call *sys_call_table(,%eax,4) movl %eax,EAX(%esp) # store the return value
(四)系统调用表地址的获取 从上面代码中,我们可以看到,系统调用表的入口地址就是上面倒数第二行中sys_call_table。那么如果获取到sys_call_table的地址呢。由于这行代码执行了函数调用的指令call,该指令对应的指令码为\xff\x14\x85。因此,我们只要在system_call函数体内搜索的前三个字节为\xff\x14\x85的内存地址,然后将该地址加3,即可获取到sys_call_table的地址。同时,还有一个问题需要确定,就是call *sys_call_table(,%eax,4)这条指令相对于system_call函数体的偏移是多少,这样我们可以确定内存搜索的范围。下面是entry.o函数反汇编的部分代码:
000000d0 : d0: 50 push %eax d1: fc cld d2: 06 push %es d3: 1e push %ds d4: 50 push %eax d5: 55 push %ebp d6: 57 push %edi d7: 56 push %esi d8: 52 push %edx d9: 51 push %ecx da: 53 push %ebx db: ba 7b 00 00 00 mov $0x7b,%edx e0: 8e da movl %edx,%ds e2: 8e c2 movl %edx,%es e4: bd 00 f0 ff ff mov $0xfffff000,%ebp e9: 21 e5 and %esp,%ebp eb: f7 44 24 30 00 01 00 testl $0x100,0x30(%esp) f2: 00 f3: 74 04 je f9 f5: 83 4d 08 10 orl $0x10,0x8(%ebp)
000000f9 : f9: 66 f7 45 08 c1 01 testw $0x1c1,0x8(%ebp) ff: 0f 85 bf 00 00 00 jne 1c4 105: 3d 3e 01 00 00 cmp $0x13e,%eax 10a: 0f 83 27 01 00 00 jae 237
00000110 : 110: ff 14 85 00 00 00 00 call *0x0(,%eax,4) 117: 89 44 24 18 mov %eax,0x18(%esp)
通过以上反汇编代码的倒数第二行可以看到,该行即执行call *sys_call_table(,%eax,4) 的代码。那么该执行相对于函数体的偏移为0x110-0xd0 = 0x40。我们实际的代码中使用偏移值100作为搜索的最大范围。 至于反汇编出来为什么只是call *0x0(,%eax,4),个人理解应该是该模块尚未与其他模块进行链接的原因。当生成内核镜像vmlinux之后,反汇编vmlinux,然后找到system_call函数,就可以看到指令call *0x0(,%eax,4)中0x0被替换为有效的地址。本人也已经在2.6.18.3的vmlinux上验证过了,实际的代码如下:
- c1003d04 :
- c1003d04: ff 14 85 e0 14 1f c1 call *0xc11f14e0(,%eax,4)
- c1003d0b: 89 44 24 18 mov %eax,0x18(%esp)
复制代码
因此可以看出,我当前系统的sys_call_table的地址为0xc11f14e0。
(五)系统调用的替换 一旦我们获取到了系统调用表的地址,需要需要替换那些系统调用,只需要将系统调用表的某个系统调用指向自己实现的系统调用即可。
- #define REPLACE(x) old_##x = my_table[__NR_##x];\
- my_table[__NR_##x] = new_##x
- REPLACE(open);
复制代码
另外,需要注意的是,在替换系统调用的时候,要先清CR0的第20位并记录原始值,不然在替换sys_call_table的时候会报错。在替换完毕之后,再将CR0的原始值恢复,代码如下:
- orig_cr0 = clear_and_return_cr0();
- setback_cr0(orig_cr0);
复制代码
以上为Linux劫持系统调用的总结。欢迎多多交流,如果不妥之处,请大家指正。
参考链接: 1. 3. |