[Binary Exploitation] Return Oriented Programming

by on under research
pwn, ROP
13 minute read

Untuk bahasa Indonesia, silakan klik link ini

English

Probably the most famous CWE in the world, buffer overflows can be exploited in numerous ways, one of those being return oriented programming. In this post, I will be explaining the different ways to exploit a buffer overflow with return oriented programming.

1. Prerequisites

Like many other binary exploitation techniques, knowledge on how assembly works is very useful. In order to understand what I will be explaining in this post, it is recommended to know:

2. Return oriented programming in a nutshell

Return oriented programming (ROP) is exactly what it’s name implies, programming by returning. Every call instruction in assembly does two things, it pushes the next instruction’s address to the stack, and modifies the value of the instruction pointer to what is determined. Almost every call instruction is paired with an upcoming ret instruction. This instruction pops whatever value is in the stack and places it’s into the instruction pointer.

This is the main goal of return oriented programming. If the attacker can change the value that was previously placed into the stack, say with a buffer overflow, they can gain access to change the value of the instruction pointer to whatever they want.

3. Gadgets

Common terminology used in exploiting return oriented programming is gadgets. Gadgets are small pieces of code that end in a ret instruction. Here is a few examples, you can find these in any old elf binary.

Error

Error

Gadgets are used to create what is called a ROP chain.

3.1. Finding gadgets

In the above example, a tool was used to find the gadgets. Examples of tools that can be used are ropper and ROPgadget

4. ROP chains

A ROP chain is a series of gadgets used by the attacker in order to run pieces of code consecutively, in order to modify registers or data in the stack, heap, or bss. ROP chains can be accomplished because most gadgets end in the ret instruction. Because of this, by placing two or more gadget addresses in the stack, after the first gadget is done executing, execution will continue to the next gadget address located in the stack. Here’s an illustration of a ROP chain (source):

Error

If you are still confused, go ahead and watch this video from LiveOverflow. I hope it helps.

5. Techniques to exploit ROP

By now you probably understand how ROP works, and what ROP chaining is. That itself isn’t enough to RCE though, so I will show you the techniques I know to exploit ROP

1. Jumping to a function
Sometimes, instead of jumping to small pieces of code, jumping to an entire function can be useful. Since (almost) all functions end with a ret instruction, creating ROP chains is still possible

2. Set the functions parameters
Most functions require parameters in order to function properly. However, for 32-bit and 64-bit architectures, the location in which the parameters are stored is different

  • For 32-bit, the function parameters are stored in the stack, directly after the return address
  • For 64-bit, the function parameters are stored in 6 registers, which are rdi, rsi, rdx, rcx, r8, and r9. If the function requires more than 6 parameters, the remaining parameters will be stored in the stack

3. The problem with 32-bit
When jumping to a function in a 32-bit binary, the parameters are stored in the stack. This can interfere with our ropchain because function parameters most likely are not valid addresses to executable area. However, there is a solution to this problem, which is creating a payload as such:

(function to return to) + (address where buffer overflow is located) + (function parameters)

With a payload like this, after the function is done executing, execution will return to the function where the buffer overflow is located. Then, we can exploit the buffer overflow again, and create a brand new ROP chain.

4. The problem with 64-bit
When jumping to a function in a 64-bit binary, the first 6 parameters are located in registers. Registers are not stored in the stack, but they are stored in a seperate location in your machine. Because of this, we can’t use the buffer overflow directly to change the register values. However, like in 32-bit, there is a solution.

In every binary compiled with gcc, there is a certain function called __libc_csu_init. This function is sort of like a constructor for your entire program. You can learn more about it here. Let’s see if we can find a useful gadgets in this function.

