[WebZine]Bypassing Linux kernel module version check 's

  • A+
所属分类:鬼仔Blog
摘要

By wzt1、 为什么要突破模块验证
2、 内核是怎么实现的
3、 怎样去突破
4、 总结
5、 参考
6、 附录

By wzt

1、 为什么要突破模块验证
2、 内核是怎么实现的
3、 怎样去突破
4、 总结
5、 参考
6、 附录

1、 为什么要突破模块验证
Linux内核版本很多,升级很快,2个小内核版本中内核函数的定义可能都不一样,为了确保不一致的驱动程序导致kernel oops,
开发者加入了模块验证机制。它在加载内核模块的时候对模块进行校验, 如果模块与主机的一些环境不一致,就会加载不成功。
看下面一个例子,它简单的输出当期系统中的模块列表:

#include <linux/kernel.h> #include <linux/module.h> #include <linux/init.h> #include <linux/version.h> #include <linux/string.h> #include <linux/list.h>  MODULE_LICENSE("GPL"); MODULE_AUTHOR("wzt");  struct module *m = &__this_module;  int print_module_test(void) {         struct module *mod;          list_for_each_entry(mod, &m->list, list) {                 printk("%s/n", mod->name);         }         return NULL; }  static int list_print_init(void) {         printk("load list_print module./n");          print_module_test();          return 0; }  static void list_print_exit(void) {         printk("unload list_print module./n"); }  module_init(list_print_init); module_exit(list_print_exit);

我们在centos5.3环境中编译一下:
[[email protected] list]# uname -a
Linux localhost.localdomain 2.6.18-128.el5 #1 SMP Wed Jan 21 10:44:23 EST 2009 i686 i686 i386 GNU/Linux
然后拷贝到另一台主机centos5.1xen上:
[[email protected] ~]# uname -a
Linux localhost.localdomain 2.6.18-53.el5xen #1 SMP Mon Nov 12 03:26:12 EST 2007 i686 i686 i386 GNU/Linux

用insmod加载:
[[email protected] ~]# insmod list.ko
insmod: error inserting ‘list.ko’: -1 Invalid module format
报错了,在看下dmesg的信息:
[[email protected] ~]# dmesg|tail -n 1
list: disagrees about version of symbol struct_module
先不管这是什么, 总之我们的模块在另一台2.6.18的主机中加载失败。 通常的做法是要在主机中对源代码进行编译,
然后才能加载成功, 但是如果主机中缺少内核编译环境的话, 我们的rootkit就不能编译, 也不能安装在主机之中,
这是多么尴尬的事情:)。 没错, 这就是linux kernel开发的特点, 你别指望像windows驱动一样,编译一个驱动,
然后可以满世界去装^_^. 一些rootkit开发者抛弃了lkm类型rk的开发, 转而去打kmem, mem的注意,像sk,
moodnt这样的rk大家都喜欢, 可以在用户层下动态patch内核,不需要编译环境, wget下来,install即可。
但是它也有很多缺点,比如很不稳定,而且在2.6.x后内核已经取消了kmem这个设备, mem文件也做了映射和读写的
限制。 rk开发者没法继续sk的神话了。反过来, 如果我们的lkm后门不需要编译环境,也可以达到直接insmod的目的,
这是件多么美好的事情,而且lkm后门更加稳定,还不用像sk在内核中添加了很多自己的数据结构。

