ELF Dosya Formatı

ELF dosya yapısı UNIX ve türevi işletim sistemlerinde en çok kullanılan dosya biçimidir. Dosya biçimini anlamak, bir programcı veya tersine mühendis açısından çok önemlidir. Programcı dosya tipine eğer hakim ise derlenmiş dosyanın boyutunun küçülmesinden, karşılaşılan sorunları hızlıca çözmeye kadar bir çok konuda güç kazanacaktır. Tersine mühendis açısından da inceleme yaptığı dosya üzerindeki atlatma, gizlenme gibi tekniklerin rahatlıkla anlaşılıp çözüm üretmesine yarar sağlayacaktır. Ben bu yazı içerisinde ELF dosya biçimini çok detaylı bir şekilde anlatmayı düşünmüyorum. Zaten o işi yapan gayet güzel Türkçe ve İngilizce belgeler mevcut. O tarz bir amaç için o belgelere başvurabilirsiniz. Ben daha çok pratik bir inceleme yapıp, temel düzeyde bir readelf benzeri bir program yazmayı düşünüyorum. Daha sonra bu programı genişletip bir CTF sorusunu nasıl çözeceğimizi göstereceğim. Herşeyi en başta elimiz ile yaptıktan sonra tabi ki işin en kolay yollarını da söylemeyi unutmayacağım.

ELF Biçimleri

Karşılaşacağımız ELF dosyaları çalıştırılabilir, yeniden konumlandırılabilir ve paylaşımlı biçimlerinde olabilirler. Örneğin aşağıdaki gibi bir hello world kodumuz olsun.

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Hello world!");
    return 0;
}

Yazdığımız kod main fonksiyonu içerdiği için, derleyicimiz bu kodun nereden itibaren çalıştırılabileceğine karar verebilir. Haliyle bu dosyayı aşağıdaki şekilde derlersek, elde edeceğimiz ELF dosya biçimi bir çalıştırılabilir dosya olacaktır.

gcc hello.c -o hello

Burada ufak bir hatırlatma yapmak istiyorum. main fonksiyon ismi gcc derleyicisinin başlangıç için aradığı kısımdır. Eğer istersek bu başlangıç noktasını gcc derleyicisine verebileceğimiz -e parametresi ile değiştirebiliriz. Buradaki kısaltma entry point kelimelerinden gelmektedir. Örneğin aşağıdaki gibi iki adet fonksiyon yazalım ve haydar fonksiyonunu entry point olarak tanımlayalım. return yerine neden exit sistem çağrısı kullandığım detayını isterseniz ufak denemeler yaparak kendiniz de anlayabilirsiniz.

#include <stdio.h>
#include <stdlib.h>

int haydar(void){
    printf("Hello haydar!");
    exit(0);
}

int main(int argc, char *argv[]) {
    printf("Hello world!");
    exit(0);
}

Şimdi de derleyelim.

gcc hello2.c -o hello2 -e haydar

Kodumuz içerisinde main olmasına rağmen biz haydar fonksiyonunu başlangıç olarak kabul ettik. Derleme sonucu elimizde oluşan dosyanın çalıştırılabilir biçimde olduğunu unutmayalım. Özetle, bu biçimdeki dosyalar, işletim sisteminin çalıştırıcısı tarafından işletilebilir durumdadırlar. Linux altında eğer yeterli izinler mvecut ise, ./dosya şeklinde çalıştırma yapılabilir. Bu işlemden sonra işletim sistemi dosya biçimini uygun şekilde işleyip, bellekte çalıştırır.

çalıştırılabilir biçiminin dışında bir de yeniden konumlandırılabilir dosya biçimi vardır. Bu dosya biçimine ulaşmak için aşağıdaki gibi bir örnek kullanabiliriz.

int haydar2(void){
    int x = 15;
    int y = 20;

    return x + y;
}

Elimizde sadece haydar2 isimli bir fonksiyona sahip C kodunu, aşağıdaki parametreler ile derleyince oluşan haydar2.o dosyası, bir yeniden konumlandırılabilir dosya biçimidir.