0x0000000000400710 <+0>:	push   r15
0x0000000000400712 <+2>:	push   r14
0x0000000000400714 <+4>:	mov    r15,rdx
0x0000000000400717 <+7>:	push   r13
0x0000000000400719 <+9>:	push   r12
0x000000000040071b <+11>:	lea    r12,[rip+0x2006ee]        # 0x600e10
0x0000000000400722 <+18>:	push   rbp
0x0000000000400723 <+19>:	lea    rbp,[rip+0x2006ee]        # 0x600e18
0x000000000040072a <+26>:	push   rbx
0x000000000040072b <+27>:	mov    r13d,edi
0x000000000040072e <+30>:	mov    r14,rsi
0x0000000000400731 <+33>:	sub    rbp,r12
0x0000000000400734 <+36>:	sub    rsp,0x8
0x0000000000400738 <+40>:	sar    rbp,0x3
0x000000000040073c <+44>:	call   0x400498 <_init>
0x0000000000400741 <+49>:	test   rbp,rbp
0x0000000000400744 <+52>:	je     0x400766 <__libc_csu_init+86>
0x0000000000400746 <+54>:	xor    ebx,ebx
0x0000000000400748 <+56>:	nop    DWORD PTR [rax+rax*1+0x0]
0x0000000000400750 <+64>:	mov    rdx,r15
0x0000000000400753 <+67>:	mov    rsi,r14
0x0000000000400756 <+70>:	mov    edi,r13d
0x0000000000400759 <+73>:	call   QWORD PTR [r12+rbx*8]
0x000000000040075d <+77>:	add    rbx,0x1
0x0000000000400761 <+81>:	cmp    rbp,rbx
0x0000000000400764 <+84>:	jne    0x400750 <__libc_csu_init+64>
0x0000000000400766 <+86>:	add    rsp,0x8
0x000000000040076a <+90>:	pop    rbx
0x000000000040076b <+91>:	pop    rbp
0x000000000040076c <+92>:	pop    r12
0x000000000040076e <+94>:	pop    r13
0x0000000000400770 <+96>:	pop    r14
0x0000000000400772 <+98>:	pop    r15
0x0000000000400774 <+100>:	ret

It might seem like there isn’t much, but this function has a very important gadget. The pop r15 and pop r14 instructions both consist of two bytes. Let’s see what they are.

0x400770 <__libc_csu_init+96>:	0x41	0x5e
0x400772 <__libc_csu_init+98>:	0x41	0x5f

They both contain 0x41, but the latter is what is really interesting. Let’s assemble them.

0x400771 <__libc_csu_init+97>:	pop    rsi
0x400773 <__libc_csu_init+99>:	pop    rdi

Well that’s interesting. Now we have two gadgets, both in a function that is almost always present, which allows use to control the first two parameters of a function. For the other 4 gadgets, gadgets such as these ones are quite rare, but functions that need 6 parameters is also not very common.

5.1. The problem with 64-bit, part 2
Later in this post, I will talk about calling mprotect. But before that, it is required to know that mprotect needs 3 parameters, which are address, length, and prot. Just before we learnt an easy way to control the first two parameters of a function, using __libc_csu_init, but we haven’t controlled the third function parameter, which is stored in the rdx register. There is actually a gadget we can use, found in __libc_csu_init:

0x0000000000400750 <+64>:	mov    rdx,r15
0x0000000000400753 <+67>:	mov    rsi,r14
0x0000000000400756 <+70>:	mov    edi,r13d
0x0000000000400759 <+73>:	call   QWORD PTR [r12+rbx*8]
0x000000000040075d <+77>:	add    rbx,0x1
0x0000000000400761 <+81>:	cmp    rbp,rbx
0x0000000000400764 <+84>:	jne    0x400750 <__libc_csu_init+64>
0x0000000000400766 <+86>:	add    rsp,0x8
0x000000000040076a <+90>:	pop    rbx
0x000000000040076b <+91>:	pop    rbp
0x000000000040076c <+92>:	pop    r12
0x000000000040076e <+94>:	pop    r13
0x0000000000400770 <+96>:	pop    r14
0x0000000000400772 <+98>:	pop    r15
0x0000000000400774 <+100>:	ret

Unlike other gadgets, this gadget is far from a ret instruction. Also, there are other things to be weary about like the call instruction and the jne instruction. To pass them, you can set r12 + rbx*8 to an address that is only the ret instruction, and set rbp to the according rbx+1. Also the add rsp, 0x8 can sometimes be problematic.

Now that you can control the first three parameters of a function, you can call functions like mprotect, execve, fgets and more. But ofcourse calling those functions require knowing their respective addresses.

6. Leaking addresses
Now that we know the basics, let’s get started to try and create an arbitrary read.

I expect you as the reader already know about ASLR, if you dont go and have a read. ASLR by default is active on most machines, and by default it randomizes stack, heap, and libc addresses. However, the addresses of executable code and bss is only randomized if the protection PIE is enabled. For some binaries, this protection is disabled, this grants us access to ROP gadgets, Global Offset Table (GOT) and Procedure Linkage Table (PLT). If you don’t know about GOT and PLT, here is a video from LiveOverflow that you can watch.

For most binaries, they will have a way to print text onto the screen. This can be done with the function puts, printf, or even write. In any case, these functions can be used to print data from (almost) any address in the binary. Using a ROP chain, we can abuse this in order to get libc and/or stack addresses.

Here’s an example, in this binary we have a buffer overflow and we can change the return address. I’ll set the return address to the the address of the puts PLT, and I’ll set the first parameter to be the address of puts GOT.

Error

Error

Error

Boom we got a libc address, more specifically the address of puts. Now we can do a calculation to get the value of the system, and eventually get a shell.

7. Shellcoding
Almost always, every binary has the W^X protection. This prevents writing new executable code during runtime, since bad things could happen ofcourse. However, certain functions in libc can change the permissions of any block of memory, one of these functions being mprotect. Mprotect takes three parameters, which are address, length, and prot. Since we can effectively control the first three function parameters, we can call this function and change the permissions of certain blocks of memory.

Here’s an example, in this binary I’ll change the bss area from rw- to rwx. By using functions like fgets, we can write shellcode to that area and then jump to it.

Error

Error

Error

8. One gadget RCE
The tricks before both required the attacker to setup function parameters before calling the functions. But as it turns out, there are certain pieces of code located in libc that can be used to invoke a shell without having to set any function parameters. However, there are certain constraints that must be satisfied before being able to use them. The contraints usually consists of setting up registers and memory locations in a certain way.

To find these gadgets, a tool called one_gadget can be used. Here’s an example of me using one_gadget:

Error

9. Static binaries
In some cases, binaries will be compiled statically instead of dynamically. As a result of this, PIE protection will never be enabled (at least for nasm), however certain libc functions such as system won’t be available for use.

In this case, there are two things I like to do, which is call the function _dl_make_stack_executable, or invoke a syscall by only using gadgets.

9.1. _dl_make_stack_executable
In statically linked binaries, there is a certain function called _dl_make_stack_executable. This function can invoke a syscall to make the permissions of the stack become rwx, but it requires two conditions to be satisfied. Let’s look in the disassembly:

0x00000000004717e0 <+0>:	mov    rsi,QWORD PTR [rip+0x250a49]        # 0x6c2230 <_dl_pagesize>
0x00000000004717e7 <+7>:	push   rbx
0x00000000004717e8 <+8>:	mov    rbx,rdi
0x00000000004717eb <+11>:	mov    rax,QWORD PTR [rdi]
0x00000000004717ee <+14>:	mov    rdi,rsi
0x00000000004717f1 <+17>:	neg    rdi
0x00000000004717f4 <+20>:	and    rdi,rax
0x00000000004717f7 <+23>:	cmp    rax,QWORD PTR [rip+0x24f78a]        # 0x6c0f88 <__libc_stack_end>
0x00000000004717fe <+30>:	jne    0x47181f <_dl_make_stack_executable+63>
0x0000000000471800 <+32>:	mov    edx,DWORD PTR [rip+0x24f7da]        # 0x6c0fe0 <__stack_prot>
0x0000000000471806 <+38>:	call   0x435690 <mprotect>
0x000000000047180b <+43>:	test   eax,eax
0x000000000047180d <+45>:	jne    0x471826 <_dl_make_stack_executable+70>
0x000000000047180f <+47>:	mov    QWORD PTR [rbx],0x0
0x0000000000471816 <+54>:	or     DWORD PTR [rip+0x2509f3],0x1        # 0x6c2210 <_dl_stack_flags>
0x000000000047181d <+61>:	pop    rbx
0x000000000047181e <+62>:	ret    
0x000000000047181f <+63>:	mov    eax,0x1
0x0000000000471824 <+68>:	pop    rbx
0x0000000000471825 <+69>:	ret    
0x0000000000471826 <+70>:	mov    rax,0xffffffffffffffc0
0x000000000047182d <+77>:	pop    rbx
0x000000000047182e <+78>:	mov    eax,DWORD PTR fs:[rax]

This is a 64-bit executable, so the function parameters are stored in registers. Looking carefully, It seems that if rdi is set to the address of __libc_stack_end and if __stack_prot is set to 7, we can get a call to mprotect.

9.2. Syscall gadgets
The other trick I like to do is to create a ropchain in such a way that we can invoke a syscall. This means in 64-bit, I have to set rax, rdi, and rsi and also jump to a syscall instruction. In 32-bit, I have to set eax, ebx, and ecx and also jump to a int 0x80 instruction. This may seem hard, but it’s actually easier than calling _dl_make_stack_executable.

10. Stack pivoting
The last trick I will be explaining is stack pivoting. Basically, what we percieve as the “stack” is actually just an abstract concept. The stack is basically the area of memory in which the stack pointer is pointed to. If we can change the value of the stack pointer to point to a different location in memory, then we can say that new location is the “stack”.

What good does this do for us? Well in some binaries the ropchain we can make can be limited, say 2-3 chains at a time. In this case, for 64-bit binaries changing rdi and jumping to puts and back to main won’t be possible.

But what if in a different location we could write a long chain. Say, bss? Or maybe even the heap? But to be able to use this location as a rop chain, we need to set the stack register’s value to point to it. In this case, we could try and find an instruction like mov rsp, rax, but it’s almost never present is dynamically linked binaries.

Instead of generic instructions like that, we can use an unexpected one, leave. According to the Nasm docs, leave does two things, mov esp, ebp and then pop ebp.

As you know, the base pointer is located right before the return address, so we can corrupt it everytime we want to corrupt the return address. But instead of jumping to functions, by jumping to another leave; ret gadget, we can pivot the stack to another location in memory.

Let’s look at an example, in this binary, I’ve created a ropchain in the heap, and have also gotten a heap leak. I’m going to pivot the stack on to the heap and get a longer ropchain.

Error

Error

Error

Error

Error

Error

6. Final words

In conclusion, ROP is a very exploitable vulnerability. This isn’t the end for ROP, as there are still techniques such as ret2dl_resolve, SROP, and more.






Indonesian

Mungkin CWE paling terkenal sedunia, buffer overflows dapat dieksploitasi dengan berbagai cara, salah satunya adalah return oriented programming. Dalam postingan ini, aku akan menjelaskan berbagai cara untuk mengeksplitasi buffer overflow dengan return oriented programming.

1. Prasyarat

Seperti teknik binary eksploit lainnya, ilmu tentang cara assembly bekerja sangat berguna. Agar dapat mengerti apa yang akan saya jelaskan di postingan ini, disarankan sudah mengetahui:

2. Return oriented programming secara singkat

Tepat seperti namanya, return oriented programming (ROP) adalah programming dengan return. Setiap instruksi call di dalam assembly akan melakukan dua hal, meletakkan alamat instruksi berikut di stack, dan memodifikasi nilal instruction pointer menjadi apa yang telah ditentukan. Hampir semua instruksi call dipasangkan dengan sebuah instruksi ret yang akan datang. Instruksi ini mengeluarkan nilai apapun yang sedang ada di stack dan meletakkannya di instruction pointer.

Ini adalah tujuan utama dari return oriented programing. Jika penyerang dapat mengubah nilai yang sebelumnya diletakkan di stack, misal dengan buffer overflow, mereka dapat mengubah nilai instruction pointer menjadi apapun yang mereka inginkan.

3. Gadgets

Terminologi yang sering digunakan saat mengeksploit return oriented programming adalah gadgets. Gadget adalah potongan kode yang kecil yang berakhir dengan sebuah instruksi ret. Berikut adalah beberapa contoh, anda dapat temukan seperti ini di binary ELF apapun.

Error

Error

Gadget digunakan untuk membuat ROP chain.

3.1. Mencari gadgets

Pada contoh diatas, sebuah tool digunakan untuk mencari gadget tersebut. Contoh tool yang dapat digunakan adalah ropper dan ROPgadget.

4. ROP chain

ROP chain merupakan rangkaian gadget yang digunakan oleh penyerang untuk menjalankan potongan kode secara berurutan, yang digunakan untuk mengubah nilai register atau data yang di stack, heap, maupun bss. ROP chain berguna sebab sebagian besar gadget berakhir dengan instruksi ret. Oleh karena itu, dengan menempatkan dua atau lebih alamat gadget di stack, setelah gadget pertama selesai dieksekusi, eksekusi akan berlanjut ke alamat gadget berikut yang terdapat distack. Berikut adalah ilustrasi ROP chain (sumber):

Error

Jika anda masih bingung, silakan menonton video berikut dari LiveOverflow. Semoga membantu.

5. Teknik mengeksploitasi ROP

Semoga sekarang anda sudah tau cara ROP bekerja, dan mengetahui apa itu ROP chain. Itu sendiri belum cukup untuk mendapatkan RCE, jadi aku akan menunjukkan cara mengeksploitasi ROP

1. Melompat ke fungsi
Kadang-kadang, daripada melompat ke potongan kode yang kecil, melompat ke fungsi bisa berguna. Karena (hampir) semua fungsi berakhir dengan instruksi ret, membuat ROP chain tetap dapat dilakukan.

2. Mengatur parameter fungsi
Sebagian besar fungsi membutuhkan parameter untuk berjalan. Akan tetapi, untuk komputer 32-bit dan 64-bit, tempat dimana parameter fungsi disimpan berbeda

  • Untuk 32-bit, parameter fungsi disimpan di stack, pas setelah return address.
  • Untuk 64-bit, parameter fungsi disimpan di 6 register, yaitu rdi, rsi, rdx, rcx, r8, dan r9. Jika fungsi yang dijalankan perlu lebih dari 6 parameter, parameter sisa akan disimpan di stack.

3. Persoalan dengan 32-bit
Ketika melompat ke suatu fungsi di binary 32-bit, parameter yang disimpan terletak di stack. Ini dapat merusak ROP chain kita sebab parameter fungsi kemungkinan besar bukan alamat yang valid ke kode yang dapat dieksekusi. Akan tetapi, terdapat solusi atas persoalan ini, yaitu membuat payload seperti berikut:

(fungsi yang ingin dipanggil) + (alamat fungsi yang memiliki buffer overflow) + (parameter fungsi)

Dengan payload seperti itu, setelah fungsi tersebut selesai bereksekusi, eksekusi akan berlanjut ke tempat buffer overflow terletak. Lalu, kita dapat mengeksploitasi buffer overflow tersebut lagi, dan membuat ROP chain yang baru.

4. Persoalan dengan 64-bit
Ketika melompat ke suatu fungsi di binary 64-bit, 6 parameter pertamanya disimpan di register. Register tidak disimpan dalam stack, tetapi disimpan di lokasi berbeda didalam komputer anda. Oleh karena itu, kita tidak dapat menggunakan buffer overflow langsung untuk mengubah nilai di register. Akan tetapi, seperti 32-bit, terdapat sebuah solusi.

Di semua binary yang di-compile dengan gcc, terdapat sebuah fungsi yang bernama __libc_csu_init. Fungsi ini bisa dibilang suatu “constructor” untuk progrm anda. Anda dapat mempelajarinya lebih lanjut di sini. Mari kita lihat kalau terdapat gadget berguna di fungsi ini.

0x0000000000400710 <+0>:	push   r15
0x0000000000400712 <+2>:	push   r14
0x0000000000400714 <+4>:	mov    r15,rdx
0x0000000000400717 <+7>:	push   r13
0x0000000000400719 <+9>:	push   r12
0x000000000040071b <+11>:	lea    r12,[rip+0x2006ee]        # 0x600e10
0x0000000000400722 <+18>:	push   rbp
0x0000000000400723 <+19>:	lea    rbp,[rip+0x2006ee]        # 0x600e18
0x000000000040072a <+26>:	push   rbx
0x000000000040072b <+27>:	mov    r13d,edi
0x000000000040072e <+30>:	mov    r14,rsi
0x0000000000400731 <+33>:	sub    rbp,r12
0x0000000000400734 <+36>:	sub    rsp,0x8
0x0000000000400738 <+40>:	sar    rbp,0x3
0x000000000040073c <+44>:	call   0x400498 <_init>
0x0000000000400741 <+49>:	test   rbp,rbp
0x0000000000400744 <+52>:	je     0x400766 <__libc_csu_init+86>
0x0000000000400746 <+54>:	xor    ebx,ebx
0x0000000000400748 <+56>:	nop    DWORD PTR [rax+rax*1+0x0]
0x0000000000400750 <+64>:	mov    rdx,r15
0x0000000000400753 <+67>:	mov    rsi,r14
0x0000000000400756 <+70>:	mov    edi,r13d
0x0000000000400759 <+73>:	call   QWORD PTR [r12+rbx*8]
0x000000000040075d <+77>:	add    rbx,0x1
0x0000000000400761 <+81>:	cmp    rbp,rbx
0x0000000000400764 <+84>:	jne    0x400750 <__libc_csu_init+64>
0x0000000000400766 <+86>:	add    rsp,0x8
0x000000000040076a <+90>:	pop    rbx
0x000000000040076b <+91>:	pop    rbp
0x000000000040076c <+92>:	pop    r12
0x000000000040076e <+94>:	pop    r13
0x0000000000400770 <+96>:	pop    r14
0x0000000000400772 <+98>:	pop    r15
0x0000000000400774 <+100>:	ret

