Debugging with GDB – C++ UG Meetup

28 Kasım Cuma günü saat 15:00’de “Debugging with GDB” adlı seminerimize herkes davetlidir.

C++ User Group Istanbul-cppturkey.org

Istanbul, TR
378 C++’ers

C++ UG GROUP ISTANBUL- aimed at sharing knowledge and experience

Check out this Meetup Group →

Advertisements

Macro’s vs Variadic Templates – 1

Variadic fonksiyonlar istenilen sayıda değişken almaları sayesinde bir çok işi kolaylaştırmışlardır. C++11 den önce C de bu ihtiyaç “…” elipsis operatörü ve va_* makrolarıyla çözülüyordu. Fakat bu çözüm çalışma zamanında işlediğinden yanlış kullanım veya beklenmedik durumlarda çalışma zamanı hataları veya seg fault dahi alınabiliyordu.

Örneğin;

int a ;

printf("%s", a);

Bu kod örneği derleme zamanında (compile time) hata vermez. Fakat çalışma zamanında seg fault alırsınız. C++11 de bu problem variadic templatelerle çözülüyor. Üstelik derleme zamanında çözüldüğü için yanlış kullanım durumunda hemen müdahale edebiliyorsunuz.

Variadic templates

Basit bir önekle devam edelim;

#include <iostream>

using namespace std;

template<typename T>
void println(T v) //A
{
    cout << v << endl;
}

template<typename T, typename... Types>
void println(T first, Types... values) //B
{
    println(first);
    println(values...);
}

int main()
{
   println("islam", "cppistanbul", 1299); //1
   println(6, 1, 0, "c++UG", 5, 7, 1); //2
  
  return 0;
}

“typename… Types” şablon parametre paketi(template parameter pack), “Types… values” ise fonksiyon parametre paketi (function parameter pack) şeklinde adlandırılıyor. template parameter packteki Types aslında bir tür listesi. Örneğin “1” durumunda bu liste “const char *, const char *, integer” şeklindedir.Function parameter pack ise bir değer listesidir. Yine “1” durumunda bu liste “islam, cppistanbul, 1299” şeklindedir. Elipsis (…) operatörüyle de bu değerler ve türler açılır(expand). Burda dikkat ederseniz iki tane println fonksiyonu var, basitçe bir oveload aslında. Bu overload ile aslında kısmi bir rekürsiv çağrı yapıyoruz. Daha iyi anlaşılması için adım adım ilerleyelim.

1.iterasyon:

“1” durumunda println fonksiyonu 3 tür ie çağırılmış. Template argument deduction yaparak B deki overload çağrılacak ve:

void println(T, Types ...) [with T = const char*; Types = {const char*, int}]

parametre bu şekilde belirlenecek. Derleyici bir tane yukarıdaki imzaya sahip bir fonksiyon oluşturacak. Ve dikkat edin A durumundaki tek parametreli template de şu şekilde derleyici tarafından yazılacak:

void println(T) [with T = const char*]

2. iterasyon:

B template fonksiyonu:

void println(T, Types ...) [with T = const char*; Types = {int}] 

şeklinde, A template fonksiyonu ise yine aynı imzaya sahip olacağından tekrar yazılmayacak.

3. iterasyon

Bu durumda ise sadece tek tür kaldıgından direkt olarak A template fonksiyonu tercih edilecek(argument deduction). Ve aşağıdaki imzaya sahip bir fonksiyon derleyici tarafından yazılacak:

void println(T) [with T = int]

Bu sayede println fonksiyonuna verdiğimiz değerlere uygun overload edilmiş println fonksiyonu çağıralarak ekrana bu değerler yazılmış olacak.

To be continued :))

Lambda İFADELERİ – C++11

C++11’le gelen bence en iyi özelliklerden biri de lambda ifadeleridir. Aslında yavaş yavaş nesne yönelimli programlamanın yerine fonksiyonel programlamanın (Functional Programming) alacağı öngörülüyordu. Lambda fonksiyonlar, auto anahtar kelimesiyle beraber FP ve multi-threading-concurrency’ye de ciddi manada destek verilmiş oldu. Zaten inşallah başka bir makalenin konusu olarak C++11 le gelen Thread(Boost.Thread) kütüphanesini inceleyeğiz. Şimdi lambdalara devam edelim.

Lambda fonksiyonlar aslında fonksiyon göstericileriyle (function pointers) başlayan Functorlarla devam eden gelişimin son halkası.

Sentaks:


#include <iostream>

using namespace std;

int main(int argc, char** argv) 
{    
    auto p = [] () -> string { return "Hello World";};
    
    cout << p() << endl;
   
    return 0;
}

[] : Lambda fonksiyonlar bu ifadeyle başlar. Aslında adı “capture specification”‘dır. Birkaç satır sonra neden bu adın verildiğini daha detaylı anlamış olacağız. Kısacası derleyici bu ifadeyi gördüğünde bir lambda ifadesinin başladığını anlar.
() : Bu ifade olağan fonksiyonlarda olduğu gibi parametre listesini verdiğimiz fonksiyon çağrı operatörü.
-> return value : Bu ifade ise lambda fonksiyonun geri dönüş değerini belirtir. Fakat bu ifade vermesekte derleyici geri dönüş değerini otomatik olarak tespit edecektir. Üstteki örnekte “-> string” ifadesini silsek dahi kod çalşacaktır.

