This challenge is brought to us courtesy of picoGym, which is picoCTF's year round training platform. The challenge provides 3 files: libc.so.6, Makefile, and vuln. All three files, and the code used in this walk-through, can be found in my GitHub repo at https://github.com/ChrisCHumphreys/picolab/tree/main/heres_a_lib_c. Also, a huge thanks to guyinatuxedo, whose resources at https://guyinatuxedo.github.io/index.html were invaluable in helping me to learn this stuff.
On my system at least, the executable would not work due to my having a different libc in use than the one the program was originally compiled with. I fixed this by running pwninit on the file and generating a version of vuln that uses the libc provided with the challenge. The name of this new modified file is vuln_patched. pwninit can be found at https://github.com/io12/pwninit.
First, running checksec on this executable shows that very few protections are enabled, but that the stack is not executable, which means we cannot just shove shellcode onto the stack and execute it. We will need to use ROP chains.
Next, running the program reveals that it doesn't do a whole lot. Mainly it just seems to take in input and return that same input, but with every other letter capitalized.
Examining the de-compiled application in Ghidra shows that the main function is not super helpful, but that there is a function named do_stuff, which is vulnerable to a buffer overflow. Bonus points for identifying the vulnerability before reading the next paragraph. 👅
The vulnerable line here is the scanf line that scans into the input buffer until a newline character is encountered. Knowing this, we next need to find out how big our overwrite needs to be in order to overwrite the rip register and hijack the flow of execution. Ghidra helps us again, the stack layout for this function shows us that the input buffer is exactly 0x88 bytes away from the RIP register.
We can also verify this with GDB by finding our input and subtracting that value form the value in rip. In this case, the string I used as input was "this-is-input". I put in a break in GDB for right after the input was accepted, and then ran "i f" to get info about the current stack frame.
Just as we suspected, the offset is 0x88 characters.
The next items we need are going to be the GOT and PLT addresses of puts. The reason for this is we will need to find a way feed the GOT address (which is different every time the program is ran), into the PLT address of puts. Essentially, what we are doing is printing the dynamic address of puts using puts. Both of these values can be found using pwntools or objdump. In my case I used pwntools. The GOT address ended up being 0x601018 and the PLT address was 0x400540.
Now we need a "pop rdi" gadget. The reason for this is in a 64 bit system, puts will print whatever is pointed at by the address in the RDI register when puts is called. To print the GOT address, we need to first load it into the RDI register. I found the gadget using the ROPGadget tool which can be found at https://github.com/JonathanSalwan/ROPgadget. The address we will be using is 0x400913.
Now, we need to find the offsets from the base of libc at which puts, system, and the string "/bin/sh" are located. The reason for this is that although every time the program is ran the addresses will be different, the space between the base of libc and these functions will remain the same. By subtracting the offset of one of these items from its location in a running program we will find the address of the base of libc. Additionally, to get a shell we need to execute the syscall "system" and have it run /bin/sh.
I used gdb to get this information. I started by simply breaking at main and then printing the items current value, and the current value of the base of libc. For the current libc base I used the gdb command vmmap.
In the above example the base is at 0x00007ffff79e4000. Next to find the addresses of puts, system, and /bin/sh, we do as in the screenshot below.
For the offsets we subtract the current value from the current base. So the offset of puts is 0x80a30, system is 0x4f4e0, and /bin/sh is 0x1b40fa.
The last piece needed is the address of main. We need this because after we leak the address of libc we need to return to main, and overflow the buffer again, but on the second run we will pop a shell using our ROP chain instead of leaking the libc address. This is easily found in Ghidra or by using objdump. The address is 0x00400771.
That was a lot of prep, but we know have everything we need to write the exploit, which can be found below.
One final note, I spent hours on this exploit without being able to get it to work properly. Finally, I took a look at a different writeup and noticed that the second chain in that exploit started with a vanilla RET call. It can be seen on line 67 in the exploit above. With this, it works perfect. Without it, it does not work at all. If you know the how or why behind this feel free to drop a comment as I myself am very curious about why this is happening.
Comments
Post a Comment