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:
- Buffer overflows
- Registers, especially the stack pointer
- Function calls in assembly
- Pointers
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.
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):
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.
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.
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:
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.
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.