Değişken Aktarımı
Lambda fonksiyonlarını bir diğer özelliği ise bulunduğu isim alanından değişkenleri kendi gövdesine enjekte edebilme yeteneğidir. Bunu hem by value hem de by reference şeklinde yapabilir. Örneğin:

[&] () {} : Bu ifade de isim alanı içerisindeki nesnelerin tümünü “by reference” olarak enjekte ederiz. Eğer örneğin [&a] () {} şeklinde yazarsak sadece ilgili değişkeni enjekte edebiliriz.

#include <iostream>

using namespace std;

int main()
{
	
	int a = 5;
	
	cout << a << endl;
	
	auto p = [&a]() { a = 6;};
	
	p();
	cout << a << endl;
}

[=] () {} : “=” ifadesi ile isim alanı içerisindeki nesneleri “by value olarak” elde ederiz. Yine aynı şekilde [=, &a] () {} şeklinde bir bildirim yaparsak a by referans olarak geri kalan değişkenler değer olarak lambda fonksiyonuna iletilir.

Lambda ve STL:
STL’de kütüphanesinde bulunan fonksiyonlarda kullanımı oldukça pratiktir. Örneğin:

#include <iostream>
#include <algorithm>

using namespace std;




int main()
{	
	vector<string> vs;
	vs.push_back("islam");
	vs.push_back("cpp");
	vs.push_back("turkiye");
	
	
	for_each(vs.begin(), vs.end(), [] (string s) { if (s == "islam") cout << s << endl;});
	
	auto p = remove_if(vs.begin(), vs.end(), [] (string s) { if (s == "cpp") return true; return false; });

	for (auto s = vs.begin(); s != p; s++)
		cout << *s << endl;
}

Peki bir lambda ifadesini başka bir fonksiyona veya sınıfa parametre olarak vermek istediğimizde nasıl bir yol izleyeceğiz. Burda imdada std::function şablon sınıfı yetişiyor. Örneğin:

#include <iostream>
#include <algorithm>

using namespace std;

class A {
private:
	string s;
	function<void (string)> handler_func;
public:
	A(function<void (string)> f) : handler_func(f), s("cppturkey.org"){}
	
	void operator ()() { handler_func(s);}
};

int main()
{	auto f = [](string s) { cout << s << endl;};
	A a(f);
	a();
}

“function<void (string)> ” ifadesi geri dönüş değeri void olan ve parametre olarakta string alan bir fonksiyon objesini belirtmektedir.

Gelecekte lambda fonksiyonlarının concurrency ve Functional Programming ile beraber daha sık kullanıldığına şahit olacağız.

C++11 SAĞ DEĞER REFERANSLARI (R-VALUE REFERENCES) – 2

Bir önceki bölümde vector<string> dizisinin eski yöntemlerle kopyalanmaya kalktığında nasıl bir maliyet oluşturacağına dikkat çekmiştik. Şimdi bu örnek üzerinden gidelim.

Eski tarz vector kopyalanması:


#include <iostream>
#include <typeinfo>
#include <vector>

using namespace std;


void copyVector(vector<string> &v1, vector<string> &v2)
{
    for (auto s : v1) {
        v2.push_back(s);
    }
}


int main(int argc, char** argv) 
{
    vector<string> v1;
    vector<string> v2;

    v1.push_back("islam");
    v1.push_back("cpp");
    v1.push_back("turkiye");
    
    
    copyVector(v1, v2);
    
    for (auto s : v2)
        cout << s << endl;
   
    return 0;
}

Burda dikkat ederseniz v2 vektörüne 3 string nesnesi oluşturularak ekleniyor. Yani string sınıfının ctorları vasıtasıyla bu değerlerle v2 sınıfında bu nesneler inşaa ediliyor. Aynı şekilde
v2.push_back(s);
yerine
v2.push_back(std::move(s)) kullanırsak copy yerine move edecek. Bu arada derleyicinizin zaten c++11 uyumlu ise otomatik olarak move edecektir. __GXX_EXPERIMENTAL_CXX0X__ değişkenini tanımladığı için kopyalama maliyetinden kurtulmuş olacaksınız.

Peki std::move() kullanılarak move edilen nesneleri nasıl yakalayacağız. Bunun içinde move ctor şöyleki:

class cVector
{

public:
	cVector(cVector &&r)
	{
		cout << "rvalue move ctor" << endl;
	}
	
	cVector& operator= (cVector &&r)
	{
		cout << "operator = rvalue" << endl;
		return *this;
	}
	
	cVector & operator= (cVector&r)
	{
		cout << "operator = lvalue" << endl;
		return *this;
	}
	
	cVector ()
	{
		cout << "Default ctor" << endl;

	}
};

int main()
{
	cVector cv;
	cVector cv2(move(cv));

	cv = cv2;
	cv = move(cv2);
}