gcc -c haydar2.c -o haydar2.o

Bu dosya biçiminin tam olarak ne işe yaradığı aslında isminden anlaşılabilir. Bu tarz dosyalar, kendi tipindeki diğer dosyalar ile bağlanıp, çalıştırılabilir bir hale gelirler. Buradaki bağlanma kelimesi de bizi yeni bir kavram olan bağlayıcı yani linker konusuna götürür. Ancak bu yazının içeriği bu kavramları açıklamak olmadığından şimdilik atlıyorum.

ELF dosya biçimine sahip son dosya biçimi de paylaşımlı nesne dosyalarıdır. Bu dosya biçimindeki dosyalar, çalıştırılabilir dosyalara dinamik olarak bağlanabilirler. Genellikle kütüphane oluştururken kullanılan bu dosya biçimine, gcc derleyicisine vereceğiniz -shared parametresi ile ulaşabilirsiniz. Dosya sisteminiz içerisinde yer alan .so veya .so.1 uzantılı dosyalar bu formattadır.

ELF Veri Tipleri

Derlenmiş dosyaların hangi biçimlerde olabileceğini gördükten sonra, şimdi ELF dosya tipinin kullandığı veri tiplerini tanımlayabiliriz. Bu veri tiplerimizi tutmak için elf.h dosyası oluşturalım. Şimdilik içeriği sadece aşağıdaki gibi olsun.

#ifndef __ELF_H__
#define __ELF_H__

#include <stdint.h>

typedef uint16_t Elf32_Half;
typedef uint32_t Elf32_Off;
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Word;
typedef int32_t  Elf32_Sword;

typedef uint16_t Elf64_Half;
typedef uint64_t Elf64_Off;
typedef uint64_t Elf64_Addr;
typedef uint32_t Elf64_Word;
typedef int32_t  Elf64_Sword;
typedef uint64_t Elf64_Xword;
typedef uint64_t Elf64_Sxword;

#endif

uint16_t, uint32_t gibi veri tipleri stdint.h dosyasında tanımlanmıştır. Bu veri tiplerini kullanarak istediğimiz bit miktarını garanti altına alabiliyoruz. Aynı zamanda koda bakıldığı anda miktarı da anlaşılabiliyor. Yukarıdaki tanımlamalar ELF bölümlerinde yer alan verilerde kullanılacak temel veri tipleridir.

ELF Başlık Yapısı

Dosya formatlarının en önemli kısımlarından birisi başlık bölümüdür. Bu bölüm altında ilgili dosyanın ne tür bir biçime sahip olduğu, hangi mimari için derlendiği ve diğer kısımlar hakkında bilgiler yer alır. ELF dosya formatı için bu başlık yapısı aşağıdaki şekilde tanımlanır. Bu tanımlamalar bir üst kısımda başladığımız elf.h dosyasının devamına eklenmektedir.

#define EI_NIDENT 16

typedef struct {   
  unsigned char     e_ident[EI_NIDENT];
  Elf32_Half        e_type;
  Elf32_Half        e_machine;
  Elf32_Word        e_version;
  Elf32_Addr        e_entry;
  Elf32_Off         e_phoff;
  Elf32_Off         e_shoff;
  Elf32_Word        e_flags;
  Elf32_Half        e_ehsize;
  Elf32_Half        e_phentsize;
  Elf32_Half        e_phnum;
  Elf32_Half        e_shentsize;
  Elf32_Half        e_shnum;
  Elf32_Half        e_shtrndx;
} Elf32_Ehdr;

typedef struct {
  unsigned char     e_ident[EI_NIDENT];
  Elf64_Half        e_type;
  Elf64_Half        e_machine;
  Elf64_Word        e_version;
  Elf64_Addr        e_entry;
  Elf64_Off         e_phoff;
  Elf64_Off         e_shoff;
  Elf64_Word        e_flags;
  Elf64_Half        e_ehsize;
  Elf64_Half        e_phentsize;
  Elf64_Half        e_phnum;
  Elf64_Half        e_shentsize;
  Elf64_Half        e_shnum;
  Elf64_Half        e_shtrndx;
} Elf64_Ehdr;

