Derleyici ne yapar: kaynaktan makine koduna
- Kaynak kodun makine koduna giden dört durakta neyin kaybolduğunu, neyin kaldığını ve geri derleyicinin neyi veremediğini öğrenirsin.
- Zincirin her adımı bilgi atar; ad, yorum ve biçim yolda kaybolur, davranış kalır.
- Bu derste tersine mühendisliğin neden kayıp bilgiyi tamamlamak değil, izden anlam kurmak olduğunu görürsün.
Puan bütün etkinlikleri cevaplayınca hesaba yazılır. Bu dersi tamamlayınca +45 XP alacaksın. Giriş yapmadan ilerlersen puan yalnız bu tarayıcıda durur.
Tersine mühendislik, derleme zincirini geriye yürümektir. Bir programcı kaynak kod yazar; derleme zinciri onu makine koduna çevirir; sen elinde yalnız o makine koduyla, geriye doğru anlam kurarsın. Bu derste zincirin adımlarını, her adımda neyin kaybolduğunu ve neyin kaldığını göreceksin.
Sonunda şu temel gerçeği kavrayacaksın: ikili dosya kaynak kodun eşdeğeri değil, izidir. İz okunabilir ama eksiktir. İyi analist bu eksiği kabul eder ve kayıp adları aramak yerine davranıştan yeni anlam kurar.
Derleme zinciri: dört durak
Kaynak koddan çalışan dosyaya giden yol dört durakta işler. Ön işlemci (preprocessor) başlık dosyalarını açar ve makroları yerine koyar. Derleyici (compiler) kaynağı assembly diline çevirir. Çevirici (assembler) assembly kodunu nesne dosyasına dönüştürür.
Bağlayıcı (linker) parçaları ve kütüphaneleri birleştirir. Bu dört durak diskte çalıştırılabilir bir dosya üretir ama o dosya henüz koşmaz. Beşinci bir oyuncu, yükleyici (loader), dosyayı belleğe yerleştirir ve giriş noktasına atlar.
Bu zincirin her adımında bilgi kaybolur. Yorum satırı daha ilk durakta gider. Değişken adı derleme sırasında yerini bir bellek uzaklığına bırakır.
Tip bilgisi kısmen kalır, çoğu zaman yalnız boyut olarak. Yapılar da yalnız alan uzaklıkları olarak yaşar: kaynakta kullanici->yas yazan satır, ikilide [rdi+0x10] gibi çıplak bir adrese dönüşür. Fonksiyon sınırları en iyileme yapılmamışsa görünür kalır ama en iyilemeyle bulanır.
Elinde kalan tek değişmez şey davranıştır: kod ne yapıyorsa hâlâ onu yapar. Tersine mühendislik bu değişmezin üstüne kurulur.
$ gcc -O0 ornek.c -o ornek_yavas
$ gcc -O2 ornek.c -o ornek_hizli
$ nm -D ornek_hizli | grep ' U '
U printf@GLIBC_2.2.5
U socket@GLIBC_2.2.5Komutları satır satır oku. İlk iki satır aynı kaynağı iki farklı en iyileme düzeyiyle derler ve iki ayrı çıktı üretir.
Üçüncü satır dinamik simgeleri listeler ve tanımsız olanları ayıklar. nm çıktısındaki U harfi undefined, yani tanımsız demektir: bu simge dosyanın içinde tanımlı değildir, dışarıdan bir kütüphaneden gelir. printf ve socket satırları, dosyanın çalışmak için hangi dış yordamlara ihtiyaç duyduğunu gösterir. Bu satırlar silinemez, çünkü dinamik bağlayıcı çalışma anında onlara ihtiyaç duyar.
Bir örneğin niyeti hakkındaki ilk ipucu çoğu zaman tam bu listede saklıdır. Ağ, dosya ve süreç çağrıları yan yana durur. Dosya hiç çalışmadan ne yapmaya hazır olduğunu böyle ele verir.
Etkinlik 1 / 7
Kaynak kodun makine koduna dönüşme adımlarını 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.
- 1Yükleyici son dosyayı belleğe alır
- 2Bağlayıcı nesne dosyalarını ve kütüphaneleri birleştirir
- 3Çevirici assembly kodunu nesne dosyasına çevirir
- 4Ön işlemci başlıkları ve makroları açar
- 5Derleyici kaynağı assembly diline çevirir
En iyileme: yapı neden bozulur
En iyileme (optimization) düzeyi çıktıyı köklü biçimde değiştirir. Sıfır düzeyinde (-O0) kod kaynağa yakın durur ve okuması kolaydır; her değişken belleğe yazılır, her fonksiyon ayrı durur. GCC bu düzeyi varsayılan yapar, çünkü hata ayıklama sonuçları beklendiği gibi çıkar. İkinci düzeyde (-O2) derleyici değerleri kayıtçılarda tutar, küçük yordamları çağıran yere gömer ve kullanılmayan kodu atar.
Üçüncü düzey (-O3) bunlara döngü soyma gibi daha cesur dönüşümler ekler. Aynı kaynak iki farklı en iyilemeyle taban tabana farklı görünen iki ikili üretir. Bu yüzden bir örneğin hangi düzeyle derlendiğini kestirmek, onu okumaya nasıl yaklaşacağını da belirler.
Gömülen yordam analizde en çok kafa karıştıran şeydir. Kaynakta ayrı duran küçük bir fonksiyon, ikilide görünmez olur; çağrı yoktur çünkü gövde doğrudan çağıran yere yerleştirilmiştir.
Yordam sayarak kaynak kodun yapısını tahmin etmek bu yüzden yanıltır. Döngü açma da benzer bir tuzaktır: yüksek en iyileme düzeyinde kaynaktaki tek bir döngü, ikilide arka arkaya tekrarlanan komut bloklarına dönüşür ve döngü hiç yokmuş gibi görünür. Yapıyı değil davranışı izlersin.
En iyilemenin ne kadar fark yarattığını somut görmek için tek bir fonksiyonu iki düzeyle derle ve çıktılarını karşılaştır. Fonksiyon iki sayıyı toplayıp döndürüyor, başka bir şey yapmıyor.
Kaynak kod her iki derlemede aynı; değişen tek şey en iyileme düzeyi. Yine de üretilen assembly taban tabana farklı görünür. Bu örnek, aynı davranışın neden çok farklı iki koda dönüşebildiğini ve okuduğun kodun uzunluğunun karmaşıklıkla aynı şey olmadığını gösterir.
; -O0 ile derlenmiş topla(a, b) push rbp mov rbp, rsp mov [rbp-0x4], edi ; a yerel yere mov [rbp-0x8], esi ; b yerel yere mov edx, [rbp-0x4] mov eax, [rbp-0x8] add eax, edx ; eax = a + b pop rbp ret
; -O2 ile derlenmiş aynı fonksiyon lea eax, [rdi+rsi] ; eax = a + b ret
İki çıktıyı karşılaştır. -O0 halinde her adım açıkça görünür: fonksiyon önce çerçeveyi kurar, iki argümanı yığındaki yerel yerlere yazar, sonra geri okuyup toplar. Okuması kolaydır ama uzundur ve gereksiz bellek erişimi içerir. -O2 halinde derleyici aynı işi iki satırda yapar.
lea eax, [rdi+rsi] iki argümanı doğrudan toplayıp eax'a koyar, belleğe hiç dokunmadan; sonra ret döner. Çerçeve yok, yığın erişimi yok, ara adım yok. İki fonksiyon da aynı davranışı üretir, ama biri dokuz satır, öteki iki satırdır. Bir örneği okurken bu farkı tanırsan, uzun ve dolambaçlı görünen kodun mutlaka karmaşık olmadığını, çoğu zaman yalnız düşük en iyileme düzeyinden geldiğini bilirsin.
Etkinlik 2 / 7
Derleyici en iyilemesi tersine mühendisi neden zorlar?
Simgeler ve hata ayıklama bilgisi
Simgeler (symbols) analiz süresini en çok belirleyen şeydir. Hata ayıklama bilgisiyle derlenen bir dosya yordam adlarını, değişken adlarını ve kaynak satır numaralarını taşır.
Bu bilgi kaynağı geri vermez ama bir harita verir: hangi adresin hangi fonksiyona, hangi satırın kaynağın kaçıncı satırına karşılık geldiğini söyler. Böyle bir dosyada analiz saatlerden dakikalara iner. Üretim dosyalarında bu bilgi çoğu zaman strip aracıyla silinir; zararlı yazılım yazarları da aynısını yapar.
Simgeler silinince geriye ne kalır? Dış kütüphane çağrılarının adları kalır, çünkü bağlayıcı onları çalışma anında çözmek zorundadır. socket, connect, RegSetValueEx gibi adlar silinemez; işletim sistemi onları ada göre bulur. Bu yüzden simgeleri silinmiş bir dosyada bile içe aktarma listesi niyet hakkında konuşmaya devam eder.
Bir başka konu da dil seçimidir. C ile yazılmış program ince ve doğrudan kod üretir. Nesne yönelimli diller sanal yordam tabloları ve isim bozma (name mangling) kuralları ekler; bozulmuş adları geri çözen araçlar sınıf ve yordam adını yeniden okunur hale getirir.
Etkinlik 3 / 7
Hata ayıklama simgeleriyle derlenen ikilide fazladan ne bulursun?
Önce kodu oku, sonra çıktıyı seç.
$ gcc -g -O0 ornek.c -o ornek $ file ornek
Simgelerin bir haritayı geri verdiğini gördün. Peki derleme boyunca neyin kaybolduğunu, neyin kaldığını net ayırabilir misin? Bu ayrım tersine mühendisliğin bütün mantığını özetler.
Kaybolanın peşinden koşmazsın; kalanın üstüne yeni bir anlam kurarsın. Ayrımın kuralı kısadır. Bir bilgiye çalışma anında makine ihtiyaç duyuyorsa ikilide kalır.
Bilgi yalnız insan okuyucu içinse derleme yolunda düşer. Hata ayıklama bilgisi bu kuralın tek istisnasıdır. O da yalnız -g ile istendiğinde eklenir ve strip ile silinmediği sürece kalır.
Etkinlik 4 / 7
Her ögeyi derlemede başına gelene göre ayır.
Ö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.
Bağlama, diller ve paketleme
Statik bağlama ile dinamik bağlama çıktıyı büyük ölçüde değiştirir. Statik bağlanan ikili, kullandığı kütüphaneleri kendi içine alır ve çok büyür; ama çalışmak için dışarıya bağımlı değildir. Dinamik bağlanan ikili ince kalır, çünkü kütüphaneleri çalışma anında yükleyiciden ister.
Bir dosyanın hangi tür olduğunu bilmek, dış çağrıları nasıl çözeceğini belirler. Dinamik bağlı bir dosyada içe aktarma listesi niyeti hemen açar; statik bağlı bir dosyada aynı çağrılar gövdeye gömülüdür ve aramak daha zordur. Zararlı yazılım çözümlemesinde bu ayrım önemlidir: statik bağlı ve simgeleri silinmiş bir dosya, kasıtlı olarak okumayı zorlaştırmak için seçilmiş olabilir.
Dil ailesi de araç seçimini değiştirir. Yönetilen ve ara koda derlenen diller makine kodu yerine bir ara gösterim üretir. Bu gösterim çoğu zaman kaynağa çok yakındır ve sınıf ile yordam adlarını çoğu zaman korur; onları okumak için ayrı çözümleyiciler kullanırsın.
Betik diliyle yazılan bir program da kimi zaman bir paketleyiciyle tek bir çalıştırılabilir dosyaya çevrilir ve içinde asıl betik sıkıştırılmış durur. Böyle bir dosyada iş, makine kodunu okumak değil, gömülü betiği çıkarmaktır. Bu yüzden analiz öncesi dili belirlersin; doğru aracı baştan seçer, saatlerce yanlış katmanda dolaşmaktan kurtulursun.
İlk keşifte hangi araca ne zaman uzanacağını bilmek de bir beceridir. file dosyanın türünü ve bağlama biçimini söyler. strings dosyadaki okunabilir dizeleri döker; alan adları, dosya yolları ve hata mesajları çoğu zaman burada görünür.
nm simge tablosunu ve tanımsız dış çağrıları listeler. objdump -d ise baytları assembly satırlarına çevirir. Bu dört araç birlikte, dosyayı hiç çalıştırmadan hızlı bir harita çıkarır ve sonraki derin okumaya nereden başlayacağını gösterir.
Etkinlik 5 / 7
Aracı yaptığı işle eşleştir.
İz okumak: ikili kaynağın kopyası değil
Geri derleyici (decompiler) bu kayıpların üstüne bir tahmin katmanı koyar. Assembly kodunu okur ve C benzeri bir gösterim üretir. Adlar uydurmadır, tipler tahmindir; yine de insan gözü için okunabilir bir başlangıç kurar.
Doğru yaklaşım geri derleyici çıktısı ile assembly satırını birlikte okumaktır. Geri derleyici yapıyı verir, assembly satırı gerçeği verir. Karar kritikse assembly satırına bakarsın; kritik değilse geri derleyici hızı kazandırır.
Bu dersin asıl dersi şudur: ikili dosya kaynak kodun eşdeğeri değil, izidir. İz okunabilir ama eksiktir. Elindeki geri derleyici çıktısı ne kadar temiz görünürse görünsün, orijinal kaynağın birebir kopyası değildir.
Adları sen verirsin, mantığı sen doğrularsın; araç yalnız okunabilir bir başlangıç noktası sunar. Kayıp adları aramak yerine, kodun ne yaptığını anlatan yeni adlar koyarsın. İyi analistin işi, eksik izi kabul edip davranıştan tutarlı bir anlam örmektir.
Bu bakış açısı savunma tarafında da işine yarar. Bir yazılımı derlerken hangi bilginin ikilide kaldığını bilirsin. O zaman üretim dosyasından hata ayıklama simgelerini silmenin neden iyi bir alışkanlık olduğunu anlarsın.
Hem dosya küçülür hem de bir saldırgana kolay okuma haritası vermemiş olursun. Aynı biçimde, statik ve dinamik bağlama arasındaki seçimi de bilinçli yaparsın. Tersine mühendisliği öğrenmek, yalnız başkasının kodunu okumak değil, kendi kodunun dışarıya ne kadar iz bıraktığını görmektir.
Etkinlik 6 / 7
Bir ELF dosyasındaki simgeleri listele ve tanımsız olanları ayıkla.
Derinleş:DWARF: hata ayıklama bilgisinin biçimi
Bir dosyanın yordam adlarını ve satır numaralarını nasıl taşıdığını hiç merak ettin mi? ELF dünyasında bu bilginin bir standardı vardır: adı DWARF olan hata ayıklama biçimi. Adın ELF ile arasındaki fantastik gönderme kasıtlıdır.
-g bayrağıyla derlediğinde, derleyici dosyaya .debug_info, .debug_line gibi ayrı bölümler ekler. Bu bölümler her makine adresinin hangi kaynak satırına, her değişkenin hangi tipe karşılık geldiğini yazar. Kod çalışırken bu bölümler kullanılmaz; yalnız hata ayıklayıcı okur.
strip aracı bu bölümleri sildiğinde dosya aynı hızla çalışmaya devam eder ama haritasını yitirir. Bir savunma ve gizlilik dengesi de buradan çıkar: üretim dosyalarından hata ayıklama bilgisi genellikle çıkarılır, çünkü hem dosyayı küçültür hem de bir saldırgana kolay okuma haritası vermemiş olursun.
Etkinlik 7 / 7
Doğru mu, yanlış mı?
Bir ikiliden kaynak kodun birebir aynısı geri üretilebilir.
Okumayı istediğin sırayla sürdürebilirsin. Ders, bütün etkinlikleri cevapladığında tamamlanır.
- Etkinlik 1· Derleme zinciri: dört durak
- Etkinlik 2· En iyileme: yapı neden bozulur
- Etkinlik 3· Simgeler ve hata ayıklama bilgisi
- Etkinlik 4· Simgeler ve hata ayıklama bilgisi
- Etkinlik 5· Bağlama, diller ve paketleme
- Etkinlik 6· İz okumak: ikili kaynağın kopyası değil
- Etkinlik 7· İz okumak: ikili kaynağın kopyası değil