2-Perfect Forwarding

Bu durum rvalue referansları fonksiyon şablonlarında kullanmak isteyince “template argument deduction” yani fonksiyon argümanı belirleme durumunda ortaya çıkıyor. Şöyle ki:

template<typename T>
void f(T&& t);

int main()
{
    X x;
    f(x);   // 1
    f(X()); // 2
}

Örnekte 1 durumunda f fonksiyonuna bir lvalue parametre olarak verilmiş dolayısıyla T türü X& olarak belirlenecektir. 2 durumunda ise f fonksiyonuna bir rvalue reference geçilmiş böylelikle T türü X olarak belirlenecektir. Peki f fonksiyonu içerisinde başka bir fonksiyon çağırılmak istense nasıl gelen nesnenin rvalue mu lvalue mu oldgunu bileceğiz ?

void g(X&& t); // A
void g(X& t);      // B

template<typename T>
void f(T&& t)
{
    g(std::forward<T>(t));
}

void h(X&& t)
{
    g(t);
}

int main()
{
    X x;
    f(x);   // 1
    f(X()); // 2
    h(x);   // 3
    h(X()); // 4
}

Örnekte std::forward(t) yardımıyla gelen tür bir rvalue ise; içerisinde static_cast(t) operatörünü görüldüğü gibi çağırarak A durumundaki g fonksiyonun çağırılmasını sağlar. Gelen nesne bir lvalue ise B durumundaki g fonksiyonu çağırılacaktır(std::forward gelen nesne bir lvalue ise sadece aldığı argümanı geri döndürür). İşte tam olarak gelen argümanın tespitine göre içeride uygun fonksiyonun çağırılmasına perfect forwarding denir.

Normal fonksiyonda (h) ise (3,4) durumunda uygun overload çağırılır. Fakat A satırındaki g nin rvalue overload fonksiyonu silinse dahi çalışır. Çünkü gelen nesne bir rvalue referansa bir lvalue referanstır. g fonksiyonunun içinde yapılanlara göre tehlikeli bir durum olabilir.

C++11 Sağ değer referansları (R-value references) – 1

Sağ değer referansları C++11 standartlarıyla beraber gelen özelliklerden biridir. Temel olarak 2 faydayı amaçlamıştır:

1-Değer Kaydırma (Move Semantics)

2-Perfect Forwarding

Yazının ilerleyen kısımlarında bu konulara değineceğiz. Onun öncesinde ise sol değer, sağ değer ne demek dilerseniz önce ona bir göz atalım.

Bu kavramlar C dilinden miras alınmıştır. Kısaca tanımlarsak sol değer atama operatörünün sol kısmında bulunabilen değerlerdir. Sağ değer ise atama operatörünün sadece sağ kısmında bulunabilen değerlerdir. Örneğin;


int a;

a = 5;  // a bir sol değerdir.

int *b  = &a  // a bir sol değerdir operatör & argüman olarak sol değere ihtiyaç duyar.İfade geçerlidir.

string & foo(); // şeklinde string & döndüren bir fonksiyon olsun

foo() = "islam"; // foo nun geri dönüş değeri sol değer referansı olduğundan ifade geçerlidir.

string func();

func() = "islam"; //burda func'ın geri dönüş değeri geçici bir string nesnesidir bu nesneler ise sağ değerdir. Bunlara atama yapılamaz

a + 5 = 19 // burda da a + 5 ifadesi bir sağ değerdir. Çünkü + operatörünün geri dönüş değeri bu toplamın soınucudur(bu örnekte 10).

++a = 15 // ifade geçerlidir. Çünkü ++ operatörü geri dönüş değeri olarak a nesnesinin kendisini döndürür.Dolayısıyla atama geçerlidir.

Başka bir tanıma göre bellekte bir yer tahsis edilmiş değerlere veya değişkenlere sol değer, bellekte bir yer tahsis edilmemiş veya ismi  (identifier) olmayan değerlere de sağ değer denir.

Sağ değer Referansları

T türünden bir nesnenin (sol değer) referansını T& şeklinde gösteriyorduk, sağ değer referansını ise T&& şeklinde ifade edeceğiz.

Örneğin;


void foo(string &s)
{
cout << "sol değer referansı" << endl;
};

void foo(string &&s)
{
cout << "sağ değer referansı" << endl;
};

int main()
{
string s("cpp turkey user group");

foo(s); // sol değer referansı
foo(string("islam")); // sağ değer referansı
}

Aslında sağ değer referansları const referanslar olduğundan her ikisini birden yakalamak istiyorsak;

void foo(string const &s)
 {
 cout << "sağ-sol değer referansı" << endl;
 }

gibi bir fonksiyon implemente edersek her iki referans türünüde yakalacaktır. Fakat sağ değer referansını ayırt edemeyiz.

Değer Kaydırma (Move Semantics)

