Pwn Ptmalloc2 Unsortbin Leak Libc

如未特殊说明, 均假定 libc 2.23, 64 位.

Unsortbin 也是一种类似缓存的机制, 目的在于快速取出 freed chunk. Unsortbin 的机制比较复杂, 这里仅介绍与 leak libc 有关的部分.

Unsortbin 就是 malloc state 中的 bins[1], 相关定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct malloc_state
{
  /* Normal bins packed as described above */
  mchunkptr bins[NBINS * 2 - 2];
};
/* The otherwise unindexable 1-bin is used to hold unsorted chunks. */
#define unsorted_chunks(M)          (bin_at (M, 1))
/* addressing -- note that bin_at(0) does not exist */
#define bin_at(m, i) \
  (mbinptr) (((char *) &((m)->bins[((i) - 1) * 2]))           \
             - offsetof (struct malloc_chunk, fd))

Unsortbin 是一个双向链表结构. 其中的 freed chunk 由 fd, bk 指针双向链接.

只看和数据结构相关的部分, free 是在双向链表的头部插入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
  ...
      /*
  Place the chunk in unsorted chunk list. Chunks are
  not placed into regular bins until after they have
  been given one chance to be used in malloc.
      */
      bck = unsorted_chunks(av);
      fwd = bck->fd;
      ...
      p->fd = fwd;
      p->bk = bck;
      ...
      bck->fd = p;
      fwd->bk = p;
      ...
}

malloc 关于 unsortbin 部分比较复杂, 单纯 leak libc 的构造应该不会涉及到. 简单用文字说明一下, 就不看源码了.

malloc 一个比 fast chunk 大的块时, 会先去 smallbin 里找. 如果没有 恰好相等 的 chunk, 就会调用去访问 fastbin, 将能够合并的 chunk 尽量合并, 然后把所有 chunk 放入 unsortbin 中. 然后开始从后往前遍历 unsortbin.

如果是请求的块大小是 small chunk, 并且上一个释放的块 (last_remainder), 正好是 unsortbin 中唯一的一块, 那么尝试分割这个 chunk, 剩下的部分放回 unsortbin 中. 返回这个块.

否则, 看当前遍历到的这个块大小是不是刚好满足, 是的话就返回, 不是的话就把这块放到对应的 bin 中去.

遍历完了以后还不满足, 再进行 largebin, top_chunk 等处理.

初始时, unsortbin 的 fd 和 bk 都指向自己. 所以, 如果我们能够泄漏双向链 表头部节点的 bk 指针, 或者 尾部节点的 fd 指针, 那么就能够泄漏出 bins[1] 的地址了. 而在 main arena 中, bins 是存在 libc 的数据段的. 所以, 相当于泄漏出了 libc 地址.

特别地, 如果 unsortbin 中只有一个节点, 那么它的 fd 和 bk 指针都可以泄漏 libc.

虽然有很多种情况, 都能往 unsortbin 中存放 chunk, 但是对利用来说, 最好就是直接把一个 smalll chunk free 掉, 这样就到了 unsortbin 中, 然后 leak fd 或者 bk.

(前半段是 unsortbin leak libc, 后半段是 malloc malloc_hook nearby)

64 位 ELF, 保护全开. 菜单题, Allocate, Fill, Free, Dump.

main:

 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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  Node *heap; // [rsp+8h] [rbp-8h]

  heap = (Node *)Init();
  while ( 1 )
  {
    menu();
    switch ( readll() )
    {
      case 1LL:
        Allocate(heap);
        break;
      case 2LL:
        Fill(heap);
        break;
      case 3LL:
        Free(heap);
        break;
      case 4LL:
        Dump(heap);
        break;
      case 5LL:
        return 0LL;
      default:
        continue;
    }
  }
}

Init 是一些 IO 初始化操作, 并 mmap 了一个足够大的地址并返回.

Allocate:

 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
void __fastcall Allocate(Node *a1)
{
  int i; // [rsp+10h] [rbp-10h]
  int size; // [rsp+14h] [rbp-Ch]
  char *str; // [rsp+18h] [rbp-8h]

  for ( i = 0; i <= 15; ++i )
  {
    if ( !a1[i].inuse )
    {
      printf("Size: ");
      size = readll();
      if ( size > 0 )
      {
        if ( size > 4096 )
          size = 4096;
        str = (char *)calloc(size, 1uLL);
        if ( !str )
          exit(-1);
        a1[i].inuse = 1;
        a1[i].size = size;
        a1[i].str = str;
        printf("Allocate Index %d\n", (unsigned int)i);
      }
      return;
    }
  }
}

可以看到, 最多可以分配 16 个堆, 堆大小最大是 4096. 且用的是 calloc, 会将用户数据部分清空.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void __fastcall Fill(Node *a1)
{
  int i; // [rsp+18h] [rbp-8h]
  int size; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  i = readll();
  if ( i >= 0 && i <= 15 && a1[i].inuse == 1 )
  {
    printf("Size: ");
    size = readll();
    if ( size > 0 )
    {
      printf("Content: ");
      my_read(a1[i].str, size);
    }
  }
}

