Secara
bahasa polymorphic artinya banyak bentuk. Polymorphic shellcode adalah
shellcode yang mempunyai banyak bentuk. Dari satu induk shellcode yang
sama bisa dilahirkan banyak sekali shellcode yang berbeda-beda dalam
level bit, maksudnya bila dilihat bit per bit semua shellcode tersebut
berbeda total, padahal semua berasal dari satu induk.
Polymorphic
shellcode diperlukan untuk bisa lolos dari deteksi Intrusion
Detection/Prevention System. IDS/IPS memeriksa paket data yang lewat.
Bila paket tersebut mengandung data yang dianggap berbahaya, maka satpam
akan membunyikan alarm atau mencegah paket tersebut lewat.
Perhatikan ilustrasi berikut ini.
Teroris bernama Bush (bukan nama sebenarnya), sebelumnya pernah berhasil meledakkan sasaran dan membunuh ribuan bayi di Irak, namun kini fotonya sudah diketahui semua orang sehingga dia tidak leluasa lagi melakukan serangan berikutnya.Agar serangan berikutnya berjalan lancar, Bush harus mengubah wajahnya dengan operasi plastik, menumbuhkan kumis, mengubah rambut dsb. Dengan wajah yang berbeda total, maka polisi tidak akan mengenali Bush, dan Bush bisa melakukan serangan dengan lancar.Dalam setiap serangannya Bush harus mengubah wajahnya agar berbeda dari wajahnya pada serangan-serangan sebelumnya.
Begitulah
ilustrasi dari polymorphic shellcode, ketika sebuah shellcode sudah
pernah dipakai dan signaturenya (ciri-ciri) sudah di-blacklist oleh
IDS/IPS, maka shellcode tersebut sudah berkurang efektivitasnya. Bila
shellcode yang sama dipakai lagi, maka IDS/IPS akan dengan mudah
mendeteksi dan mencegah serangan itu.
Untuk
menipu IDS/IPS maka shellcode sebelum dipakai dalam exploit harus
mengalami mutasi yaitu mengubah bentuk fisiknya tanpa mengubah
fungsinya. Ingat shellcode adalah kumpulan byte opcode yang
merepresentasikan instruksi assembly/bahasa mesin. Polymorphic shellcode
berarti bahwa instruksi assembly bisa berubah menjadi banyak macam
tetapi tidak mengubah fungsi utama dan hasil akhirnya.
Lho
kok bisa? Mudah saja, sebagai contoh bayangkan bahwa algoritma utamanya
adalah formula A+B*2. Kita bisa mutasi-kan formula itu menjadi banyak
bentuk:
- B*2+A
- B+B+A
- B+1+B+A-1
- B*2+B*3+A-B*2-B
Semua
mutasi di atas menghasilkan hasil akhir yang sama persis, walaupun
formulanya jauh berbeda. IDS/IPS yang hanya mem-blacklist “A+B*2″ tidak
akan menganggap paket berisi “B+B+A” atau “B+1+B+A-1″ sebagai paket
berbahaya karena tidak ada dalam kamus blacklistnya, padahal semuanya
sama saja hanya bentuknya saja yang berbeda.
Gambar
di atas adalah software untuk mengubah bentuk wajah dengan mengganti
bentuk mata, alis, rambut, kumis dsb. Semua shellcode bisa di-mutasi
menghasilkan shellcode baru yang berbeda namun tetap dengan fungsi yang
sama menggunakan script “Mutation Engine”. Mutation engine ini bisa
dibayangkan mirip dengan gambar di atas, sebuah software yang mempunyai
fasilitas untuk mengubah-ubah bentuk mata, alis, kumis, hidung untuk
membuat wajah baru yang berbeda. Namun tentu saja mutation engine
melakukan mutasi secara otomatis, tanpa harus menunggu inputan/klik dari
pengguna.
Semi Polymorphic Shellcode
Saya
akan mulai dengan membuat shellcode yang sifatnya semi polymorphic.
Semi disini berarti shellcode yang dihasilkan tidak total berbeda antar
hasil mutasi dari satu shellcode yang sama. Masih ada consecutive byte,
byte yang berurutan yang bisa dijadikan ciri khas (signature) dari
shellcode tersebut.
Semua
polymorphic shellcode dibuat dengan menggunakan teknik
encoding/decoding dengan prosedur decoder ditempatkan di awal shellcode,
hanya bedanya pada semi polymorphic, prosedur decoder relatif statis,
tidak ikut ter-mutasi. Sedangkan pada true polymorphic shellcode,
prosedur/rutin decodernya juga ikut termutasi sehingga lebih sulit
dideteksi IDS/IPS.
Algoritma
encode/decode yang dipakai tidak rumit, hanya menggunakan operasi
logika XOR. Sifat dari logika XOR adalah reversible, jika suatu bilangan
di-XOR dua kali dengan kunci yang sama, maka akan menghasilkan nilai
awalnya.
Contoh:
11001 (25) XOR 11100 (28) = 00101 (5) XOR 11100 (28) = 11001 (25)
Kenapa
diperlukan decoder? Ingat shellcode adalah kumpulan byte opcode bahasa
mesin, jadi bila shellcode tersebut di-encode maka byte opcode menjadi
opcode yang berbeda atau menjadi opcode yang tidak dikenal prosesor.
Decoder bertugas untuk mengembalikan byte shellcode yang ter-encode
menjadi normal kembali sehingga bisa dikenal dan dieksekusi prosesor.
Contohnya
bila dalam shellcode mengandung byte opcode \xCD\x80 yang dikenal
prosesor sebagai interrupt no 80 hexa. Dalam proses mutasi, opcode CD80
di-encode dengan XOR 5 menjadi \xC8\x85 yang tidak dikenal oleh prosesor
(bukan instruksi yang valid). Agar shellcode bisa dieksekusi maka
decoder harus mengembalikan \xC8\x85 menjadi normal kembali \xCD\x80.
Gambar
di atas memperlihatkan proses mutasi dari original shellcode menjadi
shellcode yang telah termutasi. Mutation engine di atas menggunakan satu
decoder yang sama dan menghasilkan banyak shellcode sesuai dengan key
yang dipakai. Key ini dipakai untuk encode dan decode menggunakan
operasi logika XOR. Setiap shellcode hasil mutation engine terdiri dari
decoder di awal dan shellcode yang ter-encode.
Menghindari Karakter Terlarang
Umumnya
shellcode di-injeksi melalui input program sebagai tipe data string.
Secara internal string adalah array of character yang diakhiri dengan
karakter NULL (‘\0′). Byte NULL tidak boleh ada dalam shellcode karena
bisa membuat shellcode gagal di-injeksi secara penuh. Byte NULL adalah
salah satu yang disebut dengan ‘bad characters’, yaitu karakter yang
terlarang ada dalam shellcode.
Bad
characters bisa berbeda-beda, tergantung dari aplikasi yang akan
di-exploit. Bila dalam aplikasi tersebut, keberadaan karakter new line
(\n) dan enter (\r) membuat shellcode gagal terinjeksi dengan sempurna,
maka character itu jangan sampai ada dalam shellcode.
Namun
terkadang sulit untuk menghindari adanya karakter terlarang dalam
shellcode. Teknik encoding shellcode ini bisa juga dipakai untuk
menghilangkan karakter terlarang. Jadi teknik ini tidak hanya berguna
untuk menghindari tertangkap IDS/IPS tapi juga membantu menghindari
karakter terlarang.
JMP/CALL GetPC
Instruksi
yang pertama dieksekusi adalah decoder. Decoder ini bertugas untuk
melakukan decode dengan operator XOR menggunakan kunci yang sama pada
waktu encoding. Masalahnya adalah shellcode ini bisa diload di alamat
memori berapapun, jadi tidak bisa di-harcode lokasinya sejak awal dalam
rutin decoder. Decoder harus tahu pada saat dieksekusi (run-time), di
mana lokasi memori tempat penyimpanan shellcode ter-encode.
Teknik
mencari lokasi memori dirinya ketika dieksekusi ini disebut dengan
GETPC (get program counter/EIP). Trik yang biasa dipakai adalah
menggunakan instruksi JMP dan CALL. Decoder akan JMP ke lokasi tepat di
atas (sebelum) shellcode yang ter-encode. Pada lokasi tersebut ada
instruksi CALL ke lokasi sesudah instruksi JUMP tadi. CALL akan mem-push
ke dalam stack return address, yaitu alamat memori instruksi sesudah
CALL. Karena lokasi shellcode ter-encode tepat sesudah instruksi CALL,
maka dalam puncak stack akan berisi alamat memori (EIP/PC) shellcode
ter-encode.
Tidak
seperti umumnya instruksi CALL yang diikuti dengan RET, dalam trik ini
kita tidak memerlukan instruksi RET karena kita tidak sedang benar-benar
memanggil subroutine. Instruksi CALL dimanfaatkan untuk mengambil
EIP/PC dari instruksi sesudah CALL.
Gambar
di atas menunjukkan alur JMP/CALL untuk mendapatkan lokasi memori
shellcode ter-encode. Pertama-tama decoder akan JMP ke lokasi point1,
yang di sana ada instruksi CALL ke point2. Tepat di bawah CALL point2
adalah lokasi memori di mana shellcode ter-encode berada. Jadi ketika
CALL dieksekusi lokasi encoded_shellcode akan di-push ke dalam stack
sebagai return address dari instruksi CALL. Pada point2, terdapat
instruksi POP ESI yang maksudnya adalah mengambl return address
instruksi CALL pada point1, yaitu lokasi memori shellcode ter-encode.
Assembly Decoder
Kita
langsung saja buat kode assembly yang melakukan decoding dengan operasi
logika XOR. Kita memanfaatkan teknik GETPC JMP/CALL seperti alur pada
gambar di atas.
Proses
decoding di atas dilakukan dengan melakukan XOR dalam loop yang dimulai
dari lokasi encoded_sc (yang disimpan di ESI) sebanyak ukuran encoded
shellcode (yang disimpan di ECX). Sebelumnya lokasi encoded_sc diketahui
dengan melakukan trik JMP/CALL GETPC dan lokasi encoded_sc disimpan di
register ESI. Setelah loop selesai, shellcode telah kembali normal dan
siap dieksekusi. Jadi setelah loop, ada instruksi JUMP ke lokasi
encoded_sc.
Sekarang kita akan mengambil opcodenya dengan cara compile, link dan melakukan objdump.
Sedikit
penjelasan mengenai opcode di atas untuk menambah pengetahuan assembly.
Di awal ada instruksi “JMP point1″. Opcode untuk JMP adalah 0xEB.
Perhatikan point1 terletak 13 byte setelah instruksi ini, oleh karena
itu opcodenya adalah “0xEB 0x0D”, yang artinya Jump sejauh 0x0D hex (13)
byte setelah instruksi ini.
Sebagai
ilustrasi perhatikan output objdump di atas, instruksi “JMP point1″ ada
di lokasi memori 0×8048060, dan kita tahu instruksi “JMP point1″
memakan ruang 2 byte (0xEB dan 0x0D), maka tujuan lompatannya adalah
0×8048060 + 2 + 13 = 0x804806f. Sekali lagi ingat, dihitungnya dari
lokasi sesudah instruksi JMP, yaitu 0×8048062. Kemudian dihitung 0x0D
(13 byte) dari lokasi 0×8048062 menjadi 0x804806f.
Sedangkan
pada point1, ada instruksi “CALL point2″ yang memakan ruang 5 byte
(0xE8 0xEE 0xFF 0xFF 0xFF). 0xE8 adalah opcode untuk CALL, sedangkan
0xEE 0xFF 0xFF 0xFF dalam notasi little-endian adalah 0xFFFFFFEE yang
merupakan representasi bilangan signed integer -18.
Kenapa
kok jaraknya -18 ? Perhatikan lagi output objdump di atas. Ingat, mirip
dengan JMP jarak lompatan dihitung dari lokasi sesudah instruksi CALL.
Lokasi memori sesudah instruksi “CALL point2″ adalah 0x804806f+5 =
0×8048074. Kalau kita hitung 18 byte sebelum 0×8048074, maka kita
dapatkan lokasi 0×8048062, yang tidak lain adalah lokasi point2.
Sekarang kita extract opcodenya dan menuliskannya dalam notasi shellcode hexadecimal.
Pada
gambar di atas terlihat opcode dari decoder yang akan kita pakai.
Perhatikan ada dua byte yang berwarna biru, yaitu ukuran shellcode pada
index ke-6 dan kunci XOR pada index ke-9. Nantinya hanya dua byte itu
saja yang berubah dalam setiap mutasi shellcode, byte selain itu selalu
sama oleh karena itu kita tidak mengatakan true-polymorphic tetapi hanya
semi-polymorphic.
Encoder: Mutation Engine
Kini
setelah kita memiliki opcode decoder, kita bisa mulai membuat mutation
engine, yaitu script yang melakukan encoding dan menghasilkan encoded
shellcode. Kita membuat dalam bahasa C, dan sebagai induk kita gunakan
shellcode yang kita pakai dalam local exploit di artikel “belajar membuat shellcode part 1“.
Sekarang kita coba jalankan dan kita lihat hasil mutasinya sebanyak 3 kali.
Pada
gambar di atas, mutation engine menghasilkan 3 mutant dengan 3 kunci
yaitu 29h, 1Ah, 45h. Pada bagian decoder, hampir tidak ada bedanya, yang
berbeda hanyalah pada byte yang menyimpan kunci XOR dan shellcode size.
Kunci
XOR disimpan di decoder pada opcode “\x80\x36\x00″, yang berarti
instruksi assembly “xor byte[esi],0×0″. Bila \x00 pada opcode
“\x80\x36\x00″ diganti menjadi x29, maka berarti kita juga memodifikasi
instruksi assemblynya menjadi “xor byte[esi],0×29″. Begitu juga bila
kita mengganti dengan \x1A dan \x45.
Opcode
“\xb1\x23″ pada decoder adalah shellcode size, yang dalam assembly
berarti “mov cl,0×23″. Dalam program tersebut kebetulan kita memakai
shellcode berukuran 35 byte (23h). Bila shellcode yang dipakai ukurannya
adalah 50 byte, maka mutation engine akan mengganti menjadi “\xb1\x32″
yang dalam assembly berarti “mov cl,0×32″.
Sedangkan
untuk bagian encoded shellcode yang berwarna ungu, dari 3 kali
dijalankan, mutation engine menghasilkan 3 encoded shellcode yang jauh
berbeda, inilah yang disebut dengan polymorphic. Namun tentu saja karena
decodernya statik, hasilnya tidak true-polymorphic, tapi cukup kita
sebut semi-polymorphic saja.
Sekarang
kita coba execute shellcode hasil mutasi dengan kunci 1Ah di atas. Saya
akan gunakan program kecil dalam bahasa C di bawah ini.
Kita akan debug dengan GDB untuk melihat dalam memori apa yang terjadi sebelum dan sesudah decoder dieksekusi.
Jangan lupa matikan dulu exec-shield dengan cara: echo “0″ > /proc/sys/kernel/exec-shield. Bila anda memakai kernel-PAE maka shellcode ini tidak bisa dieksekusi karena pada kernel-PAE ada fitur NX-bit.
Di
atas adalah hasil disassembly dari shellcode sebelum decoder
dijalankan. Instruksinya bukan instruksi shellcode original. Sekarang
kita coba run dan lihat instruksi assembly hasil decodingnya.
Karena
kita ingin melihat hasil decodingnya, maka kita pasang breakpoint pada
titik shellcode+13 (0x804954d). Pada titik tersebut ada instruksi “jmp
0×8049554 “, yaitu instruksi untuk mengeksekusi shellcode
yang telah di-decode. Setelah breakpoint dipasang, kita bisa jalankan
program dengan run. Ketika breakpoint dicapai, kita bisa lihat hasil
decodingnya pada lokasi shellcode+20 sampai shellcode+53.
Sebagai
contoh perhatikan pada shellcode+20, instruksi sebelum decode adalah
“sub ebx,edx”, instruksi itu bukan instruksi shellcode originial. Namun
setelah decoding selesai pada lokasi tersebut menjadi “xor eax,eax” yang
merupakan instruksi shellcode yang asli. Contoh lain, pada lokasi
shellcode+28 sebelum decode instruksinya adalah “xlat BYTE PTR
ds:[ebx]“, namun setelah decode kembali normal menjadi “int 0×80″.
Tabel
di bawah ini menunjukkan beberapa perbedaan antara instruksi assembly
dari shellcode original dengan instruksi assembly yang telah ter-encode.
Kolom “Before Decoding” adalah assembly shellcode yang telah di-encode,
sedangkan kolom “After Decoding” menunjukkan assembly shellcode yang
original setelah decoder selesai bekerja.
Address | Before Decoding | After Decoding |
---|---|---|
shellcode+20 | sub ebx,edx | xor eax,eax |
shellcode+22 | stos BYTE PTR es:[edi],al | mov al,0×46 |
shellcode+28 | xlat BYTE PTR ds:[ebx] | int 0×80 |
shellcode+46 | dec ecx | push ebx |
Post a Comment