Modern Bir Zafiyetin Anatomisi Vol. II

svg

İlk seride(Modern Bir Zafiyetin Anatomisi Vol. I) elimizden geldiği kadar açıklamalar ve mevzunun mahiyeti hakkında bilgiler sunmaya çalıştık şimdi seriye statik kod analizi ile devam edeceğiz. Bahse konu zafiyetin nasıl oluştuğu konusunda teknik olarak aslında bir bilgimiz var ve ona ‘taşma’ diyoruz. Aslında bu SSL yapısında ‘key exchange’ dediğimiz mevzu meydana gelirken ‘alloc-dealloc’ işleminden dolayı bir taşma meydana geliyor. Yük sahibi data hedefine vardığında, hedef geriye hata mesajı olarak “memory leak(hafıza sızıntısı)” içeriği gönderiyor, böylelikle hafızada tutulan bilgilerinde açığa çıkması sağlanıyor. Zafiyetin herkesin anlayabileceği şekilde meali böyle, peki bu hafıza sızıntısı nereden kaynaklanıyor ve sebepleri nelerdir onlar üzerine konuşmakta fayda var.

C kodu üzere tanımladığımız ssl.h kütüphanesi ile birlikte zafiyet gösteren yapıları sömürgeçimiz üzere göreceğiz. Kütüphanede mevcut olan SSL yapısına bakalım;

aşğıdaki kod parçacığımızda SSL3_STATE için *s3 işaretçisi tanımlanmıştır aynı şekilde SSL_CTX ise *ctx işaretçisi olarak tanımlanmıştır.

struct SSL {
  SSL_CTX *ctx;
  SSL3_STATE *s3;
}

Yukarıda tanımlanan yapılar ile birlikte SSL kütüphanesinin read(okuma) ve write(yazma) tampon bellekleri bir alt yapı olarak tanımlanmıştır. Bu yapılar request(istek) ve response(cevap) olarak belirtilen SSL versiyonunda hayata geçmiştir. Bu yapıların kullanılmasının sebebi ise performans odaklı olmasıdır bu konuyada değineceğiz.

struct SSL_CTX {
  SSL3_BUF_FREELIST wbuf_freelist;
  SSL3_BUF_FREELIST rbuf_freelist;
}

struct SSL3_STATE {
  SSL3_BUFFER rbuf; /* read IO goes into here */
  SSL3_BUFFER wbuf; /* write IO goes into here */
  SSL3_RECORD rrec; /* each decoded record goes in here */
  SSL3_RECORD wrec; /* goes out from here */
}

tampon için tanımlanan freelist methodu yani alloc edilmiş alanların sıralı bir şekilde alloc-dealloc işlemine tabi tutulması demek oluyor.

struct SSL3_BUF_FREELIST {
  size_t chunklen;
  unsigned int len;
  SSL3_BUF_FREELIST_ENTRY *head;
}

tampon bellek mevzusu ise tam olarak burada karşımıza çıkıyor; tampon uzunluğu, kopyalanacak yer ve kalan byte sayısı bu yapı üzere tanımlanmış gözüküyor. Alloc ve dealloc fonksiyonları tampon belleğimiz üstünde kullanılmak üzere, bağlantılı fonksiyonlarla tanımlacak.

struct SSL3_BUFFER {
  unsigned char *buf; /* at least SSL3_RT_MAX_PACKET_SIZE bytes,
                       * see ssl3_setup_buffers() */
  size_t len; /* buffer size */
  int offset; /* where to 'copy from' */
  int left; /* how many bytes left */
}

SSL3_RECORD yapısı SSL3_STATE yapısının içinde tanımlanmış olan ve SSL3_STATE *s3 tarafından oluşturulmuş bir yapıdır. Bu yapı ileriki kod bloklarında da göreceğimiz üzere SSL3_STATE yapısından referansla rrec tanımına çıkarılan değerin atanması işlemi gerçekleşecektir. Yani anahtar değişimi sonrasında gönderilecek response(cevap) bu alana tanımlanacak ve o şekilde geri dönecektir.

struct SSL3_RECORD {
  /*r */ int type; /* type of record */
  /*rw*/ unsigned int length; /* How many bytes available */
  /*r */ unsigned int off; /* read/write offset into 'buf' */
  /*rw*/ unsigned char *data; /* pointer to the record data */
  /*rw*/ unsigned char *input; /* where the decode bytes are */
  /*r */ unsigned char *comp; /* only used with decompression - malloc()ed */
}