检测 inuse, 为 1 表示该位置被分配, 即 str 有效. 但是, 这里输入的 size 没有和之前 calloc 的进行比较, 所以可以产生 堆溢出漏洞. my_read 没有漏洞, 不放代码了.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void __fastcall Free(Node *a1)
{
  int i; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  i = readll();
  if ( i >= 0 && i <= 15 && a1[i].inuse == 1 )
  {
    a1[i].inuse = 0;
    a1[i].size = 0LL;
    free(a1[i].str);
    a1[i].str = 0LL;
  }
}

全清空.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void __fastcall Dump(Node *a1)
{
  int i; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  i = readll();
  if ( i >= 0 && i <= 15 && a1[i].inuse == 1 )
  {
    puts("Content: ");
    my_puts(a1[i].str, a1[i].size);
    puts("");
  }
}

my_puts 也没漏洞, 不放了.

程序主要就是堆溢出漏洞.

由于保护全开, 所以想办法修改 hook. 那就必须要先泄漏 libc.

由于在 unsortbin 中的 chunk 的 fd / bk 保存了 bins[1] 的值, 同时有打印堆块功能. 于是可以尝试利用溢出, 覆盖 freed chunk 的 fd, 再 malloc, 造成堆重叠. Fastbin attack 比较好实现堆重叠. 但是, 我们无法知道堆块的地址. 不过, 由于初始化时, 堆其实地址要进行页对齐 (0x1000), 所以, 我们其实是知道每个堆块 低 12 位 的值, 或者说, 相对位置.

那么可以这样想, 假如我 free 了两个大小相同的 fast chunk, 那么头节点的 fd 就已经指向了堆处了. 然后可以通过溢出, 覆盖 fd 的低 8 位, 从而使其指向其他堆的其他位置. 如果这个位置正好是另一个堆的位置, 那么我们就成功造成了堆重叠.

于是, 构造如下左图这样的堆. 其中, chunk 0, 1, 2, 3 都是 fast chunk, chunk 4 是 small chunk, 也就是我们尝试 free 掉然后去泄漏 libc 的地方. 由于在 unsortbin 中的 chunk free 时会尝试合并到 top chunk, 所以这里还需要一个堆 chunk 5 块顶在上面, 才能确保 chunk 4 被放入到 unsortbin 中. 这里我们先 free 掉 chunk 4.

0 0 0 0 0 0 x x x x x x 1 0 0 0 0 0 1 8 6 4 2 0 0 0 0 0 0 0 p s r b f s p s p s p s p s p i e k d i r i r i r i r i r z s z e z e z e z e z e e i & & e s e s e s e s e s z b b i i i i i 0 e i i 0 z 0 z 0 z 0 z 0 z x n n x e x e x e x e x e 2 0 s s 9 2 2 2 2 0 x [ [ 1 0 1 0 1 0 1 0 1 0 9 1 1 0 ] ] 0 ( P R E I c c c c c c N h h h h h h U u u u u u u S n n n n n n E k k k k k k 0 5 4 3 2 1 0 ) 0 0 0 0 0 0 x x x x x x 1 0 0 0 0 0 1 8 6 4 2 0 0 0 0 0 0 0 p s r b f s p s p s p f s p s p i e k d i r i r i r d i r i r z s z e z e b f z e b z e z e e i & & e s e s k d e s k 0 e s e s z b b i i i x i i 0 e i i 0 z 0 z 0 0 0 z 0 X 0 z 0 z x n n x e x e x e 0 x e x e 2 0 s s 9 2 2 4 2 2 0 x [ [ 1 0 1 0 1 0 0 1 0 1 0 9 1 1 0 ] ] 0 ( P R E I c c c c c c N h h h h h h U u u u u u u S n n n n n n E k k k k k k 0 5 4 3 2 1 0 ) 0 0 0 0 0 0 x x x x x x 1 0 0 0 0 0 1 8 6 4 2 0 0 0 0 0 0 0 p s r b f s p s p s p f s p s p i e k d i r i r i r d i r i r z s z e z e b f z e b z e z e e i & & e s e s k d e s k 0 e s e s z b b i i i x i i 0 e i i 0 z 0 z 0 0 0 z 0 X 0 z 0 z x n n x e x e x e 0 x e x e 2 0 s s 9 2 2 4 2 2 0 x [ [ 1 0 1 0 1 0 0 1 0 1 0 9 1 1 0 ] ] 0 ( P R E I c c c c c c N h h h h h h U u u u u u u S n n n n n n E k k k k k k 0 5 4 3 2 1 0 )

首先, 需要构造一个指向堆某处的 fd. 可以先后 free 掉 chunk 2, 1. 这样根据 fastbin 的机制, chunk 1 的 fd 指针就会指向 chunk 2 处, 上中图所示. 然后对 chunk 0 进行 Fill 操作, 覆盖 chunk 1 的最低位字节为 0x80, 这样就使其指向了 chunk 4. 如上右图.

接下来, 如果想要 malloc 到 chunk 4, 还需要改一下 chunk 4 的 size 以绕过检测. 对 chunk 3 进行 Fill 操作, 覆盖 size 为 0x21 即可. 然后申请两个 0x20 的堆块, 第二个就是 chunk 4 所在位置了. 打印 Chunk 2, 就能够得到 libc 上的 &bins[1] 地址了.

后半段在这里