C++’ta şimdiye kadar nesnelerin atanmasında dilin tasarımı gereği bir nesnenin değeri diğer nesnenin bellekteki alanına kopyalanıyordu. Değer kaydırmadaki mantıkta ise bu değer kopyalanmıyor, nesne bu değerin bulunduğu bellek alanını gösteriyor. Böylelikle  kopyalama gibi maliyeti yüksek bir işlem yapılmadığından ciddi bir performans kazanımı oluyor.

Örneğin; Bir vector<string> dizinin başka bir diziye kopyalama maliyetini düşünün ctor ve dtor işlevlerinin maliyetini de kattığınızda devasa bir hesap ödemeniz gerekmektedir. Bu konuya devam edeceğiz.

Man In the Middle – 2

Örneğin şöyle bir senaryoda:

Orjinal CH içinde çok sayıda cipher suite var, ancak bizdeki OpenSSL yeni bir CH üretirken daha az cipher suite ekledi ve daha küçük bir paket oluştu diyelim. Buna cevap olarak server’dan çıkan SH, içinde önceki paketin ack değeri seq+payload_len = 181 olarak geldi bize. Bu durumda network içinde normalde sorunsuz giden ack/seq değerlerini modifiye etmeden gönderdiğimizde, 200 byte gönderen client ack içinde 181 görünce önceki paketin gitmediğini zannedip aynı paketi tekrar gönderecek ve bir türlü ilerleyemeyecek.

Ya da tam tersi durumda:

Biz arada raw socket üzerinden dinlediğimiz için her türlü geri dönen SH paketini göreceğiz. Ama tcp header’ı yeniden yazmazsak 201 bekleyen client büyük ihtimalle paketi discard edecektir. Bu tür bir durumu örneğin wireshark’ta incelediğimizde “ACK’ed unseen segment” şeklinde bir hata görünür.

Bunlara ek olarak, paketin içeriği değiştiği için her iki durumda da tcp ve ip checksum değerleri invalid olacak. Bunların da tcp ile ilgili değişiklikler bittikten sonra en son olarak yeniden hesaplanması lazım. Paket boyutunun değiştiği durumları garanti altına almak için de her zaman IP-checksum’ın da yeniden hesaplanması faydalı olacaktır.

Burada wireshark ile ilgili kısa bir not geçelim. Wireshark->Edit->Preferences->Protocols->TCP penceresinde “Validate checksum” işaretli değilse bu kontrol yapılmaz ve ilk bakışta bu tür hatalar görünmeyebilir. Bunu açtığınızda ise çok fazla pakette hata varmış gibi görünecektir. Örneğin client tarafından pcap aldığınızda, outgoing paketlerin hepsinde bu uyarı çıkabilir. Network kartı genelde bu hesaplamaları yapacak şekilde optimize edildiği için, wireshark’ın yakaladığı noktada henüz CRC hesaplanmamış olabilir. Dolayısıyla yanlış olduğunu zannediyor.

Doğru hesaplanmış bir MITM senaryosunun en basit hali şu şekilde olabilir:

Modifiye edilip gönderilecek her paket için şu formül uygulanır:

ackn = seqn-1 + lenn-1
seqn = ackn-1

Burada yine unutulmaması gereken iki nokta var. Birincisi, ack hesaplanırken seq ve len değerleri toplanmadan önce host-byte-order’a çevrilip, pakete yazılmadan önce yeniden network-byte-order’a çevrilmeli. İkincisi, burada gösterilen değerlerin tamamı offset olarak hesaplanıyor. Yani ilk CH paketi geldiğinde client’a ait initial sequence number(ISN) paketteki seq değerinden, server’a ait ISN de aynı paketteki ack değerinden alınıp sonraki hesaplamalar bu sayılara eklenerek yapılmalı.

Devam etmeden önce kısa bir not. SSL session’lardan sonra gönderilen FIN/FINACK/ACK mesajlarının da yukarıdaki şekilde işlenip gönderilmesi çok önemli. Aksi takdirde taraflar handshake’in doğruluğundan emin olamayacak ve devam etmeyeceklerdir.

Ortadaki çizgiye, yani kendi uygulamamıza biraz daha yakından bakalım. Senkronizasyonla ilgili önemli bir sorun var aslında şu an.

İlk CH paketlerinden sonra, bizim ürettiğimiz paketler hep gerçek paketlerin gelmesine bağlı. Örneğin server’dan SH geldiği anda, aslında client’dan gelen CH’ye cevap olarak gönderilecek SH çoktan FS içinde üretilmişti. server’dan gelen paket FS’yi tetikleyerek bu paketin client’a gönderilmesini sağlıyor. Ardından client’dan CKE gelerek FS’nin handshake’e devam etmesini sağlıyor. Burada en önemli varsayım client’dan gelen her pakete karşılık server’dan da bir paket geleceği. Bu doğru olsa, gelen paketleri direk FC/FS’ye besleyip, karşı tarafa ait Fake yapıda bekleyen çıktıları göndermek sorun olmayacak. Ama bu varsayım her zaman tutmuyor. (client’a SH ile gönderilen sertfikada aslında server’ın bilgilerinin bulunmaması sorununa daha sonra geleceğiz)