Bu tanımlamaları da yaptıktan sonra yazı sonunda oluşturmayı plandığımız programın temel iskeletini oluşturalım. Bu uygulama elf.c olarak isimlendirilecek ve gcc elf.c -o elf şeklinde derlenecektir.

#include <stdio.h>
#include <stdlib.h>
#include "elf.h"

void print_header(Elf32_Ehdr* file_header){

}

int main(int argc, char *argv[]) {
    if(argc < 2){
        fprintf(stderr, "Usage: %s <elf_file>", argv[0]);
        return 1;
    }else{
        FILE* fp = fopen(argv[1], "r");
        if(fp == NULL){
            fprintf(stderr, "No such file '%s'", argv[1]);
            return 1;
        }else{
            Elf32_Ehdr* file_header = (Elf32_Ehdr*)calloc(sizeof(Elf32_Ehdr), 1);
            if(file_header == NULL){
                fprintf(stderr, "Memory allocation error.");
                return 1;
            }else{
                fread(file_header, sizeof(Elf32_Ehdr), 1, fp);
                print_header(file_header);
                free(file_header);
            }
        }
    }
    return 0;
}

Yazının başından beri gerçekten anlatmak istediğim kısma yavaş yavaş geliyoruz. Yukarıda oluşturduğumuz uygulama parametre olarak bir dosya yolu beklemektedir. Bu dosyayı herhangi bir kontrol yapmadan(normalde birçok kontrolden geçirmek gerekli) açmaktadır. Dosya açma işlemi eğer başarılı olduysa, daha önceden oluşturduğumuz Elf32_Ehdr veri tipi içerisine okuma yapmaktadır. Buradaki fread fonksiyonuna, daha önceden açtğımız dosya içerisinden, file_header değişkenine, Elf32_Ehdr veri tipi boyutunda, veri okuması yap diyoruz. Bu file_header değişkeni için bellekten alan ayırma işlemi de 19 nolu satırdaki calloc çağrısı ile gerçekleşiyor. Özetle bu işlemler sonucunda file_header isminde Elf32_Ehdr veri tipinde bir değişkene sahip oluyoruz. Bu değişken içerisindeki değerleri de ekrana basmak için 25 nolu satırda print_header fonksiyonunu çağırıyoruz.

Bu işlemler sonucunda artık ilgili dosyanın başlık bilgilerine ulaşabiliriz.

ELF Dosya Tanımlaması

Elf32_Ehdr veri tipinin ilk 16 byte olarka ayrılmış kısmında, ELF dosyasını tanımlamak için gerekli bilgiler yer almaktadır. Bu bilgiler e_ident dizisi olarak tanımlanmıştır. Bu dizinin ilk 4 byte'ı Magic kısım olarak geçer ve bu dosya ELF dosyasıdır demek için gereklidir. Geri kalan kısımlardan dosyanın hangi işletim sistemi tarafından kullanılacağı, kaç bitlik bir sistem ile çalışacağı gibi bilgiler elde edilebilir. 5.byte'da yer alan EI_CLASS dosyanın sınıfını tutar. Bu değerin 0 olması Geçersiz, 1 olması 32 Bit nesne ve 2 olması 64 Bit nesne olduğunu gösterir. Bu veri yapısının içeriğinde yer alan kısımlarda çeşitli alt bölmelere ayrılmıştır. Bunları da elf.h dosyasına ekleyelim ve daha sonra ilk dosyamızı okuyalım.

enum {
    EI_MAG0         = 0, 
    EI_MAG1         = 1,
    EI_MAG2         = 2,
    EI_MAG3         = 3,
    EI_CLASS        = 4,
    EI_DATA         = 5,
    EI_VERSION      = 6,
    EI_OSABI        = 7,
    EI_ABIVERSION   = 8,
    EI_PAD          = 9
};

