ELF (Executable and Linkable Format), Unix benzeri işletim sistemlerinde (Linux, FreeBSD, Solaris vb.) yürütülebilir dosyaları, paylaşımlı kütüphaneleri ve çekirdek dosyalarını temsil etmek için standart bir dosya formatıdır. Bu format, sistemin bir programı belleğe nasıl yükleyeceğini, bağlayıcıların kütüphaneleri nasıl çözdüğünü ve hata ayıklayıcıların sembol bilgilerine nasıl eriştiğini tanımlar. ELF dosyalarını anlamak, sistem programlama, tersine mühendislik, malware analizi ve sistem güvenliği alanlarında çalışan herkes için vazgeçilmez bir beceridir.
Bu rehberde, ELF dosya yapısının derinliklerine inerek, çeşitli bölümlerini, başlıklarını ve dinamik davranışlarını kapsamlı bir şekilde inceleyeceğiz. Ayrıca, bu dosyaları çözümlemek için kullanılan temel araçları ve teknikleri adım adım ele alacağız. Amacımız, ELF dosyalarının iç işleyişini anlamanıza ve bu bilgiyi pratik analiz senaryolarında uygulamanıza yardımcı olmaktır.
1. ELF Dosya Yapısının Temelleri
Bir ELF dosyası genel olarak üç ana bölümden oluşur. Bu bölümler, dosyanın bağlama (linking) ve yükleme (loading) aşamalarında nasıl işleneceğini belirleyen kritik meta verileri barındırır:
Bağlayıcılar (linker) genellikle Bölüm Başlık Tablosu'nu kullanarak nesne dosyalarını birleştirir ve sembolleri çözerken, işletim sistemi çekirdeği bir programı belleğe yüklerken Program Başlık Tablosu'nu esas alır. Bu ayrım, ELF'in hem bağlama hem de yürütme esnekliğini sağlar.
2. ELF Başlığı (ELF Header) Detaylı İnceleme
ELF Başlığı, dosyanın en başında 64 baytlık (64-bit sistemlerde) sabit bir yapıdır ve dosya hakkında kritik yapısal bilgiler içerir. Bu başlık, bir ELF dosyasının temel kimliğini ve nasıl yorumlanması gerektiğini belirler. İşte bazı önemli alanlar ve anlamları:
Bu başlığı incelemek için `readelf -h` komutunu kullanırız. Örneğin:
Yukarıdaki çıktı, dosyanın bir 64-bit yürütülebilir dosya olduğunu, x86-64 mimarisi için derlendiğini ve giriş noktasının `0x4010a0` olduğunu gösterir. Ayrıca, Program Başlık Tablosu'nun dosya içinde 64 bayt ofsetten başladığını ve 13 girişi olduğunu da belirtir.
3. Program Başlık Tablosu (Program Header Table - PHT)
PHT, işletim sisteminin bir programı belleğe nasıl yükleyeceğini açıklayan bir dizi segment tanımlayıcısından oluşur. Her segment, dosyanın bir kısmının bellekte nasıl temsil edileceğini belirtir. İşletim sistemi, bu tablodaki bilgileri kullanarak programın sanal bellek düzenini oluşturur. Yaygın segment türleri ve anlamları şunlardır:
PHT'yi `readelf -l` komutuyla detaylı bir şekilde inceleyebiliriz:
Yukarıdaki çıktıda `LOAD` segmentlerinin `R E` (Read, Execute) ve `RW` (Read, Write) izinlerine sahip olduğunu ve bunların dosya içindeki başlangıç ofsetlerini, sanal adreslerini (`VirtAddr`) ve bellek boyutlarını (`MemSiz`) gösterdiğini görebiliriz. Bu, sistemin kodu ve veriyi sanal bellek alanına nasıl yerleştirdiğini ortaya koyar.
4. Bölüm Başlık Tablosu (Section Header Table - SHT)
SHT, bir ELF dosyasının mantıksal yapısını oluşturan bölümler hakkında bilgi sağlar. Bu bölümler, dosyanın çeşitli türdeki içeriğini düzenler ve bağlama aşamasında linker tarafından kullanılır. SHT, özellikle hata ayıklayıcılar ve analiz araçları için büyük önem taşır. Yaygın bölümler ve işlevleri şunlardır:
Bölümleri `readelf -S` komutuyla listeleyebiliriz:
Bu çıktı, dosyadaki tüm bölümleri, bunların türlerini, sanal adreslerini, dosya ofsetlerini ve boyutlarını listeler. `.text` bölümünün yürütülebilir kodu içerdiğini (`AX` bayrağı), `.data` ve `.bss` bölümlerinin yazılabilir veriyi (`WA` bayrağı) barındırdığını gözlemleyebiliriz. `.bss` bölümünün `NOBITS` türünde olması, dosya içinde yer kaplamadığını ancak bellek ayrılacağını gösterir.
5. Sembol ve Dizgi Tabloları
ELF dosyaları, programın içerdiği fonksiyonlar, değişkenler ve diğer referanslar hakkında bilgi sağlayan sembol tablolarını içerir. Bu tablolar, özellikle bağlama ve hata ayıklama süreçlerinde hayati öneme sahiptir. İki ana sembol tablosu türü vardır:
Sembolleri `readelf -s` komutuyla görüntüleyebiliriz:
Bu çıktı, `main` fonksiyonunun bir global sembol olduğunu (`GLOBAL` bağlama tipi) ve `.text` bölümünde (`Ndx 14`) yer aldığını gösterir. `puts` ve `__libc_start_main` gibi fonksiyonların ise `GLIBC_2.2.5` kütüphanesinden dinamik olarak bağlandığını (`UND` - undefined, `WEAK` - zayıf bağlama) görüyoruz. Sembol çözünürlüğü, programın harici kaynaklara nasıl eriştiğini anlamak için hayati bir adımdır.
6. Dinamik Bağlantı ve Yeniden Konumlandırma
Modern işletim sistemlerinde programlar genellikle paylaşımlı kütüphaneler (`.so` dosyaları) kullanır. Bu yaklaşım, kod tekrarını azaltır, bellek kullanımını optimize eder ve programların güncel kütüphanelerden faydalanmasını sağlar. Dinamik bağlayıcı (`ld-linux.so` gibi), bir program başladığında gerekli kütüphaneleri bulur ve programın sanal adres alanına eşler. Bu süreç, dinamik bağlama olarak adlandırılır.
Bir ELF dosyasının bağlı olduğu paylaşımlı kütüphaneleri görmek için `ldd` komutunu kullanırız:
Bu çıktı, `my_executable` adlı programın `libc.so.6` (C standart kütüphanesi) ve `ld-linux-x86-64.so.2` (dinamik bağlayıcı) gibi kütüphanelere bağımlı olduğunu ve bunların bellekteki başlangıç adreslerini gösterir.
Yeniden konumlandırma girdilerini `readelf -r` komutuyla inceleyebiliriz:
Bu çıktılar, `__libc_start_main` ve `puts` gibi fonksiyonların çalışma zamanında nasıl çözüldüğünü ve hangi bellek ofsetlerinin dinamik bağlayıcı tarafından güncellenmesi gerektiğini detaylı bir şekilde gösterir. `R_X86_64_JUMP_SLOT` türündeki yeniden konumlandırmalar genellikle PLT/GOT mekanizmasıyla ilişkilidir.
7. ELF Dosyalarını Çözümleme Araçları
ELF dosyalarını derinlemesine incelemek ve anlamak için birçok güçlü ve yaygın araç mevcuttur. Bu araçlar, farklı analiz ihtiyaçlarına göre özelleşmiştir:
8. Pratik Uygulama ve Örnek Senaryo
Basit bir C programı yazarak ELF çözümlemesini somutlaştıralım ve çeşitli araçlarla inceleyelim:
Bu kodu derleyelim ve yürütülebilir bir dosya oluşturalım:
Şimdi `hello` yürütülebilir dosyasını yukarıda bahsettiğimiz araçlarla adım adım inceleyelim:
9. Sonuç
ELF dosyalarını çözümlemek, modern işletim sistemlerinin nasıl çalıştığını ve uygulamaların düşük seviyede nasıl etkileşim kurduğunu anlamanın temelidir. Bu rehberde, ELF başlığından program ve bölüm başlık tablolarına, sembol ve dizgi tablolarından dinamik bağlama mekanizmalarına kadar ELF yapısının birçok önemli yönünü ele aldık. Ayrıca, `readelf`, `objdump`, `nm` ve `ldd` gibi güçlü Binutils araçlarını kullanarak pratik örnekler sunduk.
ELF analizi, güvenlik araştırmacılarının zararlı yazılımları incelemesi, sistem programcılarının performansı optimize etmesi ve yazılım geliştiricilerinin derlenmiş kodun davranışını anlaması için kritik bir beceridir. Bu bilgi, bir programın bellekte nasıl düzenlendiğini, harici kütüphanelerle nasıl etkileşim kurduğunu ve nihayetinde CPU üzerinde nasıl yürütüldüğünü daha derinlemesine kavramanıza yardımcı olacaktır. Daha fazla bilgi ve derinlemesine inceleme için, ELF spesifikasyonuna başvurmanız şiddetle tavsiye edilir.
Bu kapsamlı rehberin, ELF dünyasına adım atmanız için sağlam bir temel oluşturduğunu umuyoruz. Unutmayın ki pratik yapmak ve farklı türdeki ELF dosyalarını (örneğin, paylaşımlı kütüphaneler, statik derlenmiş dosyalar) incelemek, bu alandaki uzmanlığınızı pekiştirmenin ve daha karmaşık analiz görevlerine hazırlanmanın en iyi yoludur.
Bu rehberde, ELF dosya yapısının derinliklerine inerek, çeşitli bölümlerini, başlıklarını ve dinamik davranışlarını kapsamlı bir şekilde inceleyeceğiz. Ayrıca, bu dosyaları çözümlemek için kullanılan temel araçları ve teknikleri adım adım ele alacağız. Amacımız, ELF dosyalarının iç işleyişini anlamanıza ve bu bilgiyi pratik analiz senaryolarında uygulamanıza yardımcı olmaktır.
1. ELF Dosya Yapısının Temelleri
Bir ELF dosyası genel olarak üç ana bölümden oluşur. Bu bölümler, dosyanın bağlama (linking) ve yükleme (loading) aşamalarında nasıl işleneceğini belirleyen kritik meta verileri barındırır:
- ELF Başlığı (ELF Header): Dosyanın en başında yer alan ve dosyanın temel özelliklerini (türü, hedef mimarisi, giriş noktası, başlık tablolarının konumu vb.) içeren birincil veri yapısıdır.
- Program Başlık Tablosu (Program Header Table - PHT): Yürütülebilir dosyalar için dosyanın belleğe nasıl yükleneceğini ve hangi bellek segmentlerinin (örneğin, kod segmenti, veri segmenti) oluşturulacağını tanımlar. Her giriş, bir segmenti ve onun bellek özelliklerini temsil eder.
- Bölüm Başlık Tablosu (Section Header Table - SHT): Bağlanabilir dosyalar (obj, .so) için dosyanın mantıksal bölümlerini (kod, veri, sembol tabloları, hata ayıklama bilgileri vb.) tanımlar. Her giriş, bir bölümü ve onun dosya içindeki konumunu, boyutunu ve türünü belirtir.
Bağlayıcılar (linker) genellikle Bölüm Başlık Tablosu'nu kullanarak nesne dosyalarını birleştirir ve sembolleri çözerken, işletim sistemi çekirdeği bir programı belleğe yüklerken Program Başlık Tablosu'nu esas alır. Bu ayrım, ELF'in hem bağlama hem de yürütme esnekliğini sağlar.
ELF spesifikasyonu, dosya formatının modülerliğini ve genişletilebilirliğini vurgular. Bu sayede, farklı işlemci mimarileri ve işletim sistemi varyantları, aynı temel ELF yapısını kullanarak kendi özel gereksinimlerini karşılayabilirler. Bu adaptasyon yeteneği, ELF'i Unix benzeri sistemlerde fiili bir standart haline getirmiştir.
2. ELF Başlığı (ELF Header) Detaylı İnceleme
ELF Başlığı, dosyanın en başında 64 baytlık (64-bit sistemlerde) sabit bir yapıdır ve dosya hakkında kritik yapısal bilgiler içerir. Bu başlık, bir ELF dosyasının temel kimliğini ve nasıl yorumlanması gerektiğini belirler. İşte bazı önemli alanlar ve anlamları:
- e_ident:[/ Bu 16 baytlık dizi, ELF'in “sihirli sayı”sını (`\x7fELF`) içerir ve dosyanın ELF formatında olduğunu doğrular. Ayrıca, dosyanın byte sıralamasını (little-endian/big-endian), ELF sınıfını (32-bit/64-bit), ELF sürümünü ve işletim sistemi/ABI bilgilerini de barındırır. Bu alan, dosya türünü hızlıca tespit etmek için ilk bakılan yerdir.
[*] e_type:[/ Dosya türünü belirtir. Olası değerler: `ET_NONE` (bilinmeyen), `ET_REL` (yeniden konumlandırılabilir dosya/obj), `ET_EXEC` (yürütülebilir dosya), `ET_DYN` (paylaşımlı nesne/kütüphane), `ET_CORE` (çekirdek dosyası).
[*] e_machine:[/ Hedef işlemci mimarisini tanımlar (örneğin, `EM_X86_64` için x64, `EM_ARM` için ARM). Bu, doğru işlemci tarafından yürütülebilirliği garanti eder.
[*] e_version:[/ ELF spesifikasyon sürümünü gösterir. Genellikle `EV_CURRENT` (1) kullanılır.
[*] e_entry:[/ Yürütülebilir bir dosya için, programın yürütülmeye başlayacağı sanal bellek adresini (giriş noktası) belirtir. İşletim sistemi, programı bu adresten başlatır.
[*] e_phoff / e_shoff:[/ Program Başlık Tablosu'nun ve Bölüm Başlık Tablosu'nun dosya içindeki ofsetlerini (bayt cinsinden başlangıç konumlarını) gösterir. Bu ofsetler, tablolara doğrudan erişim sağlar.
[*] e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx:[/ Çeşitli başlık ve tablo boyutlarını ve sayısını belirtir. Özellikle `e_shstrndx`, bölüm adlarını içeren dizgi tablosunun indeksini işaret eder.
Bu başlığı incelemek için `readelf -h` komutunu kullanırız. Örneğin:
Kod:
$ readelf -h my_executable
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4010a0
Start of program headers: 64 (bytes into file)
Start of section headers: 11680 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29
Yukarıdaki çıktı, dosyanın bir 64-bit yürütülebilir dosya olduğunu, x86-64 mimarisi için derlendiğini ve giriş noktasının `0x4010a0` olduğunu gösterir. Ayrıca, Program Başlık Tablosu'nun dosya içinde 64 bayt ofsetten başladığını ve 13 girişi olduğunu da belirtir.
3. Program Başlık Tablosu (Program Header Table - PHT)
PHT, işletim sisteminin bir programı belleğe nasıl yükleyeceğini açıklayan bir dizi segment tanımlayıcısından oluşur. Her segment, dosyanın bir kısmının bellekte nasıl temsil edileceğini belirtir. İşletim sistemi, bu tablodaki bilgileri kullanarak programın sanal bellek düzenini oluşturur. Yaygın segment türleri ve anlamları şunlardır:
- LOAD:[/ Belleğe yüklenmesi gereken kod veya veri bölümlerini içeren en yaygın segment türüdür. Genellikle bir `LOAD` segmenti okuma/yürütme izinlerine sahip kodu (`.text`), diğeri okuma/yazma izinlerine sahip veriyi (`.data`, `.bss`) barındırır. Bu segmentler, dosya içeriğinin belirli bir bellek adres aralığına nasıl eşleneceğini tanımlar.
[*] PHDR:[/ Program Başlık Tablosu'nun kendisini tanımlayan bir segmenttir. Bu, programın kendi başlıklarına çalışma zamanında erişebilmesi için önemlidir.
[*] INTERP:[/ Dinamik bağlayıcının yolunu (örneğin, Linux'ta `/lib64/ld-linux-x86-64.so.2`) belirten segmenttir. İşletim sistemi, programı çalıştırmadan önce bu yorumlayıcıyı yükler.
[*] DYNAMIC:[/ Dinamik bağlama için gerekli bilgileri (paylaşımlı kütüphane bağımlılıkları, yeniden konumlandırma girdileri vb.) içeren segmenttir. Dinamik bağlayıcı bu bilgilere erişir.
[*] NOTE:[/ Çeşitli sistem notlarını veya özel bilgileri (örneğin, GNU Build ID) içeren segmenttir. Genellikle işletim sistemine veya hata ayıklayıcılara yönelik ekstra veriler barındırır.
[*] TLS (Thread-Local Storage): İş parçacığı yerel depolama alanlarının tanımlarını içerir.
PHT'yi `readelf -l` komutuyla detaylı bir şekilde inceleyebiliriz:
Kod:
$ readelf -l my_executable
Elf file type is EXEC (Executable file)
Entry point 0x4010a0
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000001f8 0x00000000000001f8 R 0x8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238 0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000df4 0x0000000000000df4 R E 0x1000
LOAD 0x0000000000000df8 0x0000000000400df8 0x0000000000400df8 0x000000000000021c 0x0000000000401014 RW 0x1000
DYNAMIC 0x0000000000000e00 0x0000000000400e00 0x0000000000400e00 0x00000000000001d0 0x00000000000001d0 RW 0x8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254 0x0000000000000044 0x0000000000000044 R 0x4
... (diğer segmentler)
Yukarıdaki çıktıda `LOAD` segmentlerinin `R E` (Read, Execute) ve `RW` (Read, Write) izinlerine sahip olduğunu ve bunların dosya içindeki başlangıç ofsetlerini, sanal adreslerini (`VirtAddr`) ve bellek boyutlarını (`MemSiz`) gösterdiğini görebiliriz. Bu, sistemin kodu ve veriyi sanal bellek alanına nasıl yerleştirdiğini ortaya koyar.
4. Bölüm Başlık Tablosu (Section Header Table - SHT)
SHT, bir ELF dosyasının mantıksal yapısını oluşturan bölümler hakkında bilgi sağlar. Bu bölümler, dosyanın çeşitli türdeki içeriğini düzenler ve bağlama aşamasında linker tarafından kullanılır. SHT, özellikle hata ayıklayıcılar ve analiz araçları için büyük önem taşır. Yaygın bölümler ve işlevleri şunlardır:
- .text:[/ Programın yürütülebilir makine kodunu barındırır. Bu bölüm genellikle okuma ve yürütme izinlerine sahiptir.
[*] .rodata:[/ Salt okunur verileri (örneğin, sabit dizgiler, sabit sayılar) içerir. Bu bölüme yazma izni verilmez, bu da güvenlik ve veri bütünlüğü açısından önemlidir.
[*] .data:[/ Başlatılmış global ve statik değişkenleri barındırır. Bu verilere programın çalışma zamanında okunur ve yazılır.
[*] .bss:[/ Başlatılmamış global ve statik değişkenleri barındırır. Bu bölüm dosya içinde yer kaplamaz, ancak program yüklendiğinde işletim sistemi tarafından bellekte sıfırlarla başlatılır.
[*] .symtab:[/ Sembol tablosu. Programdaki fonksiyonlar, değişkenler ve diğer referanslar hakkında bilgi (adı, değeri, boyutu, türü vb.) içerir. Hem global hem de yerel sembolleri barındırabilir.
[*] .strtab:[/ Dizgi tablosu. Sembol adlarını, bölüm adlarını ve diğer dizgileri tutan bir veri havuzudur. Sembol ve bölüm tabloları, bu tablodaki ofsetlere başvurarak isimlere erişir.
[*] .interp:[/ Dinamik bağlayıcının yolunu içeren bölüm, PHT'deki INTERP segmentine karşılık gelir.
[*] .debug_*, .debug_info, .debug_line, vb.: Hata ayıklama bilgileri bulunur (isteğe bağlı). Bu bölümler, kaynak koduyla yürütülebilir kod arasındaki eşleşmeyi sağlar ve hata ayıklayıcıların daha yüksek seviyeli dil yapılarını anlamasına yardımcı olur.
[*] .init / .fini:[/ Program başlatılırken veya sonlandırılırken yürütülen kod parçacıklarını içerir.
Bölümleri `readelf -S` komutuyla listeleyebiliriz:
Kod:
$ readelf -S my_executable
There are 30 section headers, starting at offset 0x2dc0:
Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 00000000 00000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238 0000001c 00000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254 00000020 00000000 A 0 0 4
[ 3] .note.gnu.build-id NOTE 0000000000400274 00000274 00000024 00000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298 0000001c 00000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8 000000a8 00000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400360 00000360 0000007e 00000000 A 0 0 1
[ 7] .gnu.version VERSYM 00000000004003de 000003de 0000000e 00000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 00000000004003ec 000003ec 00000020 00000000 A 6 1 8
[ 9] .rela.dyn RELA 000000000040040c 0000040c 00000048 00000018 A 5 0 8
[10] .rela.plt RELA 0000000000400454 00000454 00000030 00000018 AI 5 0 8
[11] .init PROGBITS 0000000000400484 00000484 00000017 00000000 AX 0 0 4
[12] .plt PROGBITS 00000000004004a0 000004a0 00000030 00000010 AX 0 0 16
[13] .plt.got PROGBITS 00000000004004d0 000004d0 00000008 00000008 AX 0 0 8
[14] .text PROGBITS 00000000004004e0 000004e0 00000130 00000000 AX 0 0 16
[15] .fini PROGBITS 0000000000400610 00000610 00000009 00000000 AX 0 0 4
[16] .rodata PROGBITS 0000000000400620 00000620 00000017 00000000 A 0 0 4
[17] .eh_frame_hdr PROGBITS 0000000000400638 00000638 0000003c 00000000 A 0 0 4
[18] .eh_frame PROGBITS 0000000000400678 00000678 000000f0 00000000 A 0 0 8
[19] .init_array INIT_ARRAY 0000000000400df8 00000df8 00000008 00000008 WA 0 0 8
[20] .fini_array FINI_ARRAY 0000000000400e00 00000e00 00000008 00000008 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000400e08 00000e08 000001d0 00000010 WA 6 0 8
[22] .got PROGBITS 0000000000400fd8 00000fd8 00000008 00000008 WA 0 0 8
[23] .got.plt PROGBITS 0000000000400fe0 00000fe0 00000030 00000008 WA 0 0 8
[24] .data PROGBITS 0000000000401010 00001010 00000010 00000000 WA 0 0 8
[25] .bss NOBITS 0000000000401020 00001020 00000008 00000000 WA 0 0 8
[26] .comment PROGBITS 0000000000000000 00001020 0000001a 00000001 0 0 1
[27] .symtab SYMTAB 0000000000000000 00001040 00000720 00000018 28 47 8
[28] .strtab STRTAB 0000000000000000 00001760 0000021c 00000000 0 0 1
[29] .shstrtab STRTAB 0000000000000000 0000197c 0000011f 00000000 0 0 1
Bu çıktı, dosyadaki tüm bölümleri, bunların türlerini, sanal adreslerini, dosya ofsetlerini ve boyutlarını listeler. `.text` bölümünün yürütülebilir kodu içerdiğini (`AX` bayrağı), `.data` ve `.bss` bölümlerinin yazılabilir veriyi (`WA` bayrağı) barındırdığını gözlemleyebiliriz. `.bss` bölümünün `NOBITS` türünde olması, dosya içinde yer kaplamadığını ancak bellek ayrılacağını gösterir.
5. Sembol ve Dizgi Tabloları
ELF dosyaları, programın içerdiği fonksiyonlar, değişkenler ve diğer referanslar hakkında bilgi sağlayan sembol tablolarını içerir. Bu tablolar, özellikle bağlama ve hata ayıklama süreçlerinde hayati öneme sahiptir. İki ana sembol tablosu türü vardır:
- .symtab (Symbol Table): Derleme birimindeki (örneğin, bir C dosyası) tüm global, yerel ve tanımsız sembolleri içerir. Bu semboller arasında fonksiyonlar, global değişkenler, statik değişkenler ve bazen hata ayıklama sembolleri de bulunur. Yürütülebilir dosyalarda, genellikle `strip` komutuyla kaldırılırlar, ancak geliştirme ve hata ayıklama sırasında çok değerlidirler.
- .dynsym (Dynamic Symbol Table): Yalnızca dinamik olarak bağlı sembolleri içerir. Örneğin, bir programın paylaşımlı kütüphanelerden içe aktardığı veya dışa aktardığı fonksiyonlar ve değişkenler burada listelenir. Bu tablo, dinamik bağlayıcının çalışma zamanında sembolleri çözmesi için gereklidir.
- .strtab (String Table): Sembol adlarını, bölüm adlarını ve diğer dizgileri tutan bir veri havuzudur. Sembol ve bölüm tabloları, bu dizgi tablolarındaki ofsetlere başvurarak sembol ve bölüm adlarına erişir. Bu sayede, sembol adları tekrar etse bile dosyada yalnızca bir kez saklanır ve yer kazancı sağlanır.
Sembolleri `readelf -s` komutuyla görüntüleyebiliriz:
Kod:
$ readelf -s my_executable
Symbol table '.symtab' contains 75 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS crt1.o
2: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
3: 00000000004004e0 0 FUNC LOCAL DEFAULT 14 _start
...
30: 00000000004004e0 20 FUNC GLOBAL DEFAULT 14 main
31: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
32: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
...
Bu çıktı, `main` fonksiyonunun bir global sembol olduğunu (`GLOBAL` bağlama tipi) ve `.text` bölümünde (`Ndx 14`) yer aldığını gösterir. `puts` ve `__libc_start_main` gibi fonksiyonların ise `GLIBC_2.2.5` kütüphanesinden dinamik olarak bağlandığını (`UND` - undefined, `WEAK` - zayıf bağlama) görüyoruz. Sembol çözünürlüğü, programın harici kaynaklara nasıl eriştiğini anlamak için hayati bir adımdır.
6. Dinamik Bağlantı ve Yeniden Konumlandırma
Modern işletim sistemlerinde programlar genellikle paylaşımlı kütüphaneler (`.so` dosyaları) kullanır. Bu yaklaşım, kod tekrarını azaltır, bellek kullanımını optimize eder ve programların güncel kütüphanelerden faydalanmasını sağlar. Dinamik bağlayıcı (`ld-linux.so` gibi), bir program başladığında gerekli kütüphaneleri bulur ve programın sanal adres alanına eşler. Bu süreç, dinamik bağlama olarak adlandırılır.
- PLT (Procedure Linkage Table): Prosedür Bağlantı Tablosu, yürütülebilir dosya içindeki harici fonksiyon çağrılarını dinamik bağlayıcıya yönlendiren bir mekanizmadır. Bir harici fonksiyon ilk kez çağrıldığında, PLT aracılığıyla dinamik bağlayıcıya yönlendirilir ve gerçek fonksiyon adresi bulunur. Sonraki çağrılar için bu adres GOT'a kaydedilir.
- GOT (Global Offset Table): Global Ofset Tablosu, harici sembollerin (fonksiyonlar ve değişkenler) gerçek bellek adreslerini tutan bir tablodur. Dinamik bağlayıcı, bir sembolün adresini çözümlediğinde, bu adresi GOT'a yazar. Böylece, programın sonraki referansları doğrudan doğru adrese gider.
- Yeniden Konumlandırma (Relocation): Programın veya paylaşımlı kütüphanelerin yüklendiği gerçek adresler, derleme zamanında bilinemez. Yeniden konumlandırma, bu sembol referanslarının ve bellek adreslerinin, program çalışma zamanında veya yükleme zamanında gerçek adreslerle güncellenmesi işlemidir. `.rela.dyn` ve `.rela.plt` gibi bölümler, yeniden konumlandırma girdilerini içerir ve bağlayıcıya hangi adreslerin nasıl güncelleneceğini söyler.
Bir ELF dosyasının bağlı olduğu paylaşımlı kütüphaneleri görmek için `ldd` komutunu kullanırız:
Kod:
$ ldd my_executable
linux-vdso.so.1 (0x00007ffe4776e000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3c4c9b3000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3c4cbaf000)
Bu çıktı, `my_executable` adlı programın `libc.so.6` (C standart kütüphanesi) ve `ld-linux-x86-64.so.2` (dinamik bağlayıcı) gibi kütüphanelere bağımlı olduğunu ve bunların bellekteki başlangıç adreslerini gösterir.
Yeniden konumlandırma girdilerini `readelf -r` komutuyla inceleyebiliriz:
Kod:
$ readelf -r my_executable
Relocation section '.rela.dyn' at offset 0x40c contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000401018 000100000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000401028 000200000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000401020 000300000006 R_X86_64_GLOB_DAT 0000000000000000 puts@GLIBC_2.2.5 + 0
Relocation section '.rela.plt' at offset 0x454 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000400490 000100000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000400498 000300000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
Bu çıktılar, `__libc_start_main` ve `puts` gibi fonksiyonların çalışma zamanında nasıl çözüldüğünü ve hangi bellek ofsetlerinin dinamik bağlayıcı tarafından güncellenmesi gerektiğini detaylı bir şekilde gösterir. `R_X86_64_JUMP_SLOT` türündeki yeniden konumlandırmalar genellikle PLT/GOT mekanizmasıyla ilişkilidir.
7. ELF Dosyalarını Çözümleme Araçları
ELF dosyalarını derinlemesine incelemek ve anlamak için birçok güçlü ve yaygın araç mevcuttur. Bu araçlar, farklı analiz ihtiyaçlarına göre özelleşmiştir:
- readelf:[/ GNU Binutils paketinin temel bir parçasıdır ve ELF dosyalarını incelemek için en kapsamlı ve temel araçlardan biridir. ELF başlığını, bölüm ve program başlık tablolarını, sembol tablolarını, dinamik etiketleri, yeniden konumlandırma girdilerini ve çok daha fazlasını detaylı olarak görüntülemek için kullanılır. Yukarıdaki örneklerde bolca kullandığımız bu araç, düşük seviyeli ELF yapısını anlamak için vazgeçilmezdir.
[*] objdump:[/ Yine Binutils'in bir parçası olan `objdump`, nesne dosyalarını incelemek, makine kodunu sökmek (disassembly) ve belirli bölüm içeriklerini (örneğin `.text`, `.data`) görüntülemek için kullanılır. Özellikle tersine mühendislik bağlamında kod akışını ve fonksiyonel mantığı anlamak için kritik bir araçtır.
Kod:$ objdump -d my_executable my_executable: file format elf64-x86-64 Disassembly of section .text: 00000000004004e0 <_start>: 4004e0: 31 ed xor %ebp,%ebp 4004e2: 49 89 d1 mov %rdx,%r9 4004e5: 5e pop %rsi 4004e6: 48 89 e2 mov %rsp,%rdx 4004e9: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 4004ed: 50 push %rax 4004ee: 54 push %rsp 4004ef: 4c 8d 05 c2 00 00 00 lea 0xc2(%rip),%r8 # 4005b8 <__libc_csu_fini> 4004f6: 48 8d 0d c3 00 00 00 lea 0xc3(%rip),%rcx # 4005c0 <__libc_csu_init> 4004fd: 48 8d 3d b6 00 00 00 lea 0xb6(%rip),%rdi # 4005bc <main> 400504: ff 15 2e 00 00 00 call *0x2e(%rip) # 400538 <__libc_start_main@GLIBC_2.2.5> 40050a: f4 hlt 000000000040050b <main>: 40050b: 55 push %rbp 40050c: 48 89 e5 mov %rsp,%rbp 40050f: 48 8d 3d d4 00 00 00 lea 0xd4(%rip),%rdi # 4005e8 <_IO_stdin_used+0x8> 400516: e8 c5 ff ff ff call 4004e0 <_start+0x0> 40051b: b8 00 00 00 00 mov $0x0,%eax 400520: 5d pop %rbp 400521: c3 ret
Kod:$ nm my_executable 00000000004005b8 T __libc_csu_fini 00000000004005c0 T __libc_csu_init U __libc_start_main@@GLIBC_2.2.5 000000000040050b T main U puts@@GLIBC_2.2.5 00000000004004e0 T _start
Kod:$ ldd my_executable linux-vdso.so.1 (0x00007ffe4776e000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3c4c9b3000) /lib64/ld-linux-x86-64.so.2 (0x00007f3c4cbaf000)
Kod:$ strace ./my_executable execve("./my_executable", ["./my_executable"], 0x7ffd7a04dd28 /* 64 vars */) = 0 brk(NULL) = 0x5558d11c1000 arch_prctl(ARCH_SET_FS, 0x5558d11c1880) = 0 set_tid_address(0x5558d11c1b50) = 13744 set_robust_list(0x5558d11c1b60, 24) = 0 rseq(0x5558d11c2120, 0x20000, 0, 0x5558d11c2120) = 0 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9a2f64a000 ... fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0 write(1, "Hello, ELF!\n", 14Hello, ELF! ) = 14 exit_group(0) = ? +++ exited with 0 +++
[*] Hex Editörler (xxd, hexdump): ELF dosyasının ham baytlarını görüntülemek için kullanılır. Bu araçlar, dosyanın düşük seviyede nasıl organize edildiğini ve belirli veri yapılarının (örneğin, başlıklar veya tablolar) dosya içindeki gerçek konumlarını anlamak için faydalıdır.
[*] İleri Seviye Tersine Mühendislik Araçları (IDA Pro, Ghidra, Radare2): Özellikle karmaşık ELF dosyalarını, tescilli yazılımları veya zararlı yazılımları analiz etmek için tasarlanmış kapsamlı araçlardır. Grafikli arayüzler, güçlü kod dekompilasyonu (makine kodunu daha okunabilir C benzeri bir dile çevirme) ve gelişmiş analiz yetenekleri sunarlar. Bu araçlar, büyük ikili dosyaların içindeki mantığı anlamayı kolaylaştırır. Daha fazla bilgi için Ghidra projesini veya IDA Pro'yu inceleyebilirsiniz.
8. Pratik Uygulama ve Örnek Senaryo
Basit bir C programı yazarak ELF çözümlemesini somutlaştıralım ve çeşitli araçlarla inceleyelim:
Kod:
// hello.c
#include <stdio.h>
int main() {
printf("Hello, ELF!\n");
return 0;
}
Bu kodu derleyelim ve yürütülebilir bir dosya oluşturalım:
Kod:
$ gcc hello.c -o hello
Şimdi `hello` yürütülebilir dosyasını yukarıda bahsettiğimiz araçlarla adım adım inceleyelim:
- Dosya Türünü Belirleme: `file` komutu, dosyanın temel özelliklerini hızlıca gösterir.
Kod:$ file hello hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=..., not stripped
Yukarıdaki çıktı, `hello` dosyasının 64-bit bir ELF yürütülebilir dosyası olduğunu, x86-64 mimarisi için derlendiğini ve dinamik olarak `/lib64/ld-linux-x86-64.so.2` yorumlayıcısı ile bağlandığını açıkça belirtiyor. Ayrıca, dosyanın `strip` edilmediğini (yani sembol bilgilerinin hala içeride olduğunu) de gösteriyor, bu da hata ayıklama için önemlidir. - ELF Başlığını İnceleme: `readelf -h` ile giriş noktasını bulabiliriz.
Kod:$ readelf -h hello | grep 'Entry point address' Entry point address: 0x4010a0
- Dinamik Bağlantıları Görüntüleme: `ldd` ile kütüphane bağımlılıklarını kontrol edelim.
Kod:$ ldd hello linux-vdso.so.1 (0x00007ffcc856c000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbf86de8000) /lib64/ld-linux-x86-64.so.2 (0x00007fbf86ff5000)
- Sembol Tablosunu Kontrol Etme: `nm` veya `readelf -s` kullanarak program içindeki sembolleri görebiliriz.
Kod:$ nm hello | grep ' main' 0000000000401149 T main $ nm hello | grep ' printf' U printf@@GLIBC_2.2.5
9. Sonuç
ELF dosyalarını çözümlemek, modern işletim sistemlerinin nasıl çalıştığını ve uygulamaların düşük seviyede nasıl etkileşim kurduğunu anlamanın temelidir. Bu rehberde, ELF başlığından program ve bölüm başlık tablolarına, sembol ve dizgi tablolarından dinamik bağlama mekanizmalarına kadar ELF yapısının birçok önemli yönünü ele aldık. Ayrıca, `readelf`, `objdump`, `nm` ve `ldd` gibi güçlü Binutils araçlarını kullanarak pratik örnekler sunduk.
ELF analizi, güvenlik araştırmacılarının zararlı yazılımları incelemesi, sistem programcılarının performansı optimize etmesi ve yazılım geliştiricilerinin derlenmiş kodun davranışını anlaması için kritik bir beceridir. Bu bilgi, bir programın bellekte nasıl düzenlendiğini, harici kütüphanelerle nasıl etkileşim kurduğunu ve nihayetinde CPU üzerinde nasıl yürütüldüğünü daha derinlemesine kavramanıza yardımcı olacaktır. Daha fazla bilgi ve derinlemesine inceleme için, ELF spesifikasyonuna başvurmanız şiddetle tavsiye edilir.
Bu kapsamlı rehberin, ELF dünyasına adım atmanız için sağlam bir temel oluşturduğunu umuyoruz. Unutmayın ki pratik yapmak ve farklı türdeki ELF dosyalarını (örneğin, paylaşımlı kütüphaneler, statik derlenmiş dosyalar) incelemek, bu alandaki uzmanlığınızı pekiştirmenin ve daha karmaşık analiz görevlerine hazırlanmanın en iyi yoludur.