For this challenge we’re provided the binary and a libc.so.6 binary. Just by being provided this second binary we are hinted that we will need some fuctionality from it: Rop or ret2libc probably.
I’ve recently bought the personal edition of Binary Ninja, and so will be using it for most of my static analysis.
Popping it into binja we (happily) see that it’s a rather small binary.
main is rather simple, with a menu system and a large loop, and what looks like no exit.
First a buffer of 0x80 bytes is allocated and its pointer stored at $ebp - 0x14 (buf_ptr). We’re prompted for an ID, which is at most 9 bytes (including the null) and stored locally, at $ebp - 0x20 (ID).
Next the menu is printed:
Reversing item by item we see the following:
strlen(ID)is called and its lenght is provided to
fgetslike the first time).
read_numberis called, and its return value is stored at ebp - 0x10 (dankness), it’s compared to 0x80, the size of the allocated buffer, and if greater or equal, or signed, the nothing happens. If the value is in range, however, the ‘dankness’ is copied to ebp - 0x18.
- The value at ebp - 0x18 (size) is checked to be valid. If in range,
fgetsis called to write to buf_ptr.
- Simply calls
secret_meme(ID)is called. This function is not too short, but the only interesting part is right at the start: It writes 0x69696969 to
(buf + 8). Since this function is passed the address of the ID in
main’s stack frame (0x20), this will write to main’s local at ebp - 0x18, which is size, which used to determine how much to write into the heap buf.
With these functions figured out, this is what main’s stack frame looked like:
secret_meme might look like a good way to get a huge write into the heap buffer, that is not actually where the vulnerability lies. The size is checked that it’s in range, anyway. Instead, this function helps us because it can wipe out ID’s null byte: Provided an eight-byte ID the null byte will lie over the first byte of size. By overwriting this we can change our ID, and the
strlen call will not return 8 (or less) but instead
8 + sizeof(size) + sizefof(buf_ptr), and will keep going until it finds a null byte. What is important, though, is that this will allow us to overwrite buf_ptr, and then we can use menu option 3 to write at this selected address.
To write to this address, though, I needed to pass the size check, but
secret_meme had placed an invalid value, and anyway I had overwritten it again on my way to overwriting buf_size. I could have used the second menu item to reset the size, but I instead just made sure to write a valid size when I was overwriting these bytes.
With a write-what-where primitive I can pretty much do what I want, but I don’t know the address of the stack, so I will have to attack the global offset table. Here I made it more difficult than I needed, but bear with me.
I know that I needed to leak the address of something in libc, so that I could calculated the offset from there to
system, and then patch that address into some import’s entry in the GOT. My idea was to replace
atoi@got, which is used in the menu, with the address of
printf@plt. This would allow me to provide format strings to the menu system to leak addresses on the stack. An added benefit is that I could still use the menu, because
printf returns the number of bytes printed, so to select item 3, for example, it’d just have to pass a string that prints three bytes.
One interesting thing I noticed about Binary Ninja is that they seem to name some of their symbols incorrectly. When finding the address of
atoi@got, I got myself pretty confused. What binja labels as
atoi is actually
atoi@plt. What they label as
According to Binary Ninja According to GDB.
Inspecting the stack I could see that
__libc_start_main+243 is on the stack, and would be the 23rd element printed. This means that provided the string “%23$X” would print out that address.
Now reading this address and using binja to find the address of
system I could calculate the offset. I could use this offset and the leaked run-time address to find the address of
system in memory.
With the address of
system on hand, I had to write to the
atoi@got entry again, replacing
system. This was done simply by passing a string of length three bytes (including the newline) to
printf to select menu item 3.
Finally, with the pointer in place, I simply needed to enter shell command I wanted into the menu system.
Now while this worked fine, I noticed later that I had made it more complicated than I needed to: Menu item 4 would print out the value at
buf_addr, and when I filled it with the address of
atoi@got, that value woudl be the address of
atoi (since it had already been called and so already resolved). This would get me my libc address leak without needing a format string.
Either way, I had a working exploit: