0x0 Ghost from the past

BoF

“Smashing the Stack for Fun and Profit”, by Aleph One, Phrack No.49

Buffer Overflow adalah kerentanan (vulnerability) pada sebuah perangkat lunak akibat pemberian input data pada buffer yang diluar kapasitas yang dimilikinya. Kerentanan tersebut menyebabkan terjadinya perubahan perilaku (anomali) pada program tersebut, dimana program dapat melakukan eksekusi instruksi diluar apa yang tertulis pada program tersebut. Situasi ini dimanfaatkan oleh seorang attacker untuk “membajak” program tersebut, untuk melakukan atau mengeksekusi instruksi yang diinginkannya.

Menurut penelitian Yves Younan (senior reseach engineer pada “Sourcefire Vulnerability Research Tea”) terhadap kerentanan pada 25 tahun terakhir pada periode 1988 – 2012 menunjukkan bahwa Buffer Overflow menempati tiga besar kerentanan pada berbagai perangkat lunak yang pernah dihasilkan oleh  vendor-vendor di seluruh dunia. Fakta inilah yang menunjukkan bahwa Buffer Overflow tetap menjadi hantu masa lalu yang tetap hidup hingga kini. Adapun  angka statistik hasil studi tersebut dapat dilihat pada grafik berikut ini:

Screen Shot 2016-09-05 at 9.08.49 PM

Gambar 01. “25 years of vulnerabilities 1988-2012 Research Report”, by Yves Younan “

Beberapa contoh malware yang menggunakan kerentanan ini antara lain adalah Morris Worm, sebuah internet worm pertama yang lahir pada 2 November 1988. Internet worm ini menurut Government Accountability Office telah menimbulkan kerugian mencapai $100,000 – $10,000,000 karena telah menyebabkan kerusakan pada ribuan komputer di Amerika Serikat hanya dalam waktu 15 jam saja. Kejadian ini dipercaya merupakan kejahatan komputer pertama yang melanggar undang-undang Computer Fraud and Abuse Act. Beberapa kasus internet worm lainnya antara lain adalah Witty Worm, Slammer Worm, Blaster Worm, dan seterusnya.

Memperhatikan tingginya kejadian insiden keamanan informasi akibat kerentanan ini, maka sangat penting untuk memahami bagaimana mekanisme terjadinya kerentanan tersebut, serta bagaimana seorang attacker membangun shellcode untuk melakukan eksploitasi pada kerentanan tersebut (exploit development). Dengan pemahaman yang baik maka kita dapat melakukan mitigasi terhadap resiko dari kerentanan ini secara tepat.

Artikel ini akan memulai pembahasan tentang mekanisme kerja sebuah komputer dalam melakukan berbagai instruksi pada perangkat lunak, dilanjutkan dengan pembahasan ringkas tentang arsitektur intel 32 bit (x86) dalam konteks kerentanan BOF, beberapa terminologi dasar, metodologi pengembangan shellcode serta akhirnya akan dilakukan beberapa ilustrasi dan contoh pengembangan exploit (exploit development) yang biasanya dilakukan threat actor untuk memanipulasi  beberapa aplikasi yang memiliki kerentanan tersebut. Baik kerentanan yang bersifat local maupun remote.

Bagaimana komputer bekerja

Untuk memahami bagaimana komputer bekerja, kita akan merujuk pada arsitektur komputer yang dikembangkan oleh Von Neumann pada tahun 1945. Meskipun lebih dari 50 tahun yang lalu, pendekatan arsitektur yang dikembangkan oleh Von Neumann tetap menjadi rujukan hingga kini.

Von_Neumann_Architecture

Gambar 02. Wikipedia: “The Von Neumann Architecture”

Pada dasarnya, komputer terdiri dari dari beberapa komponen yang tersusun menjadi sebuah sistem. Komponen pertama adalah, CPU (Central Processing Unit) sebagai komponen utama yang berfungsi untuk meng-eksekusi instruksi dan data sebagaimana yang diinginkan manusia. CPU mendapatkan instruksi dan data dari komponen kedua, yaitu tempat penyimpanan  yang biasa disebut “Memory Unit”. Selanjutnya, komputer berinteraksi dengan lingkungan eksternal dengan komponen ketiga, yaitu komponen input dan output atau “I/O Devices” seperti keyboard atau mouse untuk perangkat input dan monitor atau printer sebagai perangkat output.

Berdasarkan arsitektur tersebut, dapat dimengerti bagaimana komputer bekerja. Diawali dengan adanya input berupa program dan data oleh manusia, yang kemudian diterima dan disimpan didalam memori, selanjutnya CPU mengambil instruksi dan data tersebut dari memory serta mengeksekusinya, mengembalikan hasil eksekusi tersebut ke memory dan akhirnya menampilkan melalui perangkat output.

Registers dan Memory

Dari perspektif kerentanan buffer overflow, sangat penting untuk memahami bagaimana interaksi antara CPU dan Memory Unit, karena berbagai instruksi dan data menggunakan lokasi-lokasi pada memory dan tempat penyimpanan sementara di dalam CPU yang disebut register. Biasanya attacker akan mencermati pergerakan instuksi dan data dari satu lokasi ke lokasi lainnya baik di memory dan register untuk melakukan analisis dan langkah-langkah serangan yang tepat sehingga dapat “mengakali” dan  “membajak” CPU.

CPU atau prosesor adalah pengambil keputusan (decision masker) pada komputer, terkait instruksi apa yang akan dilakukan. Untuk melaksanakan peran dan fungsinya, ia menggunakan tempat penyimpanan sementara yang disebut register. Mengingat setiap prosesor memiliki arsitektur dan instruction set yang berbeda-beda, maka pada artikel ini hanya dibatasi pada prosesor arsitektur intel 32byte atau IA-32.

Apabila dicermati lebih jauh, pada dasarnya CPU memiliki empat komponen dalam menjalankan peran dan fungsinya, yaitu:

  • Control Unit, komponen ini bertanggung jawab untuk mengambil (retrieve and decoding) instruksi dari memory.
  • Execution Unit, komponen ini bertanggung jawab untuk mengeksekusi instruksi.
  • Registers,komponen ini bertanggung jawab menyediakan tempat penyimpanan sementara bagi komponen Execution Unit atau “internal memory within CPU”.
  • Flags, komponen ini bertanggung jawab menyediakan indikator terhadap berbagai kejadian (event) ketika instruksi dilakukan didalam CPU.

Screen Shot 2016-09-06 at 9.47.45 AM

Gambar 03. Arsitektur CPU (Central Processing Unit) pada umumnnya

Peran register pada CPU sangat penting, karena register berperan menjadi “penunjuk” sekaligus “penyimpanan sementara” antara  CPU dengan memory. Berikut ini adalah berbagai register pada CPU IA-32, yaitu:

  • General Purpose Registers
  • Segment Registers
  • Flags, EIP
  • Floating Unit Registers (FPU)
  • MMX Registers
  • XMM Registers

screen-shot-2016-09-08-at-4-37-14-pm

Gambar 04. Logical Diagram dari Registers pada CPU (Central Processing Unit)

Terkait dengan kerentanan Buffer Overflow yang menjadi tema artikel ini, maka kita akan mencermati “General Purpose Registers” dan “Flags, EIP“saja. Hal ini disebabkan kedua jenis register tersebut menjadi fokus analisis oleh attacker untuk mencari kerentanan Buffer Overflow serta membuat shellcode untuk mengeksploit kerentanan tersebut.

General Purpose Register terdiri dari enam buah registers yang memiliki fungsi yang berbeda-beda, adapun penjelasan singkatnya dapat dilihat sebagaimana gambar berikut ini. Selain itu masing-masing registers ada yang memiliki ukuran 32 bytes dan ada juga yang memiliki ukuran 16 byites.

screen-shot-2016-09-08-at-1-10-50-pm

Gambar 05. “Fungsi-fungsi pada General Purpose Registers (GPR)”

stackbasedbufferoverflow004

Gambar 06. “Ukuran berbagai registers pada General Purpose Registers (GPR)”

Dari perspektif kerentanan buffer overflow, ada dua registers pada General Purpose Registers yang patut untuk diperhatikan, baik dalam melakukan analisis kerentanan maupun dalam mengembangkan shellcode untuk melakukan eksploitasi terhadap kerentanan tersebut (exploit development). Kedua registers tersebut adalah ESP dan EBP.

Kedua registers tersebut digunakan CPU untuk mengelola stack pada memory, khususnya untuk menunjukkan alamat memory yang digunakan untuk menyimpan instruksi dan data pada stack. Stack adalah sebuah area di memory yang bersifat tetap (statis) ukurannya dan berfungsi menyimpan antara lain argumen pada fungsi dan variabel lokal. Berbeda dengan heap yang merupakan sebuah  area di memory yang digunakan untuk menyimpan variabel-variabel yang bersifat dinamis. Adapun visualisasi stack pada memory dapat diperhatikan pada gambar berikut ini:

screen-shot-2016-09-08-at-2-13-09-pm

Gambar 07. “Stack pada memory lay-out”

Cara CPU dalam menggunakan stack adalah dengan memperhatikan prinsip LIFO atau Last In First Out, artinya instruksi yang terakhir diletakkan pada stack adalah instruksi yang pertama kali dieksekusi. Untuk menambahkan instruksi digunakan operasi PUSH pada stack, sedangkan untuk mengambil instruksi digunakan operasi POP. Dengan demikian terjadi pergeseran alamat dan lokasi diantara instuksi-instruksi pada stack secara dinamis, akibat kedua operasi stack tersebut. Adapun pertumbuhan jumlah instruksi dan data yang ditambahkan pada stack arahnya dari high memory menuju low memory. Arah pertumbuhan ini berbeda dengan heap, yang tumbuh dari low memory ke high memory.

Untuk memudahkan melakukan analisis tentang dinamika yang terjadi diantara Register, Stack dan Memory maka tersedia berbagai tools debugger. Salah satu tools populer yang digunakan pada artikel ini adalah Immunity Debugger. Secara Visual ketiga komponen tersebut dapat dilihat pada gambar 08 sebagaimana berikut ini.

screen-shot-2016-09-15-at-6-08-17-pm

“Gambar 08. Register, Stack dan Memory pada Immunity Debugger”

Kemudian pertanyaanya, apa kaitannya antara Buffer Overflow dengan kedua registers pada CPU (ESP dan EBP) tersebut? Mengapa kedua registers tersebut penting dianalisis? Jawabannya adalah  ESP adalah registers pada CPU yang berfungsi sebagai penunjuk alamat stack yang paling atas sedangkan EBP adalah penunjuk alamat stack yang paling bawah. Kedua penunjuk alamat memory (registers) tersebut penting untuk menganalisis dan menelusuri instruksi-instruksi yang disimpan pada stack ketika sedang dieksekusi satu persatu oleh CPU.

Selanjutnya, registers lainnya yang penting untuk diperhatikan selama analisis kerentanan buffer overflow adalah EFLAGS registers. Ia merupakan registers yang berfungsi untuk berbagai event yang terjadi pada CPU selama eksekusi instruksi, biasanya registers ini akan berperan penting ketika melakukan analisis terhadap conditional statement pada bahasa assembly.

Bagian terpenting dari EFLAGS registers yang relevan dengan tema artikel ini adalah EIP atau Instruction Pointer. Registers ini sangat urgent untuk di analisis, karena ia menunjukkan pada CPU tentang instruksi selanjutnya pada memory yang harus dieksekusi. Dengan demikian, apabila EIP dimanipulasi dan dapat “dikuasai” maka attacker dapat memerintahkan CPU untuk melakukan eksekusi terhadap instruksi yang diinginkan olehnya.

Memperhatikan pentingnya peran EIP maka ia merupakan fokus dalam melakukan analisis kerentanan buffer overflow dan sekaligus merupakan target dari pengembangan shellcode  untuk melakukan eksploitasi terhadap kerentanan tersebut. EIP merupakan registers dengan ukuran 32 byte sebagaimana dapat diamati pada gambar sebelumnya.

Memahami Stack-Based Buffer Overflow

Berdasarkan pembahasan sebelumnya, mari kita pelajari salah satu contoh buffer overflow pada stack atau “Stack-Based Buffer Overflow” dengan membuat program sederhana dalam bahasa C  yang berjalan diatas sistem operasi Windows XP SP3 32 bit. Source code program tersebut adalah sebagai berikut (Gambar 09):

screen-shot-2016-09-15-at-2-31-51-pmGambar 09. Source code untuk meminta input dan mencetak ke layar

screen-shot-2016-09-15-at-3-39-47-pm

Gambar 10. Source code setelah dicompile dalam bentuk bahasa assembly

Program sederhana pada gambar 09 bertujuan untuk meminta input dari pengguna dan menampilkannya kembali pada layar. Letak kelemahan pada source code sederhana ini adalah penggunaan fungsi “gets” yang tidak memiliki kemampuan untuk memvalidasi jumlah karakter yang diinput oleh pengguna.

Selanjutnya setelah source code tersebut di-compile, maka diberikan input karakter “A” yang jumlahnya meningkat secara bertahap, sebagaimana gambar 11 berikut ini.

screen-shot-2016-09-15-at-3-17-36-pm

Gambar 11. Input karakter “A” diberikan dengan jumlah karakter berbeda-beda

Pada gambar 11 ada yang menarik kita cermati. Program diberikan input “A” sebanyak lima kali. Pada eksperimen pertama diberikan input karakter “A” sebanyak 4 karakter dan program mencetak kembali ke layar sebanyak 4 karakter, artinya program berjalan normal. Begitu juga dengan eksperimen ke 2 hingga ke 4. Adapun situasi antara registers, stack dan memory pada saat kondisi normal dapat dilihat pada gambar 12 sebagai berikut ini.

Namun, pada eksperimen kelima, dimana program diberikan input sebayak 40 karakter “A”, hasilnya terlihat  program “crash”. Pada pesan error yang tampil terlihat “Offset:41414141“.  Fenomena ini terjadi karena input yang diberikan pengguna melebihi kapasitas stack yang pada source code hanya dibatasi maksimal 10 karakter saja (lihat line nomer 10 pada gambar 09). Jumlah karakter yang melebihi kapasitas buffer ini tidak dilakukan validasi oleh fungsi “get” yang digunakan oleh source code pada baris ke-12 tersebut. Kondisi ini disebut sebagai buffer over flow, yang disebabkan fungsi GET tidak melakukan filter terhadap input dan kapasitas buffer.

screen-shot-2016-09-15-at-4-04-45-pm

“Gambar 11.  Kondisi Registers dan Stack pada saat program crash”

Untuk memahami kondisi “buffer over flow” ini, patut diingat bahwa pertumbuhan input karakter yang memasuki stack bergerak dari “Low Memory” menuju “High Memory” (hal ini bertolak belakang dengan arah pertumbuhan stack, yaitu dari “High Memory” menuju “Low Memory”). Terlihat bahwa karakter “A” bergerak dari alamat stack paling tinggi yaitu register ESP  (0022FED0) ke alamat memory yang rendah pada stack frame, yaitu (0022FEE4). Fenomena ini  dapat diperhatikan pada gambar 12 dan secara visual pada gambar 13 sebagai berikut.

screen-shot-2016-09-15-at-10-18-35-pm “Gambar 12. Karakter A memenuhi alamat stack dari yang paling tinggi ke rendah”

screen-shot-2016-09-15-at-10-09-15-pm“Gambar 13. Karakter A memenuhi dari ESP hingga Return Address pada saat crash”

Memperhatikan gambar 13 terlihat bahwa terjadi kerusakan pada stack akibat jumlah karakter input yang melebihi kapasitas buffer. Akibatnya karakter “A” memenuhi stack dari register ESP, EBP hingga Return Address. Hal ini dapat dibandingkan situasi stack pada kondisi normal seperti terlihat pada gambar 14.

screen-shot-2016-09-15-at-10-38-41-pm

“Gambar 14. Karakter A pada ESP, EBP dan Return Address pada saat normal”

Situasi “buffer over flow” pada stack akibat jumlah karakter yang melampaui kapasitas buffer, menyebabkan “return address” mengalami anomali. Ia dapat dimanipulasi untuk melakukan eksekusi instruksi sesuai keinginan attacker. Padahal instruksi tersebut tidak pernah ada dan tidak pernah dibuat oleh pembuat source code (programmer).

Memahami Return Address

Secara sederhana Return Address adalah alamat pada memory tempat kembalinya instruksi pada fungsi  (function) pada program utama (main program). Ketika kita memanggil sebuah fungsi dari program utama untuk melakukan eksekusi terhadap instruksi tertentu, maka fungsi tersebut akan “pergi” dari program utama. Ketika fungsi tersebut selesai melakukan eksekusi terhadap instruksi dan akan memberikan hasil dari eksekusinya kepada program utama, maka ia akan “kembali” menuju program utama. Alamat pada stack atau memory tempat fungsi tersebut kembali “kembali“, disebut Return Address. Sedangkan malicious instruction yang seharusnya tidak boleh di-eksekusi oleh fungsi tersebut biasanya disajikan dalam bentuk shellcode

Untuk menjelaskan konsep return address secara sederhana, marilah kita kembali pada source code pada gambar 9. Dapat dipahami bahwa alur logika  pada program utama  akan mengeksekusi function_1.  Selanjutnya function_1 akan mengeksekusi instruksi satu demi satu pada blok miliknya. Setelah seluruh instruksi pada function_1 selesai dieksekusi maka hasil akhirnya diserahkan kembali pada program utama. Nah, return address adalah alamat pada memori yang menjadi tujuan function_1 untuk menyerahkan hasil akhir instruksi yang telah dilakukannya.

Untuk memahami bagaimana attacker melakukan manipulasi terhadap return address pada saat stack mengalami crash, mari kita modifikasi source code pada gambar 09. Yaitu kita akan menambahkan sebuah fungsi hijacked (lihat gambar 15), dimana fungsi ini tidak pernah dipanggil oleh program utama. Kita akan memanipulasi program tersebut agar mengeksekusi fungsi hijacked yang tidak pernah diperintahkan oleh program utama untuk dieksekusi. 

screen-shot-2016-09-15-at-11-14-39-pm

“Gambar 15. Menambahkan fungsi yang tidak diperintahkan oleh program utama”

screen-shot-2016-09-15-at-11-26-01-pm

“Gambar 16. Source code setelah dicompile menjadi bahasa assembly”

Pada gambar 16 terlihat bahwa pada alamat 00401572 program utama menginstruksikan untuk memanggil “Function 1”. Setelah seluruh instruksi pada fungsi tersebut dieksekusi, maka hasilnya akan diserahkan ke program utama pada alamat 00401577, yaitu alamat  kembalinya “Function 1” ke program utama. Alamat “return address” dapat dilihat pada gambar 17. Alamat “return address” di-push ke dalam stack, yang dapat dilihat pada register ESP yang menunjukkan alamat tersebut.

Screen Shot 2017-03-25 at 7.42.34 AM

“Gambar 17. Return Address pada Stack”

Kita akan mengubah alamat pada return address untuk memanipulasi program utama agar melakukan eksekusi terhadap instruksi yang diperintahkan oleh attacker, yaitu “function_hijacked”. Adapun alamat dari “function_hijacked” adalah 00401500, sebagaimana dapat dilihat pada immunity debugger (lihat gambar 18).

Screen Shot 2017-03-25 at 7.57.43 AM

“Gambar 18. Alamat “function_hijacked” adalah 00401500″

Untuk mengubah dan mengganti alamat “return address” dengan alamat dari “function_hijacked” yaitu dari 00401577 menjadi 00401500, maka kita melakukan perubahan pada stack di alamat yang ditunjuk oleh register ESP. Hal ini dapat dilihat pada  tiga berikut ini, yaitu: gambar 19, gambar 20 dan gambar 21.

Screen Shot 2017-03-25 at 8.12.24 AM

