基本信息

查看启动脚本, 开启 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 {
/*
* Xen requires page-aligned LDTs with special permissions. This is
* needed to prevent us from installing evil descriptors such as
* call gates. On native, we could merge the ldt_struct and LDT
* allocations, but it's not worth trying to optimize.
*/
struct desc_struct *entries;
unsigned int nr_entries;

/*
* If PTI is in use, then the entries array is not mapped while we're
* in user mode. The whole array will be aliased at the addressed
* given by ldt_slot_va(slot). We use two slots so that we can allocate
* and map, and enable a new LDT without invalidating the mapping
* of an older, still-in-use LDT.
*
* slot will be -1 if this LDT doesn't have an alias mapping.
*/
int slot;
};

entries 指针为 desc_struct 结构体,即段描述符

1
2
3
4
5
6
7
/* 8 byte segment descriptor */
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;
}
/*
* The SYSCALL_DEFINE() macros give us an 'unsigned long'
* return type, but tht ABI for sys_modify_ldt() expects
* 'int'. This cast gives us an int-sized value in %rax
* for the return code. The 'unsigned' is necessary so
* the compiler does not try to sign-extend the negative
* return codes into the high half of the register when
* taking the value from int->long.
*/
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
/* The caller must call finalize_ldt_struct on the result. LDT starts zeroed. */
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 {

//...

/* Process credentials: */

/* Tracer's credentials at attach: */
const struct cred __rcu *ptracer_cred;

/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;

/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;

#ifdef CONFIG_KEYS
/* Cached requested key. */
struct key *cached_requested_key;
#endif

/*
* executable name, excluding path.
*
* - normally initialized setup_new_exec()
* - access it with [gs]et_task_comm()
* - lock it with task_lock()
*/
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
/* Maximum number of LDT entries supported. */
#define LDT_ENTRIES 8192
/* The size of each LDT entry. */
#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

参考链接