Mungkin terlihat bahwa tidak ada, tapi fungsi di ini terdapat suatu gadget yang sangat penting. Instruksi pop r14 dan pop r15 keduanya terdiri dari 2 byte. Mari kita liat apa keduanya.

0x400770 <__libc_csu_init+96>:	0x41	0x5e
0x400772 <__libc_csu_init+98>:	0x41	0x5f

Keduanya mengandung 0x41, tapi yang setelahnya yang menarik. Mari kita assemble kembali.

0x400771 <__libc_csu_init+97>:	pop    rsi
0x400773 <__libc_csu_init+99>:	pop    rdi

Nah ini menarik, sekarang kita mempunyai 2 gadget, keduanya didalam fungsi yang hampir selalu ada, yang memberikan kita kesempatan untuk mengatur dua parameter pertama dari fungsi. Untuk 4 parameter lain, gadget seperti ini sangat jarang, tapi fungsi yang memerlukan 6 parameter juga jarang.

5.1. Persoalan dengan 64-bit, bagian 2
Kemudian di postingan ini, aku akan menjelaskan tentang mprotect. Tapi sebelum itu, perlu diketahui bahwa mprotect membutuhkan 3 parameter, yaitu alamat, panjang, dan prot. Sebelumnya kita telah mempelajari cara untuk mengatur dua parameter pertama sebuah fungsi, dengan menggunakan __libc_csu_init, tapi kita belum bahas tentang mengatur parameter ketiga, yang disimpan di register rdx. Ternyata terdapat sebuah gadget yang dapat kita gunakan, yang dapat ditemukan di __libc_csu_init:

0x0000000000400750 <+64>:	mov    rdx,r15
0x0000000000400753 <+67>:	mov    rsi,r14
0x0000000000400756 <+70>:	mov    edi,r13d
0x0000000000400759 <+73>:	call   QWORD PTR [r12+rbx*8]
0x000000000040075d <+77>:	add    rbx,0x1
0x0000000000400761 <+81>:	cmp    rbp,rbx
0x0000000000400764 <+84>:	jne    0x400750 <__libc_csu_init+64>
0x0000000000400766 <+86>:	add    rsp,0x8
0x000000000040076a <+90>:	pop    rbx
0x000000000040076b <+91>:	pop    rbp
0x000000000040076c <+92>:	pop    r12
0x000000000040076e <+94>:	pop    r13
0x0000000000400770 <+96>:	pop    r14
0x0000000000400772 <+98>:	pop    r15
0x0000000000400774 <+100>:	ret

Berbeda dari gadget lain, gadget ini terletak jauh dari instruksi ret. Juga, terdapat hal lain yang perlu diwaspadai seperti instruksi call dan instruksi jne. Untuk melewati keduanya, anda dapat mengatur r12 + rbx*8 ke alamat yang hanya terdapat instruksi ret, dan mengatur rbp menjadi rbx+1. Dan juga add rsp, 0x8 bisa bermasalah.

Nah sekarang karena anda sudah bisa mengatur tiga parameter pertama dari sebuah fungsi, anda bisa memanggil fungsi seperti mprotect, execve, fgets dan sebagainya. Tapi tentu memanggil fungsi itu diperlukan mengetahui alamatnya.

6. Membocorkan alamat
Karena sekarang kita sudah mengetahui dasar-dasarnya, mari kita mulai coba membuat suatu arbitrary read.