2、内核是怎么实现的
我们去看看内核在加载模块的时候都干了什么, 或许我们可以发现点bug, 然后做点手脚,欺骗过去:)
grep下dmesg里的关键字, 看看它在哪个文件中:
[[email protected] linux-2.6.18]# grep -r -i ‘disagrees about’ kernel/
kernel/module.c: printk(“%s: disagrees about version of symbol %s/n”,

2.6.18/kernel/module.c:
insmod调用了sys_init_module这个系统调用, 然后进入load_module这个主函数,它解析elf格式的ko文件,然后加载
到内核中:

/* Allocate and load the module: note that size of section 0 is always    zero, and we rely on this for optional sections. */ static struct module *load_module(void __user *umod,                                   unsigned long len,                                   const char __user *uargs) { ...         if (!check_modstruct_version(sechdrs, versindex, mod)) {                 err = -ENOEXEC;                 goto free_hdr;         }          modmagic = get_modinfo(sechdrs, infoindex, "vermagic");         /* This is allowed: modprobe --force will invalidate it. */         if (!modmagic) {                 add_taint(TAINT_FORCED_MODULE);                 printk(KERN_WARNING "%s: no version magic, tainting kernel./n",                        mod->name);         } else if (!same_magic(modmagic, vermagic)) {                 printk(KERN_ERR "%s: version magic '%s' should be '%s'/n",                        mod->name, modmagic, vermagic);                 err = -ENOEXEC;                 goto free_hdr;         } ... }

check_modstruct_version就是用来计算模块符号的一些crc值,不相同就会出现我们在dmesg里看到的
“disagrees about version of symbol”信息。 get_modinfo取得了内核本身的vermagic值,然后用same_magic
函数和内核的vermagic去比较,不同也会使内核加载失败。 所以在这里,我们看到内核对模块验证的时候采用了
2层验证的方法:模块crc值和vermagic检查。

继续跟踪check_modstruct_version, 现在的内核默认的都开启了CONFIG_MODVERSIONS, 如果没有指定这个选项,
函数为空,我们的目的是要在As, Centos下安装模块,redhat不是吃干饭的, 当然开了MODVERSIONS选项。

static inline int check_modstruct_version(Elf_Shdr *sechdrs,                                           unsigned int versindex,                                           struct module *mod) {         const unsigned long *crc;         struct module *owner;          if (!__find_symbol("struct_module", &owner, &crc, 1))                 BUG();         return check_version(sechdrs, versindex, "struct_module", mod,                              crc); }

__find_symbol找到了struct_module这个符号的crc值,然后调用check_version去校验:

static int check_version(Elf_Shdr *sechdrs,                          unsigned int versindex,                          const char *symname,                          struct module *mod,                          const unsigned long *crc) {         unsigned int i, num_versions;         struct modversion_info *versions;          /* Exporting module didn't supply crcs?  OK, we're already tainted. */         if (!crc)                 return 1;          versions = (void *) sechdrs[versindex].sh_addr;         num_versions = sechdrs[versindex].sh_size                 / sizeof(struct modversion_info);          for (i = 0; i < num_versions; i++) {                 if (strcmp(versions[i].name, symname) != 0)                         continue;                  if (versions[i].crc == *crc)                         return 1;                 printk("%s: disagrees about version of symbol %s/n",                        mod->name, symname);                 DEBUGP("Found checksum %lX vs module %lX/n",                        *crc, versions[i].crc);                 return 0;         }         /* Not in module's version table.  OK, but that taints the kernel. */         if (!(tainted & TAINT_FORCED_MODULE)) {                 printk("%s: no version for /"%s/" found: kernel tainted./n",                        mod->name, symname);                 add_taint(TAINT_FORCED_MODULE);         }         return 1; }

它搜寻elf的versions小节, 循环遍历数组中的每个符号表,找到struct_module这个符号,然后去比较crc的值。
现在有个疑问, versions小节是怎么链接到模块的elf文件中去的呢? 在看下编译后的生成文件, 有一个list.mod.c

[[email protected] list]# cat list.mod.c #include <linux/module.h> #include <linux/vermagic.h> #include <linux/compiler.h>  MODULE_INFO(vermagic, VERMAGIC_STRING);  struct module __this_module __attribute__((section(".gnu.linkonce.this_module"))) = {  .name = KBUILD_MODNAME,  .init = init_module, #ifdef CONFIG_MODULE_UNLOAD  .exit = cleanup_module, #endif };  static const struct modversion_info ____versions[] __attribute_used__ __attribute__((section("__versions"))) = {         { 0x89e24b9c, "struct_module" },         { 0x1b7d4074, "printk" }, };  static const char __module_depends[] __attribute_used__ __attribute__((section(".modinfo"))) = "depends=";  MODULE_INFO(srcversion, "26DB52D8A56205333D414B9");

这个文件是模块在编译的时候,调用了linux-2.6.18/scripts/modpost这个文件生成的。
里面增加了2个小节.gnu.linkonce.this_module和__versions。 __versions小节的内容就是
一些字符串和值组成的数组,check_version就是解析这个小节去做验证。 这里还有一个
MODULE_INFO宏用来生成模块的magic字符串,这个在以后的vermagic中要做验证。

先看下vermagic的格式:
[[email protected] list]# modinfo list.ko
filename: list.ko
author: wzt
license: GPL
srcversion: 26DB52D8A56205333D414B9
depends:
vermagic: 2.6.18-128.el5 SMP mod_unload 686 REGPARM 4KSTACKS gcc-4.1

这里可以看到vermagic跟内核版本,smp,gcc版本,内核堆栈大小都有关。

/* First part is kernel version, which we ignore. */ static inline int same_magic(const char *amagic, const char *bmagic) {         amagic += strcspn(amagic, " ");         bmagic += strcspn(bmagic, " ");         return strcmp(amagic, bmagic) == 0; }

same_magic忽略了对内核版本的判断, 直接比较后面的值。

3、怎样去突破

知道了内核是怎么实现的了, 下面开始想办法绕过这些验证:)

