首页
社区
课程
招聘
内核和驱动文件的感染
发表于: 2023-5-23 20:39 19761

内核和驱动文件的感染

2023-5-23 20:39
19761

  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直播授课

收藏
免费 5
支持
分享
最新回复 (4)
雪    币: 2322
活跃值: (3029)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2
转眼都二十年了
2023-5-24 09:47
0
雪    币: 3314
活跃值: (10952)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
3
是的,原文02、03年的
2023-5-24 10:45
0
雪    币: 3736
活跃值: (3867)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
4
感谢分享!
2023-5-28 21:00
0
雪    币: 2948
活跃值: (30846)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
感谢分享
2023-5-28 22:48
1
游客
登录 | 注册 方可回帖
返回
//