突破Linux內核模塊校驗機制(轉)

  By wzt1、 爲什麼要突破模塊驗證 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環境中編譯一下: [root@localhost 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上: [root@localhost ~]# 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加載: [root@localhost ~]# insmod list.ko insmod: error inserting 'list.ko': -1 Invalid module format 報錯了,在看下dmesg的信息: [root@localhost ~]# 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裏的關鍵字, 看看它在哪個文件中: [root@localhost 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 [root@localhost list]# cat list.mod.c #include <linux突破Linux內核模塊校驗機制(轉) .M>/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的格式: [root@localhost 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改個名: [root@localhost 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看下: [root@localhost ~]# insmod list.ko [root@localhost ~]# 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以後的內核呢? 看下它是怎麼修補的: for (i = 0; i < num_versions; i++) { if (strcmp(versions[i].name, symname) != 0) continue; if (versions[i].crc == *crc) return 1; DEBUGP("Found checksum %lX vs module %lX\n", *crc, versions[i].crc); goto bad_version; } printk(KERN_WARNING "%s: no symbol version for %s\n", mod->name, symname); return 0; bad_version: printk("%s: disagrees about version of symbol %s\n", mod->name, symname); return 0; 如果沒找到struct_module也會返回0, 這樣我們就必須將struct_module的值改爲正確後, 才能繼續安裝。 如何找到模塊符號的crc值呢? 我們可以去找目標主機中那些已被系統加載的模塊的crc值,如ext3文件系統 的模塊, 自己寫個程序去解析elf文件, 就可以得到某些符號的crc值了。 還有沒有更簡單的方法呢?去/boot目錄下看看,symvers-2.6.18-128.el5.gz貌似和crc有關,gunzip解壓後看看: [root@localhost 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命令得到目標主機中某個模塊的信息: [root@localhost list]# modinfo /lib/modules/2.6.18-128.el5/kernel/fs/ext3/ext3.ko filename: /lib/modules/2.6.18-128.el5/kernel/fs/ext3/ext3.ko license: GPL description: Second Extended Filesystem with journaling extensions author: Remy Card, Stephen Tweedie, Andrew Morton, Andreas Dilger, Theodore Ts'o and others srcversion: B048AC103E5034604A721C5 depends: jbd vermagic: 2.6.18-128.el5 SMP mod_unload 686 REGPARM 4KSTACKS gcc-4.1 module_sig: 883f3504977495e4f3f897cd3dced211288209f551cc1da557 f96ea18d9a4efd6cfb0fc2612e009c8845fd776c825d586f492 ceab19e17b2319da8f 然後在用那個很土的方面,將.mod.c中vermagic值進行修改。還有一種直接修改elf文件的方法,附錄在本文後面。 4、總結 前面有一點沒有提到, 就是某些內核版本的相同接口的函數代碼可能已經變化, 這樣在使用這項技術的時候, 最好在同一個大內核版本使用。你也可能感覺要想跨平臺安裝模塊有些麻煩, 這裏還有2個方法, 作爲一個專業 搞***的人來說,他會自己在本地裝很多發行版本的linux,特別是root掉一臺主機後,會在本地裝一個一模一樣 的發行版本,smp、kernel stack size、gcc version都一樣。在本地機器裝上開發環境,這樣編譯出來的模塊 也是可以直接裝到目標主機上的,但這很麻煩,因爲linux有太多的發行版本了:), 另一個方法就是自己 裝一個linux,編譯下內核,然後將build後的開發包集成到自己的後門裏, 壓縮後大概幾m。 然後傳到主機 去解壓,編譯。慶幸的是,現在大多數主機中都有內核開發環境, 直接去主機編譯就ok了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章