Aslında daha derinde, bu sorunun en kolay sebebi disassemble edilmiş TCP paketleri olabilir. Normalde receive() ile işletim sisteminden aldığımız paketler, kernel’deki TCP implementasyonu tarafından bize reassemble edildikten sonra verildiği için bu sorunla karşılaşmıyoruz. Ama MITM sırasında raw soketler kullanmak zorunda olmamızdan dolayı, bu tür TCP işlemlerini yapabilecek ayrı bir kod hazırda yoksa, özellikle büyük CRT paketleri birkaç TCP paketine bölünmüş olarak gelip yukarıdaki senkronizasyonu kolayca bozabilir.

Reassemble gerektirmese bile, bazı SSL Server’lar SH içindeki CRT ve SHDone mesajlarını ayrı paketlerde yollayabiliyorlar. İlk SH den sonra bizim client’a ilettiğimiz toplu SH’ye karşılık client da kendi CKE’sini bize dönecek. Burada işler karışabilir. Eğer server’dan SH ile ilgili tüm paketler gelmeden client’ın CKE’si bize gelirse, bu mesajı FC’ye iletemeyeceğiz, çünkü henüz bu mesajı işleyebilecek state’e gelmedi. Öncelikle server’dan sertifikayı alıp kendi pre-master-key’ini üretebilecek duruma gelmesi gerekiyor.

Man In The Middle – 1

Amacımız: Karşıdaki bir sunucuya HTTPS ile bağlanan bir browser’ın oluşturduğu trafiği decrypted olarak görebilmek.

PROLOGUE

Bu yöntemi mümkün olduğunca bizim ilerleyişimize paralel olarak anlatmaya çalışacağım. Çünkü başlarda çok önemli görünmeyen ayrıntıların ileride ciddi tasarım değişikliklerine yol açması veya zor farkedilen detaylara neden ihtiyaç duyulduğunun farkedilmesi açısından bu daha faydalı olacaktır. Ayrıca bu süreç boyunca ilerleyişimizi de çözüme en çabuk ulaşan yolu izleyerek yapmadığımızı önceden belirtmek gerek. Konunun detaylarına başlarda hakim olmamamızdan veya inline çalışma ve ssl ile ilgili tecrübe eksikliğimizden kaynaklanan hatalar ve öğrenme odaklı yaptığımız çalışmalardan dolayı aralarda önemli  değişiklikler yapmak zorunda kalacağız.

NOT JUST ANOTHER SSL PROXY..

Önceki yazılarda anlattığım üzere, bir ssl client’ın encrypted trafiğinin çözülmesi için gereken master-key’e öğrenmek, ssl handshake sırasında kullanılan sertifikanın ait olduğu private-key’e (PrK) sahip olmaktan geçiyor. Bunun için ya kullanıcıya giden gerçek sertifikanın PrK’sini çalmanız , ya da trafiğin arasına girerek kullanıcının gerçek server ile değil sizinle konuşmasını, dolayısıyla da göndereceği verileri gerçek sertifikada değil, sizin yolladığınız sertifikadaki bilgilere göre şifrelenmiş bir şekilde yollamasını sağlamanız lazım. Bu ikinci yönteme Man In the Middle yöntemi diyoruz.

İlk gereksinimimiz, warez sitelerinde bolca bulunabilen ssl-proxy programlarının aksine, bizim bütün bağlantıları tek bir process içinde tek bir thread ile kontrol etme zorunluluğumuz. Normalde bu programlarda server için accept(), client için de connect() metodlarının her kullanımında yeni bir thread açıp, programın normal akışını etkilemeden blocking socketler ile gerekli işler halledilebiliyor. Ayrıca her thread kendi bağlantısıyla ilgili bilgileri tuttuğu için, daha sonra değineceğim tcp_header bilgilerinin hesaplanması ve session resumption durumları için ekstra bir iş yapmaya gerek kalmıyor.

Önce tek bir SSL Session için çalışabilecek bir modül hazırlamayı deniyoruz.

Aradaki adam olarak, yapmamız gereken temel iş, şifreli gelen verilerin deşifre edilip, tekrar şifrelenerek gönderilebilmesi. Bunun için OpenSSL kütüphanesini kullanıyoruz. Bu kütüphanenin genel kullanımı da ssl-proxy programlarına benzer şekilde client ve server socketleri açarak çalışıyor. Ama biz socket kullanamayacağımız için farklı bir yol izliyoruz. Kütüphanenin IO handler olarak kullanılabilecek farklı yapıları var. Genel olarak BIO_* şeklinde isimlendirilen bu yapılardan bizim kullanacağımızın ismi BIO_s_mem. Bu interface ile OpenSSL’in socketler üzerinden değil, memory’den okuyup tekrar memory’ye yazacak şekilde çalışmasını ayalayabiliyoruz.

OPENSSL FOR DUMMIES

