Yığın ve çağrı düzeni
- Yordam çağrısının yığın üstünde nasıl işlediğini, argümanların hangi kayıtçıyla geçtiğini ve call ile ret'in ne yaptığını öğrenirsin.
- Argümanların hangi kayıtçıyla geçtiği işletim sistemine göre değişir; Linux ile Windows ayrı düzen kullanır.
- Bu derste push, call ve ret komutlarının yığında ne yaptığını adım adım izler ve bir çerçeveyi elle çözersin.
Puan bütün etkinlikleri cevaplayınca hesaba yazılır. Bu dersi tamamlayınca +50 XP alacaksın. Giriş yapmadan ilerlersen puan yalnız bu tarayıcıda durur.
Yordam çağrısı sihir değildir. Yığın üstünde işleyen basit ve tekrar eden bir düzendir.
Bu düzeni bir kez oturttuğunda, tanımadığın bir yordamın nereden çağrıldığını, hangi argümanları aldığını ve nereye döneceğini kod çalışmadan okursun. Bu derste yığının nasıl çalıştığını, argümanların hangi kayıtçıyla geçtiğini ve call ile ret'in yığında ne yaptığını göreceksin. Bellek taşması açıklarının neden tam bu noktadan işlediğini de aynı düzenden çıkaracaksın.
Yığın nedir, nasıl büyür
Yığın (stack), sürecin belleğinde ayrılmış bir alandır. En ilginç yanı büyüme yönüdür: yukarıdan aşağı büyür. Yani yeni değer eklendikçe kullanılan adres küçülür, artmaz. Bu ters sezgi başta şaşırtır ama tutarlıdır.
rsp kayıtçısı her an yığının tepesini, yani en son eklenen değerin adresini gösterir. Yığına bakmak istediğinde başlangıç noktan hep rsp olur; oradan aşağı ya da yukarı ilerleyerek çerçeveyi çözersin. Her iş parçacığının (thread) kendi yığını olur ve bu alan sınırlıdır. Sonu gelmeyen bir özyineleme yığını tüketir ve programı çökertir.
push komutu iki iş yapar. Önce rsp değerini sekiz azaltır, sonra değeri o yeni adrese yazar. pop tersini yapar; önce üstteki değeri okur, sonra rsp değerini sekiz artırır.
Sekiz sayısı 64 bitlik bir kelimenin bayt karşılığıdır; her push ya da pop bir kelime taşır. Bu iki komut yığının bütün mekaniğinin temelidir. Daha karmaşık görünen call ve ret bile aslında push ve pop üstüne kuruludur.
; rsp = 0x7fffffffe300 push rax ; rsp = 0x7fffffffe2f8, [rsp] = rax push rbx ; rsp = 0x7fffffffe2f0, [rsp] = rbx pop rcx ; rcx = rbx, rsp = 0x7fffffffe2f8
Örnekteki adresleri elle takip etmek anlamayı pekiştirir. Başlangıç adresi 0x7fffffffe300 değeridir. İlk push sekiz bayt düşer ve adres 0x7fffffffe2f8 olur; değer o yeni adrese yazılır.
İkinci push aynı kuralı işletir ve adres bir sekiz bayt daha düşer. pop geldiğinde ise önce üstteki değer okunur, sonra adres sekiz bayt yükselir; sıra push işleminin tam tersidir. Bu düzeni bir kez kâğıt üstünde çalıştırdığında, sonraki her push ve pop satırını gözünde canlandırırsın.
Etkinlik 1 / 7
push rax koştuktan sonra rsp değeri ne olur?
Önce kodu oku, sonra çıktıyı seç.
; başlangıç rsp = 0x7fffffffe300 push rax
Çağrı düzeni: argümanlar hangi kayıtçıyla
Bir yordama argüman geçmenin kuralına çağrı düzeni (calling convention) denir. Bu kural işletim sistemine göre değişir ve iki büyük ekol vardır.
Linux ile BSD sistemleri System V AMD64 düzenini kullanır. Bu düzende ilk altı tam sayı argümanı sırayla rdi, rsi, rdx, rcx, r8 ve r9 kayıtçılarıyla gider. Yani bir çağrının ilk argümanını görmek istiyorsan, çağrıdan hemen önce rdi'ye ne konduğuna bakarsın.
Windows x64 düzeni farklıdır. İlk dört argüman rcx, rdx, r8 ve r9 ile geçer, kalanı yığına yazılır. Ayrıca çağıran taraf ilk dört argüman için yığında otuz iki baytlık bir alan ayırır; buna gölge alan (shadow space) denir ve çağrılan yordam onu kullanabilir.
Bir ikilinin hangi düzeni kullandığını bilmezsen, ilk argümanı yanlış kayıtçıda ararsın ve analizi baştan yanlış kurarsın. Bu yüzden işe hedefin işletim sistemini belirleyerek başlarsın.
Dönüş değeri iki düzende de rax ile gelir. Bu yüzden bir çağrının hemen ardından rax içeriğine bakmak alışkanlık olur. Çözülmüş yapılandırma, açılan tutamaç ve karşılaştırma sonucu hep oradan çıkar. Kayan noktalı sayılar ise ayrı kayıtçılarla geçer: Linux düzeninde ilk sekiz ondalık argüman xmm0 ile xmm7 kayıtçılarıyla taşınır.
Tam sayı ile ondalık argüman karışık gelirse iki sıra ayrı ayrı sayılır. Windows düzeninde ise sıra konuma bağlıdır. İkinci argüman ondalıksa xmm1 ile, tam sayıysa rdx ile gider.
Bir ikilinin hangi düzeni kullandığını çoğu zaman baştan bilirsin, çünkü dosyanın biçimi işletim sistemini ele verir: PE dosyası Windows düzenini, ELF dosyası System V düzenini kullanır. Emin değilsen çağrıdan hemen önce hangi kayıtçıların doldurulduğuna bakarsın. rdi ile rsi doluyorsa Linux düzenindesin; rcx ile rdx doluyor ve çağrıdan önce yığında otuz iki baytlık boşluk ayrılıyorsa Windows düzenindesin. Bu küçük kontrolle ilk argümanı doğru kayıtçıda ararsın ve analizi baştan doğru kurarsın.
Etkinlik 2 / 7
Her kayıtçıyı doğru çağrı düzenine koy. İlk iki tam sayı argümanı sorulur.
Önce bir seçeneğe, sonra aşağıdaki uygun kutuya bas. İstersen seçeneği kutuya sürükleyip bırak. Yerini değiştirmek için aynı işlemi tekrarla.
0/4 seçenek yerleşti.
Argüman kayıtçılarını iki düzene ayırdın. Kayıtçılar bitince argümanlar yığına gider. Linux düzeninde yedinci tam sayı argümanı ve sonrası, Windows düzeninde beşinci argüman ve sonrası yığına yazılır. Windows tarafında bu argümanlar, çağıranın ilk dört argüman için ayırdığı sabit boşluğun hemen üstünde durur.
Çağrılan yordam bu boşluğu kendi işi için kullanabilir. Boşluğun boyutunu bilmezsen yığındaki argümanları yanlış uzaklıkta ararsın. Bu boyut düzenin ayırt edici imzasıdır.
Etkinlik 3 / 7
Windows x64 düzeninde çağıran tarafın ilk dört argüman için yığında ayırdığı alanın boyutu kaç bayttır? ____
call ve ret: akışın taşıyıcısı
call komutu da iki iş yapar. Dönüş adresini yığına yazar ve hedef adrese atlar. Dönüş adresi, call satırından hemen sonraki komutun adresidir; program alt yordam bitince oraya geri dönmelidir.
Aslında call, bir push ile bir jmp işini tek komutta birleştirir. ret komutu ise o adresi yığından çeker ve oraya atlar; bir pop ile bir jmp gibi düşünülebilir. Bu iki komut bütün program akışını taşır.
Bu düzenin güvenlik açısından kritik yanı şudur: dönüş adresi yığında, veriyle aynı alanda durur. Yığındaki dönüş adresi bozulursa ret yanlış yere gider ve akış kontrolü saldırganın seçtiği adrese kayar. Bellek taşması (buffer overflow) açıkları tam bu noktadan işler.
Bir yerel diziye sınır ötesine yazılan veri, aynı çerçevedeki dönüş adresini ezerse, yordam bittiğinde program saldırganın istediği koda döner. Savunma da buradan kurulur: sınır denetimi ve az sonra göreceğin kanarya değeri.
Öğrendiklerini bir arada görmek için küçük bir çağrıyı izle. Aşağıdaki blok, iki sayıyı toplayan bir yordamı Linux çağrı düzeniyle çağırır.
Bir çağrıyı okurken hep aynı üç soruyu sorarsın: argümanlar hangi kayıtçıya kondu, dönüş değeri nereden geliyor, akış nereye dönüyor. Bu örnekte üçünün de cevabı tek blokta görünür. Üç soruyu bir çağrıda cevaplayabiliyorsan, tanımadığın yordamları da aynı kalıpla, satırları tek tek çözmeden okursun.
; topla(3, 5) çağrısı, System V AMD64 düzeni mov edi, 3 ; 1. argüman: rdi = 3 mov esi, 5 ; 2. argüman: rsi = 5 call topla ; dönüş adresi yığına, topla'ya atla mov [rbp-0x4], eax ; dönen değer yerel değişkene yazılır topla: lea eax, [rdi+rsi] ; eax = rdi + rsi = 8 ret ; dönüş adresini çek, çağrıdan sonraki satıra dön
Bloğu satır satır çöz. İlk iki satır argümanları yerleştirir: edi'ye 3, esi'ye 5. Bu satırlar 32 bitlik hedefe yazdığı için rdi ile rsi'nin üst yarısı sıfırlanır, yani ilk argüman 3, ikinci argüman 5 olur. call satırı çağrıdan sonraki satırın adresini yığına iter ve topla'ya atlar.
topla içinde lea eax, [rdi+rsi] iki argümanı toplayıp eax'a koyar; belleğe dokunmadan tek satırda toplama yapar. ret yığından dönüş adresini çeker ve call'dan sonraki satıra döner. Orada eax içindeki 8, yerel değişkene yazılır. Argümanlar rdi ile rsi'den girdi, sonuç rax'tan çıktı; bütün çağrı bu üç kayıtçıyla anlatıldı.
Etkinlik 4 / 7
call komutunun yaptığı işleri sıraya koy.
Bir öğeye odaklan, yukarı ve aşağı ok tuşlarıyla taşı. Home tuşu en üste, End tuşu en alta alır.
- 1ret sonunda dönüş adresi yığından çekilir
- 2Akış hedef adrese atlar
- 3Dönüş adresi yığına yazılır
- 4Dönüş adresi hesaplanır
- 5rsp sekiz bayt azalır
Yığın çerçevesi: prologue ve yerel değişkenler
Yordam girişinde iki satırı sık görürsün: push rbp ve mov rbp, rsp. Bu ikili yığın çerçevesini (stack frame) kurar ve girişe prologue denir. Çerçeve kurulduktan sonra yerel değişkenler rbp eksi sabit biçiminde, argümanlar rbp artı sabit biçiminde adreslenir.
rbp çağrı boyunca sabit kaldığı için, yerel değişkenlerin adresi de sabit kalır. Bu düzen yordamı okumayı kolaylaştırır: rbp-0x8 hep aynı yerel değişkeni gösterir.
Derleyici en iyileme yaparsa bu iki satırı atabilir. O zaman rbp bir çerçeve işaretçisi olarak kullanılmaz ve her şey doğrudan rsp üstünden adreslenir. Buna çerçevesiz (frameless) yordam denir. Okuması biraz daha zordur çünkü rsp yordam boyunca değişir, dolayısıyla aynı yerel değişken farklı satırlarda farklı uzaklıkta görünebilir.
Mantık aynı kalır ama izlerken rsp'nin o anki değerini takip etmen gerekir. GCC bu davranışı -O1 ve üstünde varsayılan olarak açar. Buna karşılık kimi Linux dağıtımları, başarım ölçümü kolaylaşsın diye 64 bitlik paketlerini yeniden çerçeve işaretçisiyle derlemeye başladı.
Etkinlik 5 / 7
Klasik yordam girişini (function prologue) kuran satırı bul.
Hizalama, kanarya ve geri izleme
Hizalama kuralı da düzenin parçasıdır. Çağrı anında yığın tepesi on altı bayta hizalı olmalıdır. Derleyici bunu sağlamak için bazen boş bir push ya da rsp'yi azaltan bir satır ekler.
Elle yazılan kodda bu kural unutulursa bazı kütüphane çağrıları, özellikle SIMD komutu kullananlar, çöker. Bir çağrıdan hemen önce açıklanamayan bir sub rsp ya da fazladan push görürsen, çoğu zaman hizalama içindir.
Çerçeve kanarya değeri de sık görülür. Derleyici dönüş adresinin önüne rastgele bir sayı koyar ve yordam çıkışında bu sayıyı sınar. Değer bozulmuşsa bir taşma olmuş demektir ve program kendini güvenli biçimde kapatır. Bu satırları bir taşma açığı değil, bir koruma sayarsın.
Linux'ta GCC çıktısında bu koruma, girişte fs:0x28 adresinden okunan bir değer ve çıkışta __stack_chk_fail çağrısı olarak görünür. Girişte kanaryayı yükleyen ve çıkışta karşılaştıran satırları tanırsan, yordamın hangi korumalarla derlendiğini de okumuş olursun.
Yığın çerçevelerini geriye doğru izlemek de bir beceridir. Her çerçeve bir önceki çerçeve işaretçisini ve dönüş adresini tutar. Bu zinciri takip ederek programın hangi yoldan buraya geldiğini çıkarırsın; buna geri izleme (backtrace) denir.
Bir çökme raporunda gördüğün çağrı yığını da bu zincirin okunmuş halidir. Çerçevesiz derlenen yordamlarda bu zincir kırılır, çünkü rbp artık bir önceki çerçeveyi göstermez; o durumda hata ayıklayıcı zinciri kayıtçı ve dönüş adresi desenlerinden tahmin ederek kurar.
Kayıtçıların korunma sorumluluğu da düzene yazılıdır. Bazı kayıtçıları çağrılan yordam korumak zorundadır; bir çağrıdan sonra o kayıtçının değeri değişmemiş olmalıdır. Bunlara çağrı boyunca korunan (callee-saved) kayıtçılar denir.
Linux düzeninde rbx, rbp ve r12 ile r15 arası bu kümededir; Windows düzeni bunlara rdi ile rsi'yi de ekler. Bazılarını ise çağıran taraf korur; çağrıdan sonra değerleri kaybolabileceğini varsayarsın. Bu ayrım, bir yordamın başında neden bazı kayıtçıların push edilip sonunda pop edildiğini açıklar: yordam ödünç aldığı kayıtçıları çağırana bozmadan geri vermek zorundadır.
Etkinlik 6 / 7
pwndbg yüklü gdb içinde yığının ilk 8 kelimesini göster.
Derinleş:Kırmızı bölge ve kanaryanın tarihi
System V AMD64 düzeninin ince bir kuralı vardır: rsp'nin hemen altındaki 128 bayt, kırmızı bölge (red zone) diye ayrılır. Yaprak yordam, yani başka bir yordam çağırmayan yordam, bu alanı yığın işaretçisini hiç azaltmadan yerel değişken için kullanabilir. Bu küçük en iyileme birkaç komut kazandırır.
Windows x64 düzeninde kırmızı bölge yoktur; iki düzeni ayıran ayrıntılardan biridir. Kanarya fikri ise 1998'de Crispin Cowan ve ekibinin StackGuard çalışmasıyla yaygınlaştı. Dönüş adresinin önüne bir bekçi değer koyup çıkışta sınama fikri o kadar etkili oldu ki bugün derleyicilere yerleşti: GCC tarafında stack-protector seçeneği, Microsoft derleyicisinde GS seçeneği aynı işi yapar. Bir yordamın girişinde büyük rastgele bir sabitin yüklendiğini ve çıkışta karşılaştırıldığını görürsen, otuz yıla yaklaşan bir savunma fikrinin izini okuyorsundur.
Etkinlik 7 / 7
Doğru mu, yanlış mı?
Yığın çerçevesi içinde yerel değişkenler çoğu zaman rbp'ye göre adreslenir.
Okumayı istediğin sırayla sürdürebilirsin. Ders, bütün etkinlikleri cevapladığında tamamlanır.
- Etkinlik 1· Yığın nedir, nasıl büyür
- Etkinlik 2· Çağrı düzeni: argümanlar hangi kayıtçıyla
- Etkinlik 3· Çağrı düzeni: argümanlar hangi kayıtçıyla
- Etkinlik 4· call ve ret: akışın taşıyıcısı
- Etkinlik 5· Yığın çerçevesi: prologue ve yerel değişkenler
- Etkinlik 6· Hizalama, kanarya ve geri izleme
- Etkinlik 7· Hizalama, kanarya ve geri izleme