3.1 怎么突破crc验证:

在仔细看下代码:

        for (i = 0; i < num_versions; i++) {                 if (strcmp(versions[i].name, symname) != 0)                         continue;                  if (versions[i].crc == *crc)                         return 1;                 printk("%s: disagrees about version of symbol %s/n",                        mod->name, symname);                 DEBUGP("Found checksum %lX vs module %lX/n",                        *crc, versions[i].crc);                 return 0;         }         /* Not in module's version table.  OK, but that taints the kernel. */         if (!(tainted & TAINT_FORCED_MODULE)) {                 printk("%s: no version for /"%s/" found: kernel tainted./n",                        mod->name, symname);                 add_taint(TAINT_FORCED_MODULE);         }         return 1;

check_version在循环中只是在寻找struct_module符号, 如果没找到呢? 它会直接返回1! 没错, 这是一个
逻辑bug,在正常情况下,module必会有一个struct_module的符号, 这是modpost生成的。如果我们修改elf文件,
把struct_module这个符号改名,岂不是就可以绕过crc验证了吗? 先做个实验看下:
.mod.c是由modpost这个工具生成的, 它在linux-2.6.18/scripts/Makefile.modpost文件中被调用, 去看下:

PHONY += __modpost __modpost: $(wildcard vmlinux) $(modules:.ko=.o) FORCE         $(call cmd,modpost)

我们用一个很土的方法, 就是在编译模块的时候,modpost生成.mod.c文件后, 暂停下编译,sleep 30秒吧,我们用
这个时间去改写下.mod.c, 把struct_module换个名字。

PHONY += __modpost __modpost: $(wildcard vmlinux) $(modules:.ko=.o) FORCE         $(call cmd,modpost)         @sleep 30

随便将struct_module改个名:

[[email protected] list]# cat list.mod.c #include <linux/module.h> #include <linux/vermagic.h> #include <linux/compiler.h>  MODULE_INFO(vermagic, VERMAGIC_STRING);  struct module __this_module __attribute__((section(".gnu.linkonce.this_module"))) = {  .name = KBUILD_MODNAME,  .init = init_module, #ifdef CONFIG_MODULE_UNLOAD  .exit = cleanup_module, #endif };  static const struct modversion_info ____versions[] __attribute_used__ __attribute__((section("__versions"))) = {         { 0x89e24b9c, "stauct_module" },         { 0x1b7d4074, "printk" }, };  static const char __module_depends[] __attribute_used__ __attribute__((section(".modinfo"))) = "depends=";  MODULE_INFO(srcversion, "26DB52D8A56205333D414B9");

我们是在centos5.3下编译的, 然后拷贝到centos5.1下, 在执行下insmod看下:
[[email protected] ~]# insmod list.ko
[[email protected] ~]# dmesg|tail
ata_piix
libata
sd_mod
scsi_mod
ext3
jbd
ehci_hcd
ohci_hcd
uhci_hcd
成功了! 这跟我们预期的一样, 我们用这个逻辑bug绕过了模块的crc验证! 这个bug直到2.6.31版本中
才得到修正。 我们可以用这种方法在redhat主机中任意安装模块了。 那么怎样绕过在2.6.31以后的内核呢?
看下它是怎么修补的:

/* Allocate and load the module: note that size of section 0 is always    zero, and we rely on this for optional sections. */ static struct module *load_module(void __user *umod,                                   unsigned long len,                                   const char __user *uargs) { ...         if (!check_modstruct_version(sechdrs, versindex, mod)) {                 err = -ENOEXEC;                 goto free_hdr;         }          modmagic = get_modinfo(sechdrs, infoindex, "vermagic");         /* This is allowed: modprobe --force will invalidate it. */         if (!modmagic) {                 add_taint(TAINT_FORCED_MODULE);                 printk(KERN_WARNING "%s: no version magic, tainting kernel./n",                        mod->name);         } else if (!same_magic(modmagic, vermagic)) {                 printk(KERN_ERR "%s: version magic '%s' should be '%s'/n",                        mod->name, modmagic, vermagic);                 err = -ENOEXEC;                 goto free_hdr;         } ... }