OpenSSL içinde bilinmesi gereken iki önemli yapı var. SSL ve SSL_CTX. Environment bilgileri, yani sertifika, ssl versiyonu, gibi bilgiler context içinde tutuluyor. Yapılacak her bağlantı için de (client veya server) bu context bilgileri kullanılarak yeni bir SSL oluşturuluyor. Bu yapı temelde bir SSL Session’u temsil ediyor. Bunu SSL Connection ile karıştırmamak lazım. Bir session’a yapılan ilk bağlantıdan sonra, farklı ‘resumption’ mekanizmaları kullanılarak yeni bağlantılar da oluşturulabilir. Bizim kullandığımız SSL yapısı bu devam eden Session olacak. Bunun için client ile konuşacak bir fake server(FS), server ile konuşacak bir fake client(FC) olarak kullanılmak üzere iki SSL objesi oluşturup, her ikisinin de input ve output BIO’larını BIO_s_mem kullanacak şekilde ayarlıyoruz. Bu 4 BIO_s_mem, session’lara şifreli verinin verilmesi ve şifrelenmiş verinin alınması için kullanılacak. OpenSSL tarafından çözülen açık verilerin okunması ve tekrar yazılması için de SSL_read()/write() metodlarını kullanabiliriz.

Kısaca şu şekildeki gibi bir tasarımımız var:

Memory BIO’lar içinde şifreli veriler, SSL_read metodlarında da çözülmüş veriler bulunuyor.

PEKİ SORUN NE?

Bu noktadaki tasarımımız, biraz “ssl accelerator” tadındaydı. FS ve FC’ye gelen her mesaj, direk olarak ilgili SSL’e verilip oluşan cevap geri dönüyordu. Bu yöntemin bazı avantajları olsa da pratikte pek işe yaramayacağı belli aslında. Her iki taraf da anında cevap döndüğü için, tcp header state’lerini kaydetmek zorunda kalmıyoruz. Gelen paketin seq ve ack numaralarını paketteki payload_len ile toplayarak yeni paketin header’ını kolay bir şekilde hesaplamak mümkün. Ancak iki tarafın da senkronize bir şekilde gitmesi bu tasarımda kolay değil, çünkü gelen paketlere üretilen cevaplar karşı tarafı etkilemeden olduğu gibi geri dönüyor. Dolayısıyla bir tarafta ortaya çıkacak farklılıkların (session resumption, alert) manuel olarak kontrol edilip diğer tarafa yansıtılması gerekiyor ki, bu tür temel işleri mümkün olduğunca OpenSSL’in kendisinin halletmesini istediğimizden pek tercih edilir bir durum değil.

Asıl önemli sorun ise sertifikanın nasıl oluşturulacağı. Client üzerindeki browser hangi adrese gitmek istediğini önceden biliyor. Bu adresle ilgili DNS sorgusunu yaptıktan sonra bulduğu IP’ye 443 portundan bağlanıp SSL Handshake işlemlerine başlıyor. Dolayısıyla bu handshake tamamlanıp “HTTP GET” paketi gidene kadar hangi siteye bağlanmak istediğini trafiğin ortasında iken bilmek mümkün değil. Tabiki ReverseDNS ile veya önceden bazı sitelerin IP’lerini kaydedip bunların içinden bir arama yapılabilir, ancak bu tür sorgular bizi fazlasıyla yavaşlatacağı için bu mümkün değil.

Aslında bilmemiz gereken zaten sitenin adresi de değil. Amacımız client’ı server olduğumuza inandırmak olduğu için, server’ın göndereceği sertifikadaki bilgileri gönderiyor olmak bize yetecektir. Dolayısıyla kendi sertifikamızı oluşturmadan önce server’dan gerçek sertifikayı almamız gerekiyor. Bunun için de client’dan ClientHello(CH) geldikten sonra geri dönüş yapmadan FC’yi tetikleyip karşıdan ServerHello(SH) ve Certificate(CRT) mesajlarının alınmasını beklemek gerek.

Sertifika konusunu bi tarafa, öncelikle taraflar arası bağlantıyı sağlayacak bir tasarıma geçtik..

Bu noktada basitlik adına yaptığımız bir varsayım, iki tarafta da eşit sayıda mesaj ile handshake’in tamamlanacağı şeklindeydi. Dolayısıyla iki taraftaki SSL nesnelerinin de state’leri ortak olarak devam edecekti. Bunun için yapılması gereken, client’dan ilk CH mesajını aldıktan sonra bunu FC’ye vermek. Bunu verdiğimiz anda FC içinde bu mesaja cevap olarak üretilen SH mesajı hazırlanıyor ama mem_from_fs üzerinden bu veriyi hemen okumuyoruz. Çünkü gerçek server’dan bir cevap gelmeden client’a dönmek istemiyoruz. Ayrıca bu yaklaşım, CH’den sonra hem client’a SH dönüp, aynı zamanda server’a da CH yollamak gibi tek pakete karşılık iki paket yollamak gibi bir gereksinimden de kurtarıyor.

Tüm bu varsayımlara ve tasarımlara girmeden önce, daha alt seviyede çözülmesi gereken çok daha öncelikli bir konu var:

TCP HEADER: THE NIGHTMARE