Saya kira anda sudah kenal dengan ASLR, jika belum silakan baca terlebih dahulu. ASLR hampir selalu hidup pada beberapa komputer, dan biasanya hanya mengacak stack, heap dan alamat libc. Akan tetapi, alamat dari kode hanya diacak jika proteksi PIE hidup. Untuk beberapa binary, proteksi ini tidak dihidupkan, dan ini memberikan kita akses ke ROP gadget, Global Offset Table (GOT), dan Procedure Linkage Table (PLT). Jika anda belum kenal dengna GOT dan PLT, ini adalah video dari LiveOverflow yang bisa anda tonton.

Sebagian besar binary akan ada cara untuk menampilkan tulisan ke layar. Hal ini dapat dicapai dengan fungsi puts, printf, maupun write. Dalam kasus apapun, fungsi ini (hampir) dapat mencetak data apapun yang ada pada program. Dengan ROP chain, kita dapat mengexploit ini untuk mendapatkan alamat libc/stack.

Berikut sebuah contoh, pada binary ini kita mempunyai sebuah buffer overflow dan kita dapat mengubah nilai return address. Aku akan mengatur nilainya menjadi alamat PLT puts, dan mengatur parameter pertamanya menjadi alamat GOT puts.

Error

Error

Error

Duarr kita mendapatkan sebuah alamat libc, lebih spesifik alamat dari puts. Sekarang kita dapat melakukan kalkulasi untuk mendapatkan alamat system, dan kemudian mendapatkan shell.

7. Shellcoding
Hampir selalu, setiap binary memiliki proteksi W^X. Proteksi ini melarang penambahkan kode saat program berjalan, karena ini dapat menyebabkan hal yang buruk untuk terjadi. Akan tetapi, terdapat fungsi-fungsi pada libc yang daapt mengubah permission pada blok memory, salah satu fungsi adalah mprotect. Mprotect memerlukan tiga parameter, yaitu alamat, panjang, dan prot. Karena kita dapat mengatur tiga parameter pertama secara efektif, kita dapat memanggil fungsi ini dan mengubah permission blok memory tertentu.

Berikut adalah contoh, dalam binary ini aku akan mengubah bss dari rw- menjadi rwx. Dengan menggunakan fungsi seperti fgets, kita dapat menulis shellcode pada bss dan mengeksekusinya.

Error

Error

Error

8. One gadget RCE
Kedua teknik sebelum memerlukan penyerang untuk mengatur parameter fungsi sebelum memanggil fungsinya. Ternyata, terdapat potongan kode didalam libc yang dapat digunakan untuk menjalankan shell tanpa harus mengatur parameter fungsi. Akan tetapi, terdapat berbagai kondisi yang harus dipenuhi agar dapat menggunakannya. Biasanya kondisinya terdiri dari mengatur register dan lokasi memory menjadi nilai tertentu.

Untuk mencari gadget-gadget ini, kita dapat menggunakan sebuah tool bernama one_gadget. Berikut contoh penggunaan one_gadget:

Error

9. Binary statis
Dalam beberapa kasus, program bisa di-compile secara statis daripada dinamis. Akibat ini, proteksi PIE pasti tidak hidup (untuk nasm), akan tetapi beberapa fungsi libc seperti system tidak bisa digunakan.

Pada kasus ini, terdapat dua hal yang aku biasanya lakukan, yaitu memanggil fungsi _dl_make_stack_executable, atau memanggil syscall hanya dengan menggunakan gadget.

9.1. _dl_make_stack_executable
Pada binary yang di-compile secara statis, terdapat sebuah fungsi yang bernama _dl_make_stack_executable. Fungsi ini dapat memanggil syscall untuk membuat permission stack menjadi rwx, tapi membutuhkan dua kondisi dipenuhi. Mari kita liat disassembly-nya:

