Description of the Problem
Recently I made my first Pwn challenge that was more involved than I had expected it to be. Gumbs Snottbottom is a challenge posed as a student with poor coding skills trying to show off his work to the world. All we are given is a net-cat connection and a file that appears to be a linux elf binary when we run file
on it. When we connect to the challenge we can a program that appears to take user input and spit it back out in lower-case.
Finding The Vulnerability
The first thing I always do is run pwntools checksec command on it to see what sort of protections are enabled in the binary.
We cans see that ASLR, NX, and stack canaries are disabled. Because of this, if we can reliably exploit a buffer overflow and return to our shellcode in the stack, then we should be able to own the challenge.
Lets now look at the program statically using ghidra. When we do this we see that there are three interesting function: main(), input(), and to_lower().
main()
1 | undefined8 main(void) |
input()
1 | void input(void) |
to_lower()
1 | void to_lower(char *param_1,int param_2) |
So it seems the basic flow of the program is that main prints Input Text:
, calls input() which receives 96 bytes using fgets into a 64 byte array, and then it calls to_lower() which takes the input buffer and converts each of the characters into a lower-case version of that character before printing the array.
There are two obvious vulnerabilities here both of which might be useful. The first one is the fgets() function which is taking in more bytes than is allocated in the buffer. This allows us to write past the allocated memory in the buffer and cause a buffer overflow. We can confirm this by inputing 96 characters into the program and watching it crash. The second vulnerability is that the two_lower() function directly prints the variable making vulnerable to a string format attack. This might be useful if we need to leak any addresses at runtime.
Constructing the Payload
The first thing we need to do is find how far the return pointer is from the buffer on the stack. We can do this by opening the binary in pwngdb and calculating it manually.
In this image, we input AAAA input the input function and placed a breakpoint immediately after fgets() returned. We can see that the buffer begins at address 0x7fffffffdfc0
and the return instruction pointer is located at 0x7fffffffe008
. Do a little subtraction and we’ll find that there are 72 bytes between the start of the buffer and the return address we are trying to overwrite. Now all we need to do is find a reliable way to return to the stack so that we can execute our shellcode. There are two ways of doing this, find a gadget that helps us get there or use a string-format attack to calculate the address of the stack on the system. I’ll be trying the gadget first.
If we look at the registers of the programm before input() returns we’ll see the following:
Both registers RSI and R10 contain the address of our buffer which means if we can find a gadget that jumps/returns to either of these registers then we can stet the stack pointer to container that address when we return from the inputs instruction. We can look for ROP gadgets using pwntools ROPgadget command and piping it into grep to look for specific registers.
We find the exact gadget we need at the address 0x401271
. With this information we can contruct our final payload.
1 | from pwn import * #import the library |
Run it and we get our solution.
Creation
Below is the code I wrote to create the challenge. I was able to hide the gadget in an if statement by writing inline assembly that ghidra wasn’t able to pick up on in its disassembler, but if you read the assembly inside main you can clearly see a jmp rsi
at the end of main.
1 |
|
I compiled the program with the following Makefile. As you can see I added the compile flags -O0 -fno-pie -no-pie -fno-stack-protector -fcf-protection=none -z execstack
.
-O0
No optimization makes the assembly easier to follow and prevents my ROP gadget from being optimized out of the binary.-fno-pie
and-no-pie
ensure that there won’t be any address randomization in the binary when it runs.-fcf-protection
is used to prevent attacks that take control of the control flow. So we made sure that was off.-z execstack
turns of no execution allowing us to execute shellcode on the stack.
1 | CC=gcc |
Lessons Learned
There are some mistakes that I’d like to discuss. The first mistake is that I accidentially gave the competitors the wrong binary. I gave them the one I compiled on my personal machine instead of the one compiled on the docker container hosting the problem. The main issue this caused from what I can tell is that the addresses of the stack between my local machine and container’s machine were different. This might have happened anyways due to environment variables being different but it is still bad practice to do what I did lol. Luckily, it didn’t end up being important because the challenge didn’t rely on return to libraries and I provided the user with a ROP gadget and a leak to get to the exact address of where the stack begins on the stack.
Another mistake I made was not telling the user what environment the challenge was built in. Dynamic analysis was pretty important to completing this challenge so I think I should have made a note in the description that the binary was compiled in a clean version of Ubuntu 22.04 so that they could adjust their environment as necessary to analyze the challenge. I think both of these problems can be fixed with competing in more CTFs to see how other people write and host Pwn style challenges.