想要成为 heap 高手(3):unlink

从 unlink 的触发位置、双向链表摘除过程到现代 glibc 的完整性检查。

  • malloc
    • 从恰好大小合适的 large bin 中获取 chunk。
      • 这里需要注意的是 fastbin 与 small bin 就没有使用 unlink,这就是为什么漏洞会经常出现在它们这里的原因。
      • 依次遍历处理 unsorted bin 时也没有使用 unlink 。
    • 从比请求的 chunk 所在的 bin 大的 bin 中取 chunk。
  • free
    • 后向合并,合并物理相邻低地址空闲 chunk。
    • 前向合并,合并物理相邻高地址空闲 chunk(除了 top chunk)。
  • malloc_consolidate
    • 后向合并,合并物理相邻低地址空闲 chunk。
    • 前向合并,合并物理相邻高地址空闲 chunk(除了 top chunk)。
  • realloc
    • 前向扩展,合并物理相邻高地址空闲 chunk(除了 top chunk)。

small bin unlink过程

一、unlink 前

        BK                  P                  FD
+---------------+   +---------------+   +---------------+
| prev_size     |   | prev_size     |   | prev_size     |
| size          |   | size          |   | size          |
| fd ---------- |-->| fd ---------- |-->| fd            |
| bk            |<--| bk            |<--| bk ---------- |
| unused bytes  |   | unused bytes  |   | unused bytes  |
+---------------+   +---------------+   +---------------+

链表关系:

        fd              fd
BK ------------> P ------------> FD

        bk              bk
BK <------------ P <------------ FD


此时:

P->fd = FD
P->bk = BK
二、取出 P 的前后节点

FD = P->fd
BK = P->bk


        BK                  P                  FD
+---------------+   +---------------+   +---------------+
| prev_size     |   | prev_size     |   | prev_size     |
| size          |   | size          |   | size          |
| fd ---------- |-->| fd ---------- |-->| fd            |
| bk            |<--| bk            |<--| bk ---------- |
| unused bytes  |   | unused bytes  |   | unused bytes  |
+---------------+   +---------------+   +---------------+

也就是:

FD 指向 P 后面的 chunk
BK 指向 P 前面的 chunk
三、执行 FD->bk = BK

把 FD 的 bk 指针改为 BK:

        BK                  P                  FD
+---------------+   +---------------+   +---------------+
| prev_size     |   | prev_size     |   | prev_size     |
| size          |   | size          |   | size          |
| fd ---------- |-->| fd ---------- |-->| fd            |
| bk            |<--| bk            |   | bk ---------- |
| unused bytes  |   | unused bytes  |   | unused bytes |
+---------------+   +---------------+   +-------|-------+
                                                    |
                                                    |
                                                    v
                                                   BK

链表关系变成:

BK ------------> P ------------> FD
^                                |
|                                |
+------------- FD->bk = BK ------+

最经典的场景是 堆溢出覆盖后一个 chunk 的 chunk header

假设有两个相邻 chunk:

chunk A | chunk B

其中 chunk A 存在溢出,可以覆盖 chunk B 的头部。

攻击者伪造:

B->prev_size
B->size
fake_chunk->fd
fake_chunk->bk

然后让 glibc 误以为:

B 前面的 chunk 是 free 状态

也就是清掉 B->size 里的 PREV_INUSE 位。

当程序执行:

free(B);

glibc 会检查到:

B 的前一个 chunk 是空闲的

于是它会向前合并:

prev_chunk + B

而在合并前,glibc 需要把前一个 free chunk 从 bin 链表中摘出来:

unlink(prev_chunk);

如果这个 prev_chunk 是攻击者伪造的 fake chunk,那么就会触发 unsafe unlink。

glibc 在 free 合并前后相邻空闲 chunk 时确实会调用 unlink_chunk,例如向后找到前一个 chunk 后执行 unlink,以及向前合并 next chunk 时也会 unlink。

检查一:size 与 prev_size 是否匹配

现代 glibc 会检查:

chunksize(P) == prev_size(next_chunk(P))

也就是:

当前 chunk P 的 size
必须等于
下一个物理相邻 chunk 的 prev_size

图示:

        P                         next_chunk(P)
+---------------+          +----------------------+
| prev_size     |          | prev_size            |
| size = 0x100  |--------->| should be 0x100      |
| fd            |          | size                 |
| bk            |          | ...                  |
+---------------+          +----------------------+

检查二:双向链表完整性检查

会检查:

FD = P->fd;
BK = P->bk;

if (FD->bk != P || BK->fd != P)
    malloc_printerr("corrupted double-linked list");

也就是说,glibc 要确认:

P->fd->bk == P
P->bk->fd == P

图示:

正常链表:

BK <-------> P <-------> FD

必须满足:

FD->bk == P
BK->fd == P

检查三:large bin 的 nextsize 检查

如果是 large bin,除了普通的:

fd / bk

还有:

fd_nextsize / bk_nextsize

所以 large bin unlink 还要检查:

P->fd_nextsize->bk_nextsize == P
P->bk_nextsize->fd_nextsize == P

评论