Şimdi isterseniz zafiyeti hem inceleyelim hem de bu sayede farklı bilgiler ışığında out-of-bounds(Sınırlı değer ihlali) ve heap overflow meseleleri üstünde duralım. Heartbleed(kanayan kalp) açığı out-of-bounds adını verdiğimiz ve verilen limit üstü bir okuma gerçekleştirildiğinde meydana gelir. Bu açık meydana gelirken haliyle yukarıda belirttiğimiz gibi bir “sızıntı” vuku bulur. Eğer bu açığa yetişmiş arkadaşlar varsa return value(geri dönen değer) değeri işte o hafızadan sızan veri oluyor, tabi farklı implementasyonları olması hasebiyle bu açığın kafa karıştırıcı olabilir lakin biz out-of-bounds konusu üzerine titreyeceğiz. Aşağıdaki kodumuz kanayan kalp zafiyetinin tetiklendiği kod blogu olarak bilinir. Bu fonksiyon heartbeat(kalp atışı) vasıtasıyla gelen TLS mesajlarının parse(ayırt edilmesi) konusunda kullanılır. Burada 2586. satırda SSL tipinde bir yapıdan atanan s işaretçisine dikkat ediyoruz, bu yapı SSL3_STATE yapısında bulunan s3 işaretçisini gösterir. s3 işaretçisi ile gösterdiğimiz yapı ise SSL3_RECORD ile rrec dizisini işaret eder. Artık p işaretçisini tanıyoruz(2586. satır), bu satır gelen ilk datayı gösterir(&s->s3->rrec.data[0]). 2588. satırda ise bir adet değişken tanımlaması yapıldı bu değişken bize 2593. satırda lazım olacak çünkü bahse konu satırda uzunluk değeri çıkarılacak ve 2588. satırda bahsettiğimiz değere ataması yapılacak. Bu uzunluk ise 2610. satırda kullanılıyor olacak şöyle ki; hafızada dönen cevaplar için belli bir alan ayırmamız gerekecek ve tam olarak bunu 2610. satırda hayata geçireceğiz. Bu satırda matematiksel olarak bir işlem yapacak olursak eğer (19+payload) gibi bir alan değeri karşımıza çıkacak ki payload mevzusunu artık biliyoruz, o gelen değerin uzunluğunu atadığımız bir değişken, artık 2611. satırda bellek tahsis işlemleri için değişken atamasını yapıyoruz. Ve geldik o her örnekte karşımıza çıkan memcpy fonksiyonu, hani ilk overflow muhabbetlerine başladığımızda strcpy, memcpy vs. fonksiyonlarla uğraşmıştık evet o örnek şimdi burada :slight_smile:

ek bilgi:

SYNOPSIS
#include <string.h>

 void *
 memcpy(void *restrict dst, const void *restrict src, size_t n);

kafa karıştıcı olmasın sadece memcpy fonksiyonunun kullanım şekli konumuz tam hız devam ediyor, hafıza üstünde bir aktarım yapacağız 2616. satırda kaynak olarak pl işaretçisi hedef olarak ise bp işaretçisi gösteriliyor ek olarak yukarıda bahsettiğimiz gibi uzunluk değeri ise payload olarak görünüyor.

