Linux Loader 机制与内核模块修复

0x0 前言

在吃着月饼喝着茶的闲暇时间里,突然接到某单位的应急通知。赶往现场进行初步勘察后,发现攻击者手段非常暴力,直接删除系统相关关键模块(包括 PAM 模块、基础内核驱动模块等),导致后续用户无法登录操作系统。

在尝试恢复 PAM 模块后,正常登录系统,但由于缺少基础的核心模块,网络、USB 挂载等仍无法正常使用。在下载部分二进制驱动包进行安装后,重启开始出现问题(截图为后期复现):

image.png

确认 /lib/modules/ 下确实存在此文件,但重启依然出现此问题。

而由于对 Linux Loader 机制的不熟悉,现场并未能解决此问题(菜。同时在不确定攻击者是否也删除了正常业务程序组件的情况下,恢复的成本较大,于是备份关键数据,提取相关样本与日志后,对系统进行重装,恢复正常业务。

0x1 Linux 启动与内核加载

1.1 bootloader

首先,还是从我们的老朋友 BIOS 进行加电自检(POST,Power-On Self-Test)后,开始读取 MBR 记录。MBR 为 512 字节大小的扇区,其中前 446 字节硬编码着 bootloader 程序。BIOS 从 MBR 记录读取 bootloader 程序,将其加载到内存,并运行该程序。

对于不同的操作系统,有不同的 bootloader 程序,一般在安装操作系统时,安装程序会向 MBR 写入 bootloader 程序。如 Windows 操作系统,使用自带的 bootloader 程序。对于 Linux 发行版,一般使用新版的 GRUB2 来生成并写入 bootloader。

bootloader 的工作便是载入内核文件,使操作系统正常运行。

1.2 内核文件

在 Linux 中,由 bootloader 载入的内核文件位于 /boot 节点下:

[root@centos boot]# ls --format=single-column /boot
config-2.6.32-696.el6.x86_64 # 内核编译时启用的配置信息与模块设定
efi # EFI 引导程序
grub # GRUB 引导程序支持
initramfs-2.6.32-696.el6.x86_64.img # 虚拟文件系统,主角 ;-)
symvers-2.6.32-696.el6.x86_64.gz # 驱动模块符号表
System.map-2.6.32-696.el6.x86_64 # 内核符号表
vmlinuz-2.6.32-696.el6.x86_64 # 内核文件

如上,vmlinuz 便是 bootloader 载入的 Linux 内核文件,内核文件主要负责检测基础的硬件设施,并载入相关驱动,这些驱动一般位于 /lib/modules/<内核版本>/ 目录下。

那么 initramfs 是干嘛的?当内核启动时,需要往 /lib/modudles 下读取驱动文件,以支持 SATA、USB 等接口的设备,但是明显 /lib/modules 是存储于 SATA 磁盘中,没有 SATA 驱动的情况下也无法读取挂载,这里就出现了先有鸡还是先有蛋的问题。

initramfs 便是来解决这个问题,initramfs 事实上是一个具有根目录结构的文件,其中包含基础的应用程序,以及核心的内核模块,例如 SCSI、SATA、USB 等,用于支持最基础的外部设备。

1.3 解剖 initramfs

新建目录,将 initramfs 拷贝到目标目录下:

[root@centos ~]# mkdir /tmp/initramfs
[root@centos ~]# cp /boot/initramfs-2.6.32-696.el6.x86_64.img /tmp/initramfs/initramfs.img

initramfs 使用 gzip 进行压缩,将其解压:

[root@centos ~]# cd /tmp/initramfs/
[root@centos initramfs]# file initramfs.img 
initramfs.img: gzip compressed data, from Unix, last modified: Wed Oct  4 17:17:17 2017, max compression
[root@centos initramfs]# mv initramfs.img initramfs.gz
[root@centos initramfs]# gzip -d initramfs.gz 

在解压完成后,实际上文件使用 cpio 进行归档,最后我们使用 cpio 将其解压出来:

[root@centos initramfs]# file initramfs
initramfs: ASCII cpio archive (SVR4 with no CRC)

# -i 解压文件
# -d 必要时创建目录
# -H newc 指定类型为 SVR4 的归档文件
# --no-absolute-filenames 不要将文件解压覆盖到绝对路径
[root@centos initramfs]# cpio -i -d -H newc --no-absolute-filenames < initramfs
108638 blocks

从最后的目录结构可以看到 initramfs 的真实内容:

[root@centos initramfs]# ll
total 54432
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 bin
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 cmdline
drwxr-xr-x. 3 root root     4096 Oct  5 22:50 dev
-rw-r--r--. 1 root root       23 Oct  5 22:50 dracut-004-409.el6_8.2
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 emergency
drwxr-xr-x. 7 root root     4096 Oct  5 22:50 etc
-rwxr-xr-x. 1 root root     8989 Oct  5 22:50 init
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 initqueue
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 initqueue-finished
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 initqueue-settled
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 initqueue-timeout
drwxr-xr-x. 7 root root     4096 Oct  5 22:50 lib
drwxr-xr-x. 3 root root     4096 Oct  5 22:50 lib64
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 mount
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 netroot
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 pre-mount
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 pre-pivot
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 pre-trigger
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 pre-udev
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 proc
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 sbin
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 sys
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 sysroot
drwxrwxrwt. 2 root root     4096 Oct  5 22:50 tmp
drwxr-xr-x. 7 root root     4096 Oct  5 22:50 usr
drwxr-xr-x. 4 root root     4096 Oct  5 22:50 var

列一下 lib/modules/<内核版本>/kernel/drivers 下的驱动文件:

[root@centos initramfs]# ls lib/modules/2.6.32-696.el6.x86_64/kernel/drivers/
acpi
ata
atm
auxdisplay
bcma
block
bluetooth
cdrom
<...snippets...>

initramfs 中包含最核心的驱动程序,bootloader 在启动时,将 initramfs 临时挂载到根目录,在内核文件正常加载驱动后,对其进行卸载,并挂载真正的根目录节点。

最后,内核程序启动 init / systemd 对系统服务进行加载。

1.4 bootloader 配置

在 Linux 发行版下,bootloader 通常由 GRUB 程序进行管理,而 bootloader 又负责加载内核文件,较新的发行版一般使用新的 GRUB2 进行引导。

由于笔者使用的是 CentOS 6.X,所以此时默认还是旧版的 GRUB 引导管理程序,但不同版本配置上大同小异,所以以旧版为例。

默认情况下,/boot/grub 下(新版为/boot/grub2)存放 GRUB 的配置文件:

[root@centos ~]# ls /boot/grub
grub.conf # GRUB 的引导菜单配置文件
stage1
stage2
<...snippets...>

可以注意到目录下有 stage1stage2 文件,这个其实便是 bootloader 程序。由于 MBR 区域只有可怜的 512 字节,而 bootloader 程序相对都会比较大,所以 GRUB 将启动分为两个阶段,真正存在于 MBR 处的代码便是 stage1 程序,再由此加载真正的 bootloader 程序 stage2

再看看主配置文件 /boot/grub/grub.conf 的内容:

# grub.conf generated by anaconda

default=0
timeout=5

title CentOS 6 (2.6.32-696.el6.x86_64)
	root (hd0,0)
	kernel /vmlinuz-2.6.32-696.el6.x86_64 ro root=/dev/mapper/vg_centos-lv_root nomodeset rd_NO_LUKS LANG=en_US.UTF-8 rd_LVM_LV=vg_centos/lv_swap rd_NO_MD SYSFONT=latarcyrheb-sun16 crashkernel=auto rd_LVM_LV=vg_centos/lv_root  KEYBOARDTYPE=pc KEYTABLE=us rd_NO_DM rhgb quiet
	initrd /initramfs-2.6.32-696.el6.x86_64.img

通过配置项名称应该都可以猜到相关的功能,例如 default 设置默认的启动菜单为 Centos 6 项,默认超时为 5 秒钟。

title 中,则定义了菜单项:

root (hd0,0)
kernel /vmlinuz-2.6.32-696.el6.x86_64 ro root=/dev/mapper/vg_centos-lv_root nomodeset rd_NO_LUKS LANG=en_US.UTF-8 rd_LVM_LV=vg_centos/lv_swap rd_NO_MD SYSFONT=latarcyrheb-sun16 crashkernel=auto rd_LVM_LV=vg_centos/lv_root KEYBOARDTYPE=pc KEYTABLE=us rd_NO_DM rhgb quiet
initrd /initramfs-2.6.32-696.el6.x86_64.img

root 指定 /boot 节点的位置,如果是多个磁盘,则从 hd0、hd1 等依次排列。后面的 0 代表所在的分区位置。

GRUB 从 0 开始索引分区,新版 GRUB2 则从 1 开始索引分区,磁盘索引一致,都是从 0 开始。

kernel 指令负责载入内核文件,包括载入的根目录位置、语言、键盘等参数。

initrd 负责挂载 initramfs 虚拟文件系统。

0x2 内核模块依赖重建

2.1 内核模块

Linux 中的内核模块通常与内核进行分离,作为独立的模块进行加载,当然也可以将其编译到内核中。独立的内核模块存放于 /lib/modules/<内核版本> 下。

该目录下的文件 modules.dep,记录了内核模块位置、依赖等信息。对于现在各种各样的设备驱动,手工配置依赖信息非常繁琐,所以当加入驱动后,可以通过 depmod 自动生成依赖信息:

depmod -a

依赖信息会被写入到 modules.dep 文件中,内核通过该文件对驱动进行正确加载。

2.2 问题重现

从引导过程中可以发现,vmlinuz 内核主要依赖 /lib/modules 下的内核驱动来对外部设备提供支持。

而在这起事件中,攻击者恶意删除了/lib/modules 下的所有文件。

于是在后续安装驱动中,/lib/modules 的内容被同步到 initramfs 中,但此时目录下已然没有完整的驱动信息。所以内核在挂载 initramfs 后,无法正常加载驱动程序,提示无法找到 modules.dep 文件,此时并非读取真正根目录下的 /lib/modules :-)。

