İlk seride(Modern Bir Zafiyetin Anatomisi Vol. I 22) 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 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
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 3 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 3
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 n2s(p, payload);, 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:
My heart is ok, but my eyes are bleeding | Leaf Security Research 3
Anatomy of OpenSSL's Heartbleed: Just four bytes trigger horror bug • The Register 1
CVE-2014-0160 Heartbleed Attack POC and Mass Scanner . - Blogs - Garage4hackers Forum 2
https://45h15h.files.wordpress.com/2014/07/heartbleed-poc-demo-by-0xashish.pdf 2
From Missingno to Heartbleed: Buffer Exploits and Buffer Overflows - YouTube 1
Heartbleed - Wikipedia
https://blog.cloudflare.com/answering-the-critical-question-can-you-get-private-ssl-keys-using-heartbleed/
Heartbleed Open SSL Bug FAQ & Advisory - Checkmate
*Heartbleed 漏洞分析 | 0x4C43's Blog 4
Buffer Overflow (BOF) Examples 1
Heartbleed and Static Analysis – Embedded in Academia
xkcd: Heartbleed Explanation