简单介绍一下Linux kernel pwn的预备知识,以及如何编译一个kernel。如何编写一个kernel模块以及如何运行
Kernel Pwn In CTF 简单分析一下CTF Kernel Pwn题目的形式,那2017 CISCN babydrive为例子,先对文件包解压
1 2 3 4 5 6 7 8 9 10 ➜ example ls babydriver.tar ➜ example file babydriver.tar babydriver.tar: POSIX tar archive ➜ example tar -xvf babydriver.tar boot.sh bzImage rootfs.cpio ➜ example ls babydriver.tar boot.sh bzImage rootfs.cpio
得到boot.sh
,bzImage
,rootfs.cpio
三个文件
boot.sh 1 2 3 4 5 6 7 8 9 10 11 12 ➜ example cat -n boot.sh 1 #!/bin/bash 2 qemu-system-x86_64 \ 3 -initrd rootfs.cpio \ 4 -kernel bzImage \ 5 -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \ 6 -enable-kvm \ 7 -monitor /dev/null \ 8 -m 64M \ 9 --nographic \ 10 -smp cores=1,threads=1 \ 11 -cpu kvm64,+smep
boot.sh
文件是用来启动这个程序的,调用qemu来加载rootfs.cpio
与bzImage
运行起来
上面的参数都是qemu的参数
1 2 3 4 -initrd rootfs.cpio,使用 rootfs.cpio 作为内核启动的文件系统 -kernel bzImage,使用 bzImage 作为 kernel 映像 -cpu kvm64,+smep,设置 CPU 的安全选项,这里开启了 smep -m 64M,设置虚拟 RAM 为 64M,默认为 128M
bzImage 1 2 ➜ example file bzImage bzImage: Linux kernel x86 boot executable bzImage, version 4.4.72 (atum@ubuntu) #1 SMP Thu Jun 15 19:52:50 PDT 2017, RO-rootFS, swap_dev 0x6, Normal VGA
bzImage
是经压缩过的linux内核文件
rootfs.cpio 1 2 ➜ example file rootfs.cpio rootfs.cpio: gzip compressed data, last modified: Tue Jul 4 08:39:15 2017, max compression, from Unix
这是一个linux内核文件系统压缩包,我们可以对其解压并重新压缩,从而修改这个系统的文件
新建一个文件夹来解压
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ➜ example mkdir fs && cd fs ➜ fs cp ../rootfs.cpio ./rootfs.cpio.gz ➜ fs gunzip ./rootfs.cpio.gz ➜ fs cpio -idmv < rootfs.cpio . etc etc/init.d etc/passwd etc/group bin ...... linuxrc home home/ctf 5556 blocks ➜ fs ll total 2.8M drwxrwxr-x 2 mask mask 4.0K 1月 20 12:16 bin drwxrwxr-x 3 mask mask 4.0K 1月 20 12:16 etc drwxrwxr-x 3 mask mask 4.0K 1月 20 12:16 home -rwxrwxr-x 1 mask mask 396 6月 16 2017 init drwxr-xr-x 3 mask mask 4.0K 1月 20 12:16 lib lrwxrwxrwx 1 mask mask 11 1月 20 12:16 linuxrc -> bin/busybox drwxrwxr-x 2 mask mask 4.0K 6月 15 2017 proc -rwxrwxr-x 1 mask mask 2.8M 1月 20 12:15 rootfs.cpio drwxrwxr-x 2 mask mask 4.0K 1月 20 12:16 sbin drwxrwxr-x 2 mask mask 4.0K 6月 15 2017 sys drwxrwxr-x 2 mask mask 4.0K 6月 15 2017 tmp drwxrwxr-x 4 mask mask 4.0K 1月 20 12:16 usr
这些就是运行起来后这个系统拥有的文件,查看这个init
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ➜ fs cat -n ./init 1 #!/bin/sh 2 3 mount -t proc none /proc 4 mount -t sysfs none /sys 5 mount -t devtmpfs devtmpfs /dev 6 chown root:root flag 7 chmod 400 flag 8 exec 0</dev/console 9 exec 1>/dev/console 10 exec 2>/dev/console 11 12 insmod /lib/modules/4.4.72/babydriver.ko 13 chmod 777 /dev/babydev 14 echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" 15 setsid cttyhack setuidgid 1000 sh 16 17 umount /proc 18 umount /sys 19 poweroff -d 0 -f
看到第12行的insmod /lib/modules/4.4.72/babydriver.ko
,意味着要调试这个ko文件,使用IDA对其进行分析,利用漏洞
对此文件系统进行打包也是要在这个目录下进行
1 2 3 ➜ fs find . | cpio -o --format=newc > rootfs.cpio cpio: File ./rootfs.cpio grew, 43008 new bytes not copied 5640 blocks
vmlinux 有些题目会给vmlinux
这个文件,这是编译出来的最原始的内核文件,未压缩的,是个ELF形式,方便找gadget
可以使用一个工具来从bzImage
中导出vmlinux
,extract-vmlinux
1 2 3 4 5 ➜ example ./extarct-vmlinux ./bzImage > vmlinux ➜ example file bzImage bzImage: Linux kernel x86 boot executable bzImage, version 4.4.72 (atum@ubuntu) #1 SMP Thu Jun 15 19:52:50 PDT 2017, RO-rootFS, swap_dev 0x6, Normal VGA ➜ example file vmlinux vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=e993ea9809ee28d059537a0d5e866794f27e33b4, stripped
exploit Kernel Pwn就是找出内核模块中的漏洞,然后写一个C语言程序,放入文件系统中打包,重新运行取来,此时用户一般都是普通用户,运行程序调用此模块的功能利用漏洞,从而提升权限到root用户,读取flag
1 2 3 4 5 6 7 8 9 10 11 12 13 / $ ls bin exp lib root sys dev home linuxrc rootfs.cpio tmp etc init proc sbin usr / $ whoami ctf / $ ./exp [ 18.277799] device open [ 18.278768] device open [ 18.279760] alloc done [ 18.280706] device release / # whoami root
比赛时一般是上传C语言程序的base64编码到服务器,然后运行
Kernel Pwn Debug 要对内核模块进行调试,在启动脚本中加入
然后使用gdb连接
1 gdb -q -ex "target remote localhost:1234"
如果显示Remote 'g' packet reply is too long
一长串数字,要设置一下架构
1 gdb -q -ex "set architecture i386:x86-64:intel" -ex "target remote localhost:1234"
要调试内核模块,可以先查看内核加载地址,在/sys/module/中是加载的各个模块的信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 / $ cd sys/module/ /sys/module $ ls 8250 ipv6 scsi_mod acpi kdb sg acpi_cpufreq kernel spurious acpiphp keyboard sr_mod apparmor kgdb_nmi suspend ata_generic kgdboc sysrq ata_piix libata tcp_cubic babydriver loop thermal battery md_mod tpm block module tpm_tis core mousedev uhci_hcd cpuidle netpoll uinput debug_core pata_sis usbcore dm_mod pcc_cpufreq virtio_balloon dns_resolver pci_hotplug virtio_blk dynamic_debug pci_slot virtio_mmio edd pcie_aspm virtio_net efivars pciehp virtio_pci ehci_hcd ppp_generic vt elants_i2c printk workqueue ext4 processor xen_acpi_processor firmware_class pstore xen_blkfront fuse rcupdate xen_netfront i8042 rcutree xhci_hcd ima rfkill xz_dec intel_idle rng_core zswap
获取babydrive
模块的加载地址
1 2 3 4 5 6 7 /sys/module $ cd babydriver/ /sys/module/babydriver $ ls coresize initsize notes sections taint holders initstate refcnt srcversion uevent /sys/module/babydriver $ cd sections/ /sys/module/babydriver/sections $ grep 0 .text 0xffffffffc0000000
在gdb中载入符号信息,就可以对内核模块进行下断调试
1 2 3 4 5 6 7 8 pwndbg> add-symbol-file ./fs/lib/modules/4.4.72/babydriver.ko 0xffffffffc00000 00 add symbol table from file "./fs/lib/modules/4.4.72/babydriver.ko" at .text_addr = 0xffffffffc0000000 Reading symbols from ./fs/lib/modules/4.4.72/babydriver.ko...done. pwndbg> b*babyopen Breakpoint 1 at 0xffffffffc0000030: file /home/atum/PWN/my/babydriver/kernelmo dule/babydriver.c, line 28.
Basic Knowledge Kernel Kernel是一个程序,是操作系统底层用来管理上层软件发出的各种请求的程序,Kernel将各种请求转换为指令,交给硬件去处理,简而言之,Kernel是连接软件与硬件的中间层
Kernel主要提供两个功能,与硬件交互,提供应用运行环境
在intel的CPU中,会将CPU的权限分为Ring 0,Ring 1,Ring 2,Ring 3,四个等级,权限依次递减,高权限等级可以调用低权限等级的资源
在常见的系统(Windows,Linux,MacOS)中,内核处于Ring 0级别,应用程序处于Ring 3级别
LKM 内核模块是Linux Kernel向外部提供的一个插口,叫做动态可加载内核模块(Loadable Kernel Module,LKM),LKM弥补了Linux Kernel的可拓展性与可维护性,类似搭积木一样,可以往Kernel中接入各种LKM,也可以卸载,常见的外设驱动就是一个LKM
LKM文件与用户态的可执行文件一样,在Linux中就是ELF文件,可以利用IDA进行分析
LKM是单独编译的,但是不能单独运行,他只能作为OS Kernel的一部分
与LKM相关的指令有如下几个
insmod:接入指定模块
rmmod:移除指定模块
lsmod:列出已加载模块
这些都是shell指令,可以在shell中运行查看
1 2 3 4 5 6 ➜ ~ lsmod Module Size Used by rfcomm 77824 2 vmw_vsock_vmci_transport 32768 2 vsock 36864 3 vmw_vsock_vmci_transport ......
ioctl ioctl是设备驱动程序中对设备的I/O通道进行管理的函数
所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的调用个数如下: int ioctl(int fd, ind cmd, …);
其中fd是用户程序打开设备时使用open函数返回的文件标示符,cmd是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,这个参数的有无和cmd的意义相关
ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数来控制设备的I/O通道。
意思就是说如果一个LKM中提供了iotcl功能,并且实现了对应指令的操作,那么在用户态中,通过这个驱动程序,我们可以调用ioctl来直接调用模块中的操作
Land Switch 在程序运行时,总是会经历user space与kernel space之前的切换,因为用户态应用程序在执行某些功能时,是由Kernel来执行的,这就涉及到两个space之前的切换
user land -> kernel land 当用户态程序执行系统调用,异常处理,外设终端时,会从用户态切换到内核态,切换过程如下:
swapgs
指令修改GS寄存器切换到内核态
将当前栈顶(sp)记录在CPU独占变量区域,然后将此区域里的内核栈顶赋给sp
push各寄存器的值
通过汇编指令判断是否为32位
通过系统调用号,利用函数表sys_call_table
执行响应操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ENTRY(entry_SYSCALL_64) /* SWAPGS_UNSAFE_STACK是一个宏,x86直接定义为swapgs指令 */ SWAPGS_UNSAFE_STACK /* 保存栈值,并设置内核栈 */ movq %rsp, PER_CPU_VAR(rsp_scratch) movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp /* 通过push保存寄存器值,形成一个pt_regs结构 */ /* Construct struct pt_regs on stack */ pushq $__USER_DS /* pt_regs->ss */ pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */ pushq %r11 /* pt_regs->flags */ pushq $__USER_CS /* pt_regs->cs */ pushq %rcx /* pt_regs->ip */ pushq %rax /* pt_regs->orig_ax */ pushq %rdi /* pt_regs->di */ pushq %rsi /* pt_regs->si */ pushq %rdx /* pt_regs->dx */ pushq %rcx tuichu /* pt_regs->cx */ pushq $-ENOSYS /* pt_regs->ax */ pushq %r8 /* pt_regs->r8 */ pushq %r9 /* pt_regs->r9 */ pushq %r10 /* pt_regs->r10 */ pushq %r11 /* pt_regs->r11 */ sub $(6*8), %rsp /* pt_regs->bp, bx, r12-15 not saved */
kernel land -> user land 内核态返回用户态流程:
swapgs
指令恢复用户态GS寄存器
sysretq
或者iretq
恢复到用户空间
Kernel Functions 内核态与用户态的函数有一些区别
printk:类似与printf,但是内容不一定会在终端显示起来,但是会在内核缓冲区里,可以用dmsg
命令查看
copy_from_user:实现了将用户空间的数据传送到内核空间
copy_to_user:实现了将内核空间的数据传送到用户空间
kmalloc:内核态内存分配函数
kfree:内核态内存释放函数
用来改变权限的函数:
int commit_creds(struct cred *new)
struct cred prepare_kernel_cred(struct task_struct daemon)
执行commit_creds(prepare_kernel_cred(0))
即可获得root权限
Expoit Mitigations 内核态与用户态的保护方式有所区别
相同的保护措施:DEP,Canary,ASLR,PIE,RELRO
不同的保护措施:MMAP_MIN_ADDR,KALLSYMS,RANDSTACK,STACKLEAK,SMEP,SMAP
MMAP_MIN_ADDR MMAP_MIN_ADDR保护机制不允许程序分配低内存地址,可以用来防御null pointer dereferences
如果没有这个保护,可以进行如下的攻击行为:
函数指针指针为0,程序可以分配内存到0x000000处。
程序在内存0x000000写入恶意代码。
程序触发kernel BUG()。这里说的BUG()其实是linux kernel中用于拦截内核程序超出预期的行为,属于软件主动汇报异常的一种机制。
内核执行恶意代码。
KALLSYMS /proc/kallsyms
给出内核中所有symbol
的地址,通过grep <function_name> /proc/kallsyms
就可以得到对应函数的地址,我们需要这个信息来写可靠的exploit
,否则需要自己去泄露这个信息。在低版本的内核中所有用户都可读取其中的内容,高版本的内核中缺少权限的用户读取时会返回0。
SMEP 管理模式执行保护,保护内核是其不允许执行用户空间代码。在SMEP保护关闭的情况下,若存在 kernel stack overfolw,可以将内核栈的返回地址覆盖为用户空间的代码片段执行。在开启了SMEP
保护下,当前cpu处于ring 0模式,当返回到用户态执行时会触发页错误。
操作系统是通过CR4寄存器的第20位的值来判断SMEP
是否开启,1开启,0关闭,检查SMEP是否开启
1 cat /proc/cpuinfo | grep smep
可通过mov指令给CR4寄存器赋值从而达到关闭SMEP
的目的,相关的mov指令可以通过ropper,ROPgadget等工具查找
SMAP 管理模式访问保护,禁止内核访问用户空间的数据
KASLR 内核地址空间布局随机化,并不默认开启,需要在内核命令行中添加指定指令。
qemu增加启动参数 -append "kaslr"
即可开启
Privilege Escalation 提取,越狱,就是要以root用户拿到shell,获取root的方式有几种
在内核态调用commit_creds(prepare_kernel_cred(0))
,返回用户态执行起shell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void get_r00t () { commit_creds(prepare_kernel_cred(0 )); } int main (int argc, char *argv) { ... trigger_fp_overwrite(&get_r00t); ... trigger_vuln_fp(); ... system("/bin/sh" ); }
SMEP防预这种类型的攻击的方法是:如果处理器处于ring0
模式,并试图执行有user
数据的内存时,就会触发一个页错误。
也可以修改cred结构体,cred结构体记录了进程的权限,每个进程都有一个cred结构体,保存了进程的权限等信息(uid,gid),如果修改某个进程的cred结构体(uid = gid = 0),就得到了root权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 struct cred { atomic_t usage; #ifdef CONFIG_DEBUG_CREDENTIALS atomic_t subscribers; void *put_addr; unsigned magic; #define CRED_MAGIC 0x43736564 #define CRED_MAGIC_DEAD 0x44656144 #endif kuid_t uid; kgid_t gid; kuid_t suid; kgid_t sgid; kuid_t euid; kgid_t egid; kuid_t fsuid; kgid_t fsgid; unsigned securebits; kernel_cap_t cap_inheritable; kernel_cap_t cap_permitted; kernel_cap_t cap_effective; kernel_cap_t cap_bset; kernel_cap_t cap_ambient; #ifdef CONFIG_KEYS unsigned char jit_keyring; struct key __rcu *session_keyring ; struct key *process_keyring ; struct key *thread_keyring ; struct key *request_key_auth ; #endif #ifdef CONFIG_SECURITY void *security; #endif struct user_struct *user ; struct user_namespace *user_ns ; struct group_info *group_info ; struct rcu_head rcu ; } __randomize_layout;
Build Linux Kernel Source Code 先下载一份Kernel源码,我用的是2.6.32,由于我的机子是ubuntu 16.04,预装的make与gcc版本过高,编译2.6的kernel会失败,所以需要降级
1 2 3 4 5 6 7 8 9 10 11 12 # 4.7 gcc sudo apt install gcc-4.7 g++-4.7 sudo rm /usr/bin/gcc /usr/bin/g++ sudo ln -s /usr/bin/gcc-4.7 /usr/bin/gcc sudo ln -s /usr/bin/g++-4.7 /usr/bin/g++ # 3.80 make wget https://mirrors.tuna.tsinghua.edu.cn/gnu/make/make-3.80.tar.gz tar -xvf make-3.80.tar.gz cd make-3.80/ ./configure make sudo make install
3.80的make生成在源码目录里,稍后需要用这个make文件
修改三处2.6源码文件
arch/x86/vdso/Makefile中第28行的 -m elf_x86_64
改成 -m64
,第72行的-m elf_i386
改成-m32
drivers/net/igbvf/igbvf.h中注释第128行
kernel/timeconst.pl中第373行defined(@val)
改成@val
(可选)关闭canary保护需要编辑源码中的.config
文件349行,注释掉 CONFIG_CC_STACKPROTECTOR=y
这一项
bzImage 安装必备依赖
1 sudo apt-get install build-essential libncurses5-dev
解压后进入源码目录,使用刚安装的make
1 ~/MAKE/make-3.80/make menuconfig
进入kernel hacking
,勾选Kernel debugging
,Compile-time checks and compiler options-->Compile the kernel with debug info,Compile the kernel with frame pointers
和KGDB
,然后开始编译
1 ~/MAKE/make-3.80/make bzImage
大概10分钟的样子,出现这个信息就说明编译成功了
1 2 3 4 Setup is 15036 bytes (padded to 15360 bytes). System is 3754 kB CRC 4505d1c3 Kernel: arch/x86/boot/bzImage is ready (#1)
vmlinux
在源码根目录下,bzImage
在/arch/x86/boot/
里
rootfs.cpio 编译busybox
1 2 3 4 wget https://busybox.net/downloads/busybox-1.27.2.tar.bz2 tar -jxvf busybox-1.27.2.tar.bz2 cd busybox-1.27.2 make menuconfig
勾选Busybox Settings -> Build Options -> Build Busybox as a static binary
编译完成后源码目录下会有一个_install
文件夹,进入
1 2 3 mkdir -pv {bin,sbin,etc,proc,sys,usr/{bin,sbin}} mkdir etc/init.d touch etc/init.d/init
编辑etc/inittab
文件,加入以下内容(貌似这一步可以省略)
1 2 3 4 5 6 ::sysinit:/etc/init.d/rcS ::askfirst:/bin/ash ::ctrlaltdel:/sbin/reboot ::shutdown:/sbin/swapoff -a ::shutdown:/bin/umount -a -r ::restart:/sbin/init
编辑etc/init.d/init
文件,加入以下内容
1 2 3 4 5 6 # !/bin/sh mount -t proc none /proc mount -t sys none /sys /bin/mount -n -t sysfs none /sys /bin/mount -t ramfs none /dev /sbin/mdev -s
接着就可以打包成rootfs.cpio
1 2 chmod +x ./etc/init.d/rcS find . | cpio -o --format=newc > ../rootfs.cpio
boot 得到三个文件后,可以利用qemu运行起来,启动脚本boot.sh
1 2 3 4 5 6 7 8 # !/bin/sh qemu-system-x86_64 \ -initrd rootfs.cpio \ -kernel bzImage \ -nographic \ -append "console=ttyS0 root=/dev/ram rdinit=/sbin/init" \ -m 64M \ -monitor /dev/null \
1 2 / # uname -a Linux (none) 2.6.32 #1 SMP Sun Jan 26 21:51:02 CST 2020 x86_64 GNU/Linux
Run LKM build 简单写一个hello的程序,hello.c内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/proc_fs.h> int hello_write (struct file *file, const char *buf, unsigned long len) { printk("You write something." ); return len; } static int __init hello_init (void ) { printk(KERN_ALERT "hello driver init!\n" ); create_proc_entry("hello" , 0666 , 0 )->write_proc = hello_write; return 0 ; } static void __exit hello_exit (void ) { printk(KERN_ALERT "hello driver exit\n" ); } module_init(hello_init); module_exit(hello_exit);
Makefile内容如下,注意xxx.c与xxx.o文件名一致,KERNELDR目录是内核源代码
1 2 3 4 5 6 7 8 9 obj-m := hello.o KERNELDR := /home/mask/kernel/linux-2.6.32 PWD := $(shell pwd) modules: $(MAKE) -C $(KERNELDR) M=$(PWD) modules modules_install: $(MAKE) -C $(KERNELDR) M=$(PWD) modules_install clean: $(MAKE) -C $(KERNELDR) M=$(PWD) clean
make出来后得到.ko文件
1 2 3 4 5 6 7 8 9 10 11 12 13 ➜ helloworld ls helloc.c helloc.mod.c helloc.o modules.order helloc.ko helloc.mod.o Makefile Module.symvers ➜ helloworld file helloc.ko helloc.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=08aaa94df43f8333c14 9073cddf3043e52b28107, not stripped ➜ helloworld checksec helloc.ko [*] '/home/mask/kernel/test/linux4.4/module/helloworld/helloc.ko' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x0)
再写一个调用程序call.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> int main () { int fd = open("/proc/hello" , O_WRONLY); write(fd, "Mask" , 4 ); return 0 ; }
run 将helloc.ko文件与call文件复制.
进文件系统,也就是busybox目录里的_install文件夹,重新打包rootfs.cpio
,运行起来即可看见模块
1 2 3 4 / # insmod hello.ko [ 11.743066] hello driver init! / # ./call [ 25.860294] You write something.
Reference qemu+gdb 调试 kernel
Linux Kernel Pwn ABC(Ⅰ)
Linux Syscall