0x00000000004717e0 <+0>:	mov    rsi,QWORD PTR [rip+0x250a49]        # 0x6c2230 <_dl_pagesize>
0x00000000004717e7 <+7>:	push   rbx
0x00000000004717e8 <+8>:	mov    rbx,rdi
0x00000000004717eb <+11>:	mov    rax,QWORD PTR [rdi]
0x00000000004717ee <+14>:	mov    rdi,rsi
0x00000000004717f1 <+17>:	neg    rdi
0x00000000004717f4 <+20>:	and    rdi,rax
0x00000000004717f7 <+23>:	cmp    rax,QWORD PTR [rip+0x24f78a]        # 0x6c0f88 <__libc_stack_end>
0x00000000004717fe <+30>:	jne    0x47181f <_dl_make_stack_executable+63>
0x0000000000471800 <+32>:	mov    edx,DWORD PTR [rip+0x24f7da]        # 0x6c0fe0 <__stack_prot>
0x0000000000471806 <+38>:	call   0x435690 <mprotect>
0x000000000047180b <+43>:	test   eax,eax
0x000000000047180d <+45>:	jne    0x471826 <_dl_make_stack_executable+70>
0x000000000047180f <+47>:	mov    QWORD PTR [rbx],0x0
0x0000000000471816 <+54>:	or     DWORD PTR [rip+0x2509f3],0x1        # 0x6c2210 <_dl_stack_flags>
0x000000000047181d <+61>:	pop    rbx
0x000000000047181e <+62>:	ret    
0x000000000047181f <+63>:	mov    eax,0x1
0x0000000000471824 <+68>:	pop    rbx
0x0000000000471825 <+69>:	ret    
0x0000000000471826 <+70>:	mov    rax,0xffffffffffffffc0
0x000000000047182d <+77>:	pop    rbx
0x000000000047182e <+78>:	mov    eax,DWORD PTR fs:[rax]

Ini merupakan binary 64-bit, jadi parameter fungsinya disimpan di dalam register. Melihat dengan baik, sepertinya jika rdi diatur menjadi alamat __libc_stack_end dan jika __stack_prot diatur menjadi 7, kita bisa mendapatkan panggilan ke mprotect.

9.2. Gadget syscall
Teknik lain yang saya suka gunakan adalah membuat suatu ropchain sehingga kita bisa memanggil suatu syscall. Pada 64-bit, aku perlu mengatur rax, rdi, dan rsi serta memanggil instruksi syscall. Pada 32-bit, aku perlu mengatur eax, ebx, dan ecx serta memanggil sebuah instruksi int 0x80. Cara ini mungkin terlihat susah, tapi ini jauh lebih gampang daripada memanggil _dl_make_stack_executable.

10. Memindahkan stack
Teknik terakhir yang akan aku jelaskan adalah memindahkan stack. Intinya, apa yang kita anggap sebagai “stack” adalah suatu konsep abstrak. Stack pada dasarnya adalah tempat dimana stack pointer menujuk pada. Jika kita dapat mengubah nilai stack pointer untuk menunjuk ke lokasi berbeda di memory, kita dapat menyebutkan itu sebagai “stack”.

Untuk apa kita melakukan ini? Jadi pada beberapa binary ropchain yang dapat kita buat bisa saja terbatas, hanya bisa buat ropchain sebesar 2-3. Pada kasus ini, dalam 64-bit mengatur rdi dan melompat ke puts serta balik ke main tidak bisa.

Tapi gimana jika kita bisa menulis ropchain di tempat yang berbeda. Misal, bss? Atau mungkin heap? Untuk menggunakan lokasi itu untuk menyimpan ropchain, kita perlu mengubah nilai stack register menjadi alamat tempat itu. Pada kasus ini, kita bisa mencoba cari instruksi seperti mov rsp, rax, tapi itu hampir tidak pernah ada di dalam binary yang di-compile secara dinamis

Daripada menggunakan instruksi biasa, kita dapat menggunakan instruksi leave. Menurut Nasm docs, leave melakukan dua hal, yaitu mov esp, ebp lalu pop ebp.

Seperti yang anda sudah ketahui, base pointer terletak pas sebelum return address, jadi kita dapat mengubahnya setiap kali kita ingin mengubah nilai return address. Daripada melompat ke fungsi, dengan melompat ke gadget leave; ret, kita dapat memindahkan stack ke lokasi lain dalam memory.

Mari kita liat contoh, pada binary ini, aku telah membuat ropchian di heap, dan sudah mendapatkan sebuah heap leak. Aku akan mencoba untuk memindahkan stack ke lokasi heap dan mendapatkan ropchain yang lebih panjang.

Error

Error

Error

Error

Error

Error

6. Kata penutup

Kesimpulannya, ROP merupakan kelemahan yang sangat bisa dieksploitasi. Ini bukan akhir dari kelemahan ROP, sebab masih terdapat teknik seperti ret2dl_resolve, SROP, dan sebagainya.