popping_caps
Description:
350You 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 | $ checksec popping_caps |
1 | $ file popping_caps |
Running and testing the binary:
1 | $ ./popping_caps |
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:
- Malloc with size of 0x3a8.
- free it to increase the counter of that tcachebin(0x100) to 1 creating a fake chunk of size 0x100.
- Free this fake chunk by using the negative index of -0x210 (House of spirit).
- Malloc with size of 0xf8 will return the pointer at the first tcache entry (size 0x20).
- Edit the first entry with the pointer of malloc_hook.
- Malloc with size of 0x20 the pointer returned will be malloc_hook.
- 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 fastbin1
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 |
|
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 | pwndbg> p tcache |
We need to do it like this:
1 | pwndbg> p *(struct tcache_perthread_struct *)0x55f21c0fd000 |
Showing it as hex dump:
1 | pwndbg> x/40gx 0x55f21c0fd000 |
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 | from pwn import * |