“Gambar 19. Alamat return address pada stack yang ditunjuk oleh register ESP sebelum dimanipulasi yaitu 00401577”

Screen Shot 2017-03-25 at 8.16.46 AM

“Gambar 20. Mengubah alamat return address pada stack yang ditunjuk oleh register ESP  menjadi 00401500”

Screen Shot 2017-03-25 at 8.21.46 AM

“Gambar 21. Alamat return address pada stack yang ditunjuk oleh register ESP  setelah dimanipulasi menjadi 00401500”

Selanjutnya setelah return address dimanipulasi untuk menjalankan instruksi pada “function_hijacked”, maka kita menjalankan program tersebut. Setelah program dieksekusi dan kita memberikan input “AAA”, terlihat perubahan perilaku program dibandingkan pada kondisi normal, yaitu ia mengeksekusi “function_hijacked” sehingga terlihat “[+]Execution hijacked bro … !” pada layar terminal. Hal ini dapat kita lihat pada gambar 22 sebagaimana berikut ini.

Screen Shot 2017-03-25 at 8.32.09 AM

“Gambar 22. Program melakukan eksekusi terhadap instruksi yang diperintahkan oleh attacker pada function_hijacked” 

Instruksi pada “function_hijacked” dapat diganti dengan perintah yang diinginkan oleh attacker. Biasanya perintah tersebut disebut shellcode. Ia berisi rangkaian instruksi pada tingkat binary dimana dapat langsung dieksekusi oleh CPU.

Shellcode sederhana

Pada bagian ini kita akan melanjutkan eksperimen dengan membuat shellcode. Untuk membuat Shellcode dapat dilakukan dengan bantuan tools bernama msfvenom yang merupakan bagian dari metasploit. Shellcode yang dibuat akan membuat backdoor pada komputer dan listening pada port 4444 sebagaimana gambar 23 berikut ini.

Screen Shot 2017-03-25 at 4.44.31 PM

“Gambar 23. Membuat shellcode sederhana dengan bantuan tools msfvenom”

Selanjutnya, shellcode yang dihasilkan digunakan untuk menggantikan fungsi “function_hijacked” pada source code yang telah dibahas sebelumnya pada pada gambar 15. Berikut ini adalah source code setelah di-update dengan menggunakan shellcode (gambar 24).

Screen Shot 2017-03-25 at 5.48.20 PM

“Gambar 24. Source code setelah di-update dengan shellcode”

Setelah source code di-update, program tersebut dieksekusi dengan melakukan langkah -langkah yang serupa pada bagian sebelumnya, yaitu dengan memanipulasi return address seperti pada beberapa gambar berikut ini, yaitu gambar

Screen Shot 2017-03-25 at 6.10.08 PM

“Gambar 25. Lokasi shellcode pada memory berada pada alamat 00403030”

Screen Shot 2017-03-25 at 6.58.34 PM

“Gambar 26. Mengganti return address dungan alamat 00403030”

Screen Shot 2017-03-25 at 6.25.25 PM

“Gambar 27.  Setelah program di-eksekusi dan merubah return address sesuai lokasi shellcode, maka backdoor membuat port 4444 listening”

Screen Shot 2017-03-25 at 5.23.34 PM

“Gambar 28.  Menjalankan netcat untuk meng-akses port 4444”

Kesimpulan

Berdasarkan uraian dan eksperimen yang dilakukan, dapat disimpulkan bahwa kerentanan buffer over flow pada stack ditimbulkan oleh kesalahan dalam pembuatan program yang tidak melakukan filter terhadap jumlah karakter yang diberikan terhadap kapasitas buffer pada stack yang disediakan. Akibatnya aplikasi menjadi crashed.

Attacker memanfaatkan kondisi ini dengan memanipulasi return address untuk menjalankan instruksi jahat (malicious instruction) yang mereka inginkan. Biasanya malicious instruction itu disebut shellcode, sebuah rangkaian instruksi dalam format hexadecimal yang langsung dieksekusi ditingkat binary. Berbagai jenis dan tipe malicious intruction dapat dilakukan sesuai strategi dan taktik attacker untuk mewujudkan niat jahat mereka.