#define ELFMAG0        0x7F
#define ELFMAG1        'E'
#define ELFMAG2        'L'
#define ELFMAG3        'F'

enum {
    ELFCLASSNONE    = 0,
    ELFCLASS32      = 1,
    ELFCLASS64      = 2
};

Daha önce üst kısımlarda yaptığımız basit hello world kodunu gcc hello.c -o hello -m32 parametresi ile derlediğimizi düşünürsek, print_header dosyasını şu şekilde gerçekleştirebiliriz.

void print_identifer(unsigned char e_ident[EI_NIDENT]){
    printf("%-20s %x | %c%c%c\n", "Magic Header:", e_ident[EI_MAG0], e_ident[EI_MAG1], e_ident[EI_MAG2], e_ident[EI_MAG3]);
    if(e_ident[EI_CLASS] == ELFCLASSNONE){
        printf("%-20s %s", "Class type:", "Gecersiz sinif.");
    }else if(e_ident[EI_CLASS] == ELFCLASS32){
        printf("%-20s %s", "Class type:", "32 Bit sinif.");
    }else if(e_ident[EI_CLASS] == ELFCLASS64){
        printf("%-20s %s", "Class type:", "64 Bit sinif.");
    }
}

void print_header(Elf32_Ehdr* file_header){
    print_identifer(file_header->e_ident);
}

Şimdi burada yaptıklarımıza bir bakalım. print_header fonksiyonumuz print_identifer fonksiyonunu çağırıyor. Bu fonksiyon içerisinde önce ilk 4 byte ekrana basılıyor. Tabi burada ilk 4 byte'a EI_MAG0, EI_MAG1, EI_MAG2 ve EI_MAG3 tanımlamaları ile erişiyoruz. Bunlar zaten sırayla 0, 1, 2, 3 olaran tanımlanmışlar. Bu tanımlamalardan EI_MAG0 bilgisi 0x7f olmak zorundadır. Daha sonra gelen kısımda ise ELF kelimesinin karakterleri yer almalıdır. Verilen dosyanın ELF formatında olduğunu en başta buradan anlayabiliriz. Daha sonra e_ident dizisinin EI_CLASS kısmında yer alan bilgi kontrol edilmektedir. Eğer bu değer 0 ise geçersiz, 1 ise 32 bit ve 2 ise 64 bit sınıf olarak değerlendirilmektedir. Biz yukarıda yazdığımız hello uygulamasını önce 32 bit daha sonra da 64 bit için derleyelim ve programımıza verip ilk denememizi yapalım.

gcc hello.c -o hello_32 -m32
gcc hello.c -o hello_64 -m64

gcc elf.c -o elf

Derledikten sonra şimdi denememizi yapalım.

./elf hello_32
Magic Header:        7f | ELF
Class type:          32 Bit sinif.

./elf hello_64
Magic Header:        7f | ELF
Class type:          64 Bit sinif.

Gayet güzel bir şekilde çalıştı. Artık elimizde ELF dosya tipini temel düzeyde işleyebilen bir program mevcut. En başta söylediğim gibi bu yazı ELF ile ilgili herşeye değinen bir yazı olmayacak. Sadece temel düzeyde bir okumanın nasıl gerçekleştiğini gösterecektir. Buradan itibaren ELF içerisinden veri okuma kısmını tamamlamak istiyorum. Devamı için Türkçe yazılmış bu dökümana ve kod kısmında yardımcı olması açısından bu dökümana bakabilirsiniz. Yazdığım yazı ile temelini attığınız için, devamını getirmek çok da zor değil. Bir sonraki aşama olarak Section Header bilgilerini okuyarak, tablo içeriklerini ekrana basabilirsiniz mesela. Bu tarz bir uygulama yaparken header bilgilerini nerden bulacağım diye düşünürseniz /usr/include/elf.h dosyasını kullanabilirsiniz. Eğer bu dosya yoksa aşağıdaki gibi bir komut ile header dosyasını bulup kullanabilirsiniz. Örneğin ben radare2 dosyasının içerisindeki header dosyasından faydalanmıştım.

