Pwn In Kernel基础知识有哪些
导读:本文共10749.5字符,通常情况下阅读需要36分钟。同时您也可以点击右侧朗读,来听本文内容。按键盘←(左) →(右) 方向键可以翻页。
摘要: 简单分析一下 CTF Kernel Pwn 题目的形式,以 2017 CISCN babydrive 为例。先对文件包解压➜examplelsbabydriver.tar➜examplefilebabydriver.tarbabydriver.tar:POSIXtararchive➜exampletar-xvfbabydriver.tarboot.sh... ...
目录
(为您整理了一些要点),点击可以直达。简单分析一下 CTF Kernel Pwn 题目的形式,以 2017 CISCN babydrive 为例。
先对文件包解压
得到 boot.sh,bzImage,rootfs.cpio 三个文件
boot.sh 文件是用来启动这个程序的,调用 qemu 来加载 rootfs.cpio 与 bzImage 运行起来
上面的参数都是 qemu 的参数
bzImage 是经压缩过的 linux 内核文件
这是一个 linux 内核文件系统压缩包,我们可以对其解压并重新压缩,从而修改这个系统的文件
新建一个文件夹来解压
这些就是运行起来后这个系统拥有的文件,查看这个 init 文件
看到第 12 行的 insmod /lib/modules/4.4.72/babydriver.ko,意味着要调试这个 ko 文件,使用 IDA 对其进行分析,利用漏洞
对此文件系统进行打包也是要在这个目录下进行
有些题目会给 vmlinux 这个文件,这是编译出来的最原始的内核文件,未压缩的,是个 ELF 形式,方便找 gadget
可以使用一个工具来从 bzImage 中导出 vmlinux,extract-vmlinux
Kernel Pwn 就是找出内核模块中的漏洞,然后写一个 C 语言程序,放入文件系统中打包,重新运行取来,此时用户一般都是普通用户,运行程序调用此模块的功能利用漏洞,从而提升权限到 root 用户,读取 flag
比赛时一般是上传 C 语言程序的 base64 编码到服务器,然后运行
要对内核模块进行调试,在启动脚本中加入
然后使用 gdb 连接
如果显示 Remote 'g' packet reply is too long 一长串数字,要设置一下架构
要调试内核模块,可以先查看内核加载地址,在/sys/module/中是加载的各个模块的信息
获取 babydrive 模块的加载地址
在 gdb 中载入符号信息,就可以对内核模块进行下断调试
Kernel 是一个程序,是操作系统底层用来管理上层软件发出的各种请求的程序,Kernel 将各种请求转换为指令,交给硬件去处理,简而言之,Kernel 是连接软件与硬件的中间层
Kernel 主要提供两个功能,与硬件交互,提供应用运行环境
在 intel 的 CPU 中,会将 CPU 的权限分为 Ring 0,Ring 1,Ring 2,Ring 3,四个等级,权限依次递减,高权限等级可以调用低权限等级的资源
在常见的系统(Windows,Linux,MacOS)中,内核处于 Ring 0 级别,应用程序处于 Ring 3 级别
内核模块是 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 中运行查看
ioctl 是设备驱动程序中对设备的 I/O 通道进行管理的函数
所谓对 I/O 通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的调用个数如下: int ioctl(int fd, ind cmd, …);
其中 fd 是用户程序打开设备时使用 open 函数返回的文件标示符,cmd 是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,这个参数的有无和 cmd 的意义相关
ioctl 函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对 ioctl 的支持,用户就可以在用户程序中使用 ioctl 函数来控制设备的 I/O 通道。
意思就是说如果一个 LKM 中提供了 iotcl 功能,并且实现了对应指令的操作,那么在用户态中,通过这个驱动程序,我们可以调用 ioctl 来直接调用模块中的操作
在程序运行时,总是会经历 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 执行响应操作
kernel land -> user land
内核态返回用户态流程:
1.swapgs 指令恢复用户态 GS 寄存器
2.sysretq 或者 iretq 恢复到用户空间
内核态与用户态的函数有一些区别
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 权限
内核态与用户态的保护方式有所区别
相同的保护措施:DEP,Canary,ASLR,PIE,RELRO
不同的保护措施:MMAP_MIN_ADDR,KALLSYMS,RANDSTACK,STACKLEAK,SMEP,SMAP
MMAP_MIN_ADDR
MMAP_MIN_ADDR 保护机制不允许程序分配低内存地址,可以用来防御 null pointer dereferences
如果没有这个保护,可以进行如下的攻击行为:
1.函数指针指针为 0,程序可以分配内存到 0x000000 处。
2.程序在内存 0x000000 写入恶意代码。
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 是否开启
可通过 mov 指令给 CR4 寄存器赋值从而达到关闭 SMEP 的目的,相关的 mov 指令可以通过 ropper,ROPgadget 等工具查找
SMAP
管理模式访问保护,禁止内核访问用户空间的数据
KASLR
内核地址空间布局随机化,并不默认开启,需要在内核命令行中添加指定指令。
qemu 增加启动参数 -append "kaslr" 即可开启
提取,越狱,就是要以 root 用户拿到 shell,获取 root 的方式有几种
在内核态调用 commit_creds(prepare_kernel_cred(0)),返回用户态执行起 shell
SMEP 防预这种类型的攻击的方法是:如果处理器处于 ring0 模式,并试图执行有 user 数据的内存时,就会触发一个页错误。
也可以修改 cred 结构体,cred 结构体记录了进程的权限,每个进程都有一个 cred 结构体,保存了进程的权限等信息(uid,gid),如果修改某个进程的 cred 结构体(uid = gid = 0),就得到了 root 权限
先下载一份 Kernel 源码,我用的是 2.6.32,由于我的机子是 ubuntu 16.04,预装的 make 与 gcc 版本过高,编译 2.6 的 kernel 会失败,所以需要降级
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 这一项
安装必备依赖
解压后进入源码目录,使用刚安装的 make
进入 kernel hacking,勾选 Kernel debugging,Compile-time checks and compiler options-->Compile the kernel with debug info,Compile the kernel with frame pointers 和 KGDB,然后开始编译
大概 10 分钟的样子,出现这个信息就说明编译成功了
vmlinux 在源码根目录下,bzImage 在/arch/x86/boot/里
编译 busybox
勾选 Busybox Settings -> Build Options -> Build Busybox as a static binary
编译完成后源码目录下会有一个_install 文件夹,进入
编辑 etc/inittab 文件,加入以下内容(貌似这一步可以省略)
编辑 etc/init.d/init 文件,加入以下内容
接着就可以打包成 rootfs.cpio
得到三个文件后,可以利用 qemu 运行起来,启动脚本 boot.sh
简单写一个 hello 的程序,hello.c 内容如下
Makefile 内容如下,注意 xxx.c 与 xxx.o 文件名一致,KERNELDR 目录是内核源代码
make 出来后得到.ko 文件
再写一个调用程序 call.c
将 helloc.ko 文件与 call 文件复制.
进文件系统,也就是 busybox 目录里的_install 文件夹,重新打包 rootfs.cpio,运行起来即可看见模块
Pwn In Kernel基础知识有哪些的详细内容,希望对您有所帮助,信息来源于网络。