Return-Oriented Programming (ROP) Chain

Imène ALLOUCHE
6 min readSep 3, 2023

--

Released By: Imène ALLOUCHE

Abstract

This technical article offers an in-depth analysis of Return-Oriented Programming (ROP) chain attacks, a sophisticated method of exploiting memory corruption vulnerabilities. By repurposing existing program instructions called gadgets, attackers create chains that manipulate control flow. The article covers gadget identification, chaining techniques, and defense mechanisms such as ASLR, DEP/NX, and CFI. Real-world cases illustrate the potency of ROP attacks, yielding insights valuable to developers and security enthusiasts. This piece aims to enhance comprehension of ROP attacks, aiding proactive defense strategies.

Introduction to Return-Oriented Programming (ROP)

Return-Oriented Programming (ROP), has redefined how attackers manipulate program execution paths. This section offers a technical insight into the fundamentals of ROP and examples on how they are exploited.

Introducing ROP: A Technical Perspective

At its core, ROP involves stringing together existing code fragments, known as “gadgets,” to create a chain of instructions that subvert a program’s intended control flow. Each gadget typically ends with a “return” instruction, which allows the attacker to stack these gadgets in sequence, effectively directing the program to execute malicious actions. This method cleverly leverages the limited set of available instructions to build an arbitrary computation without requiring the injection of new code.

Technical Example: ROP Exploitation with Controlled System Call(32-Bit Context)

Consider a hypothetical 32-bit program depicted by the following source code:

#include <stdio.h>
#include <stdlib.h>

char name[32];

int main() {
printf("What's your name? ");
read(0, name, 32);

printf("Hi %s\n", name);
printf("The time is currently ");
system("/bin/date");

char echo[100];
printf("What do you want me to echo back? ");
read(0, echo, 1000);

puts(echo);
return 0;
}

In this context, a stack buffer overflow vulnerability is apparent in the variable echo. The objective is to manipulate the program's control flow by overwriting the saved return address (EIP). However, the program lacks a give_shell function for direct exploitation.

To achieve control, we employ a method that invokes the system function with a controlled argument. Notably, in 32-bit Linux programs, function arguments are passed through the stack, thus enabling control over arguments with stack manipulation.

When the main function returns, our aim is to simulate a scenario as though the system function had been invoked normally. To achieve this, we construct the following stack frame for main:

     0xffff0008: 0xdeadbeef // Controlled system argument 1
0xffff0004: 0xdeadbeef // Return address for system
ESP->0xffff0000: 0x08048450 // Return address for main (system's PLT entry)

As the main function returns, the execution flow redirects to the system function's PLT (Procedure Linkage Table) entry, mirroring a legitimate invocation. The choice of the return address for system is inconsequential, as our primary goal—establishing a shell—is achieved prior to system's return.

An essential aspect is providing an argument for the system call. Given the dynamic nature of memory layout (as explained in the context of ASLR), using stack data or LIBC strings for arguments becomes challenging. However, the program offers a name global variable, conveniently located in the BSS segment and accessible from a known location in the binary.

In summary, the exploit process involves crafting a payload that:

  1. Enters a command, such as “sh,” to the name variable.
  2. Constructs the stack frame to redirect control flow to the system function.
  3. Specifies the controlled argument within the stack frame.
  4. Ensures that the altered stack frame mimics a legitimate system call.

By skillfully orchestrating these steps, attackers can effectively exploit the buffer overflow vulnerability to achieve their objectives, bypassing security mechanisms and demonstrating the intricacies of Return-Oriented Programming in a controlled environment

Technical Example: ROP Exploitation with Controlled System Call(64-Bit Context)

In 64-bit binary environments, the process of conveying arguments to functions demands greater intricacy. The fundamental concept of overwriting the saved Return Instruction Pointer (RIP) remains consistent. However, in the realm of calling conventions, 64-bit programs follow the practice of transmitting arguments via registers. In the case of executing the system function, this necessitates acquiring control over the Register Destination Index (RDI) register.

To achieve this, we employ succinct assembly segments, referred to as “gadgets,” within the binary. Gadgets typically perform the operation of popping one or more registers from the stack, followed by invoking the ret instruction. This enables us to interconnect these gadgets, effectively fabricating an artificial call stack.

Consider the scenario where control over both RDI and Register Source Index (RSI) is imperative. In such instances, program disassembly may unveil gadgets that resemble the following (uncovered using tools like rp++ or ROPgadget):

0x400c01: pop rdi; ret
0x400c03: pop rsi; pop r15; ret

Employing these gadgets, we can orchestrate a synthetic call stack, sequentially enacting the desired gadget operations. By manipulating these gadgets, we populate registers with controlled values. Subsequently, this series culminates in a leap to the system function, granting us the ability to dictate system behavior.

Here’s an illustrative example:

     0xffff0028: 0x400d00   // Destination for rsi gadget's return
0xffff0020: 0x1337beef // Desired r15 value (typically irrelevant)
0xffff0018: 0x1337beef // Desired rsi value
0xffff0010: 0x400c03 // Address for rdi gadget's return (pop rsi gadget)
0xffff0008: 0xdeadbeef // Value to be loaded into rdi
RSP->0xffff0000: 0x400c01 // Address of rdi gadget

Progressing through these instructions one by one, upon main's return, the control flows to the pop rdi gadget:

RIP = 0x400c01 (pop rdi)
RDI = UNKNOWN
RSI = UNKNOWN

Subsequently, further execution leads to the next stage, and pop rdi is executed, transferring the topmost stack value into RDI:

RIP = 0x400c02 (ret)
RDI = 0xdeadbeef
RSI = UNKNOWN

Eventually, the sequence concludes with the pop rsi gadget performing its role and transitioning into the subsequent gadget:

RIP = 0x400c03 (pop rsi)
RDI = 0xdeadbeef
RSI = UNKNOWN

Ultimately, the last gadget within this sequence — pop rsi — proceeds, concluding this orchestrated process. This final leap directs the flow of execution towards the intended function, albeit with the critical distinction that both RDI and RSI registers are under our control.

In summation, the intricate choreography of gadgets and register manipulation illustrates the finesse of Return-Oriented Programming, serving as a testament to its efficacy within the controlled parameters of 64-bit contexts.

Historical Significance: Bypassing Security Mechanisms

The journey of ROP’s emergence parallels the arms race between attackers and defenders. Originally conceived as a way to exploit buffer overflows, ROP swiftly proved its prowess in bypassing security mechanisms like data execution prevention (DEP) and address space layout randomization (ASLR). By hijacking the program’s control flow without the need for injecting new code, ROP shattered conventional notions of attack vectors. As operating systems and compilers evolved to counteract traditional ROP attacks, the demand for more sophisticated variants emerged, leading us to explore the ever-evolving landscape of advanced ROP techniques.

Evolution of ROP Variants: Keeping Pace with Security

The relentless pursuit of security improvements led to the diversification of ROP techniques. Attackers pivoted from merely exploiting return instructions to crafting intricate sequences of gadgets that evaded modern defenses. This progression spurred the development of Jump-Oriented Programming (JOP), Call-Oriented Programming (COP), and the ingenious Return-to-CSU technique. These variants represent not only the creativity of exploit developers but also their adaptability in the face of evolving security mechanisms. As we delve into these advanced ROP variants, we uncover the strategies that underpin their success in contemporary binary exploitation.

References

To Be Continued …

--

--

Imène ALLOUCHE

1CS student at ESI Algiers, CTF player and Cybersecurity enthusiast