2584 tls1_process_heartbeat(SSL *s)
2585 {
2586   unsigned char *p = &s->s3->rrec.data[0], *pl;
2587   unsigned short hbtype;
2588   unsigned int payload;
2589   unsigned int padding = 16; /* Use minimum padding */
2590
2591   /* Read type and payload length first */
2592   hbtype = *p++;
2593   n2s(p, payload);
2594   pl = p;
2595
2596   if (s->msg_callback)
2597     s->msg_callback(0, s->version, TLS1_RT_HEARTBEAT,
2598     &s->s3->rrec.data[0], s->s3->rrec.length,
2599     s, s->msg_callback_arg);
2600
2601   if (hbtype == TLS1_HB_REQUEST)
2602   {
2603     unsigned char *buffer, *bp;
2604     int r;
2605
2606   /* Allocate memory for the response, size is 1 bytes
2607   * message type, plus 2 bytes payload length, plus
2608   * payload, plus padding
2609   */
2610   buffer = OPENSSL_malloc(1 + 2 + payload + padding);
2611   bp = buffer;
2612
2613   /* Enter response type, length and copy payload */
2614   *bp++ = TLS1_HB_RESPONSE;
2615   s2n(payload, bp);
2616   memcpy(bp, pl, payload);
2617   bp += payload;
2618   /* Random padding */
2619   RAND_pseudo_bytes(bp, padding);
2620
2621   r = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, 3 + payload + padding);

Yukarıdaki kod bloğunda pl işaretçisine gelen datanın belirlenen length(uzunluk) miktarından daha fazla olduğu ve bu datanın ise memcpy fonksiyonuyla bellek üstünde taşmaya neden olduğu ve bellekteki datayı yetkisiz kullanıcıya sızdırdığını artık biliyoruz. Tabii burada derinlemesine bir analiz yapacağımız için mevzunun sadece bir tek fonksiyonla sınırlı olmadığını ve geçici hafıza tahsisi için kullandığımız(OPENSSL_malloc) fonksiyonlarıda inceleyeceğimizi unutmayalım. Haliyle destination(hedef) olarak belirlediğimiz işaretçiyi (bp) bu yolla tahsis etmiş bulunuyoruz. Haliyle memory management(hafıza yönetimi) konusunda performans odağını ön plana koyan yapının güvenlik gerekçelerini atladığını ve böyle bir zafiyetin doğduğunu anlıyoruz. Performans kaygılarından meydana gelen kod blokları daha fazla kod ve sınırları kabul etmediği gibi, işlemleri hem daha az performans gerektiren fonksiyon ve makrolara yüklemek daha cazip geliyor. Şimdi malloc fonksiyonunu incelememizde bir beis yok sanırım, devam edelim;

Bahse konu fonksiyonumuz (OPENSSL_malloc) mem.c dosya kaynaklı CRYPTO_malloc fonksiyonuna ait bir makro olarak görünüyor. CRYPTO_malloc ise farklı bir konfigürasyona sahip OPENSSL tarafından yapılandırılmış malloc_ex_func fonksiyonu ile işlev görür. Yukarıda belirttiğimiz gibi, performans odaklı bir hamle bu.

static void *(*malloc_func)(size_t) = malloc;
static void *default_malloc_ex(size_t num, const char *file, int line) { 
return malloc_func(num); }
static void *(*malloc_ex_func)(size_t, const char *file, int line) = default_malloc_ex;

Kısaca yukarıda ki fonksiyona baktığımızda malloc_ex_func fonksiyonunun malloc_func fonksiyon işaretçisini döndürdüğünü görüyoruz. Yukarıda rastladığımız OPENSSL_malloc fonksiyonumuzun malloc fonksiyonundan faydalandığını ve performans odaklı bir kaç revizyona gidildiğini her defasından söylüyoruz. Bu fonksiyonun datamızı nereye yazdığını biliyoruz lakin bu datayı nereden okuyacağımız konusunda bir fikrimiz yok. Leafsr’nin Heartbleed makalesinde bahsettiği üzere sızıntıyı anlamak için freelist kayıtlarını takip etmemiz gerekiyor. Yine aynı makaleden mütevellit SSL_CTX_new fonksiyonunu ele almamız gerektiği ve bu fonksiyonun wbuf_freelist ve rbuf_freelist üyelerinin üstünde durmamız gerektiği bahsediliyor. OPENSSL_malloc fonksiyonunda geri dönen değerler bu fonksiyonun üyeleri üstünden yönetilir, bu yüzden üstünde durmakta ve zafiyeti derinlemesine incelemekte fayda var.

Leafsr Kaynak

1677 SSL_CTX *SSL_CTX_new(const SSL_METHOD *meth)
1678 {
...
1827 #ifndef OPENSSL_NO_BUF_FREELISTS
1828   ret->freelist_max_len = SSL_MAX_BUF_FREELIST_LEN_DEFAULT;
1829   ret->rbuf_freelist = OPENSSL_malloc(sizeof(SSL3_BUF_FREELIST));
1830   if (!ret->rbuf_freelist)
1831   goto err;
1832   ret->rbuf_freelist->chunklen = 0;
1833   ret->rbuf_freelist->len = 0;
1834   ret->rbuf_freelist->head = NULL;
1835   ret->wbuf_freelist = OPENSSL_malloc(sizeof(SSL3_BUF_FREELIST));
1836   if (!ret->wbuf_freelist)
1837   {
1838     OPENSSL_free(ret->rbuf_freelist);
1839     goto err;
1840   }
1841   ret->wbuf_freelist->chunklen = 0;
1842   ret->wbuf_freelist->len = 0;
1843   ret->wbuf_freelist->head = NULL;

Yukarıda bahse konu olan kod blokları için ise rbuf_freelist ve wbuf_freelist yapıları görülmektedir. Bu yapılar öncelikli olarak OPENSSL_malloc fonksiyonuyla çağırılmaktadır. Bu listeleri destekleyen diğer parçalar ise normal malloc fonksiyonlarıyla desteklenmektedir. Öbek freelist_insert fonksiyonuyla freelist eklenmekte ve freelist_extract fonksiyonuyla bu datalara ulaşılmaktadır. Yukarıda da görüldüğü üzere freelist_extract fonksiyonu ilk defa kullanıldığında chunklen listesi 0 olarak başlatılır.

 678 static void *
 679 freelist_extract(SSL_CTX *ctx, int for_read, int sz)
 680 {
 681   SSL3_BUF_FREELIST *list;
 682   SSL3_BUF_FREELIST_ENTRY *ent = NULL;
 683   void *result = NULL;
 684
 685   CRYPTO_w_lock(CRYPTO_LOCK_SSL_CTX);
 686   list = for_read ? ctx->rbuf_freelist : ctx->wbuf_freelist;
 687   if (list != NULL && sz == (int)list->chunklen)
 688     ent = list->head;
 689   if (ent != NULL)
 690   {
 691     list->head = ent->next;
 692     result = ent;
 693     if (--list->len == 0)
 694     list->chunklen = 0;
 695   }
 696   CRYPTO_w_unlock(CRYPTO_LOCK_SSL_CTX);
 697   if (!result)
 698     result = OPENSSL_malloc(sz);
 699   return result;
 700 }

Yukarıdaki kod blokunda öbek(chunk) isteği yapıldığında yukarıda da belirttiğimiz üzere freelist_insert işlevi devreye giriyor bahse konu openssl versiyonunu indirirseniz bu kod bloklarını incelemeniz daha fazla kolaylaşacaktır ek bilgi olarak ekleyelim bunuda. Bu işlev ilk önce, istenen boyutun chunklen listesiyle aynı olup olmadığını veya chunklen listesinin 0 olup olmadığını kontrol eder. Bu kontroller esnasında freelist_max_len ve sizeof(*ent))(SSL3_BUF_FREELIST_ENTRY) kontrolleri uygulanır. Bahse konu koşullar yerine getirildiğinde ise chunklen istenilen boyuta ayarlanabilir, ent ise eklenecek öbek(chunk) değerine atanmıştır. list->next işaretçisi list->head değişkenine ayarlanır, yani bu değişken kafaya en üste gelecek demek oluyor.

 702 static void
 703 freelist_insert(SSL_CTX *ctx, int for_read, size_t sz, void *mem)
 704 {
 705   SSL3_BUF_FREELIST *list;
 706   SSL3_BUF_FREELIST_ENTRY *ent;
 707
 708   CRYPTO_w_lock(CRYPTO_LOCK_SSL_CTX);
 709   list = for_read ? ctx->rbuf_freelist : ctx->wbuf_freelist;
 710   if (list != NULL &&
 711     (sz == list->chunklen || list->chunklen == 0) &&
 712     list->len < ctx->freelist_max_len &&
 713     sz >= sizeof(*ent))
 714     {
 715       list->chunklen = sz;
 716       ent = mem;
 717       ent->next = list->head;
 718       list->head = ent;
 719       ++list->len;
 720       mem = NULL;
 721     }
 722
 723     CRYPTO_w_unlock(CRYPTO_LOCK_SSL_CTX);
 724     if (mem)
 725       OPENSSL_free(mem);
 726 }

Makalelerin çoğunda da göreceğiniz üzere freelist fonksiyonu yazının bir kaç yerinde de bahsetmiş olduğumuz üzere performans odaklı bir çalışma olarak gözümüze çarpıyor. Yani tekrar tekrar yapılan istekleri en hızlı şekilde karşılamak ve belleği rahatlatmak üzere kurulmuş bir yapı. Heartbleed olarak geçen açığın asıl esprisi ssl->s3->wbuf.buf and ssl->s3->rbuf.buf işaretçileri ve buffer = OPENSSL_malloc(1 + 2 + payload + padding); ve memcpy(bp, pl, payload); kod bloğu olarak gözümüze çarpıyor. Serinin devamı niteliğinde ki bu yazıyı son olarak III. parti ile tamamlayacağız, umarım memnun kalmışsınızdır, hoşçakalın.

Hackers realize, kiddies memorize

Kaynaklar:
https://web.archive.org/web/20140605090556/http://blog.leafsr.com:80/2014/04/11/my-heart-is-ok-but-my-eyes-are-bleeding/
https://www.theregister.co.uk/2014/04/09/heartbleed_explained/
http://garage4hackers.com/entry.php?b=2551
https://45h15h.files.wordpress.com/2014/07/heartbleed-poc-demo-by-0xashish.pdf
https://www.youtube.com/watch?v=rE5dW3BTpn4
https://en.wikipedia.org/wiki/Heartbleed
https://blog.cloudflare.com/answering-the-critical-question-can-you-get-private-ssl-keys-using-heartbleed/
https://niiconsulting.com/checkmate/2014/04/heartbleed-open-ssl-bug-faq-advisory/
*http://0x4c43.cn/2018/0701/heartbleed-vulnerability-analysis/

8 Likes