Pwn In Kernel(一):基础知识
source link: https://www.freebuf.com/articles/system/227357.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Kernel Pwn In CTF
简单分析一下 CTF Kernel Pwn 题目的形式,以 2017 CISCN babydrive 为例。
先对文件包解压
➜ 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
➜ 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 的参数
-initrd rootfs.cpio,使用 rootfs.cpio 作为内核启动的文件系统 -kernel bzImage,使用 bzImage 作为 kernel 映像 -cpu kvm64,+smep,设置 CPU 的安全选项,这里开启了 smep -m 64M,设置虚拟 RAM 为 64M,默认为 128M
bzImage
➜ 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
➜ example file rootfs.cpio rootfs.cpio: gzip compressed data, last modified: Tue Jul 4 08:39:15 2017, max compression, from Unix
这是一个 linux 内核文件系统压缩包,我们可以对其解压并重新压缩,从而修改这个系统的文件
新建一个文件夹来解压
➜ 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 文件
➜ 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 对其进行分析,利用漏洞
对此文件系统进行打包也是要在这个目录下进行
➜ 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
➜ 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
/ $ 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 tcp::1234
然后使用 gdb 连接
gdb -q -ex "target remote localhost:1234"
如果显示 Remote ‘g’ packet reply is too long 一长串数字,要设置一下架构
gdb -q -ex "set architecture i386:x86-64:intel" -ex "target remote localhost:1234"
要调试内核模块,可以先查看内核加载地址,在/sys/module/中是加载的各个模块的信息
/ $ 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 模块的加载地址
/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 中载入符号信息,就可以对内核模块进行下断调试
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 中运行查看
➜ ~ 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
当用户态程序执行系统调用,异常处理,外设终端时,会从用户态切换到内核态,切换过程如下:
1.swapgs 指令修改 GS 寄存器切换到内核态 2.将当前栈顶(sp)记录在 CPU 独占变量区域,然后将此区域里的内核栈顶赋给 sp 3.push 各寄存器的值 4.通过汇编指令判断是否为 32 位 5.通过系统调用号,利用函数表 sys_call_table 执行响应操作
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
内核态返回用户态流程:
1.swapgs 指令恢复用户态 GS 寄存器 2.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
如果没有这个保护,可以进行如下的攻击行为:
1.函数指针指针为 0,程序可以分配内存到 0×000000 处。 2.程序在内存 0×000000 写入恶意代码。 3.程序触发 kernel BUG()。这里说的 BUG() 其实是 linux kernel 中用于拦截内核程序超出预期的行为,属于软件主动汇报异常的一种机制。 4.内核执行恶意代码。
KALLSYMS
/proc/kallsyms 给出内核中所有 symbol 的地址,通过 grep /proc/kallsyms 就可以得到对应函数的地址,我们需要这个信息来写可靠的 exploit,否则需要自己去泄露这个信息。在低版本的内核中所有用户都可读取其中的内容,高版本的内核中缺少权限的用户读取时会返回 0。
SMEP
管理模式执行保护,保护内核是其不允许执行用户空间代码。在 SMEP 保护关闭的情况下,若存在 kernel stack overfolw,可以将内核栈的返回地址覆盖为用户空间的代码片段执行。在开启了 SMEP 保护下,当前 cpu 处于 ring 0 模式,当返回到用户态执行时会触发页错误。
操作系统是通过 CR4 寄存器的第 20 位的值来判断 SMEP 是否开启,1 开启,0 关闭,检查 SMEP 是否开启
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
void get_r00t() { commit_creds(prepare_kernel_cred(0)); } int main(int argc, char *argv) { ... trigger_fp_overwrite(&get_r00t); ... // trigger fp use trigger_vuln_fp(); // Kernel Executes get_r00t() ... // Now we have root system("/bin/sh"); }
SMEP 防预这种类型的攻击的方法是:如果处理器处于 ring0 模式,并试图执行有 user 数据的内存时,就会触发一个页错误。
也可以修改 cred 结构体,cred 结构体记录了进程的权限,每个进程都有一个 cred 结构体,保存了进程的权限等信息(uid,gid),如果修改某个进程的 cred 结构体(uid = gid = 0),就得到了 root 权限
struct cred { atomic_t usage; #ifdef CONFIG_DEBUG_CREDENTIALS atomic_t subscribers; /* number of processes subscribed */ void *put_addr; unsigned magic; #define CRED_MAGIC 0x43736564 #define CRED_MAGIC_DEAD 0x44656144 #endif kuid_t uid; /* real UID of the task */ kgid_t gid; /* real GID of the task */ kuid_t suid; /* saved UID of the task */ kgid_t sgid; /* saved GID of the task */ kuid_t euid; /* effective UID of the task */ kgid_t egid; /* effective GID of the task */ kuid_t fsuid; /* UID for VFS ops */ kgid_t fsgid; /* GID for VFS ops */ unsigned securebits; /* SUID-less security management */ kernel_cap_t cap_inheritable; /* caps our children can inherit */ kernel_cap_t cap_permitted; /* caps we're permitted */ kernel_cap_t cap_effective; /* caps we can actually use */ kernel_cap_t cap_bset; /* capability bounding set */ kernel_cap_t cap_ambient; /* Ambient capability set */ #ifdef CONFIG_KEYS unsigned char jit_keyring; /* default keyring to attach requested * keys to */ struct key __rcu *session_keyring; /* keyring inherited over fork */ struct key *process_keyring; /* keyring private to this process */ struct key *thread_keyring; /* keyring private to this thread */ struct key *request_key_auth; /* assumed request_key authority */ #endif #ifdef CONFIG_SECURITY void *security; /* subjective LSM security */ #endif struct user_struct *user; /* real user ID subscription */ struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */ struct group_info *group_info; /* supplementary groups for euid/fsgid */ struct rcu_head rcu; /* RCU deletion hook */ } __randomize_layout;
Build Linux Kernel
Source Code
先下载一份 Kernel 源码,我用的是 2.6.32,由于我的机子是 ubuntu 16.04,预装的 make 与 gcc 版本过高,编译 2.6 的 kernel 会失败,所以需要降级
# 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 源码文件
1.arch/x86/vdso/Makefile 中第 28 行的 -m elf_x86_64 改成 -m64,第 72 行的-m elf_i386 改成-m32 2.drivers/net/igbvf/igbvf.h 中注释第 128 行 3.kernel/timeconst.pl 中第 373 行 defined(@val) 改成 @val 4.(可选)关闭 canary 保护需要编辑源码中的.config 文件 349 行,注释掉 CONFIG_CC_STACKPROTECTOR=y 这一项
bzImage
安装必备依赖
sudo apt-get install build-essential libncurses5-dev
解压后进入源码目录,使用刚安装的 make
~/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,然后开始编译
~/MAKE/make-3.80/make bzImage
大概 10 分钟的样子,出现这个信息就说明编译成功了
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
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
make install
编译完成后源码目录下会有一个_install 文件夹,进入
mkdir -pv {bin,sbin,etc,proc,sys,usr/{bin,sbin}} mkdir etc/init.d touch etc/init.d/init
编辑 etc/inittab 文件,加入以下内容(貌似这一步可以省略)
::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 文件,加入以下内容
#!/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
chmod +x ./etc/init.d/rcS find . | cpio -o --format=newc > ../rootfs.cpio
boot
得到三个文件后,可以利用 qemu 运行起来,启动脚本 boot.sh
#!/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 \
/ # 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 内容如下
#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 目录是内核源代码
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 文件
➜ 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
#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,运行起来即可看见模块
/ # insmod hello.ko [ 11.743066] hello driver init! / # ./call [ 25.860294] You write something.
Reference
*本文作者:Mask6asok,转载请注明来自FreeBuf.COM
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK