unlink

Unlink

释放堆

释放堆时会检查相邻地址的chunk是否处于空闲状态,若是就会合并这两个chunk。合并时会进行unlink操作,将相邻地址的chunk进行unlink解链操作从bins中拿出来。

堆合并分为向前合并和向后合并。

  • 向后合并指的是在释放P时和他的pre_chunk合并(也就是相邻小地址的chunk)
  • 向前合并指的是在释放P时和他的next_chunk合并(也就是相邻大地址的chunk)

Unlink的流程

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
#define unlink(AV, P, BK, FD) {
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr ("corrupted double-linked list");
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (chunksize_nomask (P))
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr ("corrupted double-linked list (not small)");
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
} }
}
}

(代码来源于这里)
首先会检查当前chunk的大小是否和相邻next_chunk的pre_size的大小进行比较。然后会检查FD->bk == P || BK->fd == P

因此想要执行unlink攻击,需要设置fake free chunk的size字段和相邻next_chunk的pre_size大小相同。并且需要设置P->fd = ptr - 0x18 P->bk = ptr - 0x10来绕过第二个检查。

1
2
FD = P->fd = ptr - 0x18
BK = P->bk = ptr - 0x10

因此

1
2
BK->fd = ptr - 0x10 + 0x10 = ptr
FD->bk = ptr - 0x18 + 0x18 = ptr

可绕过检查。
解链后,FD->bk = BK*ptr = ptr - 0x10, BK->fd=FD*ptr = ptr - 0x18

unlink 后,对 ptr 指向的内存进行写入,如 ‘A’*0x18 + free@got, 使得 ptr 指向 free@got, 再次对 ptr 指向的内存进行写入,可以把 free@got 修改为 system 的地址,之后调用 free 可任意命令执行。

例子

这里用到的例子是how2heap中的unsafe_unlink

首先gcc unsafe_unlink.c -o unsafe_unlink -g编译,然后gdb调试。
chunk0_ptr是全局指针变量,在这里是为了伪造和chunk1_ptr相邻且已经free的chunk,因此在free(chunk1_ptr)时就会触发fake chunk的unlink操作。


这段代码是为了伪造fake chunk,绕过上面提到的检查。


这段代码是为了修改chunk1_ptr的chunk头部来使chunk1的prev_size字段值等于fake chunk的大小并且prev_inuse字段为0来使fake chunk为空闲状态。

我们用gdb调试,可以看到chunk0和chunk1两个chunk

然后填充chunk0中的fake chunk

然后伪造chunk1的prev_size位和prev_inuse位

之后free(chunk1_ptr)就会使fake chunk触发unlink操作,使得chunk0_ptr指针指向chunk0_ptr[2],也就是&chunk0_ptr - 0x18

此时让chunk0_ptr[3] = victim_string,也就是
(chunk0_ptr + 0x18) = *(&chunk0_ptr - 0x18 + 0x18) = chunk0_ptr = victim_string

可能画个图好理解一点

Tips

在这里说一下为什么chunk0_ptr[2]要填&chunk0_ptr-0x18,前面为什么要加取址符?
是因为在检查时,会检查FD->bk == P,即*((p->fd)+0x18)==p,也就是FD指针+0x18的位置所填的内容应该是p,如果chunk0_ptr[2]也就是p->fd填的是p-0x18的话,左边=p!=p=右边,所以这里需要填的应该是&chunk0_ptr-0x18

参考

Unlink


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!