Ethernet ve IP katmanından geçen veriler, işletim sistemi tarafından TCP katmanına verilir. Burada paketin hangi process’e ait olduğu (port numarasından) bu stream’deki offset’i (seq no) ve gönderilen verilerin ne kadarının karşı tarafa ulaştığı (ack no) belirlendikten sonra checksum kontrolü yapılıp ilgili process tarafından tcp header’dan sonraki kısım işlenir. Bu bizim senaryomuzda SSL verisinin ilk byte’ına denk geliyor. Yani içinde payload olmayan paketlerden uygulamanın haberi olmuyor ama ack/seq numaraları gidip gelmeye devam ediyor.

Peki işletim sistemi bu sayıları ne için kullanıyor?

tcp içinde gönderilen verilerin işletim sistemi açısından bakıldığında bir sınırı yoktur. Yani bir tcp session’ı boyunca bir tarafa giden tüm veri, sanki tek bir büyük veri bloğu gibi muamele görür. İşletim sisteminin görevi tcp header içindeki bilgileri kullanarak, bu verilerin doğru bir sırayla(1), tam olarak(2) ve bozulmadan(3) uygulamalara iletilmesini sağlamaktır.

Verilerin sıralanması için kullanılan alanlar ack ve seq numaraları. Her paketin başlangıcında, bu paketteki verilerin, stream’in ilk başındaki paketten kaç byte sonra başladığı ‘seq no’ alanında belirtilir. Burada dikkat edilmesi gereken bir nokta, bu sayının net bir offset olarak verilmez. Yani ilk pakette seq no sıfır değil, random 32bit’lik bir sayıdır. Ve tabiki network ordered olarak gelir. Dolayısıyla tam olarak offset’i hesaplamak için gelen paketteki host order’a çevrilmiş değerin ilk paketteki seq numarasının host order’a çevrilmiş halinden ne kadar fazla olduğuna bakılabilir. Burada ilk pakete göre değil bir önceki pakete göre offset alınarak da sıralama kontrolü yapılabilir tabi.

Bizim gönderdiğimiz verilerin ne kadarının karşı tarafa ulaştığı ise ack değeri ile kontrol edilir. Stream’in ilk başından şu ana kadar karşı tarafın aldığı toplam veri miktarı (byte olarak) son gelen paketteki ack değerine yine yukardaki gibi bir offset hesaplaması uygulanarak bulunur. Bu noktadan sonra gönderilecek pakete bu sonuca göre karar verilir. Şöyle ki:

Eğer bu değer bizim gönderdiğimiz son seq_no+payload’dan küçük ise, son paketlerin bir kısmı gitmemiş demektir. İşletim sistemindeki implementasyona bağlı olarak, paketin kalan kısmı veya tamamı tekrar gönderilir. Bu bizim için önemli çünkü eğer asıl paket aradaki bir makineden (bu biz oluyoruz) daha önce geçtiyse, tekrar gönderilen paketi bizim uygulamamızın çalıştığı işletim sistemi retransmission olarak algılayıp bize yollamayacaktır. Ack/seq değerleri yeniden düzenlenmeden server’a gönderilen bu retransmitted paket server tarafındaki state’i etkileyebilir. Bizim ssl state’lerimizin consistent kalması için zaten retransmitted paketleri işleme sokmadan discard edeceğiz, ama uygulamanın ihtiyaçlarına göre bu durumu göz önünde bulundurmakta fayda var.

Peki bu durum mümkün mü? Sonuçta paketin tamamı karşıya gittiyse karşıdan gelen ack değerinin de sorunsuz olarak bunu onaylaması gerekmez mi?

Normalde, paket kaybı veya başka bir aksaklık olmadığı sürece “decent” bir network içinde olmaz tabiki. Ne var ki; “these are not decent places”

Tür Dönüştürme Operatörleri

C programlama dilinde tür dönüştürme sadece bir tane “cast”(tür dönüştürme) operatörüyle  kolaylıkla yapılabiliyordu. Örneğin;


int a = 5;

short b = (short) a; // operator() burda tür dönüştürme operatörü olarak kullanılıyor

C++ da ise durum biraz daha farklı. Tür dönüştürme işlemi biraz daha detaylı olarak ele alınmış. Şöyleki tür dönüştürme işlemini neyi amaçlıyorsanız o amaca uygun tür dönüştürme operatörünü kullanmalısınız. Şimdi tek tek bu operatörleri inceleyelim.

dynamic_cast (dinamik tür dönüştürme operatörü):

Referanslar ve göstericiler için kullanılır. Geri dönüş değeri istenilen türe dönüştürme işlemi başarısız ise NULL’dır. Genel kullanım alanlarından biri çalışma zamanında tür belirlenmesi (Run Time Type Identification) içindir. Örnek kod üzerinden ilerlersek:


#include <iostream>

using namespace std;

class Hayvan
{

public:

    virtual void turIsmi(){cout << "Tür ismi: Hayvan" << endl;};
};

class Aslan: public Hayvan
{
public:
    virtual void turIsmi() {cout << "Tür ismi: Aslan" << endl;};

};