grep "EM_M32" -R / 2> /dev/null

Şimdi buraya kadar gösterdiklerimden yola çıkarak daha önceden CTF sorusunun ilk aşamasını göstermek istiyorum. İlgili dosyaya buradan ulaşabilirsiniz.

Dosyayı indirip açtıktan sonra önce file komutu ile dosyanın türünü belirlemeye çalışalım.

file elfquest2
elfquest2: ELF, unknown class 204

Gelen cevap bize bu dosyanın türünün ELF olduğu ancak bazı kısımların eksik olduğunu söylüyor. Demek ki binary düzeyde bazı kısımlarda eksiklikler var. Herhangi bir hex editor kullanarak içerisini açalım. Ben bvi kullanıyorum genellikle.

Elf File

Evet tahmin ettiğimiz gibi binary dosya üzerinde oynanmış. İlk byte 7F olduğundan, bu dosyanın bir ELF dosyası olduğunu anlayabiliyoruz. Zaten file komutu da buna bakarak bize cevap vermişti. Devamında gelen 45 4C 46 verileri de ELF dosyalarında klasik olarak bulunduğunu ve e_ident dizisinin EI_MAG1, EI_MAG2 ve EI_MAG3 olarak adlandırıldığını yazının en üst kısımlarında bahsetmiştik.

İlk 4 byte doğru olduğu hale geri kalan 12 byte CC ile doldurulmuş durumda. Bu kısımlar ELF dosyalarında e_ident dizisinin içerisine denk geliyor. Yani EI_CLASS, EI_DATA, EI_VERSION, EI_OSABI, EI_ABIVERSION ve EI_PAD kısımları değiştirilmiş durumda. Ayrıca, eğer biraz daha dikkatli bakarsak başka kısımların da değiştirildiğini görebiliriz. 60.byte'dan itibaren 4 byte ve 124.byte'dan itibaren 8 byte değiştirilmiş durumda. Bu kısımlar başlık bilgileri dışarına denk geliyor.

Olması gereken değerleri bulmak için herhangi bir C kodlu derleyerek bakabiliriz veya referans kaynaklarından analiz edebiliriz. Tabi eğer çoğu ELF uygulaması için aynı olan kısımlar değiştirildiyse. Aksi durumda işler çok fazla zorlaşacaktır. Yazı içerisinde derlediğimiz hello_32 isimli uygulamayı bvi kullanarak açalım ve değerleri tespit edelim.

Elf Patch

İlk CC bloğu 01 01 01 00 00 00 00 00 00 00 00 00 olarak değişmeli, ikincisi 34 80 04 08 ve son CC bloğu da 00 80 04 08 00 80 04 08 olarak değişmeli. bvi kullanırken dosya patchleme aşamasında :set memmove komutunu vermeyi unutmayınız. Aksi takdirde düzenleme yapamazsınız. bvi içerisinde yeni bir byte eklemek için i basıp yazmaya başlayabilirsiniz. Değiştirmek için r ve silmek için de x kullanabilirsiniz. Klasik vi komutları burada da geçerlidir. Değiştirdiğiniz dosyayı :w diyerek kaydetmeyi de unutmayınız. Şimdi yaptığımız değişiklikleri kaydetip, file ile tekrardan bakalım.

file elf_patched 
elf_patched: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=15802f12334d15b8ff7266f6b69e2a3fd4e367a9, stripped

Benim ELF ile ilgili anlatmak istediklerim şimdilik bu kadar. Karşılaştığım ilginç detayları tekrardan ufak ufak anlatmak istiyorum. Ancak başlangıç için bu kadarı yeterli gözüküyor. Yaptığım yanlışlar var ise öğrenip düzeltmeyi de çok isterim. Unutmadan yazı içerisindeki header dosyasına buradan ve kaynak kodlarına buradan ulaşabilirsiniz.

comments powered by Disqus