1、刚工作时做Linux 流控;后来做安全操作系统;再后来做操作系统加固;现在做TCP 加速。唉!没离开过类Unix!!!但是水平有限。。
全部博文(353)
分类:
2011-12-08 10:49:37
原文地址:Linux下实现劫持系统调用的总结(下)--原理分析 作者:Godbach
本文欢迎自由转载,但请标明出处和本文链接,并保持本文的完整性。
CU: Godbach
Blog:http://Godbach.cublog.cn
Dec 2, 2009
二、实现原理分析
实现的方法就是通过中断向量表,找到系统调用的中断向量,再通过系统调用时执行的指令,最终找到系统调用表的地址, 进行系统调用的替换。
(一)中断向量表地址的获取
中断向量表(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
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)
f9:
ff:
105:
3d 3e 01 00 00 cmp $0x13e,%eax
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被替换为有效的地址。本人也已经在
c1003d04
c1003d04: ff 14 85 e0 14
c1003d0b: 89 44 24 18 mov %eax,0x18(%esp)
因此可以看出,我当前系统的sys_call_table的地址为0xc
(五)系统调用的替换
一旦我们获取到了系统调用表的地址,需要需要替换那些系统调用,只需要将系统调用表的某个系统调用指向自己实现的系统调用即可。
#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.