http://phrack.org/issues/61/10.html
rootkis是一种可以隐藏文件、进程,以及可以实现其它很多事情的内核模块(驱动),最初的rootkis,都是直接利用insmod加载,这样很容易通过lsmod被发现。目前,已经有很多隐藏内存模块的技术,比如Plaguez提出的方法,以及Adore Rootkit中使用的更加刁钻的方法,几年后,又出现了通过/dev/kmem,修改内核内存镜像的方法,最后,还出现了静态捆绑内核文件的方法(见:2.内核文件感染),这个方法解决了一个重要的问题:系统reboot后,rootkis还是加载的。 本节将介绍一种新的内核模块隐藏技术,并且也能在系统reboot之后,还是加载的。稍后将会基于linux 2.4.x内核环境(实验也适用于其它使用elf格式的系统),演示这种感染技术。不过,由于内核模块是elf格式的,所以阅读后续内容之前,先要熟悉elf格式(http://www.skyfree.org/linux/references/ELF_Format.pdf ),并且要重点理解其符号表,之后才好明白,向正常内核模块注入感染代码的原理。
ELF(Executable and Linking),是Linux操作系统中可执行文件的格式,接下来仅仅介绍一下,感染内核模块,需要了解的部分:当链接2个elf文件时,需要知道这2个文件中包含哪个符号,每个elf文件(比如内核模块),都有2个包含符号信息的节区,下面就是对这2个节区的介绍。
有些elf文件,下一步要由静态ld处理(比如.o、.ko文件),或由动态ld处理(比如.so文件),有些elf文件,加载到内存就可以直接运行(比如可执行文件、内核文件),而不管哪一种elf文件类型,都要为下一步处理,提供足够的信息,所以,elf文件类型不同,具体的组成内容是有区别的。 另外,首次学习elf格式时,很容易被大量的结构体,搞的眼花缭乱,这里先放一张示意图,展示了部分重要结构体之间的联系,用于辅助理解后续内容: 比如:init_module()函数体文件内偏移的查找过程为: ① 根据elf头部中的e_shoff成员,找到节区头表; ② 遍历节区头表,找到.symtab节区头部(类型为SHT_SYMTAB); ③ 根据.symtab节区头部中的sh_offset成员,找到.symtab节区内容,根据sh_link成员,找到符号名称所在节区(.strtab); ④ 遍历.symtab节区(即Elf32_Sym[]数组),根据Elf32_Sym的sh_link成员,在.strtab节区中找到符号名称; ⑤ 如果称号名称为”init_module”,根据Elf32_Sym的st_shndx和st_value成员,最终找到由init_module()函数编译出来的机器码,在.text节区中的某个位置。
.symtab节区(符号表)的内容,是一个供链接器使用的Elf32_Sym结构数组,Elf32_Sym结构定义在内核的/usr/include/elf.h头文件:
.strtab节区的内容,是一个字符串表,包含很多以’\0’字符结尾的字符串,通过修改.strtab节区,可以将指定函数,修改为其它名称,原文9.1节提供了ElfStrChange程序,所以就没有说明原理(不过对照”ELF Basis”一节中的”额外补充”内容,还是很好理解的),只是提醒修改时要遵守一个原则:新名称不能比旧名称长,否则就会覆盖旧名称后面的’\0’字符,以及后面的其它字符串。 这里再额外补充一点:假设elf文件中既有aabb符号,又有bb符号,.strtab中可能会让它们共享一个”aabb\0”字符串,所以如果这种情况将”bb”改掉,会导致”aabb”符号无效。 稍后的内容就会介绍,具体如何修改内核模块的函数名,并且最终实现向一个内核模块,注入另一个内核模块。
接下来先分析内核模块动态加载的代码,用于进一步理解内核模块感染的原理。
内核模块是通过应用程序insmod加载链接的(高版本内核已经将链接功能移到了内核代码中,不过文中实验环境,使用的是Linux 2.4.x内核),所以需要分析insmod.c(代码见原文):
前面已经说过,内核模块的初始化函数名称”init_module”,位于.strtab节区,如果将其改为内核模块中的另外一个函数名,比如”evil_module”,就能使内核模块加载时,调用evil_module()函数,而不是init_module()函数。 原文提供了一个简单的驱动程序test.c,包含init_module()、evil_module()、cleanup_module()三个函数,然后将test.c编译为test.o,并通过objdump查看其符号表,主要关注init_module、evil_module两个符号(用于修改后进行对比)。 这时,就可以按照以下顺序,修改.strtab节区了: rename 1) init_module ------> dumm_module 2) evil_module ------> init_module 先将”init_module”改为”dumm_module”(避免和后续新的init_module重名),再将”evil_module”改为”init_module”(顶替),这些都可以通过原文9.1节提供的ElfStrChange程序进行修改: ./elfstrchange test.o init_module dumm_module ./elfstrchange test.o evil_module init_module 再用objdump查看符号表,包含的就是dumm_module、init_module两个符号了,并且init_module的文件偏移,为原来evil_module的文件偏移(0x14)。这是因为,只是”init_module”在.strtab节区中的位置变了,导致它对应的Elf32_Sym位置也变了,而这个位置上正是原来evil_module对应的Elf32_Sym,它的内容并没有改变,通过它的st_shndx和st_info成员,仍然得到0x14偏移值。
以上实验,证明了确实可以将evil_module(),伪装成init_module()函数,然而,如果能从外部,向内核模块注入evil_module()函数,那就更好了,好在这个目的可以利用ld命令轻松实现。 为了证实这一点,原文将上个实验中的test.c,划分为2份驱动代码,original.c包含init_module()和cleanup_module()函数,inject.c包含inje_module()函数,然后分别编译为original.o和inject.o,再利用ld的-r选项进行部分链接,最终展示了,得到的仍然是一个.o文件,并且通过objdump查看,同时包含init_module、cleanup_module、inje_module三个符号。这是因为内核模块本质是.o文件(即使后缀为.ko),可以与其它.o文件部分链接,拼接成一个.o文件(比如当前实验中的evil.o),并且和通过常规方式编译生成的内核模块(比如上个实验中的test.o),没有任何区别,也可以加载到内核。所以说,向已有内核模块,注入额外的代码,并不是什么难题。
通常,都是对正常使用的内核模块进行感染,然后,将init_module()改为注入函数后,启动了感染者的功能,却丧失了内核模块本身的功能,这就很容易被发现做了手脚,不过,也有办法保留内核模块原本的功能,因为.strtab节区被修改之后,init_module()函数名被改为dumm_module,所以只要在注入的evil_module()函数中,调用一下dumm_module(),实际执行的就是真正的init_module()。
修改init_module()的方法,也完全适用于修改cleanup_module(),因此,以下展示将一个完整的内核模块(著名的Adore rootkit),注入到声卡驱动(i810_audio.o),处理过程其实非常简单:
1) 修改adore.c代码: ① 在init_module()函数中,添加dumm_module()函数调用 ② 在cleanup_module()函数中,添加dummcle_module()函数调用 ③ 将init_module()函数名,修改为evil_module ④ 将cleanup_module函数名,修改为evclean_module 2) 执行make编译adore 3) 使用"ld -r"部分链接adore.o和i810_audio.o,作为新的i810_audio.o 4) 按如下方式,使用elfstrchange修改.strtab节区: replace init_module ------> dumm_module evil_module ------> init_module (内部调用dumm_module) cleanup_module ------> evclean_module evclean_module ------> cleanup_module (内部调用evclean_module) 5) 加载并测试新的i810_autio.o 原文9.2节提供了lkminfect.sh自动脚本,不过步骤1)中的前面2步,需要手动完成。
感染内核模块加载之后,有2个选择,并且各有利弊:
http://phrack.org/issues/60/8.html
本文介绍了一种方法,可以将Linux内核模块文件(LKM),捆绑到内核文件(linuxz),并且可以在系统reboot时被加载。相比insmod或者/dev/kmem实现的内核后门,这种方式更隐蔽,附件提供了具体的实现代码(依赖/boot/System.map文件),已经基于redhat 7.2进行安装和测试。 --[ 2 - Get kernel from the image vmlinuz是一个统称,包括zImage和bzImage两种形式,是指vmlinux原始内核文件经过压缩之后,与其它功能合成的文件(详细原理,可以参考《Linux内核情景分析》第10章:系统的引导和初始化)。了解vmlinuz内核文件的结构,一方面为了从中剥出vmlinux文件,进而捆绑LKM,另一方面为了将捆绑后的vmlinux,再转回vmlinuz文件。 根据/usr/src/linux/arch/i386/boot/Makefile可以看出:zImage文件(bzImage与之相似),是根据bootsect、setup、compressed/vmlinux.out三个文件合成的。其中,bootsect、setup分别由bootsect.s、setup.s汇编链接生成,compressed/vmlinux.out是通过objcopy命令,根据compressed/vmlinux文件生成。 根据/usr/src/linux/arch/i386/boot/compressed/Makefile可以看出:compressed/vmlinux文件(注意:顶层目录中的vmlinux才是原始内核文件),是由piggy.o、head.o和misc.o链接而成。其中,misc.c中定义了decompress_kernel()函数,head.S会调用该函数,解压内核文件,解压过程中会引用input_len和input_data两个符号,它们定义在piggy.o文件中,piggy.o只包含.data节区:
文章没有说明$(SYSTEM)的值,但是根据完整的Makefile(比如:https://elixir.bootlin.com/linux/2.4.0/source/arch/i386/boot/compressed/Makefile ),就会发现:SYSTEM = $(TOPDIR)/vmlinux(即顶层目录中的vmlinux),也就是说,piggy.o是根据原始内核文件生成的,具体过程为: ① 执行objcopy,将原始内核文件中的部分内容(-R移除.note和.comment节区,-S移除调试信息),拷贝到$tmppiggy临时文件; ② 将$tmppiggy文件,压缩为$tmppiggy.gz; ③ 生成链接脚本$tmppiggy.lnk:
显然,根据$tmppiggy.lnk链接生成的文件,只包含.data节区,并且.data节区包含一个LONG值,以及所有参与链接的文件的.data节区集合,其中,LONG值保存了.data节区集合的长度,所在地址用input_len符号记录,.data节区集合的地址,用input_data符号记录,misc.c中可以通过extern引用这两个变量,就是因为它们在.data节区中存在。 ④ 执行ld,链接生成piggy.o: -b binary $$tmppiggy.gz:将整个$tmppiggy.gz文件内容,添加到.data节区集合; -T $$tmppiggy.lnk:按照$tmppiggy.lnk的指示进行链接。 结论: 编译生成原始vmlinux -> 压缩从原始vmlinux中objcopy出来的内容,作为.data节区,生成piggy.o -> 链接piggy.o、head.o和misc.o,生成compressed/vmlinux -> 根据compressed/vmlinux,objcopy得到compressed/vmlinux.out -> 链接bootsect、setup和compressed/vmlinux.out,生成zImage。
加载内核时,会把紧接着内核文件的内存区域,作为.bss节区的存储空间,会被head.S中的代码,全部清为0(.bss节区的内容初始一定为全0,所以elf文件中只会记录起始和结束位置),因此,不能直接将LKM捆绑在vmlinux文件后面,否则加载到内存后,内容很快就会被擦除掉,而是先要追加一段与.bss节区同等大小的内容(文中称为”dummy data”),再追加LKM。 除此以外,还要解决另外一个问题,先看一下内容启动过程中执行的代码: 图中鼠标选中部分,估计是作者复制错了位置,以下根据linux-2.4.0内核代码截取(https://elixir.bootlin.com/linux/2.4.0/source/arch/i386/kernel/setup.c#L598): 图中的代码,将_end(即.bss节区的结束地址)赋值给了init_mm.brk和start_pfn,其中,start_pfn的值是个页面号,代表可供内核动态分配的第一个页面。显然,这个值需要调整,跳过紧接在.bss节区后面的LKM内容,否则内核的动态分配+写操作,会将加载到内存中的LKM覆盖掉。 下图是从《Linux内核情况分析》截取:
本节主要表达,.o文件中的很多地址值,在编译阶段是无法确定的,所以编译器会在.o文件中,留下重定项信息,供链接器在后期确认后加以修改。关于重定位原理,我在这篇文章中深入介绍过:32位elf格式中的10种重定位类型 。 然而,捆绑的LKM,是我们自己追加到vmlinux内核文件后面的,不是通过ld程序链接进去的,所以LKM中的重定项,也需要我们自己处理。
解决以上所有问题之后,还得找个时机,触发捆绑LKM的加载,也就是调用LKM的init_module()函数。jbtzhm提供的方法是,再在dummy data与捆绑LKM之间,添加如下指令块: 然后,再修改内核的系统调用表,具体来说,就是将SYS_getpid系统调用号对应的表项(因为reboot时,该系统调用一定会被执行,很多程序(比如init)都会调用getpid()函数),修改为init_code[]的地址,进入init_code[]后,就会在真正调用sys_getpid()之前,悄悄先加载捆绑的LKM。 另外,这段指令中涉及的地址,要根据init_code[]的实际存放位置,以及具体的内核符号表,才能确定(具体见附件中kpatch.c的实现)。
根据内核文件感染的原理可以看出,感染过程需要获取__bss_start、_end、sys_call_table这些符号的地址,并且本文提供的感染工具,是根据/boot/System.map文件获取的,但是删除/boot/System.map文件,并不能防御感染,因为Silvio展示过直接从内核获取内核符号地址的方法,所以,最终要通过检查内核文件内容是否有变化,检查是否被感染。
[2]~[5]节,介绍了感染的大体流程,至于实现过程中要解决的一些具体问题,只是抛出,并未给出答案,所以附件中提供了一份基于redhat 7.2环境开发和测试的感染程序,后续通过分析这份代码,就能明白完整的思路了。
参考文档链接。
附件需要通过以下步骤,还原成kpatch.tgz文件,然后解压: ① 创建xx.txt,并将[begine, end]区间的内容,复制进去; ② uudecode xx.txt # 还原kpatch.tgz(如果没有uudecode命令,要先安装sharutils); ③ tar zxvf kpatch.tgz # 解压得到kpatch目录,里面包含kpatch.c等代码文件。
常规情况下,LKM都是在加载时,才会由insmod和内核,使其跟vmlinux内容进行链接,捆绑的LKM显然要尽早完成链接,否则,等到可以触发insmod执行的时候,LKM可能早已被其它应用程序,通过执行getpid()系统调用执行过了,而那个时候还没有完成链接,就会导致系统崩溃。所以,kpatch程序的主要部分,就是一个链接器。 既然是链接器,就要对LKM中的很多内容进行修改,比如根据重定项,将目标位置修改为全局变量运行时的内存地址等,因此,kpatch代码中会有很多赋值语句。为了防止看代码的时候犯晕,有必要事先明确:kpatch是对自己read()的LKM内核进行修改,修改值是在reboot后运行时使用,所以赋值语句的左值位置和右值范围,一定如下图所示: 另外,要执行LKM,就要访问LKM的.text、.data等节区,加载到内存后,LKM所在的这块内存,是由谁分配的? 分析kpatch代码的时候就会看到,kpatch会修改vmlinux中的一条mov语句(源操作数为start_pfn),将其目的操作数改大,从而将LKM的大小也包含进去,这样,内核启动阶段,就会分配这块内存,而LKM是某个应用程序执行getpid()系统调用时触发执行的,这个时候内核显然已经启动完成,换句话说,这块内存也已经分配了(页目录表中有映射)。 下图是从《Linux内核情况分析》截取: 这就相当于欺骗内核,分配更多的保留页面,但实际能用到的并没有变多,也就是说内核并不会对LKM所在内存区域执行写操作,因此LKM的内容也不会糟到破坏。除此之外,在”Allocate some space in image to use”一节中还提到,修改start_pfn,还会将LKM移到内核的堆区之外,所以可以说是一举两得。
kpatch.c及其调用关系: 程序中的0xC0100000值,包含2部分:0xC0000000是内核空间的地始虚拟地址,0x100000来自链接脚本,是内核文件加载的起始地址。 下图是从《Linux内核情况分析》截取:
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
*
/
} Elf32_Sym;
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
*
/
} Elf32_Sym;
piggy.o: $(SYSTEM)
tmppiggy
=
_tmp_$$$$piggy; \
rm
-
f $$tmppiggy $$tmppiggy.gz $$tmppiggy.lnk; \
$(OBJCOPY) $(SYSTEM) $$tmppiggy; \
gzip
-
f
-
9
< $$tmppiggy > $$tmppiggy.gz; \
echo "SECTIONS { .data : { input_len
=
.; \
LONG
(input_data_end
-
input_data) input_data
=
.; \
*
(.data) input_data_end
=
.; }}" > $$tmppiggy.lnk;
$(LD)
-
r
-
o piggy.o
-
b binary $$tmppiggy.gz
-
b elf32
-
i386
-
T \
$$tmppiggy.lnk;
rm
-
f $$tmppiggy $$tmppiggy.gz $$tmppiggy.lnk
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课