For this lab, we are given a program and its corresponding source code:
If we run checksec, we can see that NX, PIE and full RELRO are enabled.
The program creates a strings array of 6 pointers to data structs.
Each element in the strings array is a pointer to a data struct.
Similarly, an array of 6 pointers to number structs is created called numbers.
If we examine each struct definition, we notice that they are both 32-bytes large. (chars each take up 1 byte, unsigned ints each take up 4 bytes)
This is interesting because this means that the memory allocater will likely reallocate chunks for number structs from freed data structs and vice-a-versa.
This kind of condition is usually a precursor to use-after-free scenarios, in which an object is freed and then a different type of object is allocated over it, but a reference to the original object still exists.
In order to defeat ASLR, we need to look for an infoleak that we can use.
We can get an infoleak if we allocate a number struct, free it, allocate a data struct over it with a small string, and print the freed number.
The number that is printed out should be the address stored in the data struct’s void (* print)(char *); member, which, because we used a small string, should be the address of small_str().
^ 0xb93da018 holds the address of small_str(), which the number struct thinks is its num member!
With the address of this function in the .text section, we can use a known offset to calculate the address of system() in libc. (Note: This is actually a flaw with Ubuntu. When PIE is enabled, the distance between the base address of the ELF executable and the base address of libc should change with each run. However, Ubuntu’s ASLR sucks which allows us to calculate the base address of libc from just the base address of the ELF executable)
Once we’ve leaked the address of system(), we need to call it while passing in /bin/sh as its argument. We can do this by again taking advantage of the use-after-free vulnerability.
To recap, so far we have a data chunk sitting on top of what used to be a number chunk. From here, in order to get code execution, we can overwrite this data chunk’s void (* print)(char *); member by freeing it, allocating a number chunk over it using the address of system() as the number, and then printing the now free’d but still in-use data chunk.
Of course, in order for us to get a shell, we will need to have passed in /bin/sh as the string for this data struct from the first time it is allocated. We can redo this step if necessary in order to call system("/bin/sh") and spawn a shell.
Putting everything together, the following exploit will grant us a shell.