Exploit Geliştiriciler için Endianness(Big/Little Endian) Kavramı

İstismar kodu geliştirmeye yeni başlayanlar için endianness meselesi
genelde kafa karıştırıcı olabilmektedir. Devam eden satırlarda
“İşlemcinin little endian veya big endian olması ne demektir?” ve
“İstismar kodu geliştiricisi için ne değiştirir?” soruları
cevaplanmıştır.

Günümüzde kullandığımız bilgisayar mimarisinde
bellekler her bir adreste bir birim (byte, word vb.) veri bulunacak
şekilde tasarlanırlar. Tersten düşünülürse her bir birim veriye bir
adres verilir. Aktif olarak kullandığımız ve yazının devamında
kastedilen belleklerde bu birim byte’tır.

Bu yapı gereği bellekte
erişilebilecek (okuma/yazma) en küçük birim byte olur. Yani bir byte’ın
son 4 bitine erişmek gibi bir seçenek olmaz en az 8 bitlik işlemler
yapılır. Bu sebepten bellekler byte-addressable, word-addressable gibi
sınıflandırılabilir.


İşlemciler bellek üzerindeki veriye
erişirken belleğin standardına uymak zorunda olmalarına karşın veriyi
anlamlandırma biçimleri farklılık gösterebilir. İşte endianness meselesi
de tam olarak bununla alakalıdır. Big endian bir işlemci bellekteki
veriye erişirken ilk adresteki değeri (en küçük adres) registerdaki en
anlamlı (most significant) byte ile eşleştirir. Little endian bir
işlemci ise tam tersini yaparak bellekteki en küçük adresi registerdaki
en anlamsız (least significant) byte ile eşleştirir.

Örneğin bellekteki 4
byte’lık bir verinin big ve little endian işlemcilerdeki değeri
(yüklendiği registerdaki hali) şu şekildedir. Tabi bu 4 byte’a tek değer
gibi (örneğin 32 bit unsigned integer) davranıldığında geçerlidir.

Bellek Adresi
Veri
100
AA
101
BB
102
CC
103
DD

Big Endian Değeri
AA
BB
CC
DD

Little Endian Değeri
DD
CC
BB
AA

İstismar kodu geliştirirken little ve big endian işlemcilerin bu
davranışının bilinip bellek adresleri (pointer) gibi değerlerin mimariye
uygun şekilde pack edilmesi gerekir. Bu sayede girdi
anlamlandırıldığında byte’ların doğru sırada olması sağlanır. Çok basit
bir C kodu ile uygulaması yapılabilir.

#include <stdio.h>

main(){
    char test[30];

    printf(“bof: “);
    scanf(“%s”, test);

    printf(“n%sn”, test);
}

İlk
olarak x86 mimarisi için (little endian, 32 bit) derlenip IDA ile debug
edilir. Devamında msf’nin pattern_create aracı ile 50 byte’lık bir
cyclic pattern oluşturulur. Uygulamaya girdi olarak bu pattern girilir
ve IDA’da uygulamanın “Segmentation fault” mesajı görülür.

Hatada
görünen adres EIP’in değeridir, bu değer pattern_offset aracına
parametre olarak verildiğinde değerin 42. byte’a denk geldiği
hesaplanır. Buradan pattern’in 42. byte’ından başlayan 4 byte’lık değer
“Ab4A” iken EIP’teki değerin “A4bA” olduğu görülür. pattern_offset
aracı, 0x41346241 olarak verilen değeri uygun şekilde pack edip pattern
içindeki yerini bulabilmiştir.

Aynı C kodu MIPS mimarisi için (big endian, 32 bit) derlenip pattern oluşturma ve debug işlemleri tekrarlanır.

EIP’in
değeri pattern_offset aracına verildiğinde (0x62324162) eşleşen bir
offset bulunamamasına karşın bu değer ters çevrilerek verildiğinde
(0x62413262) değerin, pattern’in 37. byte’ından başladığı görülür.

Buradan
şu sonuç çıkarılabilir. Pattern_offset aracı little endian sistemler
için düzgün çalışmasına karşın big endian sistemlerde aynı başarıyı
gösteremiyor ancak registerda görünen değeri ters çevirip yazarak aracı
kullanmak mümkün. Çünkü araç değeri pack ederken byte dizilimini ters
çevirmiş oluyor.
Bu cümleden little endian bir sistemin girdi olarak
aldığı verileri ters çevirdiği genellemesine gitmek yanlış bir çıkarım
olacaktır.
Buna karşın araca parametre olarak hex formatında verilen 4
byte’lık değerin byte diziliminin pack işlemi sonucu ters çevrildiği
söylenebilir.

Son olarak istismar kodu geliştirirken çok faydalı
olan python’ın struct kütüphanesinin pack, unpack fonksiyonlarının
kullanımından bahsetmek faydalı olacaktır. struct.pack fonksiyonu ikinci
parametre olarak girilen değeri ilk parametrede belirtilen formata pack
eder, struct.unpack fonksiyonu ise tam tersini yapar.
Yani 32 bit bir
pointer’ı istismar kodunda kullanılabilecek bir string (binary packed)
olarak ifade etmek için pack fonksiyonu aşağıdaki gibi kullanılabilir.

Little endian bir işlemci için:
>>> struct.pack(‘<I’, 0xAABBCCDD)
‘xddxccxbbxaa’

Big endian bir işlemci için:
>>> struct.pack(‘>I’, 0xAABBCCDD)
‘xaaxbbxccxdd’

Tam
tersi işlemi yapmak yani bellekteki bir stringin işlemcideki değerini
görmek içinse unpack fonksiyonu şu şekilde kullanılabilir.

Little endian bir işlemci için:
>>> hex(struct.unpack(‘<I’, ‘xaaxbbxccxdd’)[0])
‘0xddccbbaa’

Big endian bir işlemci için:
>>> hex(struct.unpack(‘>I’, ‘xaaxbbxccxdd’)[0])
‘0xaabbccdd’

Kaynaklar:
https://docs.python.org/2/library/struct.html
http://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Data/endian.html