2.3 修复内核模块

对于 /lib/modules 下的内核模块,正常通过编译 Linux 内核来生成,当然也可以用过 yum / apt 等在线包管理工具来重新安装 Linux 内核。

在机器与网络隔离的情况下,无法通过网络下载编译完的二进制文件,也许只能通过自行编译内核来修复。

但编译内核也需要对应的编译工具、环境、源码包等。另一种更简单的方式,可以下载对应的发行版的镜像,来进行修复。

首先通过镜像引导,进入 rescue 救援模式终端,由于发行版的版本、内核型号都相同,同时镜像也是跑在相同的机器上,所以载入的驱动也是一致。

通常进入救援模式时,都会自动帮你挂载原机的系统分区,例如 CentOS 将其挂在到 /mnt/sysimage 下。

1)先将所有驱动模块拷贝到原本的系统节点:

cp -R /lib/modules/2.6.32-696.el6.x86_64 /mnt/sysimage/lib/modules/

2)切换 /mnt/sysimage 为根目录:

chroot /mnt/sysimage

此时访问 / 下的任何内容,实际上为 /mnt/sysimage

3)进入 /boot 节点,重构 initramfs 镜像,记得备份:

cd /boot
cp initramfs-2.6.32-696.el6.x86_64.img initramfs-2.6.32-696.el6.x86_64.org

# -f 覆盖原始 initramfs 镜像
# -v 显示详细信息
dracut -f -v initramfs-2.6.32-696.el6.x86_64.img 2.6.32-696.el6.x86_64

dracut 用于重构 initramfs 镜像,最后的内核版本号要对应 /lib/modules 下的版本。

4)如果过程中修改了其他文件(例如修改了 shadow 等文件),由于 SELinux 的缘故,需要加入 SELinux 重建标识:

touch /.autorelabel

重启,首次进入时,等待 SELinux 重新建立策略:

image.png

再次重启后,正常进入系统,基本模块修复:

image.png

0x4 总结

  • 内核通过 initramfs 载入核心驱动为基础的外部设备提供支持
  • 内核模块位于 /lib/modules/<内核版本> 下,modules.dep 记录模块索引,使用 depmod -a 可以自动建立依赖信息
  • 使用 dracut 可以对 initramfs 进行重建,将 /lib/modules/ 下的核心模块打包到虚拟文件系统当中
  • 在 SELinux 的上下文下,需要建立 /.autorelabel 通知 SELinux 重新建立策略

总体而言都是在干运维的活,但是 Loader 的加载机制还是十分有趣 = =。