int main(int argc, char** argv)
{
    Hayvan *h1 = new Hayvan();
    Hayvan *h2 = new Aslan(); //burda aslında gizli bir tür dönüştürme işlemi var. Yine burda da dynamic_cast operatörü kullanılabilir.
    Hayvan *h3;

    if ((h3 = dynamic_cast<Aslan*>(h1)) == NULL)
        cout << "h1 Aslan türüne dönüştürülemez." << endl;

    if ((h3 = dynamic_cast<Aslan*>(h2)) == NULL)
        cout << "h2 Aslan türüne dönüştürülemez" << endl;

    h3->turIsmi();

    delete(h1);
    delete(h2);

  return 0;
}

Görüldüğü üzere Hayvan ve Aslan türünden birer nesne dinamik olarak oluşturuluyor. Daha sonra bu nesneler
Hayvan* türünden bir göstericiye atanıyor.Eğer neden Aslan* türünden bir nesne nasıl oluyorda Hayvan* türüne atanıyor diye sorarsanız dilin tasarımından dolayı diye cevaplandırırım. Kaldı ki bu tasarım daha öncede dediğim gibi çalışma zamanında tür belirlememize olanak sağlıyor. Bunun en geniş örneklerini GUI programlamada görebilirsiniz. Neyse konumuza geri dönelim.

Daha sonra ise dynamic_cast operatörüyle bu göstericiler Hayvan* türüne dönüştürülmeye çalışılmış tabi ki bunlardan h1 göstericisi Hayvan türüne ait bir gösterici olduğundan dynamic_cast operatörü NULL dönecektir.

h2 göstericisi(pointer) ise Aslan türünü gösterdiği için tür ödnüştürme işlemi başarılı bir şekilde gerçekleşecek. Ve 32.satırda çağırılan fonksiyon Aslan sınıfına ait olan sanal fonksiyondur.Böylelikle ekrana

Tür ismi: Aslan

yazılacaktır. Eğer burda göstericiler yerine referanslarla işlem yapsaydık başarısızlık durumunda dynamic_cast operatörü bad_cast türünden bir kural dışı nesnesi (exception) fırlatacaktır.

static_cast operatörü:
static_cast te dynamic_cast te olduğu gibi dinamiklik söz konusu değildir. Yani çalışma zamanında türün dönüşüp dönüşemediğini belirleyemezsiniz. Dolayısıyla static_cast’te tüm sorumluluk programcıya aittir. Örneğin:

class Hayvan
{

public:

void turIsmi(){cout << "Tür ismi: Hayvan" << endl;};
};

class Aslan: public Hayvan
{
public:
void turIsmi() {cout << "Tür ismi: Aslan" << endl;};

};

int main(int argc, char** argv)
{
Hayvan *h1 = new Hayvan();
Hayvan *h2 = new Aslan();

Aslan *h3;

h3 = static_cast<Aslan*>(h1);

h3->turIsmi();
delete(h1);
delete(h2);

return 0;
}

static_cast operatörüyle Hayvan* türünden bir nesne Aslan*’a dönüştürülerek yine Aslan* türünden bir göstericiye atanmıştır.
Çalışma zamanında bu kodun ne yapacağı belirsizdir(ambiguity). Bunun haricinde bu operatörle derleyici tarafından gizli(implicit)
bir şekilde yapılan tüm tür dönüştürmelerini de yapabilirsiniz.

reinterpret_cast:

Her türlü gösterici türünü  her türlü  gösterici türüne çevirebilir. Bilinçsiz kullanımı kesinlikle tavsiye edilmez. Aslında basit olarak
göstericinin diğer göstericiye binary olarak kopyalanmasıdır. Hash fonksiyonlarında pratiklik açısından adres olarak kullanılan alanı hash değerine çevirmek için kullanılır. Kullanım alanları oldukça kısıtlıdır. Örneğin;


#include <iostream>

using namespace std;

int main()
{
int a = 5;

unsigned int *b = reinterpret_cast<unsigned int*>(&a + 1);

cout << *b << endl;

return 0;
}

const_cast:

Bir türün değişmezliğini manipüle etmemizi sağlar. Örneğin:


#include <iostream>

using namespace std;

void printf(char *p)
{
cout << "char * " << p << endl;
}

int main(int argc, char** argv)
{
const char *s = "islam yasar";

char *p = const_cast<char*>(s);

printf(p);

return 0;
}

Görüldüğü üzere const olan bir nesnenin normalde const olmayan bir nesneye atanması mümkün değil. Bu tür dönüştürme operatörü ile mümkün hale geliyor.

NOT:

Madem çalışma zamanında tür belirlemeden bahsettik typeid operatörünü de araya sıkıştıralım.

typeid:

bu operatör basitçe çalışma zamanında gelen nesnenin türünü belirlememizi sağlıyor. Daha önce bunu dynamic_cast operatörüyle yapmıştık.


Hayvan *h1 = new Hayvan();
Aslan *h2 = new Aslan();

if (typeid(h1) == typeid(Aslan*))
cout << "h1 Aslan* türünden" << endl;

if (typeid(h2) == typeid(Aslan*))
cout << "h2 Aslan* türünden" << endl;

İslam Yaşar