Introduction to return oriented programming (ROP)

m4sterph0enix

Administrator
Staff member
Messages
7
Joined
Oct 1, 2020
Reaction score
3
Points
3

What is ROP?​

Return Oriented Programming (ROP) is a powerful technique used to counter common exploit prevention strategies. In particular, ROP is useful for circumventing Address Space Layout Randomization (ASLR) and DEP. When using ROP, an attacker uses his/her control over the stack right before the return from a function to direct code execution to some other location in the program. Except on very hardened binaries, attackers can easily find a portion of code that is located in a fixed location (circumventing ASLR) and which is executable (circumventing DEP). Furthermore, it is relatively straightforward to chain several payloads to achieve (almost) arbitrary code execution.

Before we begin​

If you are attempting to follow along with this tutorial, it might be helpful to have a Linux machine you can compile and run a 32-bit code on. If you install the correct libraries, you can compile a 32-bit code on a 64-bit machine with the -m32 flag via gcc -m32 hello_world.c . I will target this tutorial mostly at 32-bit programs because ROP on 64 bit follows the same principles, but is just slightly more technically challenging. For the purpose of this tutorial, I will assume that you are familiar with x86 C calling conventions and stack management. I will attempt to provide a brief explanation here 1, but you are encouraged to explore in more depth on your own. Lastly, you should be familiar with a UNIX command-line interface.

My first ROP​

The first thing we will do is use ROP to call a function in a very simple binary. In particular, we will be attempting to call not_called in the following program.

Code:
void not_called() {
    printf("Enjoy your shell!\n");
    system("/bin/bash");
}

void vulnerable_function(char* string) {
    char buffer[100];
    strcpy(buffer, string);
}

int main(int argc, char** argv) {
    vulnerable_function(argv[1]);
    return 0;
}

We disassemble the program to learn the information we will need in order to exploit it: the size of the buffer and the address of not_called

Code:
$ gdb -q a.out
Reading symbols from /home/ppp/a.out...(no debugging symbols found)...done.
(gdb) disas vulnerable_function
Dump of assembler code for function vulnerable_function:
   0x08048464 <+0>:  push   %ebp
   0x08048465 <+1>:  mov    %esp,%ebp
   0x08048467 <+3>:  sub    $0x88,%esp
   0x0804846d <+9>:  mov    0x8(%ebp),%eax
   0x08048470 <+12>: mov    %eax,0x4(%esp)
   0x08048474 <+16>: lea    -0x6c(%ebp),%eax
   0x08048477 <+19>: mov    %eax,(%esp)
   0x0804847a <+22>: call   0x8048340 <[email protected]>
   0x0804847f <+27>: leave 
   0x08048480 <+28>: ret   
End of assembler dump.
(gdb) print not_called
$1 = {<text variable, no debug info>} 0x8048444 <not_called>

 
Top Bottom