Here's a LIBC is the third challenge in the binary exploitation category. The description states:
I am once again asking for you to pwn this binary vuln libc.so.6 Makefile nc mercury.picoctf.net 49464
It provides links to the vulnerable executable, a specific libc libray, and the Makefile.
PWNTools has a lot of useful features for getting offsets.
Hmm...this sounds like maybe we need to know the offset to something in libc. I guess we won't know unless we keep diving in!
Since it's in the title, it must be important. Let's figure out what version of libc we are dealing with:
As we learned in the last challenge we did, Cache Me Outside, glibc 2.27 was the version after per-thread caches were introduced. We don't really know much more than that at the moment, though.
Checksec, a great program included with pwntools, gives us this information about the program vuln (though we also have the Makefile so we could look there).
The lack of vulnerability mitigations opens up a bunch of doors like smashing the stack or ROP/JOP. The stack is not executable, so I'm going to guess we need to execute inside of libc for this challenge. The executable is also not stripped which should make decompilation and debugging easier.
We need a matching version of the dynamic linker. We can get that with pwninit or download it off of some random website. I prefer the former.
This will download our linker and patch our executable to use the libc version it shipped with and the new linker. We can then run the patched binary to see that it contains an echo server that capitalizes every other character:
To get a sense of what is going on, let's examine it in Ghidra.
We start by examining main, which we quickly find is not that interesting. main will initialize some stuff, print out the welcome message, and then loop infinitely calling do_stuff, as shown below:
The function do_stuff, on the other hand, asks for user input via a scanf and stores the input in a 112 byte stack-based buffer. The bounds of this buffer is never checked. Rather, the scanf will continue to take input until a new-line character is provided. This allows the user to overflow the buffer and adjust contents on the stack. After reading in user input, the function loops over the first 100 characters and calls convert_case to presumably have an uppercase character followed by a lowercase character. All bytes in the buffer after index 100 are not adjusted. Finally, the function calls puts to show the user the buffer after it has been converted.
Okay, what do we know:
- We have an amd64 64-bit executable.
- NX is enabled.
- We assume ASLR is enabled on the remote host.
- We have a stack-based buffer overflow in the
do_stufffunction.
Given that the stack isn't executable, we can't just overwrite the return address and point back to some shellcode on the stack. With ASLR enabled, we also can't reliably know a fixed address to jump to to execute a subroutine inside of a loaded library. This gives us only one real option: we somehow need to leak an address from inside a library (and since we were given libc, that's probably a good starting point) and use that to find subroutines inside of that library to execute code snippets, otherwise known as Return Oriented Programming (ROP).
Okay, I committed the cardinal sin of writeups: I got ahead of myself and finished the challenge before I finished the writeup. Usually I do them in parallel. I don't feel like making the context switch to continue with this one, given that it has been a couple weeks after I've solved it. But I also can't keep a dangling branch open this long. So here we are in main without a finished writeup. I am sorry.