基本信息 查看启动脚本, 开启 kaslr, smep, smap, pti 保护:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # !/bin/sh qemu-system-x86_64 \ -m 128M \ -kernel ./bzImage \ -hda ./rootfs.img \ -append "console=ttyS0 quiet root=/dev/sda rw init=/init oops=panic panic=1 panic_on_warn=1 kaslr pti=on" \ -monitor /dev/null \ -smp cores=2,threads=2 \ -nographic \ -cpu kvm64,+smep,+smap \ -no-reboot \ -snapshot \ -s
题目 readme 中提供了一些内核的编译选项:
1 2 3 4 5 6 CONFIG_SLAB=y CONFIG_SLAB_FREELIST_RANDOM=y CONFIG_SLAB_FREELIST_HARDENED=y CONFIG_HARDENED_USERCOPY=y CONFIG_STATIC_USERMODEHELPER=y CONFIG_STATIC_USERMODEHELPER_PATH=""
可以发现使用 slab 分配器,开启了 HARDENED USERCOPY 保护,同时无法通过修改 modprobe_path 来利用。 查看 init 脚本,对应内核模块 kernote.ko:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # !/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t tmpfs tmpfs /tmp # mount -t devtmpfs devtmpfs /dev mkdir /dev/pts mount -t devpts devpts /dev/pts echo /sbin/mdev>/proc/sys/kernel/hotplug echo 1 > /proc/sys/kernel/dmesg_restrict echo 1 > /proc/sys/kernel/kptr_restrict echo "flag{testflag}">/flag chmod 660 /flag insmod /kernote.ko # /sbin/mdev -s chmod 666 /dev/kernote chmod 777 /tmp setsid cttyhack setuidgid 1000 sh poweroff -f
题目分析 分析提供的 kernote.ko 模块,注册了字符设备 kernote,注册了 ioctl 操作。
0x6666:将 buf 末尾的对象指针赋给 note,这里可能会有 UAF
0x6667:申请对象,flag 为 GFP_KERNEL,size 为 0x8,由于为 slab,其会从 kmalloc-32 中获取对象。
0x6668:释放对象,但是没有清空 note 指针
0x6669:将 note 指向对象的内容写入 arg 值
0x666a:yzloser 说是写着玩的???(源码是 get_current_user() 函数)
漏洞利用 可以利用在 kmalloc-32 内的结构体,但是由于没有读的功能,需要构造有任意读写的结构体。这里官方提供的是 ldt_struct 结构体。
ldt_struct 结构体 ldt 即局部段描述符表(Local Descriptor Table),其中存放着进程的段描述符,段寄存器当中存放着的段选择子便是段描述符表中的段描述符的索引。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct ldt_struct { struct desc_struct *entries ; unsigned int nr_entries; int slot; };
entries 指针为 desc_struct 结构体,即段描述符
1 2 3 4 5 6 7 struct desc_struct { u16 limit0; u16 base0; u16 base1: 8 , type: 4 , s: 1 , dpl: 2 , p: 1 ; u16 limit1: 4 , avl: 1 , l: 1 , d: 1 , g: 1 , base2: 8 ; } __attribute__((packed));
低 32 位
31~16
15~0
段基址的 15~0 位
段界限的 15~0 位
段基址 32 位,段界限为 20 位,其所能够表示的地址范围为:
段基址 + (段粒度大小 x (段界限+1)) - 1
高 32 位
31~24
23
22
21
20
19~16
15
14~13
12
11~8
7~0
段基址的 31~24 位
G
D/B
L
AVL
段界限的 19 ~16 位
P
DPL
S
TYPE
段基址的 23~16 位
各参数便不在此赘叙了,具其构造可以参见全局描述符表(Global Descriptor Table) - arttnba3.cn
modify_ldt 系统调用 通过 modify_ldt 可以获取或修改当前进程的 ldt
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 SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr , unsigned long , bytecount) { int ret = -ENOSYS; switch (func) { case 0 : ret = read_ldt(ptr, bytecount); break ; case 1 : ret = write_ldt(ptr, bytecount, 1 ); break ; case 2 : ret = read_default_ldt(ptr, bytecount); break ; case 0x11 : ret = write_ldt(ptr, bytecount, 0 ); break ; } return (unsigned int )ret; }
系统调用应传入三个参数:func, ptr, bytecount ,ptr为指向 user_desc 结构体的指针。
1 2 3 4 5 6 7 8 9 10 11 struct user_desc { unsigned int entry_number; unsigned int base_addr; unsigned int limit; unsigned int seg_32bit:1 ; unsigned int contents:2 ; unsigned int read_exec_only:1 ; unsigned int limit_in_pages:1 ; unsigned int seg_not_present:1 ; unsigned int useable:1 ; };
read_ldt:内核任意地址读 1 2 3 4 5 6 7 8 9 10 11 12 static int read_ldt (void __user *ptr, unsigned long bytecount) { if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) { retval = -EFAULT; goto out_unlock; } out_unlock: up_read(&mm->context.ldt_usr_sem); return retval; }
调用 copy_to_user 向用户地址拷贝数据,如果可以控制 ldt->entries 即可完成任意地址读。
write_ldt:分配新 ldt_struct 结构体 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 static int write_ldt (void __user *ptr, unsigned long bytecount, int oldmode) { error = -EINVAL; if (bytecount != sizeof (ldt_info)) goto out; error = -EFAULT; if (copy_from_user(&ldt_info, ptr, sizeof (ldt_info))) goto out; error = -EINVAL; if (ldt_info.entry_number >= LDT_ENTRIES) goto out; old_ldt = mm->context.ldt; old_nr_entries = old_ldt ? old_ldt->nr_entries : 0 ; new_nr_entries = max(ldt_info.entry_number + 1 , old_nr_entries); error = -ENOMEM; new_ldt = alloc_ldt_struct(new_nr_entries); if (!new_ldt) goto out_unlock; if (old_ldt) memcpy (new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE); new_ldt->entries[ldt_info.entry_number] = ldt; install_ldt(mm, new_ldt); unmap_ldt_struct(mm, old_ldt); free_ldt_struct(old_ldt); error = 0 ; out_unlock: up_write(&mm->context.ldt_usr_sem); out: return error; }
会调用 alloc_ldt_struct 函数来分配新的 ldt_struct 结构体并 copy 原数据到新的结构体内。 alloc_ldt_struct 函数如下,使用 kmalloc 分配,flag 为 GFP_KERNEL:
1 2 3 4 5 6 7 8 9 10 11 static struct ldt_struct *alloc_ldt_struct (unsigned int num_entries) { struct ldt_struct *new_ldt ; unsigned int alloc_size; if (num_entries > LDT_ENTRIES) return NULL ; new_ldt = kmalloc(sizeof (struct ldt_struct), GFP_KERNEL);
因此解题思路如下:
构造 UAF
通过 write_ldt 申请回该对象为 ldt_struct
通过 note 修改 ldt_struct->entries 来实现任意地址读
通过 read_ldt 搜索内核地址空间。
解法1:遍历内存修改进程 cred 提权 Step1. 泄漏 page_offset_base 由于开启 kaslr, 需要泄漏相关地址,这里我们选择直接爆破内核地址:对于无效地址,copy_to_user 会返回非0值,此时 read_ldt 会返回 -EFAULT,当执行成功时,说明命中内核空间。 不过该题开启了 HARDENED USERCOPY 保护,当 copy_to_user 源地址为内核 .text 段时会引起 kernel panic。因此这里选择直接搜索 线性映射区。也是 task_struct 所在的区域。只需要修改本进程的 task_struct ,更改 cred 的 uid 为0,即可实现提权。
kmalloc 会从 线性映射区中分配,但是 vmalloc 不会,其从 vmalloc/ioremap space 中分配内存,起始地址为 vmalloc_base,这一块区域与物理地址的映射不连续。
Step2. 泄漏进程 task_struct 地址 task_struct 源码如下,comm 字段为进程名字,因此可以通过搜索该字符串来定位进程的 task_struct 所在位置(可以通过 prctl 系统调用来修改 当前进程的 comm 字段):
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 struct task_struct { const struct cred __rcu *ptracer_cred ; const struct cred __rcu *real_cred ; const struct cred __rcu *cred ; #ifdef CONFIG_KEYS struct key *cached_requested_key ; #endif char comm[TASK_COMM_LEN]; struct nameidata *nameidata ; };
这里不能直接搜索整个线性映射区,因为可能会触发 hardened usercopy 检查。 这里使用官方提供的解法,利用 fork 函数的 clone ldt 的特性来绕过该检查。
1 2 3 4 5 6 7 8 sys_fork () kernel_clone () copy_process () copy_mm () dup_mm () dup_mmap () arch_dup_mmap () ldt_dup_context ()
ldt_dup_context 函数定义中使用 memcpy 拷贝父进程的 ldt->entries 到子进程。(这里有一个小 trick 在,本来是 篡改 的 entries 指针来搜索,memcpy 后,就是在合法的 entries 中查看了)
Step3. Double Fetch 修改 cred 结构体 在 write_ldt 中,会通过 memcpy 进行拷贝,拷贝大小为 old_nr_entries * LDT_ENTRY_SIZE,
1 2 3 4 #define LDT_ENTRIES 8192 #define LDT_ENTRY_SIZE 8
需要在 alloc 后 memcpy 前进行条件竞争,将 new_ldt->entries 修改为目标地址来实现任意地址写。 当然,因为分配的是 kmalloc-32 ,也可以通过 seq_operations 进行利用。
解法2:使用 pt_regs + seq_operations 利用
劫持 ldt_struct 爆破 page_offset_base
进而泄露内核代码段基址( page_offset_base + 0x9d000)
劫持 seq_operations + pt_regs
参考链接