0

如果没找到struct_module也会返回0, 这样我们就必须将struct_module的值改为正确后, 才能继续安装。
如何找到模块符号的crc值呢? 我们可以去找目标主机中那些已被系统加载的模块的crc值,如ext3文件系统
的模块, 自己写个程序去解析elf文件, 就可以得到某些符号的crc值了。
还有没有更简单的方法呢?去/boot目录下看看,symvers-2.6.18-128.el5.gz貌似和crc有关,gunzip解压后看看:
[[email protected] boot]# grep ‘struct_module’ symvers-2.6.18-128.el5
0x89e24b9c struct_module vmlinux EXPORT_SYMBOL
原来内核中所有符号的crc值都保存在这个文件中。如何改写struct_module的值呢,可以用上面那个土方法,
或者自己写程序去解析elf文件, 然后改写其值。本文最后附上一个小程序用来修改elf的符号和crc值。

3.2 如何突破vermagic验证:
如果我们用list.mod.c中的做法, 用MODULE_INFO宏来生成一个与目标主机相同的vermagic呢? 答案是
否定的,gcc链接的时候会把modinfo小节链接在最后,加载模块的时候还是会读取第一个.modinfo小节。
我们可以用上面那种很土的方法, 先用modinfo命令得到目标主机中某个模块的信息:

/* Allocate and load the module: note that size of section 0 is always    zero, and we rely on this for optional sections. */ static struct module *load_module(void __user *umod,                                   unsigned long len,                                   const char __user *uargs) { ...         if (!check_modstruct_version(sechdrs, versindex, mod)) {                 err = -ENOEXEC;                 goto free_hdr;         }          modmagic = get_modinfo(sechdrs, infoindex, "vermagic");         /* This is allowed: modprobe --force will invalidate it. */         if (!modmagic) {                 add_taint(TAINT_FORCED_MODULE);                 printk(KERN_WARNING "%s: no version magic, tainting kernel./n",                        mod->name);         } else if (!same_magic(modmagic, vermagic)) {                 printk(KERN_ERR "%s: version magic '%s' should be '%s'/n",                        mod->name, modmagic, vermagic);                 err = -ENOEXEC;                 goto free_hdr;         } ... }

1

然后在用那个很土的方面, 将.mod.c中vermagic值进行修改。还有一种直接修改elf文件的方法,附录在本文后面。

4、总结
前面有一点没有提到, 就是某些内核版本的相同接口的函数代码可能已经变化, 这样在使用这项技术的时候,
最好在同一个大内核版本使用。你也可能感觉要想跨平台安装模块有些麻烦, 这里还有2个方法, 作为一个专业
搞渗透的人来说,他会自己在本地装很多发行版本的linux,特别是root掉一台主机后,会在本地装一个一模一样
的发行版本,smp、kernel stack size、gcc version都一样。在本地机器装上开发环境,这样编译出来的模块
也是可以直接装到目标主机上的,但这很麻烦,因为linux有太多的发行版本了:), 另一个方法就是自己
装一个linux,编译下内核,然后将build后的开发包集成到自己的后门里, 压缩后大概几m。 然后传到主机
去解压,编译。庆幸的是,现在大多数主机中都有内核开发环境, 直接去主机编译就ok了。

5、参考
【1】 linux kernel source code
http://www.kernel.org
【2】 module injection in 2.6 kernel – Coolq
http://www.nsfocus.net/index.php?act=magazine&do=view&mid=2533

6、附录

/* Allocate and load the module: note that size of section 0 is always    zero, and we rely on this for optional sections. */ static struct module *load_module(void __user *umod,                                   unsigned long len,                                   const char __user *uargs) { ...         if (!check_modstruct_version(sechdrs, versindex, mod)) {                 err = -ENOEXEC;                 goto free_hdr;         }          modmagic = get_modinfo(sechdrs, infoindex, "vermagic");         /* This is allowed: modprobe --force will invalidate it. */         if (!modmagic) {                 add_taint(TAINT_FORCED_MODULE);                 printk(KERN_WARNING "%s: no version magic, tainting kernel./n",                        mod->name);         } else if (!same_magic(modmagic, vermagic)) {                 printk(KERN_ERR "%s: version magic '%s' should be '%s'/n",                        mod->name, modmagic, vermagic);                 err = -ENOEXEC;                 goto free_hdr;         } ... }

2

  • 我的微信
  • 这是我的微信扫一扫
  • weinxin
  • 我的微信公众号
  • 我的微信公众号